devtorch-core 3.0.1__py3-none-any.whl

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 (193) hide show
  1. devtorch_core/__init__.py +158 -0
  2. devtorch_core/aggphi_textual.py +275 -0
  3. devtorch_core/alerts/__init__.py +23 -0
  4. devtorch_core/alerts/base.py +46 -0
  5. devtorch_core/alerts/config.py +60 -0
  6. devtorch_core/alerts/dispatcher.py +110 -0
  7. devtorch_core/alerts/jira.py +96 -0
  8. devtorch_core/alerts/linear.py +72 -0
  9. devtorch_core/alerts/pagerduty.py +66 -0
  10. devtorch_core/alerts/slack.py +81 -0
  11. devtorch_core/alerts/teams.py +70 -0
  12. devtorch_core/audit/__init__.py +43 -0
  13. devtorch_core/audit/exporter.py +297 -0
  14. devtorch_core/audit/privacy.py +101 -0
  15. devtorch_core/audit/scrubber.py +149 -0
  16. devtorch_core/audit/service.py +67 -0
  17. devtorch_core/audit/signing.py +127 -0
  18. devtorch_core/broadcast/__init__.py +4 -0
  19. devtorch_core/broadcast/broadcaster.py +100 -0
  20. devtorch_core/broadcast/watcher.py +71 -0
  21. devtorch_core/capability.py +639 -0
  22. devtorch_core/cloud/__init__.py +1 -0
  23. devtorch_core/cloud/client_config.py +472 -0
  24. devtorch_core/cloud/client_configs/.claude-opencode-fallback.json +8 -0
  25. devtorch_core/cloud/client_configs/.claude-stdio.json +13 -0
  26. devtorch_core/cloud/client_configs/.cursor-mcp.json +13 -0
  27. devtorch_core/cloud/client_configs/.opencode-bridge.json +13 -0
  28. devtorch_core/cloud/client_configs/.opencode.json +15 -0
  29. devtorch_core/cloud/client_configs/.vscode-mcp.json +13 -0
  30. devtorch_core/cloud/devtorch-mcp-bridge.js +357 -0
  31. devtorch_core/cloud/mcp_client.py +229 -0
  32. devtorch_core/cloud/setup.py +144 -0
  33. devtorch_core/cloud/sync.py +143 -0
  34. devtorch_core/cloud/sync_bundle.py +603 -0
  35. devtorch_core/cloud/sync_conflicts.py +159 -0
  36. devtorch_core/cloud/sync_state.py +159 -0
  37. devtorch_core/cloud/team_sync.py +283 -0
  38. devtorch_core/codex/__init__.py +9 -0
  39. devtorch_core/codex/__main__.py +97 -0
  40. devtorch_core/codex/capture.py +208 -0
  41. devtorch_core/codex/proxy.py +412 -0
  42. devtorch_core/concept_catalog.py +209 -0
  43. devtorch_core/consolidation/__init__.py +3 -0
  44. devtorch_core/consolidation/synthesizer.py +87 -0
  45. devtorch_core/consolidation/workflow.py +175 -0
  46. devtorch_core/daemon/__init__.py +27 -0
  47. devtorch_core/daemon/supervisor.py +293 -0
  48. devtorch_core/daemon/watcher.py +244 -0
  49. devtorch_core/dashboard_api.py +2012 -0
  50. devtorch_core/deltaf.py +97 -0
  51. devtorch_core/disclosure.py +50 -0
  52. devtorch_core/divergence/__init__.py +3 -0
  53. devtorch_core/divergence/detector.py +166 -0
  54. devtorch_core/gateway/__init__.py +32 -0
  55. devtorch_core/gateway/key_manager.py +124 -0
  56. devtorch_core/gateway/metrics_webhook.py +252 -0
  57. devtorch_core/gateway/policy.py +262 -0
  58. devtorch_core/gateway/server.py +727 -0
  59. devtorch_core/gateway/sso.py +233 -0
  60. devtorch_core/gcc.py +1246 -0
  61. devtorch_core/github/__init__.py +35 -0
  62. devtorch_core/github/app.py +240 -0
  63. devtorch_core/github/comment_builder.py +113 -0
  64. devtorch_core/github/pat.py +76 -0
  65. devtorch_core/github/pr_parser.py +82 -0
  66. devtorch_core/github/pr_reporter.py +555 -0
  67. devtorch_core/gitlab/__init__.py +177 -0
  68. devtorch_core/hitl/__init__.py +4 -0
  69. devtorch_core/hitl/channels.py +129 -0
  70. devtorch_core/hitl/orchestrator.py +95 -0
  71. devtorch_core/hooks/__init__.py +17 -0
  72. devtorch_core/hooks/claude_code.py +228 -0
  73. devtorch_core/hooks/git_capture.py +341 -0
  74. devtorch_core/hooks/git_commit.py +182 -0
  75. devtorch_core/hooks/installer.py +733 -0
  76. devtorch_core/hooks/pre_commit.py +157 -0
  77. devtorch_core/hooks/runner.py +344 -0
  78. devtorch_core/identity/__init__.py +4 -0
  79. devtorch_core/identity/agent.py +86 -0
  80. devtorch_core/identity/providers.py +85 -0
  81. devtorch_core/invariants.py +182 -0
  82. devtorch_core/mcp/__init__.py +10 -0
  83. devtorch_core/mcp/auth.py +177 -0
  84. devtorch_core/mcp/server.py +1049 -0
  85. devtorch_core/metrics/__init__.py +35 -0
  86. devtorch_core/metrics/aggregate.py +215 -0
  87. devtorch_core/metrics/calibrate.py +198 -0
  88. devtorch_core/metrics/calibration.py +125 -0
  89. devtorch_core/metrics/credibility.py +288 -0
  90. devtorch_core/metrics/delivery_time.py +70 -0
  91. devtorch_core/metrics/dhs.py +126 -0
  92. devtorch_core/metrics/mcs.py +96 -0
  93. devtorch_core/metrics/roi.py +88 -0
  94. devtorch_core/metrics/session_writer.py +81 -0
  95. devtorch_core/metrics/shadow_ai.py +117 -0
  96. devtorch_core/metrics/sprint_writer.py +243 -0
  97. devtorch_core/observability/__init__.py +78 -0
  98. devtorch_core/observability/datadog.py +157 -0
  99. devtorch_core/observability/formatter.py +119 -0
  100. devtorch_core/observability/report.py +264 -0
  101. devtorch_core/observability/servicenow.py +147 -0
  102. devtorch_core/observability/splunk.py +218 -0
  103. devtorch_core/observability/webhook.py +227 -0
  104. devtorch_core/parser/__init__.py +30 -0
  105. devtorch_core/parser/blocks.py +216 -0
  106. devtorch_core/parser/inference.py +159 -0
  107. devtorch_core/parser/thinking.py +112 -0
  108. devtorch_core/projects.py +169 -0
  109. devtorch_core/prompt_artifact.py +76 -0
  110. devtorch_core/proxy/__init__.py +9 -0
  111. devtorch_core/proxy/routes/__init__.py +1 -0
  112. devtorch_core/proxy/routes/anthropic.py +264 -0
  113. devtorch_core/proxy/routes/azure_openai.py +336 -0
  114. devtorch_core/proxy/routes/gemini.py +331 -0
  115. devtorch_core/proxy/routes/groq.py +284 -0
  116. devtorch_core/proxy/routes/ollama.py +279 -0
  117. devtorch_core/proxy/routes/openai.py +287 -0
  118. devtorch_core/proxy/server.py +356 -0
  119. devtorch_core/query/__init__.py +15 -0
  120. devtorch_core/query/grep.py +181 -0
  121. devtorch_core/query/hybrid.py +86 -0
  122. devtorch_core/query/semantic.py +157 -0
  123. devtorch_core/rdp.py +105 -0
  124. devtorch_core/reasoning/__init__.py +4 -0
  125. devtorch_core/reasoning/entry.py +31 -0
  126. devtorch_core/reasoning/store.py +122 -0
  127. devtorch_core/reasoning_plus/__init__.py +70 -0
  128. devtorch_core/reasoning_plus/augmenter.py +326 -0
  129. devtorch_core/reasoning_plus/capture.py +51 -0
  130. devtorch_core/reasoning_plus/config.py +256 -0
  131. devtorch_core/reasoning_plus/context.py +262 -0
  132. devtorch_core/reasoning_plus/learning/__init__.py +72 -0
  133. devtorch_core/reasoning_plus/learning/analytics.py +141 -0
  134. devtorch_core/reasoning_plus/learning/api.py +313 -0
  135. devtorch_core/reasoning_plus/learning/chain.py +285 -0
  136. devtorch_core/reasoning_plus/learning/composer.py +74 -0
  137. devtorch_core/reasoning_plus/learning/cross_project.py +234 -0
  138. devtorch_core/reasoning_plus/learning/embeddings.py +209 -0
  139. devtorch_core/reasoning_plus/learning/extractor.py +207 -0
  140. devtorch_core/reasoning_plus/learning/models.py +116 -0
  141. devtorch_core/reasoning_plus/learning/provenance.py +126 -0
  142. devtorch_core/reasoning_plus/learning/recorder.py +81 -0
  143. devtorch_core/reasoning_plus/learning/relevance.py +122 -0
  144. devtorch_core/reasoning_plus/learning/state.py +86 -0
  145. devtorch_core/reasoning_plus/learning/store.py +160 -0
  146. devtorch_core/reasoning_plus/learning/theta_learning_bridge.py +94 -0
  147. devtorch_core/reasoning_plus/prompt.py +90 -0
  148. devtorch_core/rep.py +134 -0
  149. devtorch_core/rep_network/__init__.py +25 -0
  150. devtorch_core/rep_network/merge.py +70 -0
  151. devtorch_core/rep_network/node.py +137 -0
  152. devtorch_core/rep_network/server.py +140 -0
  153. devtorch_core/rep_network/sync.py +207 -0
  154. devtorch_core/sensitivity.py +182 -0
  155. devtorch_core/serve.py +258 -0
  156. devtorch_core/session/__init__.py +39 -0
  157. devtorch_core/session/disagreement.py +188 -0
  158. devtorch_core/session/models.py +114 -0
  159. devtorch_core/session/orchestrator.py +182 -0
  160. devtorch_core/session/planner.py +169 -0
  161. devtorch_core/session/simulator.py +132 -0
  162. devtorch_core/signing.py +290 -0
  163. devtorch_core/sis.py +197 -0
  164. devtorch_core/storage.py +308 -0
  165. devtorch_core/templates/__init__.py +6 -0
  166. devtorch_core/templates/engine.py +122 -0
  167. devtorch_core/templates/go.py +18 -0
  168. devtorch_core/templates/infra.py +19 -0
  169. devtorch_core/templates/library/__init__.py +18 -0
  170. devtorch_core/templates/library/api_design.md +27 -0
  171. devtorch_core/templates/library/bug_fix.md +27 -0
  172. devtorch_core/templates/library/decision_record.md +27 -0
  173. devtorch_core/templates/library/engine.py +228 -0
  174. devtorch_core/templates/library/security_review.md +30 -0
  175. devtorch_core/templates/python.py +19 -0
  176. devtorch_core/templates/react.py +18 -0
  177. devtorch_core/templates/typescript.py +18 -0
  178. devtorch_core/theta.py +221 -0
  179. devtorch_core/theta_synthesis.py +268 -0
  180. devtorch_core/topics.py +320 -0
  181. devtorch_core/variance.py +219 -0
  182. devtorch_core/wrapper/__init__.py +52 -0
  183. devtorch_core/wrapper/anthropic.py +487 -0
  184. devtorch_core/wrapper/base.py +562 -0
  185. devtorch_core/wrapper/bedrock.py +342 -0
  186. devtorch_core/wrapper/gemini.py +422 -0
  187. devtorch_core/wrapper/ollama.py +527 -0
  188. devtorch_core/wrapper/openai.py +461 -0
  189. devtorch_core-3.0.1.dist-info/METADATA +867 -0
  190. devtorch_core-3.0.1.dist-info/RECORD +193 -0
  191. devtorch_core-3.0.1.dist-info/WHEEL +5 -0
  192. devtorch_core-3.0.1.dist-info/entry_points.txt +2 -0
  193. devtorch_core-3.0.1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,234 @@
