coreiq 0.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- coreiq-0.2.0/PKG-INFO +31 -0
- coreiq-0.2.0/coreiq/__init__.py +43 -0
- coreiq-0.2.0/coreiq/client.py +240 -0
- coreiq-0.2.0/coreiq/config.py +45 -0
- coreiq-0.2.0/coreiq/evolution/__init__.py +11 -0
- coreiq-0.2.0/coreiq/evolution/bandit.py +84 -0
- coreiq-0.2.0/coreiq/evolution/dicl.py +67 -0
- coreiq-0.2.0/coreiq/evolution/feedback.py +49 -0
- coreiq-0.2.0/coreiq/evolution/finetune.py +101 -0
- coreiq-0.2.0/coreiq/evolution/sdk.py +151 -0
- coreiq-0.2.0/coreiq/exceptions.py +14 -0
- coreiq-0.2.0/coreiq/governance/__init__.py +11 -0
- coreiq-0.2.0/coreiq/governance/audit.py +74 -0
- coreiq-0.2.0/coreiq/governance/compliance.py +73 -0
- coreiq-0.2.0/coreiq/governance/policy.py +64 -0
- coreiq-0.2.0/coreiq/governance/rbac.py +51 -0
- coreiq-0.2.0/coreiq/models/__init__.py +9 -0
- coreiq-0.2.0/coreiq/models/adapters/__init__.py +11 -0
- coreiq-0.2.0/coreiq/models/adapters/anthropic.py +102 -0
- coreiq-0.2.0/coreiq/models/adapters/openai.py +97 -0
- coreiq-0.2.0/coreiq/models/adapters/sglang.py +96 -0
- coreiq-0.2.0/coreiq/models/adapters/vllm.py +96 -0
- coreiq-0.2.0/coreiq/models/registry.py +73 -0
- coreiq-0.2.0/coreiq/models/selector.py +47 -0
- coreiq-0.2.0/coreiq/models/shadow.py +73 -0
- coreiq-0.2.0/coreiq/observability/__init__.py +3 -0
- coreiq-0.2.0/coreiq/observability/tracer.py +41 -0
- coreiq-0.2.0/coreiq/performance/__init__.py +16 -0
- coreiq-0.2.0/coreiq/performance/batcher.py +71 -0
- coreiq-0.2.0/coreiq/performance/budget.py +80 -0
- coreiq-0.2.0/coreiq/performance/cache.py +229 -0
- coreiq-0.2.0/coreiq/performance/router.py +63 -0
- coreiq-0.2.0/coreiq/py.typed +0 -0
- coreiq-0.2.0/coreiq/security/__init__.py +11 -0
- coreiq-0.2.0/coreiq/security/injection.py +55 -0
- coreiq-0.2.0/coreiq/security/output_guard.py +51 -0
- coreiq-0.2.0/coreiq/security/pii.py +43 -0
- coreiq-0.2.0/coreiq/security/scanner.py +62 -0
- coreiq-0.2.0/coreiq/server/__init__.py +0 -0
- coreiq-0.2.0/coreiq/server/app.py +220 -0
- coreiq-0.2.0/coreiq/server/routers/__init__.py +0 -0
- coreiq-0.2.0/coreiq/server/routers/cache.py +48 -0
- coreiq-0.2.0/coreiq/server/routers/events.py +66 -0
- coreiq-0.2.0/coreiq/server/routers/evolution.py +41 -0
- coreiq-0.2.0/coreiq/server/routers/feedback.py +91 -0
- coreiq-0.2.0/coreiq/server/routers/optimiser.py +177 -0
- coreiq-0.2.0/coreiq/server/routers/query_recon.py +19 -0
- coreiq-0.2.0/coreiq/server/routers/tool_calls.py +66 -0
- coreiq-0.2.0/coreiq/server/routers/traces.py +76 -0
- coreiq-0.2.0/coreiq/status.py +134 -0
- coreiq-0.2.0/coreiq/store/__init__.py +0 -0
- coreiq-0.2.0/coreiq/store/db.py +152 -0
- coreiq-0.2.0/coreiq/store/seed.py +282 -0
- coreiq-0.2.0/coreiq/store/writer.py +175 -0
- coreiq-0.2.0/coreiq/testing.py +73 -0
- coreiq-0.2.0/coreiq/tools/__init__.py +5 -0
- coreiq-0.2.0/coreiq/tools/registry.py +52 -0
- coreiq-0.2.0/coreiq/tools/router.py +101 -0
- coreiq-0.2.0/coreiq/tools/validator.py +86 -0
- coreiq-0.2.0/coreiq/tracing.py +38 -0
- coreiq-0.2.0/coreiq.egg-info/PKG-INFO +31 -0
- coreiq-0.2.0/coreiq.egg-info/SOURCES.txt +80 -0
- coreiq-0.2.0/coreiq.egg-info/dependency_links.txt +1 -0
- coreiq-0.2.0/coreiq.egg-info/entry_points.txt +2 -0
- coreiq-0.2.0/coreiq.egg-info/requires.txt +19 -0
- coreiq-0.2.0/coreiq.egg-info/top_level.txt +1 -0
- coreiq-0.2.0/pyproject.toml +57 -0
- coreiq-0.2.0/setup.cfg +4 -0
- coreiq-0.2.0/tests/test_api_routes.py +166 -0
- coreiq-0.2.0/tests/test_async_adapters.py +146 -0
- coreiq-0.2.0/tests/test_evolution.py +287 -0
- coreiq-0.2.0/tests/test_governance.py +89 -0
- coreiq-0.2.0/tests/test_integration.py +179 -0
- coreiq-0.2.0/tests/test_models.py +99 -0
- coreiq-0.2.0/tests/test_sdk_client.py +80 -0
- coreiq-0.2.0/tests/test_security.py +114 -0
- coreiq-0.2.0/tests/test_semantic_cache.py +163 -0
- coreiq-0.2.0/tests/test_status.py +76 -0
- coreiq-0.2.0/tests/test_store_writer.py +118 -0
- coreiq-0.2.0/tests/test_testing.py +82 -0
- coreiq-0.2.0/tests/test_tools.py +282 -0
- coreiq-0.2.0/tests/test_tracing.py +66 -0
coreiq-0.2.0/PKG-INFO
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: coreiq
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Production AI middleware with observability dashboard
|
|
5
|
+
Author: CoreIQ Contributors
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/coreiq/coreiq
|
|
8
|
+
Project-URL: Documentation, https://coreiq.dev
|
|
9
|
+
Classifier: Development Status :: 3 - Alpha
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
14
|
+
Requires-Python: >=3.11
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
Requires-Dist: fastapi>=0.111.0
|
|
17
|
+
Requires-Dist: uvicorn[standard]>=0.29.0
|
|
18
|
+
Requires-Dist: httpx>=0.27.0
|
|
19
|
+
Requires-Dist: pydantic>=2.0
|
|
20
|
+
Requires-Dist: openai>=1.30.0
|
|
21
|
+
Requires-Dist: anthropic>=0.28.0
|
|
22
|
+
Provides-Extra: embeddings
|
|
23
|
+
Requires-Dist: sentence-transformers>=2.7; extra == "embeddings"
|
|
24
|
+
Requires-Dist: numpy>=1.24; extra == "embeddings"
|
|
25
|
+
Provides-Extra: tokenizers
|
|
26
|
+
Requires-Dist: tiktoken>=0.5; extra == "tokenizers"
|
|
27
|
+
Provides-Extra: dev
|
|
28
|
+
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
29
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == "dev"
|
|
30
|
+
Requires-Dist: httpx>=0.27.0; extra == "dev"
|
|
31
|
+
Requires-Dist: tiktoken>=0.7; extra == "dev"
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
from .client import CoreIQClient, log_event
|
|
2
|
+
from .exceptions import CoreIQError, ValidationError, DatabaseError, ConfigError
|
|
3
|
+
from .evolution.feedback import FeedbackSignal
|
|
4
|
+
from .status import StatusReport
|
|
5
|
+
from .testing import MockCoreIQ, assert_event_logged
|
|
6
|
+
from .security import PromptInjectionError, OutputViolationError
|
|
7
|
+
from .governance import PolicyViolation, ComplianceViolation
|
|
8
|
+
from .performance import BudgetExceeded
|
|
9
|
+
from .tools import ToolSpec, ValidationResult
|
|
10
|
+
from .evolution.sdk import VariantResult
|
|
11
|
+
|
|
12
|
+
try:
|
|
13
|
+
from .tracing import trace # noqa: F401
|
|
14
|
+
_has_tracing = True
|
|
15
|
+
except ImportError:
|
|
16
|
+
_has_tracing = False
|
|
17
|
+
|
|
18
|
+
__version__ = "0.2.0"
|
|
19
|
+
|
|
20
|
+
__all__ = [
|
|
21
|
+
"__version__",
|
|
22
|
+
"CoreIQClient",
|
|
23
|
+
"log_event",
|
|
24
|
+
"CoreIQError",
|
|
25
|
+
"ValidationError",
|
|
26
|
+
"DatabaseError",
|
|
27
|
+
"ConfigError",
|
|
28
|
+
"FeedbackSignal",
|
|
29
|
+
"StatusReport",
|
|
30
|
+
"MockCoreIQ",
|
|
31
|
+
"assert_event_logged",
|
|
32
|
+
"PromptInjectionError",
|
|
33
|
+
"OutputViolationError",
|
|
34
|
+
"PolicyViolation",
|
|
35
|
+
"ComplianceViolation",
|
|
36
|
+
"BudgetExceeded",
|
|
37
|
+
"ToolSpec",
|
|
38
|
+
"ValidationResult",
|
|
39
|
+
"VariantResult",
|
|
40
|
+
]
|
|
41
|
+
|
|
42
|
+
if _has_tracing:
|
|
43
|
+
__all__.append("trace")
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import logging
|
|
5
|
+
from typing import TYPE_CHECKING, Any, Dict, Literal, Optional
|
|
6
|
+
|
|
7
|
+
from .store.writer import get_writer
|
|
8
|
+
|
|
9
|
+
_logger = logging.getLogger("coreiq")
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
from .evolution.feedback import FeedbackRecorder
|
|
13
|
+
from .evolution.sdk import EvolutionSDK
|
|
14
|
+
from .security.scanner import ScanResult
|
|
15
|
+
from .performance.budget import BudgetTracker
|
|
16
|
+
from .performance.cache import InMemorySemanticCache
|
|
17
|
+
from .performance.router import ModelRouter
|
|
18
|
+
from .status import StatusReport
|
|
19
|
+
|
|
20
|
+
Level = Literal["debug", "info", "warning", "error", "critical"]
|
|
21
|
+
_VALID_LEVELS = {"debug", "info", "warning", "error", "critical"}
|
|
22
|
+
_KNOWN_PROVIDERS = {"openai", "anthropic", "vllm", "sglang"}
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class _ToolsInterface:
|
|
26
|
+
"""Unified interface for tool registration, validation, and routing."""
|
|
27
|
+
|
|
28
|
+
def __init__(self, registry, validator, router):
|
|
29
|
+
self._registry = registry
|
|
30
|
+
self._validator = validator
|
|
31
|
+
self._router = router
|
|
32
|
+
|
|
33
|
+
def register(self, tools) -> None:
|
|
34
|
+
self._registry.register(tools)
|
|
35
|
+
|
|
36
|
+
def validate(self, tool_name: str, args_json: str):
|
|
37
|
+
return self._validator.validate(tool_name, args_json)
|
|
38
|
+
|
|
39
|
+
def select(self, query: str, top_k: int = 5):
|
|
40
|
+
return self._router.select(query, top_k)
|
|
41
|
+
|
|
42
|
+
def token_savings(self, selected, tokens_per_tool: int = 200) -> int:
|
|
43
|
+
return self._router.token_savings(selected, tokens_per_tool)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class CoreIQClient:
|
|
47
|
+
def __init__(self, db_path=None, api_key=None, log_level="info", provider="openai", model="gpt-4o"):
|
|
48
|
+
from .exceptions import ConfigError
|
|
49
|
+
if provider not in _KNOWN_PROVIDERS:
|
|
50
|
+
raise ConfigError(f"provider must be one of {sorted(_KNOWN_PROVIDERS)}, got '{provider}'")
|
|
51
|
+
if log_level not in _VALID_LEVELS:
|
|
52
|
+
raise ConfigError(f"log_level must be one of {sorted(_VALID_LEVELS)}, got '{log_level}'")
|
|
53
|
+
self.db_path = db_path
|
|
54
|
+
self.api_key = api_key
|
|
55
|
+
self.log_level = log_level
|
|
56
|
+
self.provider = provider
|
|
57
|
+
self.model = model
|
|
58
|
+
self._feedback: FeedbackRecorder | None = None
|
|
59
|
+
self._evolution: EvolutionSDK | None = None
|
|
60
|
+
self._tools_registry = None
|
|
61
|
+
self._tools_validator = None
|
|
62
|
+
self._tools_router = None
|
|
63
|
+
self._cache: InMemorySemanticCache | None = None
|
|
64
|
+
self._budget: BudgetTracker | None = None
|
|
65
|
+
self._router: ModelRouter | None = None
|
|
66
|
+
|
|
67
|
+
def __enter__(self) -> CoreIQClient:
|
|
68
|
+
return self
|
|
69
|
+
|
|
70
|
+
def __exit__(self, *exc) -> None:
|
|
71
|
+
self.close()
|
|
72
|
+
|
|
73
|
+
def close(self) -> None:
|
|
74
|
+
"""Release resources (placeholder for future cleanup)."""
|
|
75
|
+
|
|
76
|
+
@property
|
|
77
|
+
def feedback(self) -> FeedbackRecorder:
|
|
78
|
+
if self._feedback is None:
|
|
79
|
+
from .evolution.feedback import get_feedback_recorder
|
|
80
|
+
self._feedback = get_feedback_recorder()
|
|
81
|
+
return self._feedback
|
|
82
|
+
|
|
83
|
+
@property
|
|
84
|
+
def evolution(self) -> EvolutionSDK:
|
|
85
|
+
if self._evolution is None:
|
|
86
|
+
from .evolution.sdk import EvolutionSDK
|
|
87
|
+
self._evolution = EvolutionSDK()
|
|
88
|
+
return self._evolution
|
|
89
|
+
|
|
90
|
+
@property
|
|
91
|
+
def tools(self) -> _ToolsInterface:
|
|
92
|
+
if self._tools_registry is None:
|
|
93
|
+
from .tools.registry import ToolRegistry
|
|
94
|
+
from .tools.validator import ToolValidator
|
|
95
|
+
from .tools.router import ToolRouter
|
|
96
|
+
self._tools_registry = ToolRegistry()
|
|
97
|
+
self._tools_validator = ToolValidator(self._tools_registry)
|
|
98
|
+
self._tools_router = ToolRouter(self._tools_registry)
|
|
99
|
+
return _ToolsInterface(self._tools_registry, self._tools_validator, self._tools_router)
|
|
100
|
+
|
|
101
|
+
@property
|
|
102
|
+
def cache(self) -> "InMemorySemanticCache":
|
|
103
|
+
if self._cache is None:
|
|
104
|
+
from .performance.cache import InMemorySemanticCache
|
|
105
|
+
self._cache = InMemorySemanticCache()
|
|
106
|
+
return self._cache
|
|
107
|
+
|
|
108
|
+
def scan(self, messages: list[dict]) -> ScanResult:
|
|
109
|
+
"""Scan messages for injection, PII, and output violations."""
|
|
110
|
+
from .security.scanner import scan_messages
|
|
111
|
+
return scan_messages(messages)
|
|
112
|
+
|
|
113
|
+
@property
|
|
114
|
+
def budget(self) -> BudgetTracker:
|
|
115
|
+
if self._budget is None:
|
|
116
|
+
from .performance.budget import BudgetTracker, BudgetConfig
|
|
117
|
+
self._budget = BudgetTracker(BudgetConfig())
|
|
118
|
+
return self._budget
|
|
119
|
+
|
|
120
|
+
@property
|
|
121
|
+
def router(self) -> ModelRouter:
|
|
122
|
+
if self._router is None:
|
|
123
|
+
from .performance.router import ModelRouter
|
|
124
|
+
self._router = ModelRouter(default_model=self.model)
|
|
125
|
+
return self._router
|
|
126
|
+
|
|
127
|
+
def log_event(
|
|
128
|
+
self,
|
|
129
|
+
message: str,
|
|
130
|
+
metadata: Optional[Dict[str, Any]] = None,
|
|
131
|
+
*,
|
|
132
|
+
level: Level = "info",
|
|
133
|
+
source: Optional[str] = None,
|
|
134
|
+
trace_id: Optional[str] = None,
|
|
135
|
+
**extra: Any,
|
|
136
|
+
) -> str:
|
|
137
|
+
"""Log a structured event to the CoreIQ store.
|
|
138
|
+
|
|
139
|
+
Args:
|
|
140
|
+
message: Human-readable description of the event.
|
|
141
|
+
metadata: Arbitrary key/value pairs attached to the event.
|
|
142
|
+
level: Severity — "debug", "info", "warning", "error", "critical".
|
|
143
|
+
source: Optional origin label (e.g. module name, agent ID).
|
|
144
|
+
trace_id: Optionally link this event to an existing trace.
|
|
145
|
+
**extra: Extra key/value pairs merged into metadata.
|
|
146
|
+
|
|
147
|
+
Returns:
|
|
148
|
+
The generated event ID (UUID string).
|
|
149
|
+
"""
|
|
150
|
+
if trace_id is None:
|
|
151
|
+
from .tracing import get_current_trace_id
|
|
152
|
+
trace_id = get_current_trace_id()
|
|
153
|
+
if extra:
|
|
154
|
+
metadata = {**(metadata or {}), **extra}
|
|
155
|
+
_validate_level(level)
|
|
156
|
+
try:
|
|
157
|
+
return get_writer().insert_event(
|
|
158
|
+
message=message,
|
|
159
|
+
metadata=metadata,
|
|
160
|
+
level=level,
|
|
161
|
+
source=source,
|
|
162
|
+
trace_id=trace_id,
|
|
163
|
+
)
|
|
164
|
+
except Exception as e:
|
|
165
|
+
_logger.error("Failed to write event: %s", e)
|
|
166
|
+
from .exceptions import DatabaseError
|
|
167
|
+
raise DatabaseError(f"Failed to log event: {e}") from e
|
|
168
|
+
|
|
169
|
+
def status(self) -> "StatusReport":
|
|
170
|
+
"""Return a zero-token system health summary (last 7 days).
|
|
171
|
+
|
|
172
|
+
Usage::
|
|
173
|
+
|
|
174
|
+
status = client.status()
|
|
175
|
+
print(status) # human-readable
|
|
176
|
+
status.to_dict() # machine-readable
|
|
177
|
+
"""
|
|
178
|
+
from .status import build_status
|
|
179
|
+
return build_status()
|
|
180
|
+
|
|
181
|
+
def _record_tool_call(
|
|
182
|
+
self,
|
|
183
|
+
trace_id: str,
|
|
184
|
+
tool_name: str,
|
|
185
|
+
args: Dict[str, Any],
|
|
186
|
+
valid: bool = True,
|
|
187
|
+
error: Optional[str] = None,
|
|
188
|
+
) -> None:
|
|
189
|
+
get_writer().insert_tool_call(
|
|
190
|
+
trace_id=trace_id,
|
|
191
|
+
tool_name=tool_name,
|
|
192
|
+
args_json=json.dumps(args),
|
|
193
|
+
valid=valid,
|
|
194
|
+
error=error,
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def _validate_level(level: str) -> str:
|
|
199
|
+
if level not in _VALID_LEVELS:
|
|
200
|
+
from .exceptions import ValidationError
|
|
201
|
+
raise ValidationError(f"level must be one of {sorted(_VALID_LEVELS)}, got '{level}'")
|
|
202
|
+
return level
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def log_event(
|
|
206
|
+
message: str,
|
|
207
|
+
metadata: Optional[Dict[str, Any]] = None,
|
|
208
|
+
*,
|
|
209
|
+
level: Level = "info",
|
|
210
|
+
source: Optional[str] = None,
|
|
211
|
+
trace_id: Optional[str] = None,
|
|
212
|
+
**extra: Any,
|
|
213
|
+
) -> str:
|
|
214
|
+
"""Module-level convenience wrapper for logging a structured event.
|
|
215
|
+
|
|
216
|
+
Usage::
|
|
217
|
+
|
|
218
|
+
import coreiq
|
|
219
|
+
coreiq.log_event("user signed up", {"plan": "pro", "user_id": "u_123"})
|
|
220
|
+
coreiq.log_event("payment failed", {"amount": 49.99}, level="error")
|
|
221
|
+
coreiq.log_event("signup", plan="pro", user_id="u1")
|
|
222
|
+
"""
|
|
223
|
+
if trace_id is None:
|
|
224
|
+
from .tracing import get_current_trace_id
|
|
225
|
+
trace_id = get_current_trace_id()
|
|
226
|
+
if extra:
|
|
227
|
+
metadata = {**(metadata or {}), **extra}
|
|
228
|
+
_validate_level(level)
|
|
229
|
+
try:
|
|
230
|
+
return get_writer().insert_event(
|
|
231
|
+
message=message,
|
|
232
|
+
metadata=metadata,
|
|
233
|
+
level=level,
|
|
234
|
+
source=source,
|
|
235
|
+
trace_id=trace_id,
|
|
236
|
+
)
|
|
237
|
+
except Exception as e:
|
|
238
|
+
_logger.error("Failed to write event: %s", e)
|
|
239
|
+
from .exceptions import DatabaseError
|
|
240
|
+
raise DatabaseError(f"Failed to log event: {e}") from e
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""Central configuration via environment variables."""
|
|
2
|
+
import os
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def get_db_path() -> Path:
|
|
7
|
+
raw = os.environ.get("COREIQ_DB_PATH", "")
|
|
8
|
+
if raw:
|
|
9
|
+
return Path(raw)
|
|
10
|
+
return Path.home() / ".coreiq" / "coreiq.db"
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def get_api_key() -> str | None:
|
|
14
|
+
"""If set, all /api/* requests must include X-API-Key: <value> header."""
|
|
15
|
+
return os.environ.get("COREIQ_API_KEY") or None
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def get_host() -> str:
|
|
19
|
+
return os.environ.get("COREIQ_HOST", "127.0.0.1")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def get_port() -> int:
|
|
23
|
+
return int(os.environ.get("COREIQ_PORT", "7823"))
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def get_log_level() -> str:
|
|
27
|
+
return os.environ.get("COREIQ_LOG_LEVEL", "info").lower()
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def get_log_json() -> bool:
|
|
31
|
+
"""If true, emit logs as JSON (for structured log aggregators)."""
|
|
32
|
+
return os.environ.get("COREIQ_LOG_JSON", "").lower() in ("1", "true", "yes")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def get_cors_origins() -> list[str]:
|
|
36
|
+
"""Comma-separated allowed CORS origins. Defaults to localhost dev server."""
|
|
37
|
+
raw = os.environ.get("COREIQ_CORS_ORIGINS", "")
|
|
38
|
+
if raw:
|
|
39
|
+
return [o.strip() for o in raw.split(",") if o.strip()]
|
|
40
|
+
return ["http://localhost:5173"]
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def get_rate_limit() -> int:
|
|
44
|
+
"""Max requests per minute per IP. 0 = disabled."""
|
|
45
|
+
return int(os.environ.get("COREIQ_RATE_LIMIT", "0"))
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
from .feedback import FeedbackRecorder, get_feedback_recorder
|
|
2
|
+
from .bandit import PromptBandit, Arm
|
|
3
|
+
from .dicl import ExampleStore, Example
|
|
4
|
+
from .finetune import FinetuneDataset, TrainingPair
|
|
5
|
+
|
|
6
|
+
__all__ = [
|
|
7
|
+
"FeedbackRecorder", "get_feedback_recorder",
|
|
8
|
+
"PromptBandit", "Arm",
|
|
9
|
+
"ExampleStore", "Example",
|
|
10
|
+
"FinetuneDataset", "TrainingPair",
|
|
11
|
+
]
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"""Multi-armed bandit for prompt variant selection (Thompson sampling)."""
|
|
2
|
+
import random
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import Optional
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@dataclass
|
|
8
|
+
class Arm:
|
|
9
|
+
variant_id: str
|
|
10
|
+
alpha: float = 1.0 # successes + 1 (Beta prior)
|
|
11
|
+
beta: float = 1.0 # failures + 1 (Beta prior)
|
|
12
|
+
|
|
13
|
+
@property
|
|
14
|
+
def win_rate(self) -> float:
|
|
15
|
+
return self.alpha / (self.alpha + self.beta)
|
|
16
|
+
|
|
17
|
+
@property
|
|
18
|
+
def samples(self) -> int:
|
|
19
|
+
return int(self.alpha + self.beta - 2)
|
|
20
|
+
|
|
21
|
+
def sample(self) -> float:
|
|
22
|
+
"""Draw from Beta(alpha, beta) — Thompson sampling."""
|
|
23
|
+
return random.betavariate(self.alpha, self.beta)
|
|
24
|
+
|
|
25
|
+
def update(self, reward: float) -> None:
|
|
26
|
+
"""Update with a reward in [0, 1]."""
|
|
27
|
+
self.alpha += reward
|
|
28
|
+
self.beta += (1.0 - reward)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class PromptBandit:
|
|
32
|
+
"""
|
|
33
|
+
Thompson-sampling bandit for prompt variant A/B/n testing.
|
|
34
|
+
Each variant_id maps to a Beta-distribution arm.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
def __init__(self, variant_ids: list[str]):
|
|
38
|
+
self._arms: dict[str, Arm] = {vid: Arm(variant_id=vid) for vid in variant_ids}
|
|
39
|
+
|
|
40
|
+
def select(self) -> str:
|
|
41
|
+
"""Return the variant_id with the highest Thompson sample."""
|
|
42
|
+
return max(self._arms.values(), key=lambda a: a.sample()).variant_id
|
|
43
|
+
|
|
44
|
+
def reward(self, variant_id: str, value: float) -> None:
|
|
45
|
+
"""Record a reward (0.0–1.0) for a variant."""
|
|
46
|
+
if variant_id not in self._arms:
|
|
47
|
+
self._arms[variant_id] = Arm(variant_id=variant_id)
|
|
48
|
+
self._arms[variant_id].update(max(0.0, min(1.0, value)))
|
|
49
|
+
|
|
50
|
+
def add_variant(self, variant_id: str) -> None:
|
|
51
|
+
if variant_id not in self._arms:
|
|
52
|
+
self._arms[variant_id] = Arm(variant_id=variant_id)
|
|
53
|
+
|
|
54
|
+
def stats(self) -> list[dict]:
|
|
55
|
+
return [
|
|
56
|
+
{
|
|
57
|
+
"variant_id": arm.variant_id,
|
|
58
|
+
"win_rate": round(arm.win_rate, 4),
|
|
59
|
+
"samples": arm.samples,
|
|
60
|
+
"alpha": arm.alpha,
|
|
61
|
+
"beta": arm.beta,
|
|
62
|
+
}
|
|
63
|
+
for arm in sorted(self._arms.values(), key=lambda a: -a.win_rate)
|
|
64
|
+
]
|
|
65
|
+
|
|
66
|
+
def has_arms(self) -> bool:
|
|
67
|
+
return bool(self._arms)
|
|
68
|
+
|
|
69
|
+
def get_arm(self, variant_id: str) -> Optional[Arm]:
|
|
70
|
+
return self._arms.get(variant_id)
|
|
71
|
+
|
|
72
|
+
def items(self):
|
|
73
|
+
"""Iterate (variant_id, Arm) pairs."""
|
|
74
|
+
return self._arms.items()
|
|
75
|
+
|
|
76
|
+
def restore_arm(self, variant_id: str, alpha: float, beta: float) -> None:
|
|
77
|
+
"""Restore a persisted arm state."""
|
|
78
|
+
self._arms[variant_id] = Arm(variant_id=variant_id, alpha=alpha, beta=beta)
|
|
79
|
+
|
|
80
|
+
def best(self) -> Optional[str]:
|
|
81
|
+
"""Return the variant with the highest current win_rate."""
|
|
82
|
+
if not self._arms:
|
|
83
|
+
return None
|
|
84
|
+
return max(self._arms.values(), key=lambda a: a.win_rate).variant_id
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"""DICL — Dynamic In-Context Learning: select best few-shot examples at runtime."""
|
|
2
|
+
from dataclasses import dataclass, field
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
@dataclass
|
|
6
|
+
class Example:
|
|
7
|
+
id: str
|
|
8
|
+
input: str
|
|
9
|
+
output: str
|
|
10
|
+
metadata: dict = field(default_factory=dict)
|
|
11
|
+
score: float = 1.0 # quality/relevance weight
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class ExampleStore:
|
|
15
|
+
"""In-memory store of labeled examples with cosine-similarity retrieval."""
|
|
16
|
+
|
|
17
|
+
def __init__(self):
|
|
18
|
+
self._examples: list[Example] = []
|
|
19
|
+
|
|
20
|
+
def add(self, example: Example) -> None:
|
|
21
|
+
self._examples.append(example)
|
|
22
|
+
|
|
23
|
+
def remove(self, example_id: str) -> bool:
|
|
24
|
+
before = len(self._examples)
|
|
25
|
+
self._examples = [e for e in self._examples if e.id != example_id]
|
|
26
|
+
return len(self._examples) < before
|
|
27
|
+
|
|
28
|
+
def retrieve(
|
|
29
|
+
self,
|
|
30
|
+
query: str,
|
|
31
|
+
*,
|
|
32
|
+
k: int = 3,
|
|
33
|
+
min_score: float = 0.0,
|
|
34
|
+
) -> list[Example]:
|
|
35
|
+
"""
|
|
36
|
+
Retrieve top-k examples by simple TF-IDF-inspired Jaccard similarity.
|
|
37
|
+
In production, replace with embedding-based retrieval.
|
|
38
|
+
"""
|
|
39
|
+
query_tokens = set(query.lower().split())
|
|
40
|
+
scored: list[tuple[float, Example]] = []
|
|
41
|
+
for ex in self._examples:
|
|
42
|
+
if ex.score < min_score:
|
|
43
|
+
continue
|
|
44
|
+
ex_tokens = set(ex.input.lower().split())
|
|
45
|
+
if not query_tokens and not ex_tokens:
|
|
46
|
+
sim = 1.0
|
|
47
|
+
elif not query_tokens or not ex_tokens:
|
|
48
|
+
sim = 0.0
|
|
49
|
+
else:
|
|
50
|
+
intersection = query_tokens & ex_tokens
|
|
51
|
+
union = query_tokens | ex_tokens
|
|
52
|
+
sim = len(intersection) / len(union)
|
|
53
|
+
scored.append((sim * ex.score, ex))
|
|
54
|
+
|
|
55
|
+
scored.sort(key=lambda x: -x[0])
|
|
56
|
+
return [ex for _, ex in scored[:k]]
|
|
57
|
+
|
|
58
|
+
def to_messages(self, examples: list[Example]) -> list[dict]:
|
|
59
|
+
"""Convert examples to alternating user/assistant message pairs."""
|
|
60
|
+
messages = []
|
|
61
|
+
for ex in examples:
|
|
62
|
+
messages.append({"role": "user", "content": ex.input})
|
|
63
|
+
messages.append({"role": "assistant", "content": ex.output})
|
|
64
|
+
return messages
|
|
65
|
+
|
|
66
|
+
def __len__(self) -> int:
|
|
67
|
+
return len(self._examples)
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
from enum import Enum
|
|
2
|
+
from typing import Optional
|
|
3
|
+
|
|
4
|
+
from ..store.writer import get_writer
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class FeedbackSignal(str, Enum):
|
|
8
|
+
THUMBS_UP = "thumbs_up"
|
|
9
|
+
THUMBS_DOWN = "thumbs_down"
|
|
10
|
+
SCORE = "score"
|
|
11
|
+
CORRECTION = "correction"
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class FeedbackRecorder:
|
|
15
|
+
def record(
|
|
16
|
+
self,
|
|
17
|
+
*,
|
|
18
|
+
response_id: str,
|
|
19
|
+
signal: str,
|
|
20
|
+
value: float = 0.0,
|
|
21
|
+
dimension: Optional[str] = None,
|
|
22
|
+
correction: Optional[str] = None,
|
|
23
|
+
) -> None:
|
|
24
|
+
get_writer().insert_feedback(
|
|
25
|
+
response_id=response_id,
|
|
26
|
+
signal=signal,
|
|
27
|
+
value=value,
|
|
28
|
+
dimension=dimension,
|
|
29
|
+
correction=correction,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
def thumbs_up(self, response_id: str, dimension: Optional[str] = None) -> None:
|
|
33
|
+
self.record(response_id=response_id, signal="thumbs_up", value=1.0, dimension=dimension)
|
|
34
|
+
|
|
35
|
+
def thumbs_down(self, response_id: str, dimension: Optional[str] = None, correction: Optional[str] = None) -> None:
|
|
36
|
+
self.record(response_id=response_id, signal="thumbs_down", value=0.0, dimension=dimension, correction=correction)
|
|
37
|
+
|
|
38
|
+
def score(self, response_id: str, value: float, dimension: Optional[str] = None) -> None:
|
|
39
|
+
self.record(response_id=response_id, signal="score", value=value, dimension=dimension)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
_recorder: Optional[FeedbackRecorder] = None
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def get_feedback_recorder() -> FeedbackRecorder:
|
|
46
|
+
global _recorder
|
|
47
|
+
if _recorder is None:
|
|
48
|
+
_recorder = FeedbackRecorder()
|
|
49
|
+
return _recorder
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"""Fine-tune data management — collect, curate, and export training pairs."""
|
|
2
|
+
import json
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from datetime import datetime, timezone
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass
|
|
9
|
+
class TrainingPair:
|
|
10
|
+
prompt: str
|
|
11
|
+
completion: str
|
|
12
|
+
quality_score: float = 1.0
|
|
13
|
+
source_trace_id: Optional[str] = None
|
|
14
|
+
created_at: str = field(default_factory=lambda: datetime.now(timezone.utc).isoformat())
|
|
15
|
+
tags: list[str] = field(default_factory=list)
|
|
16
|
+
|
|
17
|
+
def to_openai_format(self) -> dict:
|
|
18
|
+
"""Format as OpenAI fine-tuning JSONL record."""
|
|
19
|
+
return {
|
|
20
|
+
"messages": [
|
|
21
|
+
{"role": "user", "content": self.prompt},
|
|
22
|
+
{"role": "assistant", "content": self.completion},
|
|
23
|
+
]
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
def to_alpaca_format(self) -> dict:
|
|
27
|
+
"""Format as Alpaca-style instruction record."""
|
|
28
|
+
return {
|
|
29
|
+
"instruction": self.prompt,
|
|
30
|
+
"input": "",
|
|
31
|
+
"output": self.completion,
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class FinetuneDataset:
|
|
36
|
+
"""Collect and export fine-tuning pairs with quality filtering."""
|
|
37
|
+
|
|
38
|
+
def __init__(self, min_quality_score: float = 0.7):
|
|
39
|
+
self._pairs: list[TrainingPair] = []
|
|
40
|
+
self._min_quality = min_quality_score
|
|
41
|
+
|
|
42
|
+
def add(self, pair: TrainingPair) -> None:
|
|
43
|
+
self._pairs.append(pair)
|
|
44
|
+
|
|
45
|
+
def add_from_feedback(
|
|
46
|
+
self,
|
|
47
|
+
prompt: str,
|
|
48
|
+
completion: str,
|
|
49
|
+
*,
|
|
50
|
+
signal: str,
|
|
51
|
+
value: float = 0.0,
|
|
52
|
+
correction: Optional[str] = None,
|
|
53
|
+
trace_id: Optional[str] = None,
|
|
54
|
+
) -> Optional[TrainingPair]:
|
|
55
|
+
"""
|
|
56
|
+
Create a training pair from feedback signal.
|
|
57
|
+
- thumbs_up: use completion as-is
|
|
58
|
+
- correction: use correction text as completion
|
|
59
|
+
- thumbs_down / low score: skip
|
|
60
|
+
"""
|
|
61
|
+
if signal == "thumbs_up":
|
|
62
|
+
pair = TrainingPair(prompt=prompt, completion=completion,
|
|
63
|
+
quality_score=1.0, source_trace_id=trace_id,
|
|
64
|
+
tags=["thumbs_up"])
|
|
65
|
+
elif signal == "correction" and correction:
|
|
66
|
+
pair = TrainingPair(prompt=prompt, completion=correction,
|
|
67
|
+
quality_score=0.9, source_trace_id=trace_id,
|
|
68
|
+
tags=["correction"])
|
|
69
|
+
elif signal == "score" and value >= self._min_quality:
|
|
70
|
+
pair = TrainingPair(prompt=prompt, completion=completion,
|
|
71
|
+
quality_score=value, source_trace_id=trace_id,
|
|
72
|
+
tags=["scored"])
|
|
73
|
+
else:
|
|
74
|
+
return None
|
|
75
|
+
self._pairs.append(pair)
|
|
76
|
+
return pair
|
|
77
|
+
|
|
78
|
+
def export_jsonl(self, path: str, *, fmt: str = "openai", min_score: Optional[float] = None) -> int:
|
|
79
|
+
"""Export to JSONL file. Returns number of records written."""
|
|
80
|
+
threshold = min_score if min_score is not None else self._min_quality
|
|
81
|
+
pairs = [p for p in self._pairs if p.quality_score >= threshold]
|
|
82
|
+
with open(path, "w") as f:
|
|
83
|
+
for pair in pairs:
|
|
84
|
+
record = pair.to_openai_format() if fmt == "openai" else pair.to_alpaca_format()
|
|
85
|
+
f.write(json.dumps(record) + "\n")
|
|
86
|
+
return len(pairs)
|
|
87
|
+
|
|
88
|
+
def __len__(self) -> int:
|
|
89
|
+
return len(self._pairs)
|
|
90
|
+
|
|
91
|
+
@property
|
|
92
|
+
def stats(self) -> dict:
|
|
93
|
+
if not self._pairs:
|
|
94
|
+
return {"total": 0, "avg_quality": 0.0, "ready": 0}
|
|
95
|
+
avg_q = sum(p.quality_score for p in self._pairs) / len(self._pairs)
|
|
96
|
+
ready = sum(1 for p in self._pairs if p.quality_score >= self._min_quality)
|
|
97
|
+
return {
|
|
98
|
+
"total": len(self._pairs),
|
|
99
|
+
"avg_quality": round(avg_q, 3),
|
|
100
|
+
"ready": ready,
|
|
101
|
+
}
|