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,487 @@
|
|
|
1
|
+
"""
|
|
2
|
+
devtorch_core.wrapper.anthropic
|
|
3
|
+
================================
|
|
4
|
+
Drop-in replacement for ``anthropic.Anthropic`` that:
|
|
5
|
+
|
|
6
|
+
1. Injects the RACP system-prompt prefix into every ``messages.create`` call.
|
|
7
|
+
2. Captures reasoning / RACP blocks from the response to the .GCC/ store.
|
|
8
|
+
3. Returns clean (block-stripped) text to callers.
|
|
9
|
+
4. Degrades gracefully if anthropic is not installed or .GCC/ is missing.
|
|
10
|
+
|
|
11
|
+
Usage
|
|
12
|
+
-----
|
|
13
|
+
::
|
|
14
|
+
|
|
15
|
+
from devtorch_core.wrapper.anthropic import DevTorchAnthropic as Anthropic
|
|
16
|
+
|
|
17
|
+
client = Anthropic() # same constructor signature as anthropic.Anthropic
|
|
18
|
+
response = client.messages.create(
|
|
19
|
+
model="claude-opus-4-5",
|
|
20
|
+
max_tokens=1024,
|
|
21
|
+
messages=[{"role": "user", "content": "Hello"}],
|
|
22
|
+
)
|
|
23
|
+
"""
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
import logging
|
|
27
|
+
import uuid
|
|
28
|
+
from typing import Any, Iterator
|
|
29
|
+
|
|
30
|
+
from .base import CaptureOrchestrator, RACPInjector, _reasoning_text, find_gcc_repo, is_disabled
|
|
31
|
+
|
|
32
|
+
logger = logging.getLogger("devtorch.wrapper.anthropic")
|
|
33
|
+
|
|
34
|
+
# ---------------------------------------------------------------------------
|
|
35
|
+
# Optional import — degrade gracefully if anthropic is not installed
|
|
36
|
+
# ---------------------------------------------------------------------------
|
|
37
|
+
|
|
38
|
+
try:
|
|
39
|
+
import anthropic as _anthropic
|
|
40
|
+
|
|
41
|
+
HAS_ANTHROPIC = True
|
|
42
|
+
except ImportError:
|
|
43
|
+
HAS_ANTHROPIC = False
|
|
44
|
+
_anthropic = None # type: ignore[assignment]
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
# ---------------------------------------------------------------------------
|
|
48
|
+
# Wrapper
|
|
49
|
+
# ---------------------------------------------------------------------------
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class DevTorchAnthropic:
|
|
53
|
+
"""
|
|
54
|
+
Drop-in replacement for ``anthropic.Anthropic``.
|
|
55
|
+
|
|
56
|
+
Adds RACP injection and .GCC/ capture without altering the public API
|
|
57
|
+
visible to callers. All DevTorch errors degrade to warnings.
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
def __init__(self, *args: Any, gcc_repo: Any = None, **kwargs: Any) -> None:
|
|
61
|
+
if not HAS_ANTHROPIC:
|
|
62
|
+
raise ImportError(
|
|
63
|
+
"anthropic package not installed. "
|
|
64
|
+
"pip install devtorch[wrapper]"
|
|
65
|
+
)
|
|
66
|
+
self._client = _anthropic.Anthropic(*args, **kwargs) # type: ignore[union-attr]
|
|
67
|
+
self._gcc = gcc_repo or find_gcc_repo()
|
|
68
|
+
self._injector = RACPInjector(self._gcc) if self._gcc else None
|
|
69
|
+
self._capture = CaptureOrchestrator(self._gcc) if self._gcc else None
|
|
70
|
+
self.messages = self._Messages(self)
|
|
71
|
+
|
|
72
|
+
# Proxy attribute access to the underlying client for anything we
|
|
73
|
+
# don't explicitly intercept (e.g. .beta, .models, etc.)
|
|
74
|
+
def __getattr__(self, name: str) -> Any:
|
|
75
|
+
return getattr(self._client, name)
|
|
76
|
+
|
|
77
|
+
# ------------------------------------------------------------------
|
|
78
|
+
# Inner class mirrors anthropic.Anthropic.messages
|
|
79
|
+
# ------------------------------------------------------------------
|
|
80
|
+
|
|
81
|
+
class _Messages:
|
|
82
|
+
def __init__(self, parent: "DevTorchAnthropic") -> None:
|
|
83
|
+
self._p = parent
|
|
84
|
+
|
|
85
|
+
# --------------------------------------------------------------
|
|
86
|
+
# create — main intercept point
|
|
87
|
+
# --------------------------------------------------------------
|
|
88
|
+
|
|
89
|
+
def create(
|
|
90
|
+
self,
|
|
91
|
+
*,
|
|
92
|
+
messages: list,
|
|
93
|
+
system: str = "",
|
|
94
|
+
stream: bool = False,
|
|
95
|
+
**kwargs: Any,
|
|
96
|
+
) -> Any:
|
|
97
|
+
"""
|
|
98
|
+
Inject RACP prefix into *system*, call the real API, capture
|
|
99
|
+
response.
|
|
100
|
+
|
|
101
|
+
If ``stream=True`` returns a streaming wrapper that passes chunks
|
|
102
|
+
through transparently and captures the accumulated response after
|
|
103
|
+
the stream closes.
|
|
104
|
+
|
|
105
|
+
DevTorch errors never propagate to the caller.
|
|
106
|
+
"""
|
|
107
|
+
session_id = str(uuid.uuid4())[:8]
|
|
108
|
+
call_id = str(uuid.uuid4())
|
|
109
|
+
prompt_text = _extract_prompt_text(messages)
|
|
110
|
+
model_name = kwargs.get("model", "")
|
|
111
|
+
injected_system = self._inject(system, prompt_text, session_id, call_id, model_name)
|
|
112
|
+
|
|
113
|
+
if stream:
|
|
114
|
+
return self._stream_create(
|
|
115
|
+
messages=messages,
|
|
116
|
+
system=injected_system,
|
|
117
|
+
session_id=session_id,
|
|
118
|
+
call_id=call_id,
|
|
119
|
+
prompt_text=prompt_text,
|
|
120
|
+
model_name=model_name,
|
|
121
|
+
**kwargs,
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
# Non-streaming path
|
|
125
|
+
try:
|
|
126
|
+
response = self._p._client.messages.create(
|
|
127
|
+
messages=messages,
|
|
128
|
+
system=injected_system,
|
|
129
|
+
stream=False,
|
|
130
|
+
**kwargs,
|
|
131
|
+
)
|
|
132
|
+
except Exception:
|
|
133
|
+
# Record failure in the learning layer, then re-raise API errors.
|
|
134
|
+
self._record_llm_outcome(
|
|
135
|
+
prompt_text=prompt_text,
|
|
136
|
+
response_text="",
|
|
137
|
+
thinking_blocks=[],
|
|
138
|
+
session_id=session_id,
|
|
139
|
+
call_id=call_id,
|
|
140
|
+
model_name=model_name,
|
|
141
|
+
outcome="failure",
|
|
142
|
+
)
|
|
143
|
+
raise
|
|
144
|
+
|
|
145
|
+
text, thinking_blocks = self._capture_response(response, session_id, prompt_text=prompt_text)
|
|
146
|
+
model_name = getattr(response, "model", None) or model_name
|
|
147
|
+
self._record_llm_outcome(
|
|
148
|
+
prompt_text=prompt_text,
|
|
149
|
+
response_text=text,
|
|
150
|
+
thinking_blocks=thinking_blocks,
|
|
151
|
+
session_id=session_id,
|
|
152
|
+
call_id=call_id,
|
|
153
|
+
model_name=model_name,
|
|
154
|
+
outcome="success",
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
# Strip RACP blocks from the visible text in each content block
|
|
158
|
+
try:
|
|
159
|
+
response = self._strip_response(response)
|
|
160
|
+
except Exception as exc:
|
|
161
|
+
logger.warning("devtorch: strip_response failed — %s", exc)
|
|
162
|
+
|
|
163
|
+
return response
|
|
164
|
+
|
|
165
|
+
# --------------------------------------------------------------
|
|
166
|
+
# Streaming helper
|
|
167
|
+
# --------------------------------------------------------------
|
|
168
|
+
|
|
169
|
+
def _stream_create(
|
|
170
|
+
self,
|
|
171
|
+
*,
|
|
172
|
+
messages: list,
|
|
173
|
+
system: str,
|
|
174
|
+
session_id: str,
|
|
175
|
+
call_id: str = "",
|
|
176
|
+
prompt_text: str = "",
|
|
177
|
+
model_name: str = "",
|
|
178
|
+
**kwargs: Any,
|
|
179
|
+
) -> "_StreamingWrapper":
|
|
180
|
+
try:
|
|
181
|
+
raw_stream = self._p._client.messages.create(
|
|
182
|
+
messages=messages,
|
|
183
|
+
system=system,
|
|
184
|
+
stream=True,
|
|
185
|
+
**kwargs,
|
|
186
|
+
)
|
|
187
|
+
except Exception:
|
|
188
|
+
self._record_llm_outcome(
|
|
189
|
+
prompt_text=prompt_text,
|
|
190
|
+
response_text="",
|
|
191
|
+
thinking_blocks=[],
|
|
192
|
+
session_id=session_id,
|
|
193
|
+
call_id=call_id,
|
|
194
|
+
model_name=model_name,
|
|
195
|
+
outcome="failure",
|
|
196
|
+
)
|
|
197
|
+
raise
|
|
198
|
+
return _StreamingWrapper(
|
|
199
|
+
raw_stream, self, session_id,
|
|
200
|
+
call_id=call_id,
|
|
201
|
+
prompt_text=prompt_text,
|
|
202
|
+
model_name=model_name,
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
# --------------------------------------------------------------
|
|
206
|
+
# Injection helpers
|
|
207
|
+
# --------------------------------------------------------------
|
|
208
|
+
|
|
209
|
+
def _inject(
|
|
210
|
+
self,
|
|
211
|
+
system: str,
|
|
212
|
+
prompt_text: str = "",
|
|
213
|
+
session_id: str = "",
|
|
214
|
+
call_id: str = "",
|
|
215
|
+
model_name: str = "",
|
|
216
|
+
) -> str:
|
|
217
|
+
if self._p._injector and not is_disabled():
|
|
218
|
+
try:
|
|
219
|
+
prefix, _ = self._p._injector.build_system_prefix_with_provenance(
|
|
220
|
+
prompt_text=prompt_text or None,
|
|
221
|
+
session_id=session_id,
|
|
222
|
+
call_id=call_id,
|
|
223
|
+
model_name=model_name,
|
|
224
|
+
)
|
|
225
|
+
if prefix:
|
|
226
|
+
return (prefix + "\n\n" + system).strip()
|
|
227
|
+
except Exception as exc:
|
|
228
|
+
logger.warning("devtorch: RACP injection failed — %s", exc)
|
|
229
|
+
return system
|
|
230
|
+
|
|
231
|
+
# --------------------------------------------------------------
|
|
232
|
+
# Capture helpers
|
|
233
|
+
# --------------------------------------------------------------
|
|
234
|
+
|
|
235
|
+
def _capture_response(
|
|
236
|
+
self,
|
|
237
|
+
response: Any,
|
|
238
|
+
session_id: str,
|
|
239
|
+
*,
|
|
240
|
+
prompt_text: str = "",
|
|
241
|
+
) -> tuple[str, list]:
|
|
242
|
+
text = ""
|
|
243
|
+
thinking: list = []
|
|
244
|
+
if not self._p._capture or is_disabled():
|
|
245
|
+
return text, thinking
|
|
246
|
+
try:
|
|
247
|
+
# Build a plain dict for the parsers
|
|
248
|
+
if hasattr(response, "model_dump"):
|
|
249
|
+
response_dict = response.model_dump()
|
|
250
|
+
elif isinstance(response, dict):
|
|
251
|
+
response_dict = response
|
|
252
|
+
else:
|
|
253
|
+
response_dict = {}
|
|
254
|
+
|
|
255
|
+
# Extract visible text
|
|
256
|
+
text = _extract_text(response)
|
|
257
|
+
|
|
258
|
+
# Extract thinking blocks
|
|
259
|
+
try:
|
|
260
|
+
from devtorch_core.parser.thinking import extract_thinking_blocks
|
|
261
|
+
|
|
262
|
+
thinking = extract_thinking_blocks(response_dict)
|
|
263
|
+
except Exception:
|
|
264
|
+
pass
|
|
265
|
+
|
|
266
|
+
# Extract token usage from Anthropic response
|
|
267
|
+
prompt_tokens = 0
|
|
268
|
+
completion_tokens = 0
|
|
269
|
+
usage = response_dict.get("usage") if isinstance(response_dict, dict) else None
|
|
270
|
+
if usage is None and hasattr(response, "usage"):
|
|
271
|
+
usage = getattr(response, "usage", None)
|
|
272
|
+
if usage is not None:
|
|
273
|
+
if isinstance(usage, dict):
|
|
274
|
+
prompt_tokens = usage.get("input_tokens", 0)
|
|
275
|
+
completion_tokens = usage.get("output_tokens", 0)
|
|
276
|
+
else:
|
|
277
|
+
prompt_tokens = getattr(usage, "input_tokens", 0)
|
|
278
|
+
completion_tokens = getattr(usage, "output_tokens", 0)
|
|
279
|
+
|
|
280
|
+
self._p._capture.capture(
|
|
281
|
+
text,
|
|
282
|
+
thinking,
|
|
283
|
+
session_id,
|
|
284
|
+
prompt_text=prompt_text,
|
|
285
|
+
prompt_tokens=prompt_tokens,
|
|
286
|
+
completion_tokens=completion_tokens,
|
|
287
|
+
)
|
|
288
|
+
except Exception as exc:
|
|
289
|
+
logger.warning("devtorch: _capture_response failed — %s", exc)
|
|
290
|
+
return text, thinking
|
|
291
|
+
|
|
292
|
+
def _record_llm_outcome(
|
|
293
|
+
self,
|
|
294
|
+
*,
|
|
295
|
+
prompt_text: str,
|
|
296
|
+
response_text: str,
|
|
297
|
+
thinking_blocks: list,
|
|
298
|
+
session_id: str,
|
|
299
|
+
call_id: str,
|
|
300
|
+
model_name: str,
|
|
301
|
+
outcome: str,
|
|
302
|
+
) -> None:
|
|
303
|
+
if not self._p._capture or is_disabled():
|
|
304
|
+
return
|
|
305
|
+
try:
|
|
306
|
+
reasoning = _reasoning_text(thinking_blocks)
|
|
307
|
+
self._p._capture.record_learning_call(
|
|
308
|
+
call_type="llm",
|
|
309
|
+
reasoning=reasoning,
|
|
310
|
+
session_id=session_id,
|
|
311
|
+
call_id=call_id,
|
|
312
|
+
name=model_name,
|
|
313
|
+
input=prompt_text,
|
|
314
|
+
output=response_text,
|
|
315
|
+
outcome=outcome,
|
|
316
|
+
)
|
|
317
|
+
except Exception as exc:
|
|
318
|
+
logger.warning("devtorch: _record_llm_outcome failed — %s", exc)
|
|
319
|
+
|
|
320
|
+
# --------------------------------------------------------------
|
|
321
|
+
# Strip RACP markup from the response object
|
|
322
|
+
# --------------------------------------------------------------
|
|
323
|
+
|
|
324
|
+
def _strip_response(self, response: Any) -> Any:
|
|
325
|
+
"""
|
|
326
|
+
Return response with RACP blocks and inline <thinking> blocks stripped
|
|
327
|
+
from text content blocks. Modifies in-place where possible.
|
|
328
|
+
"""
|
|
329
|
+
from devtorch_core.parser.blocks import strip_racp_blocks
|
|
330
|
+
from devtorch_core.reasoning_plus.capture import strip_inline_thinking
|
|
331
|
+
|
|
332
|
+
content = getattr(response, "content", None)
|
|
333
|
+
if content is None:
|
|
334
|
+
return response
|
|
335
|
+
|
|
336
|
+
for block in content:
|
|
337
|
+
if hasattr(block, "text") and isinstance(block.text, str):
|
|
338
|
+
try:
|
|
339
|
+
text = strip_racp_blocks(block.text)
|
|
340
|
+
text = strip_inline_thinking(text)
|
|
341
|
+
block.text = text
|
|
342
|
+
except Exception:
|
|
343
|
+
pass
|
|
344
|
+
|
|
345
|
+
return response
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
# ---------------------------------------------------------------------------
|
|
349
|
+
# Streaming wrapper
|
|
350
|
+
# ---------------------------------------------------------------------------
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
class _StreamingWrapper:
|
|
354
|
+
"""
|
|
355
|
+
Thin wrapper around the raw Anthropic streaming response.
|
|
356
|
+
|
|
357
|
+
Passes chunks through transparently while accumulating the full text
|
|
358
|
+
in a shadow buffer. Fires capture when the stream closes and records the
|
|
359
|
+
outcome in the Reasoning Plus Learning layer.
|
|
360
|
+
"""
|
|
361
|
+
|
|
362
|
+
def __init__(
|
|
363
|
+
self,
|
|
364
|
+
raw_stream: Any,
|
|
365
|
+
messages_obj: "DevTorchAnthropic._Messages",
|
|
366
|
+
session_id: str,
|
|
367
|
+
call_id: str = "",
|
|
368
|
+
prompt_text: str = "",
|
|
369
|
+
model_name: str = "",
|
|
370
|
+
) -> None:
|
|
371
|
+
self._raw = raw_stream
|
|
372
|
+
self._msgs = messages_obj
|
|
373
|
+
self._session_id = session_id
|
|
374
|
+
self._call_id = call_id
|
|
375
|
+
self._prompt_text = prompt_text
|
|
376
|
+
self._model_name = model_name
|
|
377
|
+
self._accumulated_text = ""
|
|
378
|
+
self._recorded = False
|
|
379
|
+
|
|
380
|
+
def __iter__(self) -> Iterator[Any]:
|
|
381
|
+
try:
|
|
382
|
+
for chunk in self._raw:
|
|
383
|
+
# Accumulate text from delta events
|
|
384
|
+
try:
|
|
385
|
+
delta = getattr(chunk, "delta", None)
|
|
386
|
+
if delta is not None and hasattr(delta, "text"):
|
|
387
|
+
self._accumulated_text += delta.text or ""
|
|
388
|
+
elif isinstance(chunk, dict):
|
|
389
|
+
delta_d = chunk.get("delta", {})
|
|
390
|
+
self._accumulated_text += delta_d.get("text", "")
|
|
391
|
+
except Exception:
|
|
392
|
+
pass
|
|
393
|
+
yield chunk
|
|
394
|
+
except Exception:
|
|
395
|
+
self._on_close(outcome="failure")
|
|
396
|
+
raise
|
|
397
|
+
finally:
|
|
398
|
+
if not self._recorded:
|
|
399
|
+
self._on_close(outcome="success")
|
|
400
|
+
|
|
401
|
+
def __enter__(self) -> "_StreamingWrapper":
|
|
402
|
+
if hasattr(self._raw, "__enter__"):
|
|
403
|
+
self._raw.__enter__()
|
|
404
|
+
return self
|
|
405
|
+
|
|
406
|
+
def __exit__(self, *args: Any) -> None:
|
|
407
|
+
try:
|
|
408
|
+
if hasattr(self._raw, "__exit__"):
|
|
409
|
+
self._raw.__exit__(*args)
|
|
410
|
+
finally:
|
|
411
|
+
if not self._recorded:
|
|
412
|
+
self._on_close(outcome="success")
|
|
413
|
+
|
|
414
|
+
def _on_close(self, outcome: str = "success") -> None:
|
|
415
|
+
if self._recorded:
|
|
416
|
+
return
|
|
417
|
+
self._recorded = True
|
|
418
|
+
if self._accumulated_text:
|
|
419
|
+
try:
|
|
420
|
+
self._msgs._p._capture.capture(
|
|
421
|
+
self._accumulated_text, [], self._session_id,
|
|
422
|
+
prompt_text=self._prompt_text,
|
|
423
|
+
)
|
|
424
|
+
except Exception as exc:
|
|
425
|
+
logger.warning("devtorch: stream capture failed — %s", exc)
|
|
426
|
+
try:
|
|
427
|
+
self._msgs._record_llm_outcome(
|
|
428
|
+
prompt_text=self._prompt_text,
|
|
429
|
+
response_text=self._accumulated_text,
|
|
430
|
+
thinking_blocks=[],
|
|
431
|
+
session_id=self._session_id,
|
|
432
|
+
call_id=self._call_id,
|
|
433
|
+
model_name=self._model_name,
|
|
434
|
+
outcome=outcome,
|
|
435
|
+
)
|
|
436
|
+
except Exception as exc:
|
|
437
|
+
logger.warning("devtorch: stream learning outcome failed — %s", exc)
|
|
438
|
+
|
|
439
|
+
# Proxy everything else to the raw stream
|
|
440
|
+
def __getattr__(self, name: str) -> Any:
|
|
441
|
+
return getattr(self._raw, name)
|
|
442
|
+
|
|
443
|
+
|
|
444
|
+
# ---------------------------------------------------------------------------
|
|
445
|
+
# Internal helpers
|
|
446
|
+
# ---------------------------------------------------------------------------
|
|
447
|
+
|
|
448
|
+
|
|
449
|
+
def _extract_text(response: Any) -> str:
|
|
450
|
+
"""Pull plain text from an Anthropic response object or dict."""
|
|
451
|
+
text = ""
|
|
452
|
+
content = getattr(response, "content", None) or (
|
|
453
|
+
response.get("content") if isinstance(response, dict) else None
|
|
454
|
+
)
|
|
455
|
+
if not content:
|
|
456
|
+
return text
|
|
457
|
+
for block in content:
|
|
458
|
+
if isinstance(block, dict):
|
|
459
|
+
if block.get("type") == "text":
|
|
460
|
+
text += block.get("text", "")
|
|
461
|
+
elif hasattr(block, "text"):
|
|
462
|
+
text += block.text or ""
|
|
463
|
+
return text
|
|
464
|
+
|
|
465
|
+
|
|
466
|
+
def _extract_prompt_text(messages: list) -> str:
|
|
467
|
+
"""Best-effort concatenation of user-visible message text for smart context."""
|
|
468
|
+
parts: list[str] = []
|
|
469
|
+
for m in messages or []:
|
|
470
|
+
if isinstance(m, dict):
|
|
471
|
+
role = m.get("role", "")
|
|
472
|
+
content = m.get("content", "")
|
|
473
|
+
if role in ("user", "system") and content:
|
|
474
|
+
if isinstance(content, str):
|
|
475
|
+
parts.append(content)
|
|
476
|
+
elif isinstance(content, list):
|
|
477
|
+
for item in content:
|
|
478
|
+
if isinstance(item, dict) and item.get("type") == "text":
|
|
479
|
+
parts.append(item.get("text", ""))
|
|
480
|
+
elif isinstance(item, str):
|
|
481
|
+
parts.append(item)
|
|
482
|
+
elif hasattr(m, "role") and hasattr(m, "content"):
|
|
483
|
+
role = getattr(m, "role", "")
|
|
484
|
+
content = getattr(m, "content", "")
|
|
485
|
+
if role in ("user", "system") and content:
|
|
486
|
+
parts.append(str(content))
|
|
487
|
+
return "\n".join(parts)[:4000]
|