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,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]