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,326 @@
|
|
|
1
|
+
"""
|
|
2
|
+
devtorch_core.reasoning_plus.augmenter
|
|
3
|
+
======================================
|
|
4
|
+
High-level Reasoning Plus orchestrator.
|
|
5
|
+
|
|
6
|
+
- Decides whether to augment based on config and workspace state.
|
|
7
|
+
- Builds the augmented system prompt (RACP + smart context + reasoning directive).
|
|
8
|
+
- Captures inline <thinking> blocks from the response and strips them before returning.
|
|
9
|
+
"""
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import hashlib
|
|
13
|
+
import logging
|
|
14
|
+
import time
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from typing import Any
|
|
17
|
+
|
|
18
|
+
from devtorch_core.reasoning_plus.capture import (
|
|
19
|
+
commit_reasoning,
|
|
20
|
+
extract_inline_thinking,
|
|
21
|
+
strip_inline_thinking,
|
|
22
|
+
)
|
|
23
|
+
from devtorch_core.reasoning_plus.config import ReasoningPlusConfig, SettingsStore
|
|
24
|
+
from devtorch_core.reasoning_plus.context import gather_smart_context
|
|
25
|
+
from devtorch_core.reasoning_plus.learning import ReasoningPlusLearning
|
|
26
|
+
from devtorch_core.reasoning_plus.prompt import build_reasoning_plus_system_prompt
|
|
27
|
+
|
|
28
|
+
logger = logging.getLogger("devtorch.reasoning_plus")
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class ReasoningPlusAugmenter:
|
|
32
|
+
"""
|
|
33
|
+
Per-repo augmenter. Caches smart context and config for performance.
|
|
34
|
+
|
|
35
|
+
Usage:
|
|
36
|
+
augmenter = ReasoningPlusAugmenter(repo_path)
|
|
37
|
+
if augmenter.should_augment():
|
|
38
|
+
system_prompt = augmenter.build_system_prompt(base_prompt, user_text)
|
|
39
|
+
clean_text = augmenter.capture_and_strip(raw_text, session_id, user_text)
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
def __init__(
|
|
43
|
+
self,
|
|
44
|
+
repo_path: Path | str,
|
|
45
|
+
gcc_repo: Any | None = None,
|
|
46
|
+
config: ReasoningPlusConfig | None = None,
|
|
47
|
+
) -> None:
|
|
48
|
+
self._repo_path = Path(repo_path)
|
|
49
|
+
self._gcc_repo = gcc_repo
|
|
50
|
+
|
|
51
|
+
# If a gcc_repo object is supplied, prefer its gcc_dir for settings.
|
|
52
|
+
gcc_dir = getattr(gcc_repo, "gcc_dir", None)
|
|
53
|
+
self._store = SettingsStore(gcc_dir)
|
|
54
|
+
self._config, self._config_source = (
|
|
55
|
+
(config, "provided") if config is not None else self._store.load()
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
# Learning layer: only if a gcc_dir is available and learning is enabled.
|
|
59
|
+
self._learning: ReasoningPlusLearning | None = None
|
|
60
|
+
if gcc_dir and self._config.learning_enabled:
|
|
61
|
+
try:
|
|
62
|
+
self._learning = ReasoningPlusLearning(
|
|
63
|
+
gcc_dir=gcc_dir,
|
|
64
|
+
repo_path=self._repo_path,
|
|
65
|
+
config=self._config,
|
|
66
|
+
)
|
|
67
|
+
except Exception as exc:
|
|
68
|
+
logger.warning("devtorch: failed to initialize learning layer — %s", exc)
|
|
69
|
+
|
|
70
|
+
# Cache: prompt hash -> (contents, timestamp)
|
|
71
|
+
self._cache: dict[str, tuple[str, float]] = {}
|
|
72
|
+
|
|
73
|
+
# ------------------------------------------------------------------
|
|
74
|
+
# Public API
|
|
75
|
+
# ------------------------------------------------------------------
|
|
76
|
+
|
|
77
|
+
@property
|
|
78
|
+
def config(self) -> ReasoningPlusConfig:
|
|
79
|
+
return self._config
|
|
80
|
+
|
|
81
|
+
@property
|
|
82
|
+
def config_source(self) -> str:
|
|
83
|
+
return self._config_source
|
|
84
|
+
|
|
85
|
+
def should_augment(self) -> bool:
|
|
86
|
+
"""Return True if Reasoning Plus should be applied to this call."""
|
|
87
|
+
if not self._config.enabled:
|
|
88
|
+
return False
|
|
89
|
+
# Must be a git repo to gather smart context
|
|
90
|
+
return self._is_git_repo()
|
|
91
|
+
|
|
92
|
+
def build_system_prompt(
|
|
93
|
+
self,
|
|
94
|
+
base_prompt: str,
|
|
95
|
+
prompt_text_for_context: str | None = None,
|
|
96
|
+
*,
|
|
97
|
+
task_prompt: str = "",
|
|
98
|
+
) -> str:
|
|
99
|
+
"""
|
|
100
|
+
Return a Reasoning Plus system prompt.
|
|
101
|
+
|
|
102
|
+
If the augmenter is disabled, returns `base_prompt` unchanged.
|
|
103
|
+
"""
|
|
104
|
+
if not self._config.enabled:
|
|
105
|
+
return base_prompt
|
|
106
|
+
|
|
107
|
+
smart_context = ""
|
|
108
|
+
if prompt_text_for_context:
|
|
109
|
+
smart_context = self._get_cached_smart_context(prompt_text_for_context)
|
|
110
|
+
|
|
111
|
+
return build_reasoning_plus_system_prompt(
|
|
112
|
+
base_prompt,
|
|
113
|
+
smart_context=smart_context,
|
|
114
|
+
require_thinking=self._config.require_thinking,
|
|
115
|
+
task_prompt=task_prompt,
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
def capture_and_strip(
|
|
119
|
+
self,
|
|
120
|
+
response_text: str,
|
|
121
|
+
session_id: str,
|
|
122
|
+
prompt_text: str | None = None,
|
|
123
|
+
) -> str:
|
|
124
|
+
"""
|
|
125
|
+
Extract inline reasoning from the response, commit it, and return text
|
|
126
|
+
with the <thinking> blocks stripped.
|
|
127
|
+
"""
|
|
128
|
+
if not self._config.enabled:
|
|
129
|
+
return response_text
|
|
130
|
+
|
|
131
|
+
thinking_blocks = extract_inline_thinking(response_text)
|
|
132
|
+
if thinking_blocks:
|
|
133
|
+
combined = "\n\n".join(thinking_blocks)
|
|
134
|
+
try:
|
|
135
|
+
commit_reasoning(
|
|
136
|
+
self._gcc_repo,
|
|
137
|
+
combined,
|
|
138
|
+
session_id,
|
|
139
|
+
prompt_text or "",
|
|
140
|
+
)
|
|
141
|
+
except Exception as exc:
|
|
142
|
+
logger.warning("devtorch: reasoning_plus capture failed — %s", exc)
|
|
143
|
+
|
|
144
|
+
cleaned_text = strip_inline_thinking(response_text)
|
|
145
|
+
|
|
146
|
+
# Record this LLM call in the learning layer so its reasoning can be reused.
|
|
147
|
+
if self._learning and thinking_blocks:
|
|
148
|
+
try:
|
|
149
|
+
call_id = self._learning.record_call(
|
|
150
|
+
call_type="llm",
|
|
151
|
+
reasoning="\n\n".join(thinking_blocks),
|
|
152
|
+
input=prompt_text or "",
|
|
153
|
+
output=cleaned_text,
|
|
154
|
+
outcome="unknown",
|
|
155
|
+
session_id=session_id,
|
|
156
|
+
)
|
|
157
|
+
self._learning.extract_learnings(call_id)
|
|
158
|
+
except Exception as exc:
|
|
159
|
+
logger.warning("devtorch: learning layer recording failed — %s", exc)
|
|
160
|
+
|
|
161
|
+
return cleaned_text
|
|
162
|
+
|
|
163
|
+
def get_learning_context(self, context: str, session_id: str = "") -> str:
|
|
164
|
+
"""
|
|
165
|
+
Return a compact <learnings> block for the given context, or "" if none.
|
|
166
|
+
|
|
167
|
+
Callers should append the returned block to the user prompt.
|
|
168
|
+
"""
|
|
169
|
+
block, _ = self.get_learning_context_with_provenance(context, session_id=session_id)
|
|
170
|
+
return block
|
|
171
|
+
|
|
172
|
+
def get_learning_context_with_provenance(
|
|
173
|
+
self,
|
|
174
|
+
context: str,
|
|
175
|
+
session_id: str = "",
|
|
176
|
+
call_id: str = "",
|
|
177
|
+
model_name: str = "",
|
|
178
|
+
) -> tuple[str, list[str]]:
|
|
179
|
+
"""
|
|
180
|
+
Return a compact <learnings> block plus the IDs of injected learnings.
|
|
181
|
+
|
|
182
|
+
If *call_id* is provided, a provenance record is saved so outcomes can be
|
|
183
|
+
attributed back to the injected learnings.
|
|
184
|
+
"""
|
|
185
|
+
if not self._learning:
|
|
186
|
+
return "", []
|
|
187
|
+
try:
|
|
188
|
+
learnings = self._learning.get_relevant_learnings(
|
|
189
|
+
context,
|
|
190
|
+
top_n=self._config.learning_top_n,
|
|
191
|
+
)
|
|
192
|
+
block = self._learning.compose_user_prompt("", learnings).strip()
|
|
193
|
+
learning_ids = [l.id for l in learnings]
|
|
194
|
+
if call_id and learning_ids:
|
|
195
|
+
self._learning.record_injection(
|
|
196
|
+
learning_ids,
|
|
197
|
+
call_id=call_id,
|
|
198
|
+
session_id=session_id,
|
|
199
|
+
prompt_text=context,
|
|
200
|
+
model_name=model_name,
|
|
201
|
+
)
|
|
202
|
+
return block, learning_ids
|
|
203
|
+
except Exception as exc:
|
|
204
|
+
logger.warning("devtorch: learning context retrieval failed — %s", exc)
|
|
205
|
+
return "", []
|
|
206
|
+
|
|
207
|
+
def build_system_prompt_with_learning_context(
|
|
208
|
+
self,
|
|
209
|
+
base_prompt: str,
|
|
210
|
+
prompt_text_for_context: str | None = None,
|
|
211
|
+
*,
|
|
212
|
+
task_prompt: str = "",
|
|
213
|
+
call_id: str = "",
|
|
214
|
+
session_id: str = "",
|
|
215
|
+
model_name: str = "",
|
|
216
|
+
) -> tuple[str, list[str]]:
|
|
217
|
+
"""
|
|
218
|
+
Build a Reasoning Plus system prompt and append relevant learnings.
|
|
219
|
+
|
|
220
|
+
Returns the augmented prompt and the list of learning IDs that were
|
|
221
|
+
injected (if any). A provenance record is written when *call_id* is set.
|
|
222
|
+
"""
|
|
223
|
+
system_prompt = self.build_system_prompt(
|
|
224
|
+
base_prompt,
|
|
225
|
+
prompt_text_for_context=prompt_text_for_context,
|
|
226
|
+
task_prompt=task_prompt,
|
|
227
|
+
)
|
|
228
|
+
learning_ids: list[str] = []
|
|
229
|
+
if prompt_text_for_context:
|
|
230
|
+
block, learning_ids = self.get_learning_context_with_provenance(
|
|
231
|
+
prompt_text_for_context,
|
|
232
|
+
session_id=session_id,
|
|
233
|
+
call_id=call_id,
|
|
234
|
+
model_name=model_name,
|
|
235
|
+
)
|
|
236
|
+
if block:
|
|
237
|
+
system_prompt = f"{system_prompt}\n\n[DevTorch learnings]\n{block}"
|
|
238
|
+
return system_prompt, learning_ids
|
|
239
|
+
|
|
240
|
+
def record_learning_call(
|
|
241
|
+
self,
|
|
242
|
+
*,
|
|
243
|
+
call_type: str,
|
|
244
|
+
reasoning: str,
|
|
245
|
+
session_id: str = "",
|
|
246
|
+
name: str = "",
|
|
247
|
+
input: str = "",
|
|
248
|
+
output: str = "",
|
|
249
|
+
outcome: str = "unknown",
|
|
250
|
+
) -> str:
|
|
251
|
+
"""
|
|
252
|
+
Record a tool/memory/action call in the learning layer.
|
|
253
|
+
|
|
254
|
+
Returns the call ID so the caller can apply feedback later.
|
|
255
|
+
"""
|
|
256
|
+
if not self._learning:
|
|
257
|
+
return ""
|
|
258
|
+
try:
|
|
259
|
+
return self._learning.record_call(
|
|
260
|
+
call_type=call_type,
|
|
261
|
+
reasoning=reasoning,
|
|
262
|
+
session_id=session_id,
|
|
263
|
+
name=name,
|
|
264
|
+
input=input,
|
|
265
|
+
output=output,
|
|
266
|
+
outcome=outcome,
|
|
267
|
+
)
|
|
268
|
+
except Exception as exc:
|
|
269
|
+
logger.warning("devtorch: learning call recording failed — %s", exc)
|
|
270
|
+
return ""
|
|
271
|
+
|
|
272
|
+
def invalidate_cache(self) -> None:
|
|
273
|
+
"""Clear the smart context cache (call after a devtorch commit)."""
|
|
274
|
+
self._cache.clear()
|
|
275
|
+
|
|
276
|
+
# ------------------------------------------------------------------
|
|
277
|
+
# Private helpers
|
|
278
|
+
# ------------------------------------------------------------------
|
|
279
|
+
|
|
280
|
+
def _get_cached_smart_context(self, prompt_text: str) -> str:
|
|
281
|
+
"""Return cached smart context, or gather and cache it."""
|
|
282
|
+
prompt_hash = _hash_prompt(prompt_text)
|
|
283
|
+
now = time.monotonic()
|
|
284
|
+
ttl = self._config.cache_ttl_secs
|
|
285
|
+
|
|
286
|
+
cached = self._cache.get(prompt_hash)
|
|
287
|
+
if cached and (now - cached[1]) < ttl:
|
|
288
|
+
return cached[0]
|
|
289
|
+
|
|
290
|
+
_, contents = gather_smart_context(
|
|
291
|
+
self._repo_path,
|
|
292
|
+
prompt_text,
|
|
293
|
+
top_n=self._config.smart_top_n,
|
|
294
|
+
max_lines=self._config.smart_max_lines,
|
|
295
|
+
max_keywords=self._config.max_keywords,
|
|
296
|
+
)
|
|
297
|
+
self._cache[prompt_hash] = (contents, now)
|
|
298
|
+
return contents
|
|
299
|
+
|
|
300
|
+
def _is_git_repo(self) -> bool:
|
|
301
|
+
try:
|
|
302
|
+
import subprocess
|
|
303
|
+
result = subprocess.run(
|
|
304
|
+
["git", "rev-parse", "--git-dir"],
|
|
305
|
+
cwd=self._repo_path,
|
|
306
|
+
capture_output=True,
|
|
307
|
+
text=True,
|
|
308
|
+
errors="ignore",
|
|
309
|
+
timeout=5,
|
|
310
|
+
)
|
|
311
|
+
return result.returncode == 0 and result.stdout.strip() != ""
|
|
312
|
+
except Exception:
|
|
313
|
+
return False
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
def _hash_prompt(prompt_text: str) -> str:
|
|
317
|
+
"""Stable hash for cache keys."""
|
|
318
|
+
return hashlib.sha256(prompt_text.encode("utf-8")).hexdigest()[:16]
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
def is_enabled(repo_path: Path | None = None) -> bool:
|
|
322
|
+
"""Convenience helper: is Reasoning Plus enabled for the given repo path?"""
|
|
323
|
+
if not repo_path:
|
|
324
|
+
config, _ = SettingsStore().load()
|
|
325
|
+
return config.enabled
|
|
326
|
+
return ReasoningPlusAugmenter(repo_path).should_augment()
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""
|
|
2
|
+
devtorch_core.reasoning_plus.capture
|
|
3
|
+
====================================
|
|
4
|
+
Extract inline <thinking> blocks from LLM output and commit them to .GCC/.
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import logging
|
|
9
|
+
import re
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger("devtorch.reasoning_plus")
|
|
13
|
+
|
|
14
|
+
_THINKING_RE = re.compile(r"<thinking>(.*?)</thinking>", re.DOTALL | re.IGNORECASE)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def extract_inline_thinking(text: str) -> list[str]:
|
|
18
|
+
"""Return all <thinking>...</thinking> bodies found in `text`."""
|
|
19
|
+
return [m.strip() for m in _THINKING_RE.findall(text or "")]
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def strip_inline_thinking(text: str) -> str:
|
|
23
|
+
"""Remove <thinking>...</thinking> blocks from `text` and collapse whitespace."""
|
|
24
|
+
cleaned = _THINKING_RE.sub("\n", text or "")
|
|
25
|
+
# Collapse multiple blank lines introduced by stripping
|
|
26
|
+
cleaned = re.sub(r"\n{3,}", "\n\n", cleaned)
|
|
27
|
+
return cleaned.strip()
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def commit_reasoning(
|
|
31
|
+
repo: Any,
|
|
32
|
+
reasoning_text: str,
|
|
33
|
+
session_id: str,
|
|
34
|
+
prompt_text: str = "",
|
|
35
|
+
) -> None:
|
|
36
|
+
"""
|
|
37
|
+
Commit a reasoning block to .GCC/ via the standard repo.commit() primitive.
|
|
38
|
+
|
|
39
|
+
This keeps the same audit path as all other DevTorch captures.
|
|
40
|
+
"""
|
|
41
|
+
try:
|
|
42
|
+
summary = reasoning_text.strip().replace("\n", " ")[:200]
|
|
43
|
+
prompt_summary = prompt_text.strip().replace("\n", " ")[:100]
|
|
44
|
+
message = (
|
|
45
|
+
f"[session={session_id}] reasoning_plus(conf=1.0): {summary}"
|
|
46
|
+
)
|
|
47
|
+
if prompt_summary:
|
|
48
|
+
message += f" | prompt: {prompt_summary}"
|
|
49
|
+
repo.commit(message=message)
|
|
50
|
+
except Exception as exc:
|
|
51
|
+
logger.warning("devtorch: failed to commit reasoning — %s", exc)
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
"""
|
|
2
|
+
devtorch_core.reasoning_plus.config
|
|
3
|
+
=================================
|
|
4
|
+
Configuration and settings for Reasoning Plus.
|
|
5
|
+
|
|
6
|
+
Resolution order (highest priority first):
|
|
7
|
+
1. DEVTORCH_REASONING_PLUS env var (0/1).
|
|
8
|
+
2. Per-project .GCC/settings.json.
|
|
9
|
+
3. Global ~/.devtorch/settings.json.
|
|
10
|
+
4. Built-in defaults (enabled by default).
|
|
11
|
+
|
|
12
|
+
All settings access is defensive: malformed JSON or missing files return
|
|
13
|
+
defaults so that wrappers never fail because of config errors.
|
|
14
|
+
"""
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import json
|
|
18
|
+
import logging
|
|
19
|
+
import os
|
|
20
|
+
from dataclasses import dataclass, field, fields
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
from typing import Any
|
|
23
|
+
|
|
24
|
+
logger = logging.getLogger("devtorch.reasoning_plus")
|
|
25
|
+
|
|
26
|
+
REASONING_PLUS_ENV_VAR = "DEVTORCH_REASONING_PLUS"
|
|
27
|
+
GLOBAL_SETTINGS_DIR = Path.home() / ".devtorch"
|
|
28
|
+
GLOBAL_SETTINGS_PATH = GLOBAL_SETTINGS_DIR / "settings.json"
|
|
29
|
+
PROJECT_SETTINGS_FILENAME = "settings.json"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass
|
|
33
|
+
class ReasoningPlusConfig:
|
|
34
|
+
"""Immutable-ish configuration for Reasoning Plus.
|
|
35
|
+
|
|
36
|
+
Use `with_values()` to create a copy with selected overrides.
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
enabled: bool = True
|
|
40
|
+
smart_top_n: int = 3
|
|
41
|
+
smart_max_lines: int = 150
|
|
42
|
+
context_k: int = 4000
|
|
43
|
+
require_thinking: bool = True
|
|
44
|
+
max_keywords: int = 15
|
|
45
|
+
cache_ttl_secs: int = 60
|
|
46
|
+
learning_enabled: bool = True
|
|
47
|
+
learning_top_n: int = 3
|
|
48
|
+
learning_max_lines: int = 100
|
|
49
|
+
learning_embedding_backend: str = "keyword"
|
|
50
|
+
learning_embedding_model: str | None = None
|
|
51
|
+
learning_extraction_strategy: str = "rule"
|
|
52
|
+
learning_relevance_cache_ttl: int = 60
|
|
53
|
+
|
|
54
|
+
def with_values(self, **kwargs: Any) -> "ReasoningPlusConfig":
|
|
55
|
+
"""Return a new config with the provided field overrides."""
|
|
56
|
+
valid = {f.name for f in fields(self)}
|
|
57
|
+
extra = set(kwargs) - valid
|
|
58
|
+
if extra:
|
|
59
|
+
raise ValueError(f"Unknown ReasoningPlusConfig fields: {extra}")
|
|
60
|
+
return ReasoningPlusConfig(
|
|
61
|
+
**{f.name: kwargs.get(f.name, getattr(self, f.name)) for f in fields(self)}
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
def to_dict(self) -> dict[str, Any]:
|
|
65
|
+
return {
|
|
66
|
+
"enabled": self.enabled,
|
|
67
|
+
"smart_top_n": self.smart_top_n,
|
|
68
|
+
"smart_max_lines": self.smart_max_lines,
|
|
69
|
+
"context_k": self.context_k,
|
|
70
|
+
"require_thinking": self.require_thinking,
|
|
71
|
+
"max_keywords": self.max_keywords,
|
|
72
|
+
"cache_ttl_secs": self.cache_ttl_secs,
|
|
73
|
+
"learning_enabled": self.learning_enabled,
|
|
74
|
+
"learning_top_n": self.learning_top_n,
|
|
75
|
+
"learning_max_lines": self.learning_max_lines,
|
|
76
|
+
"learning_embedding_backend": self.learning_embedding_backend,
|
|
77
|
+
"learning_embedding_model": self.learning_embedding_model,
|
|
78
|
+
"learning_extraction_strategy": self.learning_extraction_strategy,
|
|
79
|
+
"learning_relevance_cache_ttl": self.learning_relevance_cache_ttl,
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
@classmethod
|
|
83
|
+
def from_dict(cls, data: dict[str, Any]) -> "ReasoningPlusConfig":
|
|
84
|
+
"""Build from a dict, ignoring unknown keys and using defaults for missing ones."""
|
|
85
|
+
return cls(
|
|
86
|
+
enabled=_bool(data.get("enabled", True)),
|
|
87
|
+
smart_top_n=_int(data.get("smart_top_n", 3), 3),
|
|
88
|
+
smart_max_lines=_int(data.get("smart_max_lines", 150), 150),
|
|
89
|
+
context_k=_int(data.get("context_k", 4000), 4000),
|
|
90
|
+
require_thinking=_bool(data.get("require_thinking", True)),
|
|
91
|
+
max_keywords=_int(data.get("max_keywords", 15), 15),
|
|
92
|
+
cache_ttl_secs=_int(data.get("cache_ttl_secs", 60), 60),
|
|
93
|
+
learning_enabled=_bool(data.get("learning_enabled", True)),
|
|
94
|
+
learning_top_n=_int(data.get("learning_top_n", 3), 3),
|
|
95
|
+
learning_max_lines=_int(data.get("learning_max_lines", 100), 100),
|
|
96
|
+
learning_embedding_backend=_str(data.get("learning_embedding_backend", "keyword"), "keyword"),
|
|
97
|
+
learning_embedding_model=_optional_str(data.get("learning_embedding_model")),
|
|
98
|
+
learning_extraction_strategy=_str(data.get("learning_extraction_strategy", "rule"), "rule"),
|
|
99
|
+
learning_relevance_cache_ttl=_int(data.get("learning_relevance_cache_ttl", 60), 60),
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
class SettingsStore:
|
|
104
|
+
"""Read and write Reasoning Plus settings at project and global levels."""
|
|
105
|
+
|
|
106
|
+
def __init__(self, gcc_dir: Path | None = None) -> None:
|
|
107
|
+
self._project_dir = gcc_dir
|
|
108
|
+
self._global_path = GLOBAL_SETTINGS_PATH
|
|
109
|
+
|
|
110
|
+
# ------------------------------------------------------------------
|
|
111
|
+
# Read
|
|
112
|
+
# ------------------------------------------------------------------
|
|
113
|
+
|
|
114
|
+
def load(self) -> tuple[ReasoningPlusConfig, str]:
|
|
115
|
+
"""Return (config, source) where source describes where the config came from."""
|
|
116
|
+
# 1. Env var (highest priority)
|
|
117
|
+
env_val = os.environ.get(REASONING_PLUS_ENV_VAR, "").strip()
|
|
118
|
+
if env_val:
|
|
119
|
+
enabled = env_val in ("1", "true", "yes", "on")
|
|
120
|
+
config = ReasoningPlusConfig(enabled=enabled)
|
|
121
|
+
return config, "environment"
|
|
122
|
+
|
|
123
|
+
# 2. Project settings
|
|
124
|
+
project = self._read_project_settings()
|
|
125
|
+
if project is not None:
|
|
126
|
+
return ReasoningPlusConfig.from_dict(project.get("reasoning_plus", {})), "project"
|
|
127
|
+
|
|
128
|
+
# 3. Global settings
|
|
129
|
+
global_ = self._read_global_settings()
|
|
130
|
+
if global_ is not None:
|
|
131
|
+
return ReasoningPlusConfig.from_dict(global_.get("reasoning_plus", {})), "global"
|
|
132
|
+
|
|
133
|
+
# 4. Defaults
|
|
134
|
+
return ReasoningPlusConfig(), "default"
|
|
135
|
+
|
|
136
|
+
def _read_project_settings(self) -> dict[str, Any] | None:
|
|
137
|
+
if not self._project_dir:
|
|
138
|
+
return None
|
|
139
|
+
path = self._project_dir / PROJECT_SETTINGS_FILENAME
|
|
140
|
+
return _read_json(path)
|
|
141
|
+
|
|
142
|
+
def _read_global_settings(self) -> dict[str, Any] | None:
|
|
143
|
+
return _read_json(self._global_path)
|
|
144
|
+
|
|
145
|
+
# ------------------------------------------------------------------
|
|
146
|
+
# Write
|
|
147
|
+
# ------------------------------------------------------------------
|
|
148
|
+
|
|
149
|
+
def save_project(self, config: ReasoningPlusConfig) -> None:
|
|
150
|
+
"""Write the Reasoning Plus section to .GCC/settings.json (creates file if needed)."""
|
|
151
|
+
if not self._project_dir:
|
|
152
|
+
raise ValueError("No project directory available for saving settings")
|
|
153
|
+
self._project_dir.mkdir(parents=True, exist_ok=True)
|
|
154
|
+
path = self._project_dir / PROJECT_SETTINGS_FILENAME
|
|
155
|
+
settings = _read_json(path) or {}
|
|
156
|
+
settings["reasoning_plus"] = config.to_dict()
|
|
157
|
+
settings.setdefault("version", 1)
|
|
158
|
+
_write_json(path, settings)
|
|
159
|
+
|
|
160
|
+
def save_global(self, config: ReasoningPlusConfig) -> None:
|
|
161
|
+
"""Write the Reasoning Plus section to ~/.devtorch/settings.json."""
|
|
162
|
+
GLOBAL_SETTINGS_DIR.mkdir(parents=True, exist_ok=True)
|
|
163
|
+
settings = _read_json(self._global_path) or {}
|
|
164
|
+
settings["reasoning_plus"] = config.to_dict()
|
|
165
|
+
settings.setdefault("version", 1)
|
|
166
|
+
_write_json(self._global_path, settings)
|
|
167
|
+
|
|
168
|
+
def reset_project(self) -> None:
|
|
169
|
+
"""Remove the Reasoning Plus section from project settings.
|
|
170
|
+
|
|
171
|
+
If the settings file is empty after removal (only version remains), delete it.
|
|
172
|
+
"""
|
|
173
|
+
if not self._project_dir:
|
|
174
|
+
return
|
|
175
|
+
path = self._project_dir / PROJECT_SETTINGS_FILENAME
|
|
176
|
+
settings = _read_json(path)
|
|
177
|
+
if settings and "reasoning_plus" in settings:
|
|
178
|
+
del settings["reasoning_plus"]
|
|
179
|
+
remaining_keys = [k for k in settings.keys() if k != "version"]
|
|
180
|
+
if remaining_keys:
|
|
181
|
+
_write_json(path, settings)
|
|
182
|
+
else:
|
|
183
|
+
try:
|
|
184
|
+
path.unlink()
|
|
185
|
+
except Exception:
|
|
186
|
+
pass
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
# ---------------------------------------------------------------------------
|
|
190
|
+
# Helpers
|
|
191
|
+
# ---------------------------------------------------------------------------
|
|
192
|
+
|
|
193
|
+
def _bool(value: Any) -> bool:
|
|
194
|
+
if isinstance(value, bool):
|
|
195
|
+
return value
|
|
196
|
+
if isinstance(value, str):
|
|
197
|
+
return value.strip().lower() in ("1", "true", "yes", "on")
|
|
198
|
+
return bool(value)
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def _int(value: Any, default: int) -> int:
|
|
202
|
+
try:
|
|
203
|
+
return int(value)
|
|
204
|
+
except (TypeError, ValueError):
|
|
205
|
+
return default
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def _str(value: Any, default: str) -> str:
|
|
209
|
+
if isinstance(value, str):
|
|
210
|
+
return value.strip()
|
|
211
|
+
return default
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def _optional_str(value: Any) -> str | None:
|
|
215
|
+
if value is None:
|
|
216
|
+
return None
|
|
217
|
+
if isinstance(value, str):
|
|
218
|
+
value = value.strip()
|
|
219
|
+
return value if value else None
|
|
220
|
+
return None
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def _read_json(path: Path) -> dict[str, Any] | None:
|
|
224
|
+
if not path.exists():
|
|
225
|
+
return None
|
|
226
|
+
try:
|
|
227
|
+
with open(path, "r", encoding="utf-8") as f:
|
|
228
|
+
data = json.load(f)
|
|
229
|
+
if isinstance(data, dict):
|
|
230
|
+
return data
|
|
231
|
+
except Exception as exc:
|
|
232
|
+
logger.warning("devtorch: failed to read settings from %s — %s", path, exc)
|
|
233
|
+
return None
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def _write_json(path: Path, data: dict[str, Any]) -> None:
|
|
237
|
+
"""Atomic JSON write (temp file + rename)."""
|
|
238
|
+
tmp = path.with_suffix(path.suffix + ".tmp")
|
|
239
|
+
try:
|
|
240
|
+
with open(tmp, "w", encoding="utf-8") as f:
|
|
241
|
+
json.dump(data, f, indent=2)
|
|
242
|
+
tmp.replace(path)
|
|
243
|
+
except Exception as exc:
|
|
244
|
+
logger.warning("devtorch: failed to write settings to %s — %s", path, exc)
|
|
245
|
+
raise
|
|
246
|
+
finally:
|
|
247
|
+
if tmp.exists():
|
|
248
|
+
try:
|
|
249
|
+
tmp.unlink()
|
|
250
|
+
except Exception:
|
|
251
|
+
pass
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def load_config(gcc_dir: Path | None = None) -> tuple[ReasoningPlusConfig, str]:
|
|
255
|
+
"""Convenience function: load config + source."""
|
|
256
|
+
return SettingsStore(gcc_dir).load()
|