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,342 @@
|
|
|
1
|
+
"""
|
|
2
|
+
devtorch_core.wrapper.bedrock
|
|
3
|
+
==============================
|
|
4
|
+
Stub wrapper around the AWS Bedrock ``boto3`` client.
|
|
5
|
+
|
|
6
|
+
Intercepts ``invoke_model`` for Anthropic Claude models served via Bedrock
|
|
7
|
+
(body format: ``{"anthropic_version": ..., "messages": [...], "system": ...}``).
|
|
8
|
+
|
|
9
|
+
Other model families (Titan, Llama, Mistral, etc.) pass through unchanged
|
|
10
|
+
with a debug log.
|
|
11
|
+
|
|
12
|
+
Usage
|
|
13
|
+
-----
|
|
14
|
+
::
|
|
15
|
+
|
|
16
|
+
from devtorch_core.wrapper.bedrock import DevTorchBedrock
|
|
17
|
+
|
|
18
|
+
client = DevTorchBedrock(region_name="us-east-1")
|
|
19
|
+
response = client.invoke_model(
|
|
20
|
+
modelId="anthropic.claude-opus-4-5-20240229-v1:0",
|
|
21
|
+
body=json.dumps({
|
|
22
|
+
"anthropic_version": "bedrock-2023-05-31",
|
|
23
|
+
"max_tokens": 1024,
|
|
24
|
+
"messages": [{"role": "user", "content": "Hello"}],
|
|
25
|
+
}),
|
|
26
|
+
contentType="application/json",
|
|
27
|
+
accept="application/json",
|
|
28
|
+
)
|
|
29
|
+
"""
|
|
30
|
+
from __future__ import annotations
|
|
31
|
+
|
|
32
|
+
import json
|
|
33
|
+
import logging
|
|
34
|
+
import uuid
|
|
35
|
+
from typing import Any
|
|
36
|
+
|
|
37
|
+
from .base import CaptureOrchestrator, RACPInjector, _reasoning_text, find_gcc_repo, is_disabled
|
|
38
|
+
|
|
39
|
+
logger = logging.getLogger("devtorch.wrapper.bedrock")
|
|
40
|
+
|
|
41
|
+
# ---------------------------------------------------------------------------
|
|
42
|
+
# Optional import
|
|
43
|
+
# ---------------------------------------------------------------------------
|
|
44
|
+
|
|
45
|
+
try:
|
|
46
|
+
import boto3 as _boto3 # type: ignore[import]
|
|
47
|
+
|
|
48
|
+
HAS_BOTO3 = True
|
|
49
|
+
except ImportError:
|
|
50
|
+
HAS_BOTO3 = False
|
|
51
|
+
_boto3 = None # type: ignore[assignment]
|
|
52
|
+
|
|
53
|
+
# Model ID prefixes that use the Anthropic Claude Bedrock body format
|
|
54
|
+
_ANTHROPIC_BEDROCK_PREFIXES = (
|
|
55
|
+
"anthropic.claude",
|
|
56
|
+
"us.anthropic.claude",
|
|
57
|
+
"eu.anthropic.claude",
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
# ---------------------------------------------------------------------------
|
|
62
|
+
# Wrapper
|
|
63
|
+
# ---------------------------------------------------------------------------
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class DevTorchBedrock:
|
|
67
|
+
"""
|
|
68
|
+
Wrapper around a ``boto3`` Bedrock runtime client.
|
|
69
|
+
|
|
70
|
+
Intercepts ``invoke_model`` for Anthropic Claude models to inject RACP
|
|
71
|
+
context and capture reasoning. All other calls are passed through
|
|
72
|
+
unchanged. Degrades gracefully if boto3 is not installed or .GCC/ is
|
|
73
|
+
missing.
|
|
74
|
+
"""
|
|
75
|
+
|
|
76
|
+
def __init__(
|
|
77
|
+
self,
|
|
78
|
+
*args: Any,
|
|
79
|
+
gcc_repo: Any = None,
|
|
80
|
+
service_name: str = "bedrock-runtime",
|
|
81
|
+
**kwargs: Any,
|
|
82
|
+
) -> None:
|
|
83
|
+
if not HAS_BOTO3:
|
|
84
|
+
raise ImportError(
|
|
85
|
+
"boto3 package not installed. "
|
|
86
|
+
"pip install devtorch[wrapper]"
|
|
87
|
+
)
|
|
88
|
+
self._client = _boto3.client(service_name, *args, **kwargs) # type: ignore[union-attr]
|
|
89
|
+
self._gcc = gcc_repo or find_gcc_repo()
|
|
90
|
+
self._injector = RACPInjector(self._gcc) if self._gcc else None
|
|
91
|
+
self._capture = CaptureOrchestrator(self._gcc) if self._gcc else None
|
|
92
|
+
|
|
93
|
+
# ------------------------------------------------------------------
|
|
94
|
+
# Intercepted method
|
|
95
|
+
# ------------------------------------------------------------------
|
|
96
|
+
|
|
97
|
+
def invoke_model(self, *, modelId: str, body: Any, **kwargs: Any) -> Any:
|
|
98
|
+
"""
|
|
99
|
+
Intercept invoke_model for Claude-via-Bedrock calls.
|
|
100
|
+
|
|
101
|
+
1. Decode body JSON.
|
|
102
|
+
2. If Anthropic Claude model: inject RACP into ``system`` field.
|
|
103
|
+
3. Call real client.
|
|
104
|
+
4. Decode response body and capture.
|
|
105
|
+
5. Return raw boto3 response (stream-compatible).
|
|
106
|
+
"""
|
|
107
|
+
session_id = str(uuid.uuid4())[:8]
|
|
108
|
+
call_id = str(uuid.uuid4())
|
|
109
|
+
is_anthropic = any(
|
|
110
|
+
modelId.startswith(pfx) for pfx in _ANTHROPIC_BEDROCK_PREFIXES
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
# Parse body for prompt text and possible injection
|
|
114
|
+
body_dict = self._parse_body(body)
|
|
115
|
+
prompt_text = _extract_prompt_text(body_dict.get("messages", []))
|
|
116
|
+
|
|
117
|
+
if is_anthropic and not is_disabled():
|
|
118
|
+
body = self._inject_anthropic(body_dict, prompt_text, session_id, call_id, modelId)
|
|
119
|
+
elif not is_anthropic:
|
|
120
|
+
logger.debug(
|
|
121
|
+
"devtorch: Bedrock model %s is not Anthropic Claude — "
|
|
122
|
+
"passing through without RACP injection",
|
|
123
|
+
modelId,
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
# Call the real Bedrock client
|
|
127
|
+
try:
|
|
128
|
+
response = self._client.invoke_model(modelId=modelId, body=body, **kwargs)
|
|
129
|
+
except Exception:
|
|
130
|
+
self._record_llm_outcome(
|
|
131
|
+
prompt_text=prompt_text,
|
|
132
|
+
response_text="",
|
|
133
|
+
thinking_blocks=[],
|
|
134
|
+
session_id=session_id,
|
|
135
|
+
call_id=call_id,
|
|
136
|
+
model_name=modelId,
|
|
137
|
+
outcome="failure",
|
|
138
|
+
)
|
|
139
|
+
raise
|
|
140
|
+
|
|
141
|
+
# Capture response for Anthropic models
|
|
142
|
+
if is_anthropic:
|
|
143
|
+
text, thinking_blocks = self._capture_response(response, session_id)
|
|
144
|
+
self._record_llm_outcome(
|
|
145
|
+
prompt_text=prompt_text,
|
|
146
|
+
response_text=text,
|
|
147
|
+
thinking_blocks=thinking_blocks,
|
|
148
|
+
session_id=session_id,
|
|
149
|
+
call_id=call_id,
|
|
150
|
+
model_name=modelId,
|
|
151
|
+
outcome="success",
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
return response
|
|
155
|
+
|
|
156
|
+
# ------------------------------------------------------------------
|
|
157
|
+
# Injection
|
|
158
|
+
# ------------------------------------------------------------------
|
|
159
|
+
|
|
160
|
+
def _parse_body(self, body: Any) -> dict:
|
|
161
|
+
"""Best-effort parse of the Bedrock request body into a dict."""
|
|
162
|
+
try:
|
|
163
|
+
if isinstance(body, (bytes, bytearray)):
|
|
164
|
+
return json.loads(body.decode("utf-8"))
|
|
165
|
+
elif isinstance(body, str):
|
|
166
|
+
return json.loads(body)
|
|
167
|
+
elif isinstance(body, dict):
|
|
168
|
+
return dict(body)
|
|
169
|
+
except Exception:
|
|
170
|
+
pass
|
|
171
|
+
return {}
|
|
172
|
+
|
|
173
|
+
def _inject_anthropic(
|
|
174
|
+
self,
|
|
175
|
+
body_dict: dict,
|
|
176
|
+
prompt_text: str = "",
|
|
177
|
+
session_id: str = "",
|
|
178
|
+
call_id: str = "",
|
|
179
|
+
model_name: str = "",
|
|
180
|
+
) -> str:
|
|
181
|
+
"""Inject RACP prefix into the ``system`` field of an Anthropic body."""
|
|
182
|
+
if not self._injector:
|
|
183
|
+
return json.dumps(body_dict)
|
|
184
|
+
|
|
185
|
+
try:
|
|
186
|
+
prefix, _ = self._injector.build_system_prefix_with_provenance(
|
|
187
|
+
prompt_text=prompt_text or None,
|
|
188
|
+
session_id=session_id,
|
|
189
|
+
call_id=call_id,
|
|
190
|
+
model_name=model_name,
|
|
191
|
+
)
|
|
192
|
+
if prefix:
|
|
193
|
+
existing_system = body_dict.get("system", "")
|
|
194
|
+
body_dict["system"] = (
|
|
195
|
+
(prefix + "\n\n" + existing_system).strip()
|
|
196
|
+
if existing_system
|
|
197
|
+
else prefix
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
return json.dumps(body_dict)
|
|
201
|
+
except Exception as exc:
|
|
202
|
+
logger.warning("devtorch: Bedrock RACP injection failed — %s", exc)
|
|
203
|
+
return json.dumps(body_dict)
|
|
204
|
+
|
|
205
|
+
# ------------------------------------------------------------------
|
|
206
|
+
# Capture
|
|
207
|
+
# ------------------------------------------------------------------
|
|
208
|
+
|
|
209
|
+
def _capture_response(self, response: Any, session_id: str) -> tuple[str, list]:
|
|
210
|
+
text = ""
|
|
211
|
+
thinking: list = []
|
|
212
|
+
if not self._capture or is_disabled():
|
|
213
|
+
return text, thinking
|
|
214
|
+
try:
|
|
215
|
+
# Read and re-inject body stream
|
|
216
|
+
body_stream = response.get("body")
|
|
217
|
+
if body_stream is None:
|
|
218
|
+
return text, thinking
|
|
219
|
+
raw = body_stream.read()
|
|
220
|
+
response_dict = json.loads(raw.decode("utf-8"))
|
|
221
|
+
|
|
222
|
+
# Extract text from Anthropic Bedrock response format
|
|
223
|
+
for block in response_dict.get("content", []):
|
|
224
|
+
if isinstance(block, dict) and block.get("type") == "text":
|
|
225
|
+
text += block.get("text", "")
|
|
226
|
+
|
|
227
|
+
# Extract thinking blocks
|
|
228
|
+
try:
|
|
229
|
+
from devtorch_core.parser.thinking import extract_thinking_blocks
|
|
230
|
+
|
|
231
|
+
thinking = extract_thinking_blocks(response_dict)
|
|
232
|
+
except Exception:
|
|
233
|
+
pass
|
|
234
|
+
|
|
235
|
+
self._capture.capture(text, thinking, session_id)
|
|
236
|
+
|
|
237
|
+
# Strip inline <thinking> blocks from the response body before re-wrapping
|
|
238
|
+
try:
|
|
239
|
+
from devtorch_core.reasoning_plus.capture import strip_inline_thinking
|
|
240
|
+
|
|
241
|
+
for block in response_dict.get("content", []):
|
|
242
|
+
if isinstance(block, dict) and block.get("type") == "text":
|
|
243
|
+
block["text"] = strip_inline_thinking(block.get("text", ""))
|
|
244
|
+
raw = json.dumps(response_dict).encode("utf-8")
|
|
245
|
+
except Exception as exc:
|
|
246
|
+
logger.warning("devtorch: Bedrock strip thinking failed — %s", exc)
|
|
247
|
+
|
|
248
|
+
# Wrap body bytes back into a StreamingBody-compatible object
|
|
249
|
+
# so the caller can still read the response
|
|
250
|
+
import io
|
|
251
|
+
|
|
252
|
+
response["body"] = _BytesBodyWrapper(raw)
|
|
253
|
+
except Exception as exc:
|
|
254
|
+
logger.warning("devtorch: Bedrock _capture_response failed — %s", exc)
|
|
255
|
+
return text, thinking
|
|
256
|
+
|
|
257
|
+
def _record_llm_outcome(
|
|
258
|
+
self,
|
|
259
|
+
*,
|
|
260
|
+
prompt_text: str,
|
|
261
|
+
response_text: str,
|
|
262
|
+
thinking_blocks: list,
|
|
263
|
+
session_id: str,
|
|
264
|
+
call_id: str,
|
|
265
|
+
model_name: str,
|
|
266
|
+
outcome: str,
|
|
267
|
+
) -> None:
|
|
268
|
+
if not self._capture or is_disabled():
|
|
269
|
+
return
|
|
270
|
+
try:
|
|
271
|
+
reasoning = _reasoning_text(thinking_blocks)
|
|
272
|
+
self._capture.record_learning_call(
|
|
273
|
+
call_type="llm",
|
|
274
|
+
reasoning=reasoning,
|
|
275
|
+
session_id=session_id,
|
|
276
|
+
call_id=call_id,
|
|
277
|
+
name=model_name,
|
|
278
|
+
input=prompt_text,
|
|
279
|
+
output=response_text,
|
|
280
|
+
outcome=outcome,
|
|
281
|
+
)
|
|
282
|
+
except Exception as exc:
|
|
283
|
+
logger.warning("devtorch: _record_llm_outcome failed — %s", exc)
|
|
284
|
+
|
|
285
|
+
# ------------------------------------------------------------------
|
|
286
|
+
# Proxy everything else to the underlying boto3 client
|
|
287
|
+
# ------------------------------------------------------------------
|
|
288
|
+
|
|
289
|
+
def __getattr__(self, name: str) -> Any:
|
|
290
|
+
return getattr(self._client, name)
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
# ---------------------------------------------------------------------------
|
|
294
|
+
# Internal helpers
|
|
295
|
+
# ---------------------------------------------------------------------------
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
def _extract_prompt_text(messages: list) -> str:
|
|
299
|
+
"""Best-effort concatenation of user-visible message text for smart context."""
|
|
300
|
+
parts: list[str] = []
|
|
301
|
+
for m in messages or []:
|
|
302
|
+
if isinstance(m, dict):
|
|
303
|
+
role = m.get("role", "")
|
|
304
|
+
content = m.get("content", "")
|
|
305
|
+
if role in ("user", "system") and content:
|
|
306
|
+
if isinstance(content, str):
|
|
307
|
+
parts.append(content)
|
|
308
|
+
elif isinstance(content, list):
|
|
309
|
+
for item in content:
|
|
310
|
+
if isinstance(item, dict) and item.get("type") == "text":
|
|
311
|
+
parts.append(item.get("text", ""))
|
|
312
|
+
elif isinstance(item, str):
|
|
313
|
+
parts.append(item)
|
|
314
|
+
elif hasattr(m, "role") and hasattr(m, "content"):
|
|
315
|
+
role = getattr(m, "role", "")
|
|
316
|
+
content = getattr(m, "content", "")
|
|
317
|
+
if role in ("user", "system") and content:
|
|
318
|
+
parts.append(str(content))
|
|
319
|
+
return "\n".join(parts)[:4000]
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
# ---------------------------------------------------------------------------
|
|
323
|
+
# Helper: re-wrap consumed body bytes
|
|
324
|
+
# ---------------------------------------------------------------------------
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
class _BytesBodyWrapper:
|
|
328
|
+
"""
|
|
329
|
+
Minimal wrapper that restores a boto3-style StreamingBody after we've
|
|
330
|
+
read all bytes for capture.
|
|
331
|
+
"""
|
|
332
|
+
|
|
333
|
+
def __init__(self, data: bytes) -> None:
|
|
334
|
+
import io
|
|
335
|
+
|
|
336
|
+
self._stream = io.BytesIO(data)
|
|
337
|
+
|
|
338
|
+
def read(self, amt: int = -1) -> bytes: # type: ignore[override]
|
|
339
|
+
return self._stream.read() if amt == -1 else self._stream.read(amt)
|
|
340
|
+
|
|
341
|
+
def __iter__(self) -> Any:
|
|
342
|
+
return iter(self._stream)
|