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,331 @@
1
+ """
2
+ DevTorch proxy route: Google Gemini
3
+
4
+ /gemini/{path} → https://generativelanguage.googleapis.com/{path}
5
+
6
+ RACP is injected into the first user content part (body["contents"]).
7
+ Gemini uses "contents" instead of "messages".
8
+ """
9
+ from __future__ import annotations
10
+
11
+ import asyncio
12
+ import json
13
+ import logging
14
+ import uuid
15
+ from typing import Any
16
+
17
+ logger = logging.getLogger("devtorch.proxy.gemini")
18
+
19
+ UPSTREAM = "https://generativelanguage.googleapis.com"
20
+
21
+
22
+ # ---------------------------------------------------------------------------
23
+ # Lightweight RACP prefix builder
24
+ # ---------------------------------------------------------------------------
25
+
26
+ def _build_racp_prefix(gcc_repo: Any) -> str:
27
+ if gcc_repo is None:
28
+ return ""
29
+
30
+ parts: list[str] = []
31
+
32
+ try:
33
+ racp_prompt = gcc_repo.prompt_get()
34
+ if racp_prompt:
35
+ parts.append(racp_prompt.strip())
36
+ except Exception as exc:
37
+ logger.debug("devtorch proxy[gemini]: prompt_get failed — %s", exc)
38
+
39
+ try:
40
+ theta = gcc_repo.get_theta()
41
+ cv = theta.get("coordination_vector", {})
42
+ if cv:
43
+ def _mean_conf(entry: Any) -> float:
44
+ if isinstance(entry, dict):
45
+ return float(entry.get("mean_confidence", 0.0))
46
+ return 0.0
47
+
48
+ top = sorted(cv.items(), key=lambda kv: _mean_conf(kv[1]), reverse=True)[:10]
49
+ if top:
50
+ lines = ["[DevTorch Θ — top concepts]"]
51
+ for concept, data in top:
52
+ conf = _mean_conf(data)
53
+ lines.append(f" {concept}: mean_confidence={conf:.3f}")
54
+ parts.append("\n".join(lines))
55
+ except Exception as exc:
56
+ logger.debug("devtorch proxy[gemini]: theta_summary failed — %s", exc)
57
+
58
+ try:
59
+ bundle = gcc_repo.context_bundle(
60
+ k_tokens=8000,
61
+ policy={"sensitivity_policy": "default"},
62
+ )
63
+ artifacts = bundle.get("artifacts", [])
64
+ if artifacts:
65
+ lines = ["[DevTorch context bundle]"]
66
+ for art in artifacts:
67
+ path = art.get("path", "?")
68
+ reason = art.get("reason", "")
69
+ lines.append(f" - {path}: {reason}" if reason else f" - {path}")
70
+ parts.append("\n".join(lines))
71
+ except Exception as exc:
72
+ logger.debug("devtorch proxy[gemini]: context_bundle failed — %s", exc)
73
+
74
+ return "\n\n".join(parts)
75
+
76
+
77
+ # ---------------------------------------------------------------------------
78
+ # Capture helper
79
+ # ---------------------------------------------------------------------------
80
+
81
+ def _schedule_capture(gcc_repo: Any, response_text: str, session_id: str) -> None:
82
+ async def _do_capture() -> None:
83
+ try:
84
+ from devtorch_core.wrapper.base import CaptureOrchestrator
85
+ orchestrator = CaptureOrchestrator(gcc_repo)
86
+ orchestrator.capture(
87
+ response_text=response_text,
88
+ thinking_blocks=[],
89
+ session_id=session_id,
90
+ )
91
+ except Exception as exc:
92
+ logger.warning("devtorch proxy[gemini]: capture failed — %s", exc)
93
+
94
+ try:
95
+ loop = asyncio.get_event_loop()
96
+ if loop.is_running():
97
+ asyncio.ensure_future(_do_capture())
98
+ else:
99
+ loop.run_until_complete(_do_capture())
100
+ except Exception as exc:
101
+ logger.warning("devtorch proxy[gemini]: could not schedule capture — %s", exc)
102
+
103
+
104
+ # ---------------------------------------------------------------------------
105
+ # Gemini body injection
106
+ # ---------------------------------------------------------------------------
107
+
108
+ def _inject_racp_into_contents(body: dict, prefix: str) -> dict:
109
+ """
110
+ Inject RACP prefix into the first user content part's text.
111
+ Gemini body structure:
112
+ {
113
+ "contents": [
114
+ {"role": "user", "parts": [{"text": "..."}]},
115
+ ...
116
+ ],
117
+ "systemInstruction": {"parts": [{"text": "..."}]} # optional
118
+ }
119
+
120
+ Strategy:
121
+ - If body has "systemInstruction", prepend prefix there.
122
+ - Otherwise inject into the first user content text part.
123
+ """
124
+ modified = dict(body)
125
+
126
+ if "systemInstruction" in modified:
127
+ si = dict(modified["systemInstruction"])
128
+ parts = list(si.get("parts", []))
129
+ if parts and isinstance(parts[0], dict) and "text" in parts[0]:
130
+ existing = parts[0]["text"]
131
+ parts[0] = {"text": prefix + "\n\n" + existing}
132
+ else:
133
+ parts.insert(0, {"text": prefix})
134
+ si["parts"] = parts
135
+ modified["systemInstruction"] = si
136
+ return modified
137
+
138
+ # Fall back to first user content part
139
+ contents = list(modified.get("contents", []))
140
+ if not contents:
141
+ return modified
142
+
143
+ # Find first user turn
144
+ for i, content in enumerate(contents):
145
+ if content.get("role", "user") == "user":
146
+ content = dict(content)
147
+ parts = list(content.get("parts", []))
148
+ if parts and isinstance(parts[0], dict) and "text" in parts[0]:
149
+ existing = parts[0]["text"]
150
+ parts[0] = {"text": prefix + "\n\n" + existing}
151
+ else:
152
+ parts.insert(0, {"text": prefix})
153
+ content["parts"] = parts
154
+ contents[i] = content
155
+ break
156
+
157
+ modified["contents"] = contents
158
+ return modified
159
+
160
+
161
+ # ---------------------------------------------------------------------------
162
+ # Response text extraction for capture
163
+ # ---------------------------------------------------------------------------
164
+
165
+ def _extract_text_from_response(resp_json: Any) -> str:
166
+ """Extract generated text from a Gemini API response dict."""
167
+ try:
168
+ parts: list[str] = []
169
+ for candidate in resp_json.get("candidates", []):
170
+ content = candidate.get("content", {})
171
+ for part in content.get("parts", []):
172
+ text = part.get("text", "")
173
+ if text:
174
+ parts.append(text)
175
+ return "\n".join(parts)
176
+ except Exception:
177
+ return ""
178
+
179
+
180
+ # ---------------------------------------------------------------------------
181
+ # Route handler
182
+ # ---------------------------------------------------------------------------
183
+
184
+ async def proxy_gemini(path: str, request: "Any", gcc_repo: Any) -> "Any":
185
+ """
186
+ Proxy /gemini/{path} → https://generativelanguage.googleapis.com/{path}
187
+ """
188
+ try:
189
+ from fastapi import Response
190
+ from fastapi.responses import StreamingResponse
191
+ import httpx
192
+ except ImportError:
193
+ raise RuntimeError("proxy deps not installed: pip install devtorch[proxy]")
194
+
195
+ # -----------------------------------------------------------------------
196
+ # 1. Read request body
197
+ # -----------------------------------------------------------------------
198
+ try:
199
+ body_bytes = await request.body()
200
+ body: dict = json.loads(body_bytes) if body_bytes else {}
201
+ except Exception:
202
+ body = {}
203
+ body_bytes = b""
204
+
205
+ # -----------------------------------------------------------------------
206
+ # 2. Inject RACP into first user content part
207
+ # -----------------------------------------------------------------------
208
+ modified_body = body
209
+ try:
210
+ if gcc_repo is not None and gcc_repo.is_initialized():
211
+ prefix = _build_racp_prefix(gcc_repo)
212
+ if prefix:
213
+ modified_body = _inject_racp_into_contents(body, prefix)
214
+ except Exception as exc:
215
+ logger.warning("devtorch proxy[gemini]: RACP injection failed — %s", exc)
216
+ modified_body = body
217
+
218
+ send_body = json.dumps(modified_body).encode() if modified_body else body_bytes
219
+
220
+ # -----------------------------------------------------------------------
221
+ # 3. Build forwarding headers
222
+ # -----------------------------------------------------------------------
223
+ forward_headers = {
224
+ k: v for k, v in request.headers.items()
225
+ if k.lower() not in ("host", "content-length", "transfer-encoding")
226
+ }
227
+ forward_headers["host"] = "generativelanguage.googleapis.com"
228
+ if send_body:
229
+ forward_headers["content-length"] = str(len(send_body))
230
+
231
+ upstream_url = f"{UPSTREAM}/{path}"
232
+ if request.url.query:
233
+ upstream_url = f"{upstream_url}?{request.url.query}"
234
+
235
+ session_id = str(uuid.uuid4())[:8]
236
+
237
+ # Gemini uses ?alt=sse for streaming; check both stream flag and path
238
+ is_streaming = bool(
239
+ modified_body.get("stream", False)
240
+ or "streamGenerateContent" in path
241
+ )
242
+
243
+ # -----------------------------------------------------------------------
244
+ # 4 & 5. Stream or buffer
245
+ # -----------------------------------------------------------------------
246
+ if is_streaming:
247
+ async def _stream_generator():
248
+ buffer_parts: list[bytes] = []
249
+ try:
250
+ async with httpx.AsyncClient(timeout=300.0) as client:
251
+ async with client.stream(
252
+ method=request.method,
253
+ url=upstream_url,
254
+ headers=forward_headers,
255
+ content=send_body,
256
+ ) as upstream_resp:
257
+ async for chunk in upstream_resp.aiter_bytes():
258
+ buffer_parts.append(chunk)
259
+ yield chunk
260
+ except Exception as exc:
261
+ logger.warning("devtorch proxy[gemini]: streaming error — %s", exc)
262
+
263
+ raw_text = b"".join(buffer_parts).decode("utf-8", errors="replace")
264
+ # Try to parse as JSON array (Gemini streams JSON objects)
265
+ try:
266
+ combined = json.loads(raw_text)
267
+ if isinstance(combined, list):
268
+ extracted = "\n".join(
269
+ _extract_text_from_response(item) for item in combined
270
+ )
271
+ else:
272
+ extracted = _extract_text_from_response(combined)
273
+ except Exception:
274
+ extracted = raw_text
275
+ _schedule_capture(gcc_repo, extracted or raw_text, session_id)
276
+
277
+ return StreamingResponse(
278
+ _stream_generator(),
279
+ media_type="application/json",
280
+ )
281
+ else:
282
+ try:
283
+ async with httpx.AsyncClient(timeout=300.0) as client:
284
+ upstream_resp = await client.request(
285
+ method=request.method,
286
+ url=upstream_url,
287
+ headers=forward_headers,
288
+ content=send_body,
289
+ )
290
+
291
+ try:
292
+ resp_json = upstream_resp.json()
293
+ resp_text = _extract_text_from_response(resp_json) or upstream_resp.text
294
+ except Exception:
295
+ resp_text = upstream_resp.text
296
+
297
+ _schedule_capture(gcc_repo, resp_text, session_id)
298
+
299
+ excluded = {"transfer-encoding", "content-encoding", "content-length"}
300
+ resp_headers = {
301
+ k: v for k, v in upstream_resp.headers.items()
302
+ if k.lower() not in excluded
303
+ }
304
+ return Response(
305
+ content=upstream_resp.content,
306
+ status_code=upstream_resp.status_code,
307
+ headers=resp_headers,
308
+ media_type=upstream_resp.headers.get("content-type", "application/json"),
309
+ )
310
+ except Exception as exc:
311
+ logger.warning("devtorch proxy[gemini]: upstream request failed — %s", exc)
312
+ try:
313
+ async with httpx.AsyncClient(timeout=300.0) as client:
314
+ fallback_resp = await client.request(
315
+ method=request.method,
316
+ url=upstream_url,
317
+ headers=forward_headers,
318
+ content=body_bytes,
319
+ )
320
+ return Response(
321
+ content=fallback_resp.content,
322
+ status_code=fallback_resp.status_code,
323
+ media_type=fallback_resp.headers.get("content-type", "application/json"),
324
+ )
325
+ except Exception as exc2:
326
+ logger.error("devtorch proxy[gemini]: fallback also failed — %s", exc2)
327
+ return Response(
328
+ content=json.dumps({"error": "proxy error", "detail": str(exc2)}).encode(),
329
+ status_code=502,
330
+ media_type="application/json",
331
+ )
@@ -0,0 +1,284 @@
1
+ """
2
+ DevTorch proxy route: Groq
3
+
4
+ /groq/{path} → https://api.groq.com/openai/v1/{path}
5
+
6
+ Groq exposes an OpenAI-compatible API. RACP is injected as a system message
7
+ at position 0 in body["messages"].
8
+ """
9
+ from __future__ import annotations
10
+
11
+ import asyncio
12
+ import json
13
+ import logging
14
+ import uuid
15
+ from typing import Any
16
+
17
+ logger = logging.getLogger("devtorch.proxy.groq")
18
+
19
+ UPSTREAM = "https://api.groq.com/openai/v1"
20
+
21
+
22
+ # ---------------------------------------------------------------------------
23
+ # Lightweight RACP prefix builder
24
+ # ---------------------------------------------------------------------------
25
+
26
+ def _build_racp_prefix(gcc_repo: Any) -> str:
27
+ if gcc_repo is None:
28
+ return ""
29
+
30
+ parts: list[str] = []
31
+
32
+ try:
33
+ racp_prompt = gcc_repo.prompt_get()
34
+ if racp_prompt:
35
+ parts.append(racp_prompt.strip())
36
+ except Exception as exc:
37
+ logger.debug("devtorch proxy[groq]: prompt_get failed — %s", exc)
38
+
39
+ try:
40
+ theta = gcc_repo.get_theta()
41
+ cv = theta.get("coordination_vector", {})
42
+ if cv:
43
+ def _mean_conf(entry: Any) -> float:
44
+ if isinstance(entry, dict):
45
+ return float(entry.get("mean_confidence", 0.0))
46
+ return 0.0
47
+
48
+ top = sorted(cv.items(), key=lambda kv: _mean_conf(kv[1]), reverse=True)[:10]
49
+ if top:
50
+ lines = ["[DevTorch Θ — top concepts]"]
51
+ for concept, data in top:
52
+ conf = _mean_conf(data)
53
+ lines.append(f" {concept}: mean_confidence={conf:.3f}")
54
+ parts.append("\n".join(lines))
55
+ except Exception as exc:
56
+ logger.debug("devtorch proxy[groq]: theta_summary failed — %s", exc)
57
+
58
+ try:
59
+ bundle = gcc_repo.context_bundle(
60
+ k_tokens=8000,
61
+ policy={"sensitivity_policy": "default"},
62
+ )
63
+ artifacts = bundle.get("artifacts", [])
64
+ if artifacts:
65
+ lines = ["[DevTorch context bundle]"]
66
+ for art in artifacts:
67
+ path = art.get("path", "?")
68
+ reason = art.get("reason", "")
69
+ lines.append(f" - {path}: {reason}" if reason else f" - {path}")
70
+ parts.append("\n".join(lines))
71
+ except Exception as exc:
72
+ logger.debug("devtorch proxy[groq]: context_bundle failed — %s", exc)
73
+
74
+ return "\n\n".join(parts)
75
+
76
+
77
+ # ---------------------------------------------------------------------------
78
+ # Capture helper
79
+ # ---------------------------------------------------------------------------
80
+
81
+ def _schedule_capture(gcc_repo: Any, response_text: str, session_id: str) -> None:
82
+ async def _do_capture() -> None:
83
+ try:
84
+ from devtorch_core.wrapper.base import CaptureOrchestrator
85
+ orchestrator = CaptureOrchestrator(gcc_repo)
86
+ orchestrator.capture(
87
+ response_text=response_text,
88
+ thinking_blocks=[],
89
+ session_id=session_id,
90
+ )
91
+ except Exception as exc:
92
+ logger.warning("devtorch proxy[groq]: capture failed — %s", exc)
93
+
94
+ try:
95
+ loop = asyncio.get_event_loop()
96
+ if loop.is_running():
97
+ asyncio.ensure_future(_do_capture())
98
+ else:
99
+ loop.run_until_complete(_do_capture())
100
+ except Exception as exc:
101
+ logger.warning("devtorch proxy[groq]: could not schedule capture — %s", exc)
102
+
103
+
104
+ # ---------------------------------------------------------------------------
105
+ # SSE accumulator: extract assistant text from streamed chunks
106
+ # ---------------------------------------------------------------------------
107
+
108
+ def _extract_text_from_sse(raw: str) -> str:
109
+ """
110
+ Parse SSE stream and concatenate content delta text for capture.
111
+ """
112
+ parts: list[str] = []
113
+ for line in raw.splitlines():
114
+ line = line.strip()
115
+ if not line.startswith("data:"):
116
+ continue
117
+ data_str = line[5:].strip()
118
+ if data_str == "[DONE]":
119
+ break
120
+ try:
121
+ obj = json.loads(data_str)
122
+ for choice in obj.get("choices", []):
123
+ delta = choice.get("delta", {})
124
+ content = delta.get("content", "")
125
+ if content:
126
+ parts.append(content)
127
+ except Exception:
128
+ pass
129
+ return "".join(parts)
130
+
131
+
132
+ # ---------------------------------------------------------------------------
133
+ # Route handler
134
+ # ---------------------------------------------------------------------------
135
+
136
+ async def proxy_groq(path: str, request: "Any", gcc_repo: Any) -> "Any":
137
+ """
138
+ Proxy /groq/{path} → https://api.groq.com/openai/v1/{path}
139
+ """
140
+ try:
141
+ from fastapi import Response
142
+ from fastapi.responses import StreamingResponse
143
+ import httpx
144
+ except ImportError:
145
+ raise RuntimeError("proxy deps not installed: pip install devtorch[proxy]")
146
+
147
+ # -----------------------------------------------------------------------
148
+ # 1. Read request body
149
+ # -----------------------------------------------------------------------
150
+ try:
151
+ body_bytes = await request.body()
152
+ body: dict = json.loads(body_bytes) if body_bytes else {}
153
+ except Exception:
154
+ body = {}
155
+ body_bytes = b""
156
+
157
+ # -----------------------------------------------------------------------
158
+ # 2. Inject RACP as system message at position 0
159
+ # -----------------------------------------------------------------------
160
+ modified_body = body
161
+ try:
162
+ if gcc_repo is not None and gcc_repo.is_initialized():
163
+ prefix = _build_racp_prefix(gcc_repo)
164
+ if prefix:
165
+ messages = list(body.get("messages", []))
166
+ racp_msg = {"role": "system", "content": prefix}
167
+ if messages and messages[0].get("role") == "system":
168
+ existing_content = messages[0].get("content", "")
169
+ messages[0] = {
170
+ "role": "system",
171
+ "content": prefix + "\n\n" + existing_content,
172
+ }
173
+ else:
174
+ messages.insert(0, racp_msg)
175
+ modified_body = {**body, "messages": messages}
176
+ except Exception as exc:
177
+ logger.warning("devtorch proxy[groq]: RACP injection failed — %s", exc)
178
+ modified_body = body
179
+
180
+ send_body = json.dumps(modified_body).encode() if modified_body else body_bytes
181
+
182
+ # -----------------------------------------------------------------------
183
+ # 3. Build forwarding headers
184
+ # -----------------------------------------------------------------------
185
+ forward_headers = {
186
+ k: v for k, v in request.headers.items()
187
+ if k.lower() not in ("host", "content-length", "transfer-encoding")
188
+ }
189
+ forward_headers["host"] = "api.groq.com"
190
+ if send_body:
191
+ forward_headers["content-length"] = str(len(send_body))
192
+
193
+ upstream_url = f"{UPSTREAM}/{path}"
194
+ if request.url.query:
195
+ upstream_url = f"{upstream_url}?{request.url.query}"
196
+
197
+ session_id = str(uuid.uuid4())[:8]
198
+ is_streaming = bool(modified_body.get("stream", False))
199
+
200
+ # -----------------------------------------------------------------------
201
+ # 4 & 5. Stream or buffer
202
+ # -----------------------------------------------------------------------
203
+ if is_streaming:
204
+ async def _stream_generator():
205
+ buffer_parts: list[bytes] = []
206
+ try:
207
+ async with httpx.AsyncClient(timeout=300.0) as client:
208
+ async with client.stream(
209
+ method=request.method,
210
+ url=upstream_url,
211
+ headers=forward_headers,
212
+ content=send_body,
213
+ ) as upstream_resp:
214
+ async for chunk in upstream_resp.aiter_bytes():
215
+ buffer_parts.append(chunk)
216
+ yield chunk
217
+ except Exception as exc:
218
+ logger.warning("devtorch proxy[groq]: streaming error — %s", exc)
219
+
220
+ raw_text = b"".join(buffer_parts).decode("utf-8", errors="replace")
221
+ extracted = _extract_text_from_sse(raw_text)
222
+ _schedule_capture(gcc_repo, extracted or raw_text, session_id)
223
+
224
+ return StreamingResponse(
225
+ _stream_generator(),
226
+ media_type="text/event-stream",
227
+ )
228
+ else:
229
+ try:
230
+ async with httpx.AsyncClient(timeout=300.0) as client:
231
+ upstream_resp = await client.request(
232
+ method=request.method,
233
+ url=upstream_url,
234
+ headers=forward_headers,
235
+ content=send_body,
236
+ )
237
+
238
+ try:
239
+ resp_json = upstream_resp.json()
240
+ text_parts: list[str] = []
241
+ for choice in resp_json.get("choices", []):
242
+ msg = choice.get("message", {})
243
+ content = msg.get("content", "")
244
+ if content:
245
+ text_parts.append(content)
246
+ resp_text = "\n".join(text_parts) or upstream_resp.text
247
+ except Exception:
248
+ resp_text = upstream_resp.text
249
+
250
+ _schedule_capture(gcc_repo, resp_text, session_id)
251
+
252
+ excluded = {"transfer-encoding", "content-encoding", "content-length"}
253
+ resp_headers = {
254
+ k: v for k, v in upstream_resp.headers.items()
255
+ if k.lower() not in excluded
256
+ }
257
+ return Response(
258
+ content=upstream_resp.content,
259
+ status_code=upstream_resp.status_code,
260
+ headers=resp_headers,
261
+ media_type=upstream_resp.headers.get("content-type", "application/json"),
262
+ )
263
+ except Exception as exc:
264
+ logger.warning("devtorch proxy[groq]: upstream request failed — %s", exc)
265
+ try:
266
+ async with httpx.AsyncClient(timeout=300.0) as client:
267
+ fallback_resp = await client.request(
268
+ method=request.method,
269
+ url=upstream_url,
270
+ headers=forward_headers,
271
+ content=body_bytes,
272
+ )
273
+ return Response(
274
+ content=fallback_resp.content,
275
+ status_code=fallback_resp.status_code,
276
+ media_type=fallback_resp.headers.get("content-type", "application/json"),
277
+ )
278
+ except Exception as exc2:
279
+ logger.error("devtorch proxy[groq]: fallback also failed — %s", exc2)
280
+ return Response(
281
+ content=json.dumps({"error": "proxy error", "detail": str(exc2)}).encode(),
282
+ status_code=502,
283
+ media_type="application/json",
284
+ )