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,422 @@
|
|
|
1
|
+
"""
|
|
2
|
+
devtorch_core.wrapper.gemini
|
|
3
|
+
=============================
|
|
4
|
+
Wrapper around ``google.generativeai`` that injects RACP context and
|
|
5
|
+
captures LLM reasoning to .GCC/.
|
|
6
|
+
|
|
7
|
+
Because the Gemini API does not expose a separate ``system`` parameter in the
|
|
8
|
+
same way Anthropic/OpenAI do, the RACP prefix is prepended to the text of
|
|
9
|
+
the first user message part instead.
|
|
10
|
+
|
|
11
|
+
Usage
|
|
12
|
+
-----
|
|
13
|
+
::
|
|
14
|
+
|
|
15
|
+
from devtorch_core.wrapper.gemini import DevTorchGemini
|
|
16
|
+
|
|
17
|
+
client = DevTorchGemini(model_name="gemini-1.5-pro")
|
|
18
|
+
response = client.generate_content(
|
|
19
|
+
[{"role": "user", "parts": [{"text": "Hello"}]}]
|
|
20
|
+
)
|
|
21
|
+
"""
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
import logging
|
|
25
|
+
import uuid
|
|
26
|
+
from typing import Any, Iterator
|
|
27
|
+
|
|
28
|
+
from .base import CaptureOrchestrator, RACPInjector, _reasoning_text, find_gcc_repo, is_disabled
|
|
29
|
+
|
|
30
|
+
logger = logging.getLogger("devtorch.wrapper.gemini")
|
|
31
|
+
|
|
32
|
+
# ---------------------------------------------------------------------------
|
|
33
|
+
# Optional import
|
|
34
|
+
# ---------------------------------------------------------------------------
|
|
35
|
+
|
|
36
|
+
try:
|
|
37
|
+
import google.generativeai as _genai # type: ignore[import]
|
|
38
|
+
|
|
39
|
+
HAS_GEMINI = True
|
|
40
|
+
except ImportError:
|
|
41
|
+
HAS_GEMINI = False
|
|
42
|
+
_genai = None # type: ignore[assignment]
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
# ---------------------------------------------------------------------------
|
|
46
|
+
# Wrapper
|
|
47
|
+
# ---------------------------------------------------------------------------
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class DevTorchGemini:
|
|
51
|
+
"""
|
|
52
|
+
Wrapper around ``google.generativeai.GenerativeModel``.
|
|
53
|
+
|
|
54
|
+
Provides a ``generate_content`` method that injects RACP context into the
|
|
55
|
+
first user message and captures the response to .GCC/.
|
|
56
|
+
"""
|
|
57
|
+
|
|
58
|
+
def __init__(
|
|
59
|
+
self,
|
|
60
|
+
*args: Any,
|
|
61
|
+
model_name: str = "gemini-1.5-pro",
|
|
62
|
+
gcc_repo: Any = None,
|
|
63
|
+
**kwargs: Any,
|
|
64
|
+
) -> None:
|
|
65
|
+
if not HAS_GEMINI:
|
|
66
|
+
raise ImportError(
|
|
67
|
+
"google-generativeai package not installed. "
|
|
68
|
+
"pip install devtorch[wrapper]"
|
|
69
|
+
)
|
|
70
|
+
self._model = _genai.GenerativeModel(model_name, **kwargs) # type: ignore[union-attr]
|
|
71
|
+
self._model_name = model_name
|
|
72
|
+
self._gcc = gcc_repo or find_gcc_repo()
|
|
73
|
+
self._injector = RACPInjector(self._gcc) if self._gcc else None
|
|
74
|
+
self._capture = CaptureOrchestrator(self._gcc) if self._gcc else None
|
|
75
|
+
|
|
76
|
+
# ------------------------------------------------------------------
|
|
77
|
+
# Public API
|
|
78
|
+
# ------------------------------------------------------------------
|
|
79
|
+
|
|
80
|
+
def generate_content(
|
|
81
|
+
self,
|
|
82
|
+
messages: list,
|
|
83
|
+
stream: bool = False,
|
|
84
|
+
**kwargs: Any,
|
|
85
|
+
) -> Any:
|
|
86
|
+
"""
|
|
87
|
+
Inject RACP prefix into the first user message part, call the
|
|
88
|
+
GenerativeModel, and capture the response.
|
|
89
|
+
|
|
90
|
+
If ``stream=True`` yields chunks while accumulating the full text
|
|
91
|
+
for capture.
|
|
92
|
+
|
|
93
|
+
DevTorch errors never propagate to the caller.
|
|
94
|
+
"""
|
|
95
|
+
session_id = str(uuid.uuid4())[:8]
|
|
96
|
+
call_id = str(uuid.uuid4())
|
|
97
|
+
prompt_text = _extract_prompt_text(messages)
|
|
98
|
+
injected_messages = self._inject(list(messages), prompt_text, session_id, call_id, self._model_name)
|
|
99
|
+
|
|
100
|
+
if stream:
|
|
101
|
+
return self._stream_generate(
|
|
102
|
+
injected_messages, session_id,
|
|
103
|
+
call_id=call_id,
|
|
104
|
+
prompt_text=prompt_text,
|
|
105
|
+
model_name=self._model_name,
|
|
106
|
+
**kwargs,
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
# Non-streaming
|
|
110
|
+
try:
|
|
111
|
+
response = self._model.generate_content(injected_messages, **kwargs)
|
|
112
|
+
except Exception:
|
|
113
|
+
self._record_llm_outcome(
|
|
114
|
+
prompt_text=prompt_text,
|
|
115
|
+
response_text="",
|
|
116
|
+
thinking_blocks=[],
|
|
117
|
+
session_id=session_id,
|
|
118
|
+
call_id=call_id,
|
|
119
|
+
model_name=self._model_name,
|
|
120
|
+
outcome="failure",
|
|
121
|
+
)
|
|
122
|
+
raise
|
|
123
|
+
|
|
124
|
+
text, thinking_blocks = self._capture_response(response, session_id)
|
|
125
|
+
self._record_llm_outcome(
|
|
126
|
+
prompt_text=prompt_text,
|
|
127
|
+
response_text=text,
|
|
128
|
+
thinking_blocks=thinking_blocks,
|
|
129
|
+
session_id=session_id,
|
|
130
|
+
call_id=call_id,
|
|
131
|
+
model_name=self._model_name,
|
|
132
|
+
outcome="success",
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
# Strip RACP blocks from text parts
|
|
136
|
+
try:
|
|
137
|
+
response = self._strip_response(response)
|
|
138
|
+
except Exception as exc:
|
|
139
|
+
logger.warning("devtorch: strip_response failed — %s", exc)
|
|
140
|
+
|
|
141
|
+
return response
|
|
142
|
+
|
|
143
|
+
# ------------------------------------------------------------------
|
|
144
|
+
# Streaming helper
|
|
145
|
+
# ------------------------------------------------------------------
|
|
146
|
+
|
|
147
|
+
def _stream_generate(
|
|
148
|
+
self,
|
|
149
|
+
messages: list,
|
|
150
|
+
session_id: str,
|
|
151
|
+
call_id: str = "",
|
|
152
|
+
prompt_text: str = "",
|
|
153
|
+
model_name: str = "",
|
|
154
|
+
**kwargs: Any,
|
|
155
|
+
) -> "_GeminiStreamingWrapper":
|
|
156
|
+
raw_stream = self._model.generate_content(messages, stream=True, **kwargs)
|
|
157
|
+
return _GeminiStreamingWrapper(
|
|
158
|
+
raw_stream, self, session_id,
|
|
159
|
+
call_id=call_id,
|
|
160
|
+
prompt_text=prompt_text,
|
|
161
|
+
model_name=model_name,
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
# ------------------------------------------------------------------
|
|
165
|
+
# Injection
|
|
166
|
+
# ------------------------------------------------------------------
|
|
167
|
+
|
|
168
|
+
def _inject(
|
|
169
|
+
self,
|
|
170
|
+
messages: list,
|
|
171
|
+
prompt_text: str = "",
|
|
172
|
+
session_id: str = "",
|
|
173
|
+
call_id: str = "",
|
|
174
|
+
model_name: str = "",
|
|
175
|
+
) -> list:
|
|
176
|
+
"""Prepend RACP prefix to the first user message's first text part."""
|
|
177
|
+
if not self._injector or is_disabled():
|
|
178
|
+
return messages
|
|
179
|
+
|
|
180
|
+
try:
|
|
181
|
+
prefix, _ = self._injector.build_system_prefix_with_provenance(
|
|
182
|
+
prompt_text=prompt_text or None,
|
|
183
|
+
session_id=session_id,
|
|
184
|
+
call_id=call_id,
|
|
185
|
+
model_name=model_name,
|
|
186
|
+
)
|
|
187
|
+
if not prefix:
|
|
188
|
+
return messages
|
|
189
|
+
if (
|
|
190
|
+
messages
|
|
191
|
+
and isinstance(messages[0], dict)
|
|
192
|
+
and "parts" in messages[0]
|
|
193
|
+
and messages[0]["parts"]
|
|
194
|
+
):
|
|
195
|
+
first_part = messages[0]["parts"][0]
|
|
196
|
+
if isinstance(first_part, dict) and "text" in first_part:
|
|
197
|
+
messages[0]["parts"][0]["text"] = (
|
|
198
|
+
prefix + "\n\n" + first_part["text"]
|
|
199
|
+
)
|
|
200
|
+
elif isinstance(first_part, str):
|
|
201
|
+
messages[0]["parts"][0] = prefix + "\n\n" + first_part
|
|
202
|
+
except Exception as exc:
|
|
203
|
+
logger.warning("devtorch: RACP injection failed — %s", exc)
|
|
204
|
+
|
|
205
|
+
return messages
|
|
206
|
+
|
|
207
|
+
# ------------------------------------------------------------------
|
|
208
|
+
# Capture
|
|
209
|
+
# ------------------------------------------------------------------
|
|
210
|
+
|
|
211
|
+
def _capture_response(self, response: Any, session_id: str) -> tuple[str, list]:
|
|
212
|
+
text = ""
|
|
213
|
+
thinking: list = []
|
|
214
|
+
if not self._capture or is_disabled():
|
|
215
|
+
return text, thinking
|
|
216
|
+
try:
|
|
217
|
+
text = _extract_text(response)
|
|
218
|
+
self._capture.capture(text, thinking, session_id)
|
|
219
|
+
except Exception as exc:
|
|
220
|
+
logger.warning("devtorch: _capture_response failed — %s", exc)
|
|
221
|
+
return text, thinking
|
|
222
|
+
|
|
223
|
+
def _record_llm_outcome(
|
|
224
|
+
self,
|
|
225
|
+
*,
|
|
226
|
+
prompt_text: str,
|
|
227
|
+
response_text: str,
|
|
228
|
+
thinking_blocks: list,
|
|
229
|
+
session_id: str,
|
|
230
|
+
call_id: str,
|
|
231
|
+
model_name: str,
|
|
232
|
+
outcome: str,
|
|
233
|
+
) -> None:
|
|
234
|
+
if not self._capture or is_disabled():
|
|
235
|
+
return
|
|
236
|
+
try:
|
|
237
|
+
reasoning = _reasoning_text(thinking_blocks)
|
|
238
|
+
self._capture.record_learning_call(
|
|
239
|
+
call_type="llm",
|
|
240
|
+
reasoning=reasoning,
|
|
241
|
+
session_id=session_id,
|
|
242
|
+
call_id=call_id,
|
|
243
|
+
name=model_name,
|
|
244
|
+
input=prompt_text,
|
|
245
|
+
output=response_text,
|
|
246
|
+
outcome=outcome,
|
|
247
|
+
)
|
|
248
|
+
except Exception as exc:
|
|
249
|
+
logger.warning("devtorch: _record_llm_outcome failed — %s", exc)
|
|
250
|
+
|
|
251
|
+
# ------------------------------------------------------------------
|
|
252
|
+
# Strip
|
|
253
|
+
# ------------------------------------------------------------------
|
|
254
|
+
|
|
255
|
+
def _strip_response(self, response: Any) -> Any:
|
|
256
|
+
try:
|
|
257
|
+
from devtorch_core.parser.blocks import strip_racp_blocks
|
|
258
|
+
from devtorch_core.reasoning_plus.capture import strip_inline_thinking
|
|
259
|
+
|
|
260
|
+
# Gemini response.text is a property; try to mutate via parts
|
|
261
|
+
candidates = getattr(response, "candidates", None)
|
|
262
|
+
if candidates:
|
|
263
|
+
for candidate in candidates:
|
|
264
|
+
content = getattr(candidate, "content", None)
|
|
265
|
+
if content:
|
|
266
|
+
parts = getattr(content, "parts", None)
|
|
267
|
+
if parts:
|
|
268
|
+
for part in parts:
|
|
269
|
+
if hasattr(part, "text") and isinstance(part.text, str):
|
|
270
|
+
part.text = strip_inline_thinking(strip_racp_blocks(part.text))
|
|
271
|
+
except Exception as exc:
|
|
272
|
+
logger.warning("devtorch: strip_response failed — %s", exc)
|
|
273
|
+
return response
|
|
274
|
+
|
|
275
|
+
# Proxy everything else to the underlying model
|
|
276
|
+
def __getattr__(self, name: str) -> Any:
|
|
277
|
+
return getattr(self._model, name)
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
# ---------------------------------------------------------------------------
|
|
281
|
+
# Streaming wrapper
|
|
282
|
+
# ---------------------------------------------------------------------------
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
class _GeminiStreamingWrapper:
|
|
286
|
+
"""
|
|
287
|
+
Thin wrapper around the raw Gemini streaming response.
|
|
288
|
+
"""
|
|
289
|
+
|
|
290
|
+
def __init__(
|
|
291
|
+
self,
|
|
292
|
+
raw_stream: Any,
|
|
293
|
+
wrapper: "DevTorchGemini",
|
|
294
|
+
session_id: str,
|
|
295
|
+
call_id: str = "",
|
|
296
|
+
prompt_text: str = "",
|
|
297
|
+
model_name: str = "",
|
|
298
|
+
) -> None:
|
|
299
|
+
self._raw = raw_stream
|
|
300
|
+
self._wrapper = wrapper
|
|
301
|
+
self._session_id = session_id
|
|
302
|
+
self._call_id = call_id
|
|
303
|
+
self._prompt_text = prompt_text
|
|
304
|
+
self._model_name = model_name
|
|
305
|
+
self._accumulated_text = ""
|
|
306
|
+
self._recorded = False
|
|
307
|
+
|
|
308
|
+
def __iter__(self) -> Iterator[Any]:
|
|
309
|
+
try:
|
|
310
|
+
for chunk in self._raw:
|
|
311
|
+
try:
|
|
312
|
+
chunk_text = _extract_text(chunk)
|
|
313
|
+
self._accumulated_text += chunk_text
|
|
314
|
+
except Exception:
|
|
315
|
+
pass
|
|
316
|
+
yield chunk
|
|
317
|
+
except Exception:
|
|
318
|
+
self._on_close(outcome="failure")
|
|
319
|
+
raise
|
|
320
|
+
finally:
|
|
321
|
+
if not self._recorded:
|
|
322
|
+
self._on_close(outcome="success")
|
|
323
|
+
|
|
324
|
+
def __enter__(self) -> "_GeminiStreamingWrapper":
|
|
325
|
+
if hasattr(self._raw, "__enter__"):
|
|
326
|
+
self._raw.__enter__()
|
|
327
|
+
return self
|
|
328
|
+
|
|
329
|
+
def __exit__(self, *args: Any) -> None:
|
|
330
|
+
try:
|
|
331
|
+
if hasattr(self._raw, "__exit__"):
|
|
332
|
+
self._raw.__exit__(*args)
|
|
333
|
+
finally:
|
|
334
|
+
if not self._recorded:
|
|
335
|
+
self._on_close(outcome="success")
|
|
336
|
+
|
|
337
|
+
def _on_close(self, outcome: str = "success") -> None:
|
|
338
|
+
if self._recorded:
|
|
339
|
+
return
|
|
340
|
+
self._recorded = True
|
|
341
|
+
if self._accumulated_text:
|
|
342
|
+
try:
|
|
343
|
+
self._wrapper._capture_response(
|
|
344
|
+
type("_R", (), {"text": self._accumulated_text})(),
|
|
345
|
+
self._session_id,
|
|
346
|
+
)
|
|
347
|
+
except Exception as exc:
|
|
348
|
+
logger.warning("devtorch: stream capture failed — %s", exc)
|
|
349
|
+
try:
|
|
350
|
+
self._wrapper._record_llm_outcome(
|
|
351
|
+
prompt_text=self._prompt_text,
|
|
352
|
+
response_text=self._accumulated_text,
|
|
353
|
+
thinking_blocks=[],
|
|
354
|
+
session_id=self._session_id,
|
|
355
|
+
call_id=self._call_id,
|
|
356
|
+
model_name=self._model_name,
|
|
357
|
+
outcome=outcome,
|
|
358
|
+
)
|
|
359
|
+
except Exception as exc:
|
|
360
|
+
logger.warning("devtorch: stream learning outcome failed — %s", exc)
|
|
361
|
+
|
|
362
|
+
def __getattr__(self, name: str) -> Any:
|
|
363
|
+
return getattr(self._raw, name)
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
# ---------------------------------------------------------------------------
|
|
367
|
+
# Helpers
|
|
368
|
+
# ---------------------------------------------------------------------------
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
def _extract_text(response: Any) -> str:
|
|
372
|
+
"""Pull plain text from a Gemini response object."""
|
|
373
|
+
# response.text is the simplest path
|
|
374
|
+
if hasattr(response, "text") and isinstance(response.text, str):
|
|
375
|
+
return response.text
|
|
376
|
+
# Fallback: iterate candidates → content → parts
|
|
377
|
+
text = ""
|
|
378
|
+
try:
|
|
379
|
+
candidates = getattr(response, "candidates", None) or []
|
|
380
|
+
for candidate in candidates:
|
|
381
|
+
content = getattr(candidate, "content", None)
|
|
382
|
+
if content:
|
|
383
|
+
parts = getattr(content, "parts", None) or []
|
|
384
|
+
for part in parts:
|
|
385
|
+
if hasattr(part, "text") and isinstance(part.text, str):
|
|
386
|
+
text += part.text
|
|
387
|
+
except Exception:
|
|
388
|
+
pass
|
|
389
|
+
return text
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
def _extract_prompt_text(messages: list) -> str:
|
|
393
|
+
"""Best-effort concatenation of user-visible message text for smart context."""
|
|
394
|
+
parts: list[str] = []
|
|
395
|
+
for m in messages or []:
|
|
396
|
+
if isinstance(m, dict):
|
|
397
|
+
role = m.get("role", "")
|
|
398
|
+
content = m.get("content", "")
|
|
399
|
+
parts_text = ""
|
|
400
|
+
if isinstance(content, str):
|
|
401
|
+
parts_text = content
|
|
402
|
+
elif isinstance(content, list):
|
|
403
|
+
for item in content:
|
|
404
|
+
if isinstance(item, dict) and item.get("type") == "text":
|
|
405
|
+
parts_text += item.get("text", "")
|
|
406
|
+
elif isinstance(item, str):
|
|
407
|
+
parts_text += item
|
|
408
|
+
elif isinstance(content, dict):
|
|
409
|
+
# Gemini shape: {"parts": [...]}
|
|
410
|
+
for part in content.get("parts", []):
|
|
411
|
+
if isinstance(part, dict) and "text" in part:
|
|
412
|
+
parts_text += part["text"]
|
|
413
|
+
elif isinstance(part, str):
|
|
414
|
+
parts_text += part
|
|
415
|
+
if role in ("user", "system") and parts_text:
|
|
416
|
+
parts.append(parts_text)
|
|
417
|
+
elif hasattr(m, "role") and hasattr(m, "content"):
|
|
418
|
+
role = getattr(m, "role", "")
|
|
419
|
+
content = getattr(m, "content", "")
|
|
420
|
+
if role in ("user", "system") and content:
|
|
421
|
+
parts.append(str(content))
|
|
422
|
+
return "\n".join(parts)[:4000]
|