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.
Files changed (193) hide show
  1. devtorch_core/__init__.py +158 -0
  2. devtorch_core/aggphi_textual.py +275 -0
  3. devtorch_core/alerts/__init__.py +23 -0
  4. devtorch_core/alerts/base.py +46 -0
  5. devtorch_core/alerts/config.py +60 -0
  6. devtorch_core/alerts/dispatcher.py +110 -0
  7. devtorch_core/alerts/jira.py +96 -0
  8. devtorch_core/alerts/linear.py +72 -0
  9. devtorch_core/alerts/pagerduty.py +66 -0
  10. devtorch_core/alerts/slack.py +81 -0
  11. devtorch_core/alerts/teams.py +70 -0
  12. devtorch_core/audit/__init__.py +43 -0
  13. devtorch_core/audit/exporter.py +297 -0
  14. devtorch_core/audit/privacy.py +101 -0
  15. devtorch_core/audit/scrubber.py +149 -0
  16. devtorch_core/audit/service.py +67 -0
  17. devtorch_core/audit/signing.py +127 -0
  18. devtorch_core/broadcast/__init__.py +4 -0
  19. devtorch_core/broadcast/broadcaster.py +100 -0
  20. devtorch_core/broadcast/watcher.py +71 -0
  21. devtorch_core/capability.py +639 -0
  22. devtorch_core/cloud/__init__.py +1 -0
  23. devtorch_core/cloud/client_config.py +472 -0
  24. devtorch_core/cloud/client_configs/.claude-opencode-fallback.json +8 -0
  25. devtorch_core/cloud/client_configs/.claude-stdio.json +13 -0
  26. devtorch_core/cloud/client_configs/.cursor-mcp.json +13 -0
  27. devtorch_core/cloud/client_configs/.opencode-bridge.json +13 -0
  28. devtorch_core/cloud/client_configs/.opencode.json +15 -0
  29. devtorch_core/cloud/client_configs/.vscode-mcp.json +13 -0
  30. devtorch_core/cloud/devtorch-mcp-bridge.js +357 -0
  31. devtorch_core/cloud/mcp_client.py +229 -0
  32. devtorch_core/cloud/setup.py +144 -0
  33. devtorch_core/cloud/sync.py +143 -0
  34. devtorch_core/cloud/sync_bundle.py +603 -0
  35. devtorch_core/cloud/sync_conflicts.py +159 -0
  36. devtorch_core/cloud/sync_state.py +159 -0
  37. devtorch_core/cloud/team_sync.py +283 -0
  38. devtorch_core/codex/__init__.py +9 -0
  39. devtorch_core/codex/__main__.py +97 -0
  40. devtorch_core/codex/capture.py +208 -0
  41. devtorch_core/codex/proxy.py +412 -0
  42. devtorch_core/concept_catalog.py +209 -0
  43. devtorch_core/consolidation/__init__.py +3 -0
  44. devtorch_core/consolidation/synthesizer.py +87 -0
  45. devtorch_core/consolidation/workflow.py +175 -0
  46. devtorch_core/daemon/__init__.py +27 -0
  47. devtorch_core/daemon/supervisor.py +293 -0
  48. devtorch_core/daemon/watcher.py +244 -0
  49. devtorch_core/dashboard_api.py +2012 -0
  50. devtorch_core/deltaf.py +97 -0
  51. devtorch_core/disclosure.py +50 -0
  52. devtorch_core/divergence/__init__.py +3 -0
  53. devtorch_core/divergence/detector.py +166 -0
  54. devtorch_core/gateway/__init__.py +32 -0
  55. devtorch_core/gateway/key_manager.py +124 -0
  56. devtorch_core/gateway/metrics_webhook.py +252 -0
  57. devtorch_core/gateway/policy.py +262 -0
  58. devtorch_core/gateway/server.py +727 -0
  59. devtorch_core/gateway/sso.py +233 -0
  60. devtorch_core/gcc.py +1246 -0
  61. devtorch_core/github/__init__.py +35 -0
  62. devtorch_core/github/app.py +240 -0
  63. devtorch_core/github/comment_builder.py +113 -0
  64. devtorch_core/github/pat.py +76 -0
  65. devtorch_core/github/pr_parser.py +82 -0
  66. devtorch_core/github/pr_reporter.py +555 -0
  67. devtorch_core/gitlab/__init__.py +177 -0
  68. devtorch_core/hitl/__init__.py +4 -0
  69. devtorch_core/hitl/channels.py +129 -0
  70. devtorch_core/hitl/orchestrator.py +95 -0
  71. devtorch_core/hooks/__init__.py +17 -0
  72. devtorch_core/hooks/claude_code.py +228 -0
  73. devtorch_core/hooks/git_capture.py +341 -0
  74. devtorch_core/hooks/git_commit.py +182 -0
  75. devtorch_core/hooks/installer.py +733 -0
  76. devtorch_core/hooks/pre_commit.py +157 -0
  77. devtorch_core/hooks/runner.py +344 -0
  78. devtorch_core/identity/__init__.py +4 -0
  79. devtorch_core/identity/agent.py +86 -0
  80. devtorch_core/identity/providers.py +85 -0
  81. devtorch_core/invariants.py +182 -0
  82. devtorch_core/mcp/__init__.py +10 -0
  83. devtorch_core/mcp/auth.py +177 -0
  84. devtorch_core/mcp/server.py +1049 -0
  85. devtorch_core/metrics/__init__.py +35 -0
  86. devtorch_core/metrics/aggregate.py +215 -0
  87. devtorch_core/metrics/calibrate.py +198 -0
  88. devtorch_core/metrics/calibration.py +125 -0
  89. devtorch_core/metrics/credibility.py +288 -0
  90. devtorch_core/metrics/delivery_time.py +70 -0
  91. devtorch_core/metrics/dhs.py +126 -0
  92. devtorch_core/metrics/mcs.py +96 -0
  93. devtorch_core/metrics/roi.py +88 -0
  94. devtorch_core/metrics/session_writer.py +81 -0
  95. devtorch_core/metrics/shadow_ai.py +117 -0
  96. devtorch_core/metrics/sprint_writer.py +243 -0
  97. devtorch_core/observability/__init__.py +78 -0
  98. devtorch_core/observability/datadog.py +157 -0
  99. devtorch_core/observability/formatter.py +119 -0
  100. devtorch_core/observability/report.py +264 -0
  101. devtorch_core/observability/servicenow.py +147 -0
  102. devtorch_core/observability/splunk.py +218 -0
  103. devtorch_core/observability/webhook.py +227 -0
  104. devtorch_core/parser/__init__.py +30 -0
  105. devtorch_core/parser/blocks.py +216 -0
  106. devtorch_core/parser/inference.py +159 -0
  107. devtorch_core/parser/thinking.py +112 -0
  108. devtorch_core/projects.py +169 -0
  109. devtorch_core/prompt_artifact.py +76 -0
  110. devtorch_core/proxy/__init__.py +9 -0
  111. devtorch_core/proxy/routes/__init__.py +1 -0
  112. devtorch_core/proxy/routes/anthropic.py +264 -0
  113. devtorch_core/proxy/routes/azure_openai.py +336 -0
  114. devtorch_core/proxy/routes/gemini.py +331 -0
  115. devtorch_core/proxy/routes/groq.py +284 -0
  116. devtorch_core/proxy/routes/ollama.py +279 -0
  117. devtorch_core/proxy/routes/openai.py +287 -0
  118. devtorch_core/proxy/server.py +356 -0
  119. devtorch_core/query/__init__.py +15 -0
  120. devtorch_core/query/grep.py +181 -0
  121. devtorch_core/query/hybrid.py +86 -0
  122. devtorch_core/query/semantic.py +157 -0
  123. devtorch_core/rdp.py +105 -0
  124. devtorch_core/reasoning/__init__.py +4 -0
  125. devtorch_core/reasoning/entry.py +31 -0
  126. devtorch_core/reasoning/store.py +122 -0
  127. devtorch_core/reasoning_plus/__init__.py +70 -0
  128. devtorch_core/reasoning_plus/augmenter.py +326 -0
  129. devtorch_core/reasoning_plus/capture.py +51 -0
  130. devtorch_core/reasoning_plus/config.py +256 -0
  131. devtorch_core/reasoning_plus/context.py +262 -0
  132. devtorch_core/reasoning_plus/learning/__init__.py +72 -0
  133. devtorch_core/reasoning_plus/learning/analytics.py +141 -0
  134. devtorch_core/reasoning_plus/learning/api.py +313 -0
  135. devtorch_core/reasoning_plus/learning/chain.py +285 -0
  136. devtorch_core/reasoning_plus/learning/composer.py +74 -0
  137. devtorch_core/reasoning_plus/learning/cross_project.py +234 -0
  138. devtorch_core/reasoning_plus/learning/embeddings.py +209 -0
  139. devtorch_core/reasoning_plus/learning/extractor.py +207 -0
  140. devtorch_core/reasoning_plus/learning/models.py +116 -0
  141. devtorch_core/reasoning_plus/learning/provenance.py +126 -0
  142. devtorch_core/reasoning_plus/learning/recorder.py +81 -0
  143. devtorch_core/reasoning_plus/learning/relevance.py +122 -0
  144. devtorch_core/reasoning_plus/learning/state.py +86 -0
  145. devtorch_core/reasoning_plus/learning/store.py +160 -0
  146. devtorch_core/reasoning_plus/learning/theta_learning_bridge.py +94 -0
  147. devtorch_core/reasoning_plus/prompt.py +90 -0
  148. devtorch_core/rep.py +134 -0
  149. devtorch_core/rep_network/__init__.py +25 -0
  150. devtorch_core/rep_network/merge.py +70 -0
  151. devtorch_core/rep_network/node.py +137 -0
  152. devtorch_core/rep_network/server.py +140 -0
  153. devtorch_core/rep_network/sync.py +207 -0
  154. devtorch_core/sensitivity.py +182 -0
  155. devtorch_core/serve.py +258 -0
  156. devtorch_core/session/__init__.py +39 -0
  157. devtorch_core/session/disagreement.py +188 -0
  158. devtorch_core/session/models.py +114 -0
  159. devtorch_core/session/orchestrator.py +182 -0
  160. devtorch_core/session/planner.py +169 -0
  161. devtorch_core/session/simulator.py +132 -0
  162. devtorch_core/signing.py +290 -0
  163. devtorch_core/sis.py +197 -0
  164. devtorch_core/storage.py +308 -0
  165. devtorch_core/templates/__init__.py +6 -0
  166. devtorch_core/templates/engine.py +122 -0
  167. devtorch_core/templates/go.py +18 -0
  168. devtorch_core/templates/infra.py +19 -0
  169. devtorch_core/templates/library/__init__.py +18 -0
  170. devtorch_core/templates/library/api_design.md +27 -0
  171. devtorch_core/templates/library/bug_fix.md +27 -0
  172. devtorch_core/templates/library/decision_record.md +27 -0
  173. devtorch_core/templates/library/engine.py +228 -0
  174. devtorch_core/templates/library/security_review.md +30 -0
  175. devtorch_core/templates/python.py +19 -0
  176. devtorch_core/templates/react.py +18 -0
  177. devtorch_core/templates/typescript.py +18 -0
  178. devtorch_core/theta.py +221 -0
  179. devtorch_core/theta_synthesis.py +268 -0
  180. devtorch_core/topics.py +320 -0
  181. devtorch_core/variance.py +219 -0
  182. devtorch_core/wrapper/__init__.py +52 -0
  183. devtorch_core/wrapper/anthropic.py +487 -0
  184. devtorch_core/wrapper/base.py +562 -0
  185. devtorch_core/wrapper/bedrock.py +342 -0
  186. devtorch_core/wrapper/gemini.py +422 -0
  187. devtorch_core/wrapper/ollama.py +527 -0
  188. devtorch_core/wrapper/openai.py +461 -0
  189. devtorch_core-3.0.1.dist-info/METADATA +867 -0
  190. devtorch_core-3.0.1.dist-info/RECORD +193 -0
  191. devtorch_core-3.0.1.dist-info/WHEEL +5 -0
  192. devtorch_core-3.0.1.dist-info/entry_points.txt +2 -0
  193. 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]