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.
Files changed (82) hide show
  1. coreiq-0.2.0/PKG-INFO +31 -0
  2. coreiq-0.2.0/coreiq/__init__.py +43 -0
  3. coreiq-0.2.0/coreiq/client.py +240 -0
  4. coreiq-0.2.0/coreiq/config.py +45 -0
  5. coreiq-0.2.0/coreiq/evolution/__init__.py +11 -0
  6. coreiq-0.2.0/coreiq/evolution/bandit.py +84 -0
  7. coreiq-0.2.0/coreiq/evolution/dicl.py +67 -0
  8. coreiq-0.2.0/coreiq/evolution/feedback.py +49 -0
  9. coreiq-0.2.0/coreiq/evolution/finetune.py +101 -0
  10. coreiq-0.2.0/coreiq/evolution/sdk.py +151 -0
  11. coreiq-0.2.0/coreiq/exceptions.py +14 -0
  12. coreiq-0.2.0/coreiq/governance/__init__.py +11 -0
  13. coreiq-0.2.0/coreiq/governance/audit.py +74 -0
  14. coreiq-0.2.0/coreiq/governance/compliance.py +73 -0
  15. coreiq-0.2.0/coreiq/governance/policy.py +64 -0
  16. coreiq-0.2.0/coreiq/governance/rbac.py +51 -0
  17. coreiq-0.2.0/coreiq/models/__init__.py +9 -0
  18. coreiq-0.2.0/coreiq/models/adapters/__init__.py +11 -0
  19. coreiq-0.2.0/coreiq/models/adapters/anthropic.py +102 -0
  20. coreiq-0.2.0/coreiq/models/adapters/openai.py +97 -0
  21. coreiq-0.2.0/coreiq/models/adapters/sglang.py +96 -0
  22. coreiq-0.2.0/coreiq/models/adapters/vllm.py +96 -0
  23. coreiq-0.2.0/coreiq/models/registry.py +73 -0
  24. coreiq-0.2.0/coreiq/models/selector.py +47 -0
  25. coreiq-0.2.0/coreiq/models/shadow.py +73 -0
  26. coreiq-0.2.0/coreiq/observability/__init__.py +3 -0
  27. coreiq-0.2.0/coreiq/observability/tracer.py +41 -0
  28. coreiq-0.2.0/coreiq/performance/__init__.py +16 -0
  29. coreiq-0.2.0/coreiq/performance/batcher.py +71 -0
  30. coreiq-0.2.0/coreiq/performance/budget.py +80 -0
  31. coreiq-0.2.0/coreiq/performance/cache.py +229 -0
  32. coreiq-0.2.0/coreiq/performance/router.py +63 -0
  33. coreiq-0.2.0/coreiq/py.typed +0 -0
  34. coreiq-0.2.0/coreiq/security/__init__.py +11 -0
  35. coreiq-0.2.0/coreiq/security/injection.py +55 -0
  36. coreiq-0.2.0/coreiq/security/output_guard.py +51 -0
  37. coreiq-0.2.0/coreiq/security/pii.py +43 -0
  38. coreiq-0.2.0/coreiq/security/scanner.py +62 -0
  39. coreiq-0.2.0/coreiq/server/__init__.py +0 -0
  40. coreiq-0.2.0/coreiq/server/app.py +220 -0
  41. coreiq-0.2.0/coreiq/server/routers/__init__.py +0 -0
  42. coreiq-0.2.0/coreiq/server/routers/cache.py +48 -0
  43. coreiq-0.2.0/coreiq/server/routers/events.py +66 -0
  44. coreiq-0.2.0/coreiq/server/routers/evolution.py +41 -0
  45. coreiq-0.2.0/coreiq/server/routers/feedback.py +91 -0
  46. coreiq-0.2.0/coreiq/server/routers/optimiser.py +177 -0
  47. coreiq-0.2.0/coreiq/server/routers/query_recon.py +19 -0
  48. coreiq-0.2.0/coreiq/server/routers/tool_calls.py +66 -0
  49. coreiq-0.2.0/coreiq/server/routers/traces.py +76 -0
  50. coreiq-0.2.0/coreiq/status.py +134 -0
  51. coreiq-0.2.0/coreiq/store/__init__.py +0 -0
  52. coreiq-0.2.0/coreiq/store/db.py +152 -0
  53. coreiq-0.2.0/coreiq/store/seed.py +282 -0
  54. coreiq-0.2.0/coreiq/store/writer.py +175 -0
  55. coreiq-0.2.0/coreiq/testing.py +73 -0
  56. coreiq-0.2.0/coreiq/tools/__init__.py +5 -0
  57. coreiq-0.2.0/coreiq/tools/registry.py +52 -0
  58. coreiq-0.2.0/coreiq/tools/router.py +101 -0
  59. coreiq-0.2.0/coreiq/tools/validator.py +86 -0
  60. coreiq-0.2.0/coreiq/tracing.py +38 -0
  61. coreiq-0.2.0/coreiq.egg-info/PKG-INFO +31 -0
  62. coreiq-0.2.0/coreiq.egg-info/SOURCES.txt +80 -0
  63. coreiq-0.2.0/coreiq.egg-info/dependency_links.txt +1 -0
  64. coreiq-0.2.0/coreiq.egg-info/entry_points.txt +2 -0
  65. coreiq-0.2.0/coreiq.egg-info/requires.txt +19 -0
  66. coreiq-0.2.0/coreiq.egg-info/top_level.txt +1 -0
  67. coreiq-0.2.0/pyproject.toml +57 -0
  68. coreiq-0.2.0/setup.cfg +4 -0
  69. coreiq-0.2.0/tests/test_api_routes.py +166 -0
  70. coreiq-0.2.0/tests/test_async_adapters.py +146 -0
  71. coreiq-0.2.0/tests/test_evolution.py +287 -0
  72. coreiq-0.2.0/tests/test_governance.py +89 -0
  73. coreiq-0.2.0/tests/test_integration.py +179 -0
  74. coreiq-0.2.0/tests/test_models.py +99 -0
  75. coreiq-0.2.0/tests/test_sdk_client.py +80 -0
  76. coreiq-0.2.0/tests/test_security.py +114 -0
  77. coreiq-0.2.0/tests/test_semantic_cache.py +163 -0
  78. coreiq-0.2.0/tests/test_status.py +76 -0
  79. coreiq-0.2.0/tests/test_store_writer.py +118 -0
  80. coreiq-0.2.0/tests/test_testing.py +82 -0
  81. coreiq-0.2.0/tests/test_tools.py +282 -0
  82. 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
+ }