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