1
+ """
2
+ devtorch_core.reasoning_plus.learning.cross_project
3
+ ====================================================
4
+ Cross-project learning suggestions.
5
+
6
+ Maintains a machine-level registry of governed projects at
7
+ ``~/.devtorch/registry.json`` under the ``governed_projects`` key and lets a
8
+ project surface relevant learnings from sibling projects with matching tags or
9
+ names.
10
+ """
11
+ from __future__ import annotations
12
+
13
+ import json
14
+ import logging
15
+ import os
16
+ from dataclasses import asdict, dataclass
17
+ from datetime import datetime, timezone
18
+ from pathlib import Path
19
+ from typing import Any
20
+
21
+ from devtorch_core.reasoning_plus.learning.models import Learning
22
+ from devtorch_core.reasoning_plus.learning.relevance import RelevanceEngine
23
+ from devtorch_core.reasoning_plus.learning.store import LearningStore
24
+ from devtorch_core.reasoning_plus.context import extract_keywords
25
+
26
+ logger = logging.getLogger("devtorch.reasoning_plus.learning.cross_project")
27
+
28
+
29
+ DEFAULT_REGISTRY_DIR = Path.home() / ".devtorch"
30
+ DEFAULT_REGISTRY_FILE = "registry.json"
31
+ REGISTRY_PATH_ENV = "DEVTORCH_REGISTRY_PATH"
32
+
33
+
34
+ @dataclass
35
+ class GovernedProject:
36
+ """One governed project entry in the cross-project learning registry."""
37
+
38
+ project_root: str
39
+ name: str
40
+ tags: list[str]
41
+ gcc_dir: str
42
+ added_at: str
43
+ updated_at: str
44
+
45
+
46
+ class CrossProjectRegistry:
47
+ """Machine-level registry of governed projects used for cross-project learning.
48
+
49
+ Stored under the ``governed_projects`` key in ``~/.devtorch/registry.json``
50
+ so it coexists with the Sprint 19 project registry (which uses the
51
+ ``projects`` key).
52
+ """
53
+
54
+ def __init__(self, registry_path: Path | str | None = None) -> None:
55
+ if registry_path is None:
56
+ env_path = os.environ.get(REGISTRY_PATH_ENV, "").strip()
57
+ if env_path:
58
+ registry_path = Path(env_path)
59
+ else:
60
+ registry_path = DEFAULT_REGISTRY_DIR / DEFAULT_REGISTRY_FILE
61
+ self.registry_path = Path(registry_path)
62
+ self._ensure_dir()
63
+
64
+ def _ensure_dir(self) -> None:
65
+ self.registry_path.parent.mkdir(parents=True, exist_ok=True)
66
+
67
+ def _read(self) -> dict[str, Any]:
68
+ if not self.registry_path.exists():
69
+ return {"version": "1", "projects": [], "governed_projects": []}
70
+ try:
71
+ data = json.loads(self.registry_path.read_text(encoding="utf-8"))
72
+ if not isinstance(data, dict):
73
+ return {"version": "1", "projects": [], "governed_projects": []}
74
+ return data
75
+ except (json.JSONDecodeError, OSError):
76
+ return {"version": "1", "projects": [], "governed_projects": []}
77
+
78
+ def _write(self, data: dict[str, Any]) -> None:
79
+ self._ensure_dir()
80
+ self.registry_path.write_text(
81
+ json.dumps(data, indent=2, sort_keys=True), encoding="utf-8"
82
+ )
83
+
84
+ def list(self) -> list[GovernedProject]:
85
+ """Return all registered governed projects."""
86
+ data = self._read()
87
+ entries: list[GovernedProject] = []
88
+ for raw in data.get("governed_projects", []):
89
+ try:
90
+ entries.append(GovernedProject(**raw))
91
+ except (TypeError, ValueError):
92
+ logger.debug("devtorch: skipping malformed governed project entry: %s", raw)
93
+ continue
94
+ return entries
95
+
96
+ def add(self, gcc_dir: Path | str, name: str, tags: list[str] | None = None) -> GovernedProject:
97
+ """Add or update a governed project entry keyed by gcc_dir."""
98
+ gcc_dir = Path(gcc_dir).resolve()
99
+ project_root = gcc_dir.parent
100
+ name = name or project_root.name
101
+ tags = sorted(set((tags or [])))
102
+
103
+ data = self._read()
104
+ projects = data.setdefault("governed_projects", [])
105
+
106
+ resolved_root = str(project_root)
107
+ for raw in projects:
108
+ if Path(raw.get("gcc_dir", "")).resolve() == gcc_dir:
109
+ raw["project_root"] = resolved_root
110
+ raw["name"] = name
111
+ raw["tags"] = tags
112
+ raw["updated_at"] = _now_iso()
113
+ self._write(data)
114
+ return GovernedProject(**raw)
115
+
116
+ entry = GovernedProject(
117
+ project_root=resolved_root,
118
+ name=name,
119
+ tags=tags,
120
+ gcc_dir=str(gcc_dir),
121
+ added_at=_now_iso(),
122
+ updated_at=_now_iso(),
123
+ )
124
+ projects.append(asdict(entry))
125
+ self._write(data)
126
+ return entry
127
+
128
+ def remove(self, gcc_dir: Path | str) -> bool:
129
+ """Remove a governed project by gcc_dir. Returns True if removed."""
130
+ gcc_dir = Path(gcc_dir).resolve()
131
+ data = self._read()
132
+ projects = data.get("governed_projects", [])
133
+ original_len = len(projects)
134
+ projects = [
135
+ raw for raw in projects
136
+ if Path(raw.get("gcc_dir", "")).resolve() != gcc_dir
137
+ ]
138
+ if len(projects) == original_len:
139
+ return False
140
+ data["governed_projects"] = projects
141
+ self._write(data)
142
+ return True
143
+
144
+
145
+ def _registry() -> CrossProjectRegistry:
146
+ return CrossProjectRegistry()
147
+
148
+
149
+ def register_project(gcc_dir: Path | str, name: str, tags: list[str] | None = None) -> GovernedProject:
150
+ """Add or update the current project in the governed project registry."""
151
+ return _registry().add(gcc_dir, name, tags)
152
+
153
+
154
+ def list_projects() -> list[GovernedProject]:
155
+ """Return all registered governed projects."""
156
+ return _registry().list()
157
+
158
+
159
+ def _project_match_score(query: str, project: GovernedProject) -> float:
160
+ """Keyword overlap score between the query and project name/tags."""
161
+ query_tokens = set(extract_keywords(query, max_keywords=20))
162
+ if not query_tokens:
163
+ return 0.0
164
+
165
+ project_tokens: set[str] = set()
166
+ project_tokens.update(extract_keywords(project.name, max_keywords=20))
167
+ for tag in project.tags:
168
+ project_tokens.update(extract_keywords(tag, max_keywords=20))
169
+ project_tokens.add(tag.lower())
170
+
171
+ if not project_tokens:
172
+ return 0.0
173
+
174
+ intersection = query_tokens & project_tokens
175
+ return len(intersection) / max(len(query_tokens), len(project_tokens))
176
+
177
+
178
+ def _load_active_learnings(gcc_dir: Path | str) -> list[Learning]:
179
+ """Load active learnings from a project's .GCC directory."""
180
+ try:
181
+ store = LearningStore(gcc_dir)
182
+ return store.list(validity="active")
183
+ except Exception as exc:
184
+ logger.warning("devtorch: failed to load learnings from %s — %s", gcc_dir, exc)
185
+ return []
186
+
187
+
188
+ def cross_project_suggest(
189
+ query: str,
190
+ current_project_root: Path | str,
191
+ top_n: int = 3,
192
+ project_match_threshold: float = 0.1,
193
+ ) -> list[Learning]:
194
+ """Find relevant learnings from sibling governed projects.
195
+
196
+ Projects are matched by keyword overlap with the query using their name and
197
+ tags. Active learnings from matched projects are loaded via ``LearningStore``
198
+ and ranked with the keyword-based ``RelevanceEngine``. The current project is
199
+ excluded so suggestions come from other governed projects.
200
+ """
201
+ current_project_root = Path(current_project_root).resolve()
202
+ registry = _registry()
203
+ projects = registry.list()
204
+
205
+ # Exclude current project to avoid suggesting its own learnings back to it.
206
+ sibling_projects = [
207
+ p for p in projects
208
+ if Path(p.project_root).resolve() != current_project_root
209
+ ]
210
+
211
+ # Match projects by name/tags.
212
+ scored_projects = [
213
+ (score, p)
214
+ for p in sibling_projects
215
+ if (score := _project_match_score(query, p)) >= project_match_threshold
216
+ ]
217
+ scored_projects.sort(key=lambda x: x[0], reverse=True)
218
+
219
+ # Collect active learnings from all matched projects.
220
+ all_learnings: list[Learning] = []
221
+ for _, project in scored_projects:
222
+ learnings = _load_active_learnings(project.gcc_dir)
223
+ all_learnings.extend(learnings)
224
+
225
+ if not all_learnings:
226
+ return []
227
+
228
+ # Rank across projects using the same keyword backend used by the facade.
229
+ engine = RelevanceEngine()
230
+ return engine.rank(query, all_learnings, top_n=top_n)
231
+
232
+
233
+ def _now_iso() -> str:
234
+ return datetime.now(timezone.utc).isoformat()
@@ -0,0 +1,209 @@
1
+ """
2
+ devtorch_core.reasoning_plus.learning.embeddings
3
+ ================================================
4
+ Pluggable embedding backends for DRPL relevance.
5
+
6
+ The default backend is keyword-based (no extra dependencies). Users can opt
7
+ into dense semantic embeddings by installing the optional ``embeddings`` extra:
8
+
9
+ pip install devtorch-core[embeddings]
10
+
11
+ and then configuring the backend:
12
+
13
+ devtorch reasoning-plus config \
14
+ --learning-embedding-backend sentence_transformers \
15
+ --learning-embedding-model all-MiniLM-L6-v2
16
+
17
+ Supported backends:
18
+ - ``keyword`` — Jaccard similarity over extracted keywords (default)
19
+ - ``sentence_transformers`` — cosine similarity over dense embeddings (local)
20
+ - ``openai`` — cosine similarity over OpenAI embeddings (remote)
21
+ """
22
+ from __future__ import annotations
23
+
24
+ import logging
25
+ import math
26
+ from abc import ABC, abstractmethod
27
+ from typing import Any
28
+
29
+ from devtorch_core.reasoning_plus.context import extract_keywords
30
+
31
+ logger = logging.getLogger("devtorch.reasoning_plus.learning")
32
+
33
+
34
+ # ---------------------------------------------------------------------------
35
+ # Base class
36
+ # ---------------------------------------------------------------------------
37
+
38
+ class EmbeddingBackend(ABC):
39
+ """Abstract embedding backend for DRPL relevance scoring."""
40
+
41
+ @abstractmethod
42
+ def encode(self, text: str) -> Any:
43
+ """Encode *text* into a backend-specific representation."""
44
+ ...
45
+
46
+ @abstractmethod
47
+ def similarity(self, a: Any, b: Any) -> float:
48
+ """Return a similarity score in [0, 1] for two encodings."""
49
+ ...
50
+
51
+ @property
52
+ @abstractmethod
53
+ def name(self) -> str:
54
+ """Short identifier for this backend."""
55
+ ...
56
+
57
+
58
+ # ---------------------------------------------------------------------------
59
+ # Keyword backend (default, no dependencies)
60
+ # ---------------------------------------------------------------------------
61
+
62
+ class KeywordEmbeddingBackend(EmbeddingBackend):
63
+ """Keyword-overlap backend that uses extracted concepts as a sparse vector."""
64
+
65
+ def __init__(self, max_keywords: int = 15) -> None:
66
+ self.max_keywords = max_keywords
67
+
68
+ def encode(self, text: str) -> set[str]:
69
+ return set(extract_keywords(text, max_keywords=self.max_keywords))
70
+
71
+ def similarity(self, a: set[str], b: set[str]) -> float:
72
+ if not a and not b:
73
+ return 0.0
74
+ overlap = len(a & b)
75
+ total = len(a | b)
76
+ return overlap / total if total else 0.0
77
+
78
+ @property
79
+ def name(self) -> str:
80
+ return "keyword"
81
+
82
+
83
+ # ---------------------------------------------------------------------------
84
+ # Dense-vector helpers
85
+ # ---------------------------------------------------------------------------
86
+
87
+ def _cosine_similarity(a: list[float], b: list[float]) -> float:
88
+ dot = sum(x * y for x, y in zip(a, b))
89
+ norm_a = math.sqrt(sum(x * x for x in a))
90
+ norm_b = math.sqrt(sum(x * x for x in b))
91
+ if norm_a == 0.0 or norm_b == 0.0:
92
+ return 0.0
93
+ return max(0.0, min(1.0, dot / (norm_a * norm_b)))
94
+
95
+
96
+ # ---------------------------------------------------------------------------
97
+ # Sentence-transformers backend (optional)
98
+ # ---------------------------------------------------------------------------
99
+
100
+ class SentenceTransformerBackend(EmbeddingBackend):
101
+ """Local dense embedding backend via sentence-transformers."""
102
+
103
+ def __init__(self, model_name: str = "all-MiniLM-L6-v2") -> None:
104
+ try:
105
+ from sentence_transformers import SentenceTransformer
106
+ except ImportError as exc:
107
+ raise ImportError(
108
+ "sentence-transformers is not installed. "
109
+ "Install the embeddings extra: pip install devtorch-core[embeddings]"
110
+ ) from exc
111
+ self._model = SentenceTransformer(model_name)
112
+ self._model_name = model_name
113
+
114
+ def encode(self, text: str) -> list[float]:
115
+ import numpy as np
116
+
117
+ vector = self._model.encode(text)
118
+ return vector.tolist()
119
+
120
+ def similarity(self, a: list[float], b: list[float]) -> float:
121
+ return _cosine_similarity(a, b)
122
+
123
+ @property
124
+ def name(self) -> str:
125
+ return "sentence_transformers"
126
+
127
+
128
+ # ---------------------------------------------------------------------------
129
+ # OpenAI backend (optional)
130
+ # ---------------------------------------------------------------------------
131
+
132
+ class OpenAIEmbeddingBackend(EmbeddingBackend):
133
+ """Remote dense embedding backend via OpenAI embeddings API."""
134
+
135
+ def __init__(
136
+ self,
137
+ model_name: str = "text-embedding-3-small",
138
+ api_key: str | None = None,
139
+ ) -> None:
140
+ try:
141
+ import openai
142
+ except ImportError as exc:
143
+ raise ImportError(
144
+ "openai is not installed. "
145
+ "Install the wrapper extra: pip install devtorch-core[wrapper]"
146
+ ) from exc
147
+ self._client = openai.OpenAI(api_key=api_key)
148
+ self._model_name = model_name
149
+
150
+ def encode(self, text: str) -> list[float]:
151
+ response = self._client.embeddings.create(
152
+ input=text,
153
+ model=self._model_name,
154
+ )
155
+ return response.data[0].embedding
156
+
157
+ def similarity(self, a: list[float], b: list[float]) -> float:
158
+ return _cosine_similarity(a, b)
159
+
160
+ @property
161
+ def name(self) -> str:
162
+ return "openai"
163
+
164
+
165
+ # ---------------------------------------------------------------------------
166
+ # Fake backend (for tests)
167
+ # ---------------------------------------------------------------------------
168
+
169
+ class FakeEmbeddingBackend(EmbeddingBackend):
170
+ """Deterministic fake backend for unit tests."""
171
+
172
+ def __init__(self, embeddings: dict[str, list[float]] | None = None, dim: int = 8) -> None:
173
+ self._embeddings: dict[str, list[float]] = embeddings or {}
174
+ self._dim = dim
175
+
176
+ def encode(self, text: str) -> list[float]:
177
+ return self._embeddings.get(text, [0.0] * self._dim)
178
+
179
+ def similarity(self, a: list[float], b: list[float]) -> float:
180
+ return _cosine_similarity(a, b)
181
+
182
+ @property
183
+ def name(self) -> str:
184
+ return "fake"
185
+
186
+
187
+ # ---------------------------------------------------------------------------
188
+ # Factory
189
+ # ---------------------------------------------------------------------------
190
+
191
+ def get_embedding_backend(
192
+ name: str | None = None,
193
+ model_name: str | None = None,
194
+ max_keywords: int = 15,
195
+ ) -> EmbeddingBackend:
196
+ """
197
+ Return an embedding backend by name.
198
+
199
+ Supported names: ``keyword`` (default), ``sentence_transformers``,
200
+ ``sentence-transformers``, ``st``, ``openai``.
201
+ """
202
+ name = (name or "keyword").lower().strip().replace("-", "_")
203
+ if name in ("keyword", "keywords"):
204
+ return KeywordEmbeddingBackend(max_keywords=max_keywords)
205
+ if name in ("sentence_transformers", "sentence_transformer", "st"):
206
+ return SentenceTransformerBackend(model_name or "all-MiniLM-L6-v2")
207
+ if name == "openai":
208
+ return OpenAIEmbeddingBackend(model_name or "text-embedding-3-small")
209
+ raise ValueError(f"Unknown embedding backend: {name}")
@@ -0,0 +1,207 @@
1
+ """
2
+ devtorch_core.reasoning_plus.learning.extractor
3
+ ===============================================
4
+ Distill ReasoningCall records into reusable Learning objects.
5
+
6
+ Two strategies are supported:
7
+
8
+ 1. **Rule-based** (default, no dependencies) — pair captured reasoning with the
9
+ observed outcome and synthesize a compact learning sentence.
10
+ 2. **LLM-based** (optional) — pass the reasoning, input, output, and outcome to
11
+ a small LLM prompt that returns a concise learning summary. The caller
12
+ supplies the LLM client, so the core library remains dependency-free.
13
+ """
14
+ from __future__ import annotations
15
+
16
+ import logging
17
+ from typing import Callable
18
+
19
+ from devtorch_core.reasoning_plus.context import extract_keywords
20
+ from devtorch_core.reasoning_plus.learning.models import Learning, ReasoningCall
21
+
22
+ logger = logging.getLogger("devtorch.reasoning_plus.learning")
23
+
24
+ DEFAULT_EXTRACTION_PROMPT = """\
25
+ You are distilling a previous reasoning step into a short, reusable insight.
26
+ Given the reasoning, the action's input, its output, and the observed outcome,
27
+ write one concise sentence that another AI agent could reuse when facing a
28
+ similar situation.
29
+
30
+ Outcome: {outcome}
31
+ Name: {name}
32
+ Input: {input}
33
+ Output: {output}
34
+ Reasoning: {reasoning}
35
+
36
+ Insight:"""
37
+
38
+
39
+ # ---------------------------------------------------------------------------
40
+ # Helpers
41
+ # ---------------------------------------------------------------------------
42
+
43
+ def _type_for_outcome(outcome: str) -> str:
44
+ if outcome == "success":
45
+ return "confirm"
46
+ if outcome == "failure":
47
+ return "correction"
48
+ if outcome == "partial":
49
+ return "insight"
50
+ return "insight"
51
+
52
+
53
+ def _extract_concepts(call: ReasoningCall) -> list[str]:
54
+ text = f"{call.reasoning} {call.input} {call.output}"
55
+ return extract_keywords(text, max_keywords=15)
56
+
57
+
58
+ def _fallback_reasoning(call: ReasoningCall) -> str:
59
+ """When the call has no captured reasoning, synthesize a short summary."""
60
+ if call.outcome not in ("success", "failure"):
61
+ return ""
62
+ name = call.name or call.call_type
63
+ if call.outcome == "failure":
64
+ return f"{name} failed. Input: {call.input}. Output: {call.output}."
65
+ return f"{name} succeeded. Input: {call.input}. Output: {call.output}."
66
+
67
+
68
+ def _compose_content_from_reasoning(call: ReasoningCall, reasoning: str) -> str:
69
+ reasoning = reasoning.strip().replace("\n", " ")
70
+ # Truncate very long reasoning
71
+ if len(reasoning) > 400:
72
+ reasoning = reasoning[:397].rstrip() + "..."
73
+
74
+ name = call.name or call.call_type
75
+ if call.outcome == "failure":
76
+ return (
77
+ f"Previous {name} attempt failed. The reasoning was: {reasoning}. "
78
+ f"Consider a different approach before trying the same path again."
79
+ )
80
+ if call.outcome == "success":
81
+ return (
82
+ f"Previous {name} attempt succeeded. The reasoning was: {reasoning}. "
83
+ f"This approach is worth reusing when the context is similar."
84
+ )
85
+ return (
86
+ f"Previous {name} attempt had outcome '{call.outcome}'. The reasoning was: {reasoning}."
87
+ )
88
+
89
+
90
+ def _build_learning(call: ReasoningCall, reasoning: str) -> Learning:
91
+ """Build a single Learning object from a reasoning summary."""
92
+ learning_type = _type_for_outcome(call.outcome)
93
+ content = _compose_content_from_reasoning(call, reasoning)
94
+ concepts = _extract_concepts(call)
95
+
96
+ # Start with moderate confidence so feedback can raise or lower it.
97
+ base_confidence = min(call.confidence, 0.8)
98
+
99
+ return Learning(
100
+ content=content,
101
+ type=learning_type,
102
+ trigger_concepts=concepts,
103
+ state_hash=call.state_hash,
104
+ confidence=base_confidence,
105
+ source_reasoning_ids=[call.id],
106
+ sensitivity=call.sensitivity,
107
+ meta={"call_type": call.call_type, "name": call.name, "outcome": call.outcome},
108
+ )
109
+
110
+
111
+ # ---------------------------------------------------------------------------
112
+ # Rule-based extractor
113
+ # ---------------------------------------------------------------------------
114
+
115
+ class LearningExtractor:
116
+ """Extract learnings from a single reasoning call using lightweight rules."""
117
+
118
+ def extract(self, call: ReasoningCall) -> list[Learning]:
119
+ """
120
+ Return one or more Learning objects derived from the call.
121
+
122
+ For now we produce a single learning that summarizes the reasoning + outcome.
123
+ """
124
+ reasoning = call.reasoning.strip()
125
+ if not reasoning:
126
+ reasoning = _fallback_reasoning(call)
127
+
128
+ if not reasoning:
129
+ return []
130
+
131
+ return [_build_learning(call, reasoning)]
132
+
133
+
134
+ # Backwards-compatible alias
135
+ RuleBasedLearningExtractor = LearningExtractor
136
+
137
+
138
+ # ---------------------------------------------------------------------------
139
+ # LLM-based extractor
140
+ # ---------------------------------------------------------------------------
141
+
142
+ class LLMBasedLearningExtractor(LearningExtractor):
143
+ """
144
+ Extract learnings by asking an LLM to summarize a reasoning call.
145
+
146
+ The caller supplies ``client``, a callable that takes a prompt string and
147
+ returns a completion string. This keeps the core library free of any LLM
148
+ SDK dependency.
149
+ """
150
+
151
+ def __init__(
152
+ self,
153
+ client: Callable[[str], str],
154
+ prompt_template: str | None = None,
155
+ ) -> None:
156
+ self._client = client
157
+ self._prompt_template = prompt_template or DEFAULT_EXTRACTION_PROMPT
158
+
159
+ def extract(self, call: ReasoningCall) -> list[Learning]:
160
+ """Use an LLM to summarize the reasoning; fall back to rule-based if the LLM fails."""
161
+ reasoning = call.reasoning.strip()
162
+ if not reasoning:
163
+ reasoning = _fallback_reasoning(call)
164
+
165
+ if not reasoning:
166
+ return []
167
+
168
+ prompt = self._prompt_template.format(
169
+ reasoning=reasoning,
170
+ input=call.input,
171
+ output=call.output,
172
+ outcome=call.outcome,
173
+ name=call.name or call.call_type,
174
+ )
175
+
176
+ try:
177
+ summary = self._client(prompt).strip()
178
+ except Exception as exc:
179
+ logger.warning("devtorch: LLM-based extraction failed — %s", exc)
180
+ return [_build_learning(call, reasoning)]
181
+
182
+ if not summary:
183
+ return []
184
+
185
+ return [_build_learning(call, summary)]
186
+
187
+
188
+ # ---------------------------------------------------------------------------
189
+ # Factory
190
+ # ---------------------------------------------------------------------------
191
+
192
+ def make_extractor(
193
+ strategy: str = "rule",
194
+ llm_client: Callable[[str], str] | None = None,
195
+ ) -> LearningExtractor:
196
+ """
197
+ Build a learning extractor for the given strategy.
198
+
199
+ Strategies:
200
+ - ``rule`` (default): lightweight rule-based extraction.
201
+ - ``llm``: LLM-based extraction; requires a ``llm_client`` callable.
202
+
203
+ If ``llm`` is requested but no client is provided, falls back to rule-based.
204
+ """
205
+ if strategy == "llm" and llm_client is not None:
206
+ return LLMBasedLearningExtractor(llm_client)
207
+ return LearningExtractor()