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.
- devtorch_core/__init__.py +158 -0
- devtorch_core/aggphi_textual.py +275 -0
- devtorch_core/alerts/__init__.py +23 -0
- devtorch_core/alerts/base.py +46 -0
- devtorch_core/alerts/config.py +60 -0
- devtorch_core/alerts/dispatcher.py +110 -0
- devtorch_core/alerts/jira.py +96 -0
- devtorch_core/alerts/linear.py +72 -0
- devtorch_core/alerts/pagerduty.py +66 -0
- devtorch_core/alerts/slack.py +81 -0
- devtorch_core/alerts/teams.py +70 -0
- devtorch_core/audit/__init__.py +43 -0
- devtorch_core/audit/exporter.py +297 -0
- devtorch_core/audit/privacy.py +101 -0
- devtorch_core/audit/scrubber.py +149 -0
- devtorch_core/audit/service.py +67 -0
- devtorch_core/audit/signing.py +127 -0
- devtorch_core/broadcast/__init__.py +4 -0
- devtorch_core/broadcast/broadcaster.py +100 -0
- devtorch_core/broadcast/watcher.py +71 -0
- devtorch_core/capability.py +639 -0
- devtorch_core/cloud/__init__.py +1 -0
- devtorch_core/cloud/client_config.py +472 -0
- devtorch_core/cloud/client_configs/.claude-opencode-fallback.json +8 -0
- devtorch_core/cloud/client_configs/.claude-stdio.json +13 -0
- devtorch_core/cloud/client_configs/.cursor-mcp.json +13 -0
- devtorch_core/cloud/client_configs/.opencode-bridge.json +13 -0
- devtorch_core/cloud/client_configs/.opencode.json +15 -0
- devtorch_core/cloud/client_configs/.vscode-mcp.json +13 -0
- devtorch_core/cloud/devtorch-mcp-bridge.js +357 -0
- devtorch_core/cloud/mcp_client.py +229 -0
- devtorch_core/cloud/setup.py +144 -0
- devtorch_core/cloud/sync.py +143 -0
- devtorch_core/cloud/sync_bundle.py +603 -0
- devtorch_core/cloud/sync_conflicts.py +159 -0
- devtorch_core/cloud/sync_state.py +159 -0
- devtorch_core/cloud/team_sync.py +283 -0
- devtorch_core/codex/__init__.py +9 -0
- devtorch_core/codex/__main__.py +97 -0
- devtorch_core/codex/capture.py +208 -0
- devtorch_core/codex/proxy.py +412 -0
- devtorch_core/concept_catalog.py +209 -0
- devtorch_core/consolidation/__init__.py +3 -0
- devtorch_core/consolidation/synthesizer.py +87 -0
- devtorch_core/consolidation/workflow.py +175 -0
- devtorch_core/daemon/__init__.py +27 -0
- devtorch_core/daemon/supervisor.py +293 -0
- devtorch_core/daemon/watcher.py +244 -0
- devtorch_core/dashboard_api.py +2012 -0
- devtorch_core/deltaf.py +97 -0
- devtorch_core/disclosure.py +50 -0
- devtorch_core/divergence/__init__.py +3 -0
- devtorch_core/divergence/detector.py +166 -0
- devtorch_core/gateway/__init__.py +32 -0
- devtorch_core/gateway/key_manager.py +124 -0
- devtorch_core/gateway/metrics_webhook.py +252 -0
- devtorch_core/gateway/policy.py +262 -0
- devtorch_core/gateway/server.py +727 -0
- devtorch_core/gateway/sso.py +233 -0
- devtorch_core/gcc.py +1246 -0
- devtorch_core/github/__init__.py +35 -0
- devtorch_core/github/app.py +240 -0
- devtorch_core/github/comment_builder.py +113 -0
- devtorch_core/github/pat.py +76 -0
- devtorch_core/github/pr_parser.py +82 -0
- devtorch_core/github/pr_reporter.py +555 -0
- devtorch_core/gitlab/__init__.py +177 -0
- devtorch_core/hitl/__init__.py +4 -0
- devtorch_core/hitl/channels.py +129 -0
- devtorch_core/hitl/orchestrator.py +95 -0
- devtorch_core/hooks/__init__.py +17 -0
- devtorch_core/hooks/claude_code.py +228 -0
- devtorch_core/hooks/git_capture.py +341 -0
- devtorch_core/hooks/git_commit.py +182 -0
- devtorch_core/hooks/installer.py +733 -0
- devtorch_core/hooks/pre_commit.py +157 -0
- devtorch_core/hooks/runner.py +344 -0
- devtorch_core/identity/__init__.py +4 -0
- devtorch_core/identity/agent.py +86 -0
- devtorch_core/identity/providers.py +85 -0
- devtorch_core/invariants.py +182 -0
- devtorch_core/mcp/__init__.py +10 -0
- devtorch_core/mcp/auth.py +177 -0
- devtorch_core/mcp/server.py +1049 -0
- devtorch_core/metrics/__init__.py +35 -0
- devtorch_core/metrics/aggregate.py +215 -0
- devtorch_core/metrics/calibrate.py +198 -0
- devtorch_core/metrics/calibration.py +125 -0
- devtorch_core/metrics/credibility.py +288 -0
- devtorch_core/metrics/delivery_time.py +70 -0
- devtorch_core/metrics/dhs.py +126 -0
- devtorch_core/metrics/mcs.py +96 -0
- devtorch_core/metrics/roi.py +88 -0
- devtorch_core/metrics/session_writer.py +81 -0
- devtorch_core/metrics/shadow_ai.py +117 -0
- devtorch_core/metrics/sprint_writer.py +243 -0
- devtorch_core/observability/__init__.py +78 -0
- devtorch_core/observability/datadog.py +157 -0
- devtorch_core/observability/formatter.py +119 -0
- devtorch_core/observability/report.py +264 -0
- devtorch_core/observability/servicenow.py +147 -0
- devtorch_core/observability/splunk.py +218 -0
- devtorch_core/observability/webhook.py +227 -0
- devtorch_core/parser/__init__.py +30 -0
- devtorch_core/parser/blocks.py +216 -0
- devtorch_core/parser/inference.py +159 -0
- devtorch_core/parser/thinking.py +112 -0
- devtorch_core/projects.py +169 -0
- devtorch_core/prompt_artifact.py +76 -0
- devtorch_core/proxy/__init__.py +9 -0
- devtorch_core/proxy/routes/__init__.py +1 -0
- devtorch_core/proxy/routes/anthropic.py +264 -0
- devtorch_core/proxy/routes/azure_openai.py +336 -0
- devtorch_core/proxy/routes/gemini.py +331 -0
- devtorch_core/proxy/routes/groq.py +284 -0
- devtorch_core/proxy/routes/ollama.py +279 -0
- devtorch_core/proxy/routes/openai.py +287 -0
- devtorch_core/proxy/server.py +356 -0
- devtorch_core/query/__init__.py +15 -0
- devtorch_core/query/grep.py +181 -0
- devtorch_core/query/hybrid.py +86 -0
- devtorch_core/query/semantic.py +157 -0
- devtorch_core/rdp.py +105 -0
- devtorch_core/reasoning/__init__.py +4 -0
- devtorch_core/reasoning/entry.py +31 -0
- devtorch_core/reasoning/store.py +122 -0
- devtorch_core/reasoning_plus/__init__.py +70 -0
- devtorch_core/reasoning_plus/augmenter.py +326 -0
- devtorch_core/reasoning_plus/capture.py +51 -0
- devtorch_core/reasoning_plus/config.py +256 -0
- devtorch_core/reasoning_plus/context.py +262 -0
- devtorch_core/reasoning_plus/learning/__init__.py +72 -0
- devtorch_core/reasoning_plus/learning/analytics.py +141 -0
- devtorch_core/reasoning_plus/learning/api.py +313 -0
- devtorch_core/reasoning_plus/learning/chain.py +285 -0
- devtorch_core/reasoning_plus/learning/composer.py +74 -0
- devtorch_core/reasoning_plus/learning/cross_project.py +234 -0
- devtorch_core/reasoning_plus/learning/embeddings.py +209 -0
- devtorch_core/reasoning_plus/learning/extractor.py +207 -0
- devtorch_core/reasoning_plus/learning/models.py +116 -0
- devtorch_core/reasoning_plus/learning/provenance.py +126 -0
- devtorch_core/reasoning_plus/learning/recorder.py +81 -0
- devtorch_core/reasoning_plus/learning/relevance.py +122 -0
- devtorch_core/reasoning_plus/learning/state.py +86 -0
- devtorch_core/reasoning_plus/learning/store.py +160 -0
- devtorch_core/reasoning_plus/learning/theta_learning_bridge.py +94 -0
- devtorch_core/reasoning_plus/prompt.py +90 -0
- devtorch_core/rep.py +134 -0
- devtorch_core/rep_network/__init__.py +25 -0
- devtorch_core/rep_network/merge.py +70 -0
- devtorch_core/rep_network/node.py +137 -0
- devtorch_core/rep_network/server.py +140 -0
- devtorch_core/rep_network/sync.py +207 -0
- devtorch_core/sensitivity.py +182 -0
- devtorch_core/serve.py +258 -0
- devtorch_core/session/__init__.py +39 -0
- devtorch_core/session/disagreement.py +188 -0
- devtorch_core/session/models.py +114 -0
- devtorch_core/session/orchestrator.py +182 -0
- devtorch_core/session/planner.py +169 -0
- devtorch_core/session/simulator.py +132 -0
- devtorch_core/signing.py +290 -0
- devtorch_core/sis.py +197 -0
- devtorch_core/storage.py +308 -0
- devtorch_core/templates/__init__.py +6 -0
- devtorch_core/templates/engine.py +122 -0
- devtorch_core/templates/go.py +18 -0
- devtorch_core/templates/infra.py +19 -0
- devtorch_core/templates/library/__init__.py +18 -0
- devtorch_core/templates/library/api_design.md +27 -0
- devtorch_core/templates/library/bug_fix.md +27 -0
- devtorch_core/templates/library/decision_record.md +27 -0
- devtorch_core/templates/library/engine.py +228 -0
- devtorch_core/templates/library/security_review.md +30 -0
- devtorch_core/templates/python.py +19 -0
- devtorch_core/templates/react.py +18 -0
- devtorch_core/templates/typescript.py +18 -0
- devtorch_core/theta.py +221 -0
- devtorch_core/theta_synthesis.py +268 -0
- devtorch_core/topics.py +320 -0
- devtorch_core/variance.py +219 -0
- devtorch_core/wrapper/__init__.py +52 -0
- devtorch_core/wrapper/anthropic.py +487 -0
- devtorch_core/wrapper/base.py +562 -0
- devtorch_core/wrapper/bedrock.py +342 -0
- devtorch_core/wrapper/gemini.py +422 -0
- devtorch_core/wrapper/ollama.py +527 -0
- devtorch_core/wrapper/openai.py +461 -0
- devtorch_core-3.0.1.dist-info/METADATA +867 -0
- devtorch_core-3.0.1.dist-info/RECORD +193 -0
- devtorch_core-3.0.1.dist-info/WHEEL +5 -0
- devtorch_core-3.0.1.dist-info/entry_points.txt +2 -0
- devtorch_core-3.0.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
"""
|
|
2
|
+
devtorch_core.reasoning_plus.context
|
|
3
|
+
====================================
|
|
4
|
+
Generic smart workspace context retrieval.
|
|
5
|
+
|
|
6
|
+
Lifts and generalizes the helpers originally in benchmarks/swe_bench/context_utils.py
|
|
7
|
+
so they can be used by any DevTorch wrapper, not just the SWE-Bench runner.
|
|
8
|
+
|
|
9
|
+
The functions are defensive: if `git` is missing, the directory is not a repo, or a
|
|
10
|
+
subprocess call fails, they return empty results rather than raising.
|
|
11
|
+
"""
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import re
|
|
15
|
+
import subprocess
|
|
16
|
+
from collections import Counter
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
from typing import Iterable
|
|
19
|
+
|
|
20
|
+
# Common English words to ignore when extracting keywords from user prompt text.
|
|
21
|
+
_STOPWORDS = {
|
|
22
|
+
"a", "an", "the", "and", "or", "but", "is", "are", "was", "were",
|
|
23
|
+
"be", "been", "being", "to", "of", "in", "for", "on", "at", "by",
|
|
24
|
+
"with", "from", "as", "it", "its", "this", "that", "these", "those",
|
|
25
|
+
"i", "you", "he", "she", "we", "they", "my", "your", "his", "her",
|
|
26
|
+
"our", "their", "will", "would", "could", "should", "may", "might",
|
|
27
|
+
"can", "must", "shall", "have", "has", "had", "do", "does", "did",
|
|
28
|
+
"not", "no", "yes", "if", "then", "else", "when", "where", "why",
|
|
29
|
+
"how", "what", "which", "who", "whom", "whose", "all", "any", "both",
|
|
30
|
+
"each", "every", "few", "more", "most", "other", "some", "such",
|
|
31
|
+
"only", "own", "same", "so", "than", "too", "very", "just", "also",
|
|
32
|
+
"now", "here", "there", "up", "out", "down", "over", "under", "again",
|
|
33
|
+
"further", "once", "about", "before", "after", "above", "below",
|
|
34
|
+
"between", "into", "through", "during", "before", "after", "until",
|
|
35
|
+
"while", "because", "although", "though", "unless", "since", "although",
|
|
36
|
+
"however", "therefore", "thus", "hence", "moreover", "furthermore",
|
|
37
|
+
"nevertheless", "nonetheless", "otherwise", "instead", "meanwhile",
|
|
38
|
+
"otherwise", "nevertheless", "nonetheless", "additionally", "consequently",
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
# Default file patterns to search. Overrideable by callers.
|
|
42
|
+
DEFAULT_INCLUDE_GLOBS = ("*.py", "*.js", "*.ts", "*.tsx", "*.java", "*.kt", "*.go",
|
|
43
|
+
"*.rs", "*.cpp", "*.c", "*.h", "*.hpp", "*.md", "*.yaml",
|
|
44
|
+
"*.yml", "*.json", "*.toml")
|
|
45
|
+
|
|
46
|
+
# Default exclude globs. Overrideable by callers.
|
|
47
|
+
DEFAULT_EXCLUDE_GLOBS = ("*test*", "*tests*", "*__pycache__*", "*.min.js",
|
|
48
|
+
"*.bundle.js", "node_modules/*", "dist/*", "build/*",
|
|
49
|
+
"*.lock", "*.svg", "*.png", "*.jpg")
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def extract_keywords(text: str, max_keywords: int = 15) -> list[str]:
|
|
53
|
+
"""
|
|
54
|
+
Extract technical keywords from prompt text.
|
|
55
|
+
|
|
56
|
+
Strategy:
|
|
57
|
+
- Keep code-like identifiers: snake_case, camelCase, dotted.module.paths.
|
|
58
|
+
- Keep words that contain digits or non-alphabetic characters.
|
|
59
|
+
- Drop common English stopwords and very short words.
|
|
60
|
+
- Return the most frequent technical terms, deduplicated.
|
|
61
|
+
"""
|
|
62
|
+
if not text:
|
|
63
|
+
return []
|
|
64
|
+
|
|
65
|
+
tokens = re.findall(r"[a-zA-Z_][a-zA-Z0-9_]*(?:\.[a-zA-Z_][a-zA-Z0-9_]*)*", text)
|
|
66
|
+
|
|
67
|
+
candidates = []
|
|
68
|
+
for tok in tokens:
|
|
69
|
+
t = tok.lower()
|
|
70
|
+
if len(t) < 2:
|
|
71
|
+
continue
|
|
72
|
+
if t in _STOPWORDS:
|
|
73
|
+
continue
|
|
74
|
+
if (
|
|
75
|
+
"_" in t
|
|
76
|
+
or any(c.isdigit() for c in t)
|
|
77
|
+
or any(c.isupper() for c in tok)
|
|
78
|
+
or "." in t
|
|
79
|
+
):
|
|
80
|
+
candidates.append(t)
|
|
81
|
+
elif len(t) > 4:
|
|
82
|
+
candidates.append(t)
|
|
83
|
+
|
|
84
|
+
counts = Counter(candidates)
|
|
85
|
+
ranked = [t for t, _ in counts.most_common(max_keywords * 2)]
|
|
86
|
+
seen: set[str] = set()
|
|
87
|
+
out: list[str] = []
|
|
88
|
+
for t in ranked:
|
|
89
|
+
if t not in seen:
|
|
90
|
+
seen.add(t)
|
|
91
|
+
out.append(t)
|
|
92
|
+
if len(out) >= max_keywords:
|
|
93
|
+
break
|
|
94
|
+
return out
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def search_repo(
|
|
98
|
+
repo_path: Path,
|
|
99
|
+
keywords: list[str],
|
|
100
|
+
base_commit: str | None = None,
|
|
101
|
+
top_n: int = 3,
|
|
102
|
+
max_matches_per_keyword: int = 20,
|
|
103
|
+
include_globs: Iterable[str] = (),
|
|
104
|
+
exclude_globs: Iterable[str] = (),
|
|
105
|
+
) -> list[str]:
|
|
106
|
+
"""
|
|
107
|
+
Search a git repo for files related to the given keywords.
|
|
108
|
+
|
|
109
|
+
Returns up to `top_n` file paths ranked by keyword hit count.
|
|
110
|
+
Returns empty list if the path is not a git repo or `git` is unavailable.
|
|
111
|
+
"""
|
|
112
|
+
if not keywords or not repo_path.exists():
|
|
113
|
+
return []
|
|
114
|
+
|
|
115
|
+
include_globs = tuple(include_globs) or DEFAULT_INCLUDE_GLOBS
|
|
116
|
+
exclude_globs = tuple(exclude_globs) or DEFAULT_EXCLUDE_GLOBS
|
|
117
|
+
|
|
118
|
+
file_scores: Counter[str] = Counter()
|
|
119
|
+
|
|
120
|
+
for kw in keywords:
|
|
121
|
+
try:
|
|
122
|
+
pathspecs = ["--", *include_globs]
|
|
123
|
+
cmd = [
|
|
124
|
+
"git", "grep", "-l", "-i", "-E", re.escape(kw),
|
|
125
|
+
*pathspecs,
|
|
126
|
+
]
|
|
127
|
+
if base_commit:
|
|
128
|
+
cmd = ["git", "grep", "-l", "-i", "-E", re.escape(kw), base_commit, "--", *include_globs]
|
|
129
|
+
|
|
130
|
+
result = subprocess.run(
|
|
131
|
+
cmd,
|
|
132
|
+
cwd=repo_path,
|
|
133
|
+
capture_output=True,
|
|
134
|
+
text=True,
|
|
135
|
+
errors="ignore",
|
|
136
|
+
timeout=30,
|
|
137
|
+
)
|
|
138
|
+
if result.returncode != 0:
|
|
139
|
+
continue
|
|
140
|
+
files = result.stdout.strip().splitlines()
|
|
141
|
+
for fp in files[:max_matches_per_keyword]:
|
|
142
|
+
if _is_excluded(fp, exclude_globs):
|
|
143
|
+
continue
|
|
144
|
+
file_scores[fp] += 1
|
|
145
|
+
except Exception:
|
|
146
|
+
continue
|
|
147
|
+
|
|
148
|
+
ranked = [fp for fp, _ in file_scores.most_common()]
|
|
149
|
+
return ranked[:top_n]
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def read_files_at_commit(
|
|
153
|
+
repo_path: Path,
|
|
154
|
+
file_paths: list[str],
|
|
155
|
+
commit: str | None = None,
|
|
156
|
+
max_lines: int = 150,
|
|
157
|
+
) -> str:
|
|
158
|
+
"""
|
|
159
|
+
Read file contents at a specific commit, capped per file.
|
|
160
|
+
|
|
161
|
+
If `commit` is None, reads from the working tree.
|
|
162
|
+
"""
|
|
163
|
+
chunks = []
|
|
164
|
+
for fp in file_paths:
|
|
165
|
+
try:
|
|
166
|
+
if commit:
|
|
167
|
+
result = subprocess.run(
|
|
168
|
+
["git", "show", f"{commit}:{fp}"],
|
|
169
|
+
cwd=repo_path,
|
|
170
|
+
capture_output=True,
|
|
171
|
+
text=True,
|
|
172
|
+
errors="ignore",
|
|
173
|
+
timeout=10,
|
|
174
|
+
)
|
|
175
|
+
if result.returncode != 0:
|
|
176
|
+
continue
|
|
177
|
+
text = result.stdout
|
|
178
|
+
else:
|
|
179
|
+
target = repo_path / fp
|
|
180
|
+
if not target.exists():
|
|
181
|
+
continue
|
|
182
|
+
text = target.read_text(encoding="utf-8", errors="ignore")
|
|
183
|
+
except Exception:
|
|
184
|
+
continue
|
|
185
|
+
|
|
186
|
+
lines = text.splitlines()
|
|
187
|
+
if len(lines) > max_lines:
|
|
188
|
+
text = "\n".join(lines[:max_lines]) + f"\n\n... [{len(lines) - max_lines} lines truncated]\n"
|
|
189
|
+
chunks.append(f"### File: {fp}\n```\n{text}\n```\n")
|
|
190
|
+
return "\n".join(chunks)
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def gather_smart_context(
|
|
194
|
+
repo_path: Path | str,
|
|
195
|
+
prompt_text: str,
|
|
196
|
+
*,
|
|
197
|
+
base_commit: str | None = None,
|
|
198
|
+
top_n: int = 3,
|
|
199
|
+
max_lines: int = 150,
|
|
200
|
+
max_keywords: int = 15,
|
|
201
|
+
include_globs: Iterable[str] = (),
|
|
202
|
+
exclude_globs: Iterable[str] = (),
|
|
203
|
+
) -> tuple[list[str], str]:
|
|
204
|
+
"""
|
|
205
|
+
End-to-end smart context: extract keywords, search repo, read files.
|
|
206
|
+
|
|
207
|
+
Returns a tuple of (file_paths, contents_markdown).
|
|
208
|
+
"""
|
|
209
|
+
repo_path = Path(repo_path)
|
|
210
|
+
if not _is_git_repo(repo_path):
|
|
211
|
+
return [], ""
|
|
212
|
+
|
|
213
|
+
keywords = extract_keywords(prompt_text, max_keywords=max_keywords)
|
|
214
|
+
if not keywords:
|
|
215
|
+
return [], ""
|
|
216
|
+
|
|
217
|
+
extra_paths = search_repo(
|
|
218
|
+
repo_path,
|
|
219
|
+
keywords,
|
|
220
|
+
base_commit=base_commit,
|
|
221
|
+
top_n=top_n,
|
|
222
|
+
include_globs=include_globs,
|
|
223
|
+
exclude_globs=exclude_globs,
|
|
224
|
+
)
|
|
225
|
+
if not extra_paths:
|
|
226
|
+
return [], ""
|
|
227
|
+
|
|
228
|
+
contents = read_files_at_commit(
|
|
229
|
+
repo_path,
|
|
230
|
+
extra_paths,
|
|
231
|
+
commit=base_commit,
|
|
232
|
+
max_lines=max_lines,
|
|
233
|
+
)
|
|
234
|
+
return extra_paths, contents
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def _is_git_repo(path: Path) -> bool:
|
|
238
|
+
"""Return True if `path` is inside a git repository."""
|
|
239
|
+
try:
|
|
240
|
+
result = subprocess.run(
|
|
241
|
+
["git", "rev-parse", "--git-dir"],
|
|
242
|
+
cwd=path,
|
|
243
|
+
capture_output=True,
|
|
244
|
+
text=True,
|
|
245
|
+
errors="ignore",
|
|
246
|
+
timeout=5,
|
|
247
|
+
)
|
|
248
|
+
return result.returncode == 0 and result.stdout.strip() != ""
|
|
249
|
+
except Exception:
|
|
250
|
+
return False
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def _is_excluded(file_path: str, exclude_globs: Iterable[str]) -> bool:
|
|
254
|
+
"""Simple glob check; any glob that is a substring/endswith match."""
|
|
255
|
+
for g in exclude_globs:
|
|
256
|
+
if g.endswith("*"):
|
|
257
|
+
prefix = g[:-1]
|
|
258
|
+
if file_path.startswith(prefix) or f"/{prefix}" in file_path:
|
|
259
|
+
return True
|
|
260
|
+
if g in file_path:
|
|
261
|
+
return True
|
|
262
|
+
return False
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"""
|
|
2
|
+
devtorch_core.reasoning_plus.learning
|
|
3
|
+
=====================================
|
|
4
|
+
Reasoning Plus Learning (DRPL): capture, distill, and reuse reasoning
|
|
5
|
+
across LLM, tool, and memory calls.
|
|
6
|
+
|
|
7
|
+
Public API:
|
|
8
|
+
ReasoningPlusLearning — high-level facade
|
|
9
|
+
ReasoningCall — dataclass for a recorded call
|
|
10
|
+
Learning — dataclass for a distilled learning
|
|
11
|
+
CallRecorder — persist calls to .GCC/
|
|
12
|
+
LearningStore — persist learnings to .GCC/
|
|
13
|
+
LearningExtractor — derive learnings from calls
|
|
14
|
+
RelevanceEngine — rank learnings by relevance
|
|
15
|
+
PromptComposer — format learnings for prompts
|
|
16
|
+
workspace_state_hash — compute a state hash for staleness
|
|
17
|
+
cross_project_suggest — suggest learnings from sibling governed projects
|
|
18
|
+
register_project — register a project for cross-project learning
|
|
19
|
+
list_projects — list registered governed projects
|
|
20
|
+
"""
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
from devtorch_core.reasoning_plus.learning.api import ReasoningPlusLearning
|
|
24
|
+
from devtorch_core.reasoning_plus.learning.composer import PromptComposer
|
|
25
|
+
from devtorch_core.reasoning_plus.learning.cross_project import (
|
|
26
|
+
CrossProjectRegistry,
|
|
27
|
+
GovernedProject,
|
|
28
|
+
cross_project_suggest,
|
|
29
|
+
list_projects,
|
|
30
|
+
register_project,
|
|
31
|
+
)
|
|
32
|
+
from devtorch_core.reasoning_plus.learning.extractor import LearningExtractor
|
|
33
|
+
from devtorch_core.reasoning_plus.learning.models import Learning, ReasoningCall
|
|
34
|
+
from devtorch_core.reasoning_plus.learning.recorder import CallRecorder
|
|
35
|
+
from devtorch_core.reasoning_plus.learning.relevance import RelevanceEngine
|
|
36
|
+
from devtorch_core.reasoning_plus.learning.state import workspace_state_hash
|
|
37
|
+
from devtorch_core.reasoning_plus.learning.store import LearningStore
|
|
38
|
+
from devtorch_core.reasoning_plus.learning.analytics import (
|
|
39
|
+
concept_stats,
|
|
40
|
+
danger_zones,
|
|
41
|
+
concept_health_section,
|
|
42
|
+
)
|
|
43
|
+
from devtorch_core.reasoning_plus.learning.theta_learning_bridge import (
|
|
44
|
+
sync_learnings_to_theta,
|
|
45
|
+
)
|
|
46
|
+
from devtorch_core.reasoning_plus.learning.chain import (
|
|
47
|
+
build_chain,
|
|
48
|
+
format_chains,
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
__all__ = [
|
|
52
|
+
"ReasoningPlusLearning",
|
|
53
|
+
"ReasoningCall",
|
|
54
|
+
"Learning",
|
|
55
|
+
"CallRecorder",
|
|
56
|
+
"LearningStore",
|
|
57
|
+
"LearningExtractor",
|
|
58
|
+
"RelevanceEngine",
|
|
59
|
+
"PromptComposer",
|
|
60
|
+
"workspace_state_hash",
|
|
61
|
+
"cross_project_suggest",
|
|
62
|
+
"register_project",
|
|
63
|
+
"list_projects",
|
|
64
|
+
"CrossProjectRegistry",
|
|
65
|
+
"GovernedProject",
|
|
66
|
+
"concept_stats",
|
|
67
|
+
"danger_zones",
|
|
68
|
+
"concept_health_section",
|
|
69
|
+
"sync_learnings_to_theta",
|
|
70
|
+
"build_chain",
|
|
71
|
+
"format_chains",
|
|
72
|
+
]
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
"""
|
|
2
|
+
devtorch_core.reasoning_plus.learning.analytics
|
|
3
|
+
================================================
|
|
4
|
+
Per-concept learning analytics: stats, danger-zone detection, health dashboard.
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import logging
|
|
9
|
+
from collections import Counter
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
from devtorch_core.reasoning_plus.learning.store import LearningStore
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger("devtorch.reasoning_plus.learning")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def concept_stats(store: LearningStore) -> dict[str, dict[str, Any]]:
|
|
18
|
+
"""Return per-concept statistics aggregated from all stored learnings.
|
|
19
|
+
|
|
20
|
+
Returns a dict keyed by lowercased concept name with:
|
|
21
|
+
- total: total learnings tagged with this concept
|
|
22
|
+
- active: learnings with validity="active"
|
|
23
|
+
- stale: learnings with validity="stale"
|
|
24
|
+
- deprecated: learnings with validity="deprecated"
|
|
25
|
+
- by_type: Counter of learning types (insight, correction, pattern, avoid, confirm)
|
|
26
|
+
- by_outcome: Counter of outcomes among source calls (success, failure, partial, unknown)
|
|
27
|
+
- avg_confidence: mean confidence across all learnings for this concept
|
|
28
|
+
- success_rate: fraction of (non-unknown) outcomes that are "success"
|
|
29
|
+
"""
|
|
30
|
+
all_learnings = store.list()
|
|
31
|
+
concept_map: dict[str, dict[str, Any]] = {}
|
|
32
|
+
|
|
33
|
+
for learning in all_learnings:
|
|
34
|
+
concepts = learning.trigger_concepts
|
|
35
|
+
if not concepts:
|
|
36
|
+
concepts = ["__untyped__"]
|
|
37
|
+
for concept in concepts:
|
|
38
|
+
cname = concept.lower()
|
|
39
|
+
if cname not in concept_map:
|
|
40
|
+
concept_map[cname] = {
|
|
41
|
+
"total": 0,
|
|
42
|
+
"active": 0,
|
|
43
|
+
"stale": 0,
|
|
44
|
+
"deprecated": 0,
|
|
45
|
+
"by_type": Counter(),
|
|
46
|
+
"by_outcome": Counter(),
|
|
47
|
+
"confidences": [],
|
|
48
|
+
}
|
|
49
|
+
concept_map[cname]["total"] += 1
|
|
50
|
+
concept_map[cname][learning.validity] += 1
|
|
51
|
+
concept_map[cname]["by_type"][learning.type] += 1
|
|
52
|
+
concept_map[cname]["confidences"].append(learning.confidence)
|
|
53
|
+
|
|
54
|
+
for cname, data in concept_map.items():
|
|
55
|
+
confs = data.pop("confidences")
|
|
56
|
+
data["avg_confidence"] = sum(confs) / len(confs) if confs else 0.0
|
|
57
|
+
data["success_rate"] = _compute_success_rate(store, cname)
|
|
58
|
+
|
|
59
|
+
return dict(sorted(concept_map.items()))
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _compute_success_rate(store: LearningStore, concept: str) -> float:
|
|
63
|
+
"""Query call records for the given concept and compute success fraction."""
|
|
64
|
+
from devtorch_core.reasoning_plus.learning.models import ReasoningCall
|
|
65
|
+
from devtorch_core.reasoning_plus.learning.recorder import CallRecorder
|
|
66
|
+
|
|
67
|
+
recorder = CallRecorder(store._gcc_dir)
|
|
68
|
+
calls = recorder.list()
|
|
69
|
+
matched = [c for c in calls if concept in {co.lower() for co in c.concepts}]
|
|
70
|
+
non_unknown = [c for c in matched if c.outcome not in ("unknown", "")]
|
|
71
|
+
if not non_unknown:
|
|
72
|
+
return 0.0
|
|
73
|
+
successes = sum(1 for c in non_unknown if c.outcome == "success")
|
|
74
|
+
return successes / len(non_unknown)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def danger_zones(
|
|
78
|
+
store: LearningStore,
|
|
79
|
+
threshold: float = 0.4,
|
|
80
|
+
min_samples: int = 3,
|
|
81
|
+
) -> list[dict[str, Any]]:
|
|
82
|
+
"""Return concepts with success rate below *threshold*.
|
|
83
|
+
|
|
84
|
+
Each entry contains:
|
|
85
|
+
- concept: concept name
|
|
86
|
+
- success_rate: computed success fraction
|
|
87
|
+
- total_learnings: total learnings for this concept
|
|
88
|
+
- avg_confidence: mean learning confidence
|
|
89
|
+
Only concepts with at least *min_samples* total learnings are considered.
|
|
90
|
+
"""
|
|
91
|
+
stats = concept_stats(store)
|
|
92
|
+
zones: list[dict[str, Any]] = []
|
|
93
|
+
for cname, data in stats.items():
|
|
94
|
+
if cname == "__untyped__":
|
|
95
|
+
continue
|
|
96
|
+
if data["total"] < min_samples:
|
|
97
|
+
continue
|
|
98
|
+
if data["success_rate"] < threshold:
|
|
99
|
+
zones.append({
|
|
100
|
+
"concept": cname,
|
|
101
|
+
"success_rate": data["success_rate"],
|
|
102
|
+
"total_learnings": data["total"],
|
|
103
|
+
"avg_confidence": data["avg_confidence"],
|
|
104
|
+
})
|
|
105
|
+
zones.sort(key=lambda x: x["success_rate"])
|
|
106
|
+
return zones
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def concept_health_section(store: LearningStore, threshold: float = 0.4) -> str:
|
|
110
|
+
"""Return a formatted concept health block for the ``devtorch today`` dashboard."""
|
|
111
|
+
stats = concept_stats(store)
|
|
112
|
+
if not stats:
|
|
113
|
+
return ""
|
|
114
|
+
|
|
115
|
+
lines = ["─── Concept health ───"]
|
|
116
|
+
|
|
117
|
+
zones = danger_zones(store, threshold=threshold)
|
|
118
|
+
if zones:
|
|
119
|
+
lines.append(f" \u26a0 DANGER ZONES (success rate < {threshold:.0%}):")
|
|
120
|
+
for z in zones[:5]:
|
|
121
|
+
lines.append(
|
|
122
|
+
f" {z['concept']}: success {z['success_rate']:.0%}, "
|
|
123
|
+
f"{z['total_learnings']} learning(s), "
|
|
124
|
+
f"avg confidence {z['avg_confidence']:.2f}"
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
healthy = [(c, d) for c, d in stats.items()
|
|
128
|
+
if c != "__untyped__" and d["success_rate"] >= threshold]
|
|
129
|
+
if healthy:
|
|
130
|
+
lines.append(f" Healthy concepts ({len(healthy)}):")
|
|
131
|
+
for c, d in sorted(healthy, key=lambda x: x[1]["success_rate"], reverse=True)[:8]:
|
|
132
|
+
lines.append(
|
|
133
|
+
f" {c}: {d['active']} active, "
|
|
134
|
+
f"{d['success_rate']:.0%} success"
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
untyped = stats.get("__untyped__")
|
|
138
|
+
if untyped and untyped["total"] > 0:
|
|
139
|
+
lines.append(f" Untagged learnings: {untyped['total']}")
|
|
140
|
+
|
|
141
|
+
return "\n".join(lines)
|