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,264 @@
1
+ """
2
+ DevTorch proxy route: Anthropic
3
+
4
+ /anthropic/{path} → https://api.anthropic.com/{path}
5
+
6
+ Request pipeline:
7
+ 1. Parse body as JSON.
8
+ 2. Inject RACP system prefix into body["system"].
9
+ 3. Forward to api.anthropic.com with original headers (x-api-key passed through).
10
+ 4. Stream chunks back while buffering.
11
+ 5. After stream closes, schedule .GCC/ capture as non-blocking asyncio task.
12
+
13
+ All errors degrade gracefully — the original LLM response is always returned.
14
+ """
15
+ from __future__ import annotations
16
+
17
+ import asyncio
18
+ import json
19
+ import logging
20
+ import time
21
+ import uuid
22
+ from typing import Any
23
+
24
+ logger = logging.getLogger("devtorch.proxy.anthropic")
25
+
26
+ UPSTREAM = "https://api.anthropic.com"
27
+
28
+
29
+ # ---------------------------------------------------------------------------
30
+ # Lightweight RACP prefix builder (no import from wrapper.base to avoid circulars)
31
+ # ---------------------------------------------------------------------------
32
+
33
+ def _build_racp_prefix(gcc_repo: Any) -> str:
34
+ """
35
+ Build the RACP system prefix from a GCCRepository instance.
36
+ Returns empty string on any error so callers can safely concatenate.
37
+ """
38
+ if gcc_repo is None:
39
+ return ""
40
+
41
+ parts: list[str] = []
42
+
43
+ # 1. RACP canonical system prompt
44
+ try:
45
+ racp_prompt = gcc_repo.prompt_get()
46
+ if racp_prompt:
47
+ parts.append(racp_prompt.strip())
48
+ except Exception as exc:
49
+ logger.debug("devtorch proxy: prompt_get failed — %s", exc)
50
+
51
+ # 2. Θ top-10 concepts by mean_confidence
52
+ try:
53
+ theta = gcc_repo.get_theta()
54
+ cv = theta.get("coordination_vector", {})
55
+ if cv:
56
+ def _mean_conf(entry: Any) -> float:
57
+ if isinstance(entry, dict):
58
+ return float(entry.get("mean_confidence", 0.0))
59
+ return 0.0
60
+
61
+ top = sorted(cv.items(), key=lambda kv: _mean_conf(kv[1]), reverse=True)[:10]
62
+ if top:
63
+ lines = ["[DevTorch Θ — top concepts]"]
64
+ for concept, data in top:
65
+ conf = _mean_conf(data)
66
+ lines.append(f" {concept}: mean_confidence={conf:.3f}")
67
+ parts.append("\n".join(lines))
68
+ except Exception as exc:
69
+ logger.debug("devtorch proxy: theta_summary failed — %s", exc)
70
+
71
+ # 3. Context bundle summary
72
+ try:
73
+ bundle = gcc_repo.context_bundle(
74
+ k_tokens=8000,
75
+ policy={"sensitivity_policy": "default"},
76
+ )
77
+ artifacts = bundle.get("artifacts", [])
78
+ if artifacts:
79
+ lines = ["[DevTorch context bundle]"]
80
+ for art in artifacts:
81
+ path = art.get("path", "?")
82
+ reason = art.get("reason", "")
83
+ lines.append(f" - {path}: {reason}" if reason else f" - {path}")
84
+ parts.append("\n".join(lines))
85
+ except Exception as exc:
86
+ logger.debug("devtorch proxy: context_bundle failed — %s", exc)
87
+
88
+ return "\n\n".join(parts)
89
+
90
+
91
+ # ---------------------------------------------------------------------------
92
+ # Capture helper
93
+ # ---------------------------------------------------------------------------
94
+
95
+ def _schedule_capture(gcc_repo: Any, response_text: str, session_id: str) -> None:
96
+ """Schedule .GCC/ capture as a fire-and-forget asyncio task."""
97
+ async def _do_capture() -> None:
98
+ try:
99
+ from devtorch_core.wrapper.base import CaptureOrchestrator
100
+ orchestrator = CaptureOrchestrator(gcc_repo)
101
+ orchestrator.capture(
102
+ response_text=response_text,
103
+ thinking_blocks=[],
104
+ session_id=session_id,
105
+ )
106
+ except Exception as exc:
107
+ logger.warning("devtorch proxy: capture failed — %s", exc)
108
+
109
+ try:
110
+ loop = asyncio.get_event_loop()
111
+ if loop.is_running():
112
+ asyncio.ensure_future(_do_capture())
113
+ else:
114
+ loop.run_until_complete(_do_capture())
115
+ except Exception as exc:
116
+ logger.warning("devtorch proxy: could not schedule capture — %s", exc)
117
+
118
+
119
+ # ---------------------------------------------------------------------------
120
+ # Route handler
121
+ # ---------------------------------------------------------------------------
122
+
123
+ async def proxy_anthropic(path: str, request: "Any", gcc_repo: Any) -> "Any":
124
+ """
125
+ Proxy /anthropic/{path} → https://api.anthropic.com/{path}
126
+ """
127
+ try:
128
+ from fastapi import Response
129
+ from fastapi.responses import StreamingResponse
130
+ import httpx
131
+ except ImportError:
132
+ raise RuntimeError("proxy deps not installed: pip install devtorch[proxy]")
133
+
134
+ # -----------------------------------------------------------------------
135
+ # 1. Read request body
136
+ # -----------------------------------------------------------------------
137
+ try:
138
+ body_bytes = await request.body()
139
+ body: dict = json.loads(body_bytes) if body_bytes else {}
140
+ except Exception:
141
+ body = {}
142
+ body_bytes = b""
143
+
144
+ # -----------------------------------------------------------------------
145
+ # 2. Inject RACP prefix into body["system"]
146
+ # -----------------------------------------------------------------------
147
+ modified_body = body
148
+ injection_failed = False
149
+ try:
150
+ if gcc_repo is not None and gcc_repo.is_initialized():
151
+ prefix = _build_racp_prefix(gcc_repo)
152
+ if prefix:
153
+ existing = body.get("system", "")
154
+ separator = "\n\n" if existing else ""
155
+ modified_body = {**body, "system": prefix + separator + existing}
156
+ except Exception as exc:
157
+ logger.warning("devtorch proxy: RACP injection failed — %s", exc)
158
+ injection_failed = True
159
+ modified_body = body
160
+
161
+ send_body = json.dumps(modified_body).encode() if modified_body else body_bytes
162
+
163
+ # -----------------------------------------------------------------------
164
+ # 3. Build forwarding headers (replace host, keep everything else)
165
+ # -----------------------------------------------------------------------
166
+ forward_headers = {
167
+ k: v for k, v in request.headers.items()
168
+ if k.lower() not in ("host", "content-length", "transfer-encoding")
169
+ }
170
+ forward_headers["host"] = "api.anthropic.com"
171
+ if send_body:
172
+ forward_headers["content-length"] = str(len(send_body))
173
+
174
+ upstream_url = f"{UPSTREAM}/{path}"
175
+ if request.url.query:
176
+ upstream_url = f"{upstream_url}?{request.url.query}"
177
+
178
+ session_id = str(uuid.uuid4())[:8]
179
+ is_streaming = bool(modified_body.get("stream", False))
180
+
181
+ # -----------------------------------------------------------------------
182
+ # 4 & 5. Stream or buffer response; schedule capture after completion
183
+ # -----------------------------------------------------------------------
184
+ if is_streaming:
185
+ async def _stream_generator():
186
+ buffer_parts: list[bytes] = []
187
+ try:
188
+ async with httpx.AsyncClient(timeout=300.0) as client:
189
+ async with client.stream(
190
+ method=request.method,
191
+ url=upstream_url,
192
+ headers=forward_headers,
193
+ content=send_body,
194
+ ) as upstream_resp:
195
+ async for chunk in upstream_resp.aiter_bytes():
196
+ buffer_parts.append(chunk)
197
+ yield chunk
198
+ except Exception as exc:
199
+ logger.warning("devtorch proxy: streaming error — %s", exc)
200
+
201
+ # After stream done, fire capture
202
+ full_text = b"".join(buffer_parts).decode("utf-8", errors="replace")
203
+ _schedule_capture(gcc_repo, full_text, session_id)
204
+
205
+ response_headers = {}
206
+ # We can't get upstream status before streaming; return 200 with streamed content
207
+ return StreamingResponse(
208
+ _stream_generator(),
209
+ media_type="text/event-stream",
210
+ headers=response_headers,
211
+ )
212
+ else:
213
+ # Non-streaming: buffer the full response
214
+ try:
215
+ async with httpx.AsyncClient(timeout=300.0) as client:
216
+ upstream_resp = await client.request(
217
+ method=request.method,
218
+ url=upstream_url,
219
+ headers=forward_headers,
220
+ content=send_body,
221
+ )
222
+
223
+ # Schedule capture after receiving full response
224
+ try:
225
+ resp_text = upstream_resp.text
226
+ _schedule_capture(gcc_repo, resp_text, session_id)
227
+ except Exception:
228
+ pass
229
+
230
+ # Filter hop-by-hop headers
231
+ excluded = {"transfer-encoding", "content-encoding", "content-length"}
232
+ resp_headers = {
233
+ k: v for k, v in upstream_resp.headers.items()
234
+ if k.lower() not in excluded
235
+ }
236
+ return Response(
237
+ content=upstream_resp.content,
238
+ status_code=upstream_resp.status_code,
239
+ headers=resp_headers,
240
+ media_type=upstream_resp.headers.get("content-type", "application/json"),
241
+ )
242
+ except Exception as exc:
243
+ logger.warning("devtorch proxy: upstream request failed — %s", exc)
244
+ # Forward original unmodified request as fallback
245
+ try:
246
+ async with httpx.AsyncClient(timeout=300.0) as client:
247
+ fallback_resp = await client.request(
248
+ method=request.method,
249
+ url=upstream_url,
250
+ headers=forward_headers,
251
+ content=body_bytes,
252
+ )
253
+ return Response(
254
+ content=fallback_resp.content,
255
+ status_code=fallback_resp.status_code,
256
+ media_type=fallback_resp.headers.get("content-type", "application/json"),
257
+ )
258
+ except Exception as exc2:
259
+ logger.error("devtorch proxy: fallback also failed — %s", exc2)
260
+ return Response(
261
+ content=json.dumps({"error": "proxy error", "detail": str(exc2)}).encode(),
262
+ status_code=502,
263
+ media_type="application/json",
264
+ )
@@ -0,0 +1,336 @@
1
+ """
2
+ DevTorch proxy route: Azure OpenAI
3
+
4
+ /azure-openai/{path} → {AZURE_OPENAI_ENDPOINT}/{path}?api-version={...}
5
+
6
+ The Azure OpenAI endpoint and API version are resolved from request headers
7
+ first, then environment variables:
8
+ - AZURE_OPENAI_ENDPOINT (e.g. https://myresource.openai.azure.com)
9
+ - AZURE_OPENAI_API_VERSION (e.g. 2024-06-01)
10
+
11
+ If the caller passes X-Azure-OpenAI-Endpoint and X-Azure-OpenAI-Api-Version
12
+ headers, those override the environment. The api-key header is passed through
13
+ unchanged.
14
+
15
+ RACP is injected as a system message at position 0 in body["messages"], just
16
+ like the OpenAI route.
17
+ """
18
+ from __future__ import annotations
19
+
20
+ import asyncio
21
+ import json
22
+ import logging
23
+ import os
24
+ import uuid
25
+ from typing import Any
26
+
27
+ logger = logging.getLogger("devtorch.proxy.azure_openai")
28
+
29
+ DEFAULT_API_VERSION = "2024-06-01"
30
+
31
+
32
+ # ---------------------------------------------------------------------------
33
+ # Endpoint resolution
34
+ # ---------------------------------------------------------------------------
35
+
36
+ def _resolve_endpoint(request: "Any") -> tuple[str, str]:
37
+ """
38
+ Resolve (endpoint, api_version) from headers or environment.
39
+ """
40
+ headers = {k.lower(): v for k, v in request.headers.items()}
41
+ endpoint = headers.get("x-azure-openai-endpoint", "")
42
+ api_version = headers.get("x-azure-openai-api-version", "")
43
+
44
+ if not endpoint:
45
+ endpoint = os.environ.get("AZURE_OPENAI_ENDPOINT", "")
46
+ if not api_version:
47
+ api_version = os.environ.get("AZURE_OPENAI_API_VERSION", DEFAULT_API_VERSION)
48
+
49
+ return endpoint.rstrip("/"), api_version
50
+
51
+
52
+ # ---------------------------------------------------------------------------
53
+ # Lightweight RACP prefix builder
54
+ # ---------------------------------------------------------------------------
55
+
56
+ def _build_racp_prefix(gcc_repo: Any) -> str:
57
+ if gcc_repo is None:
58
+ return ""
59
+
60
+ parts: list[str] = []
61
+
62
+ try:
63
+ racp_prompt = gcc_repo.prompt_get()
64
+ if racp_prompt:
65
+ parts.append(racp_prompt.strip())
66
+ except Exception as exc:
67
+ logger.debug("devtorch proxy[azure_openai]: prompt_get failed — %s", exc)
68
+
69
+ try:
70
+ theta = gcc_repo.get_theta()
71
+ cv = theta.get("coordination_vector", {})
72
+ if cv:
73
+ def _mean_conf(entry: Any) -> float:
74
+ if isinstance(entry, dict):
75
+ return float(entry.get("mean_confidence", 0.0))
76
+ return 0.0
77
+
78
+ top = sorted(cv.items(), key=lambda kv: _mean_conf(kv[1]), reverse=True)[:10]
79
+ if top:
80
+ lines = ["[DevTorch Θ — top concepts]"]
81
+ for concept, data in top:
82
+ conf = _mean_conf(data)
83
+ lines.append(f" {concept}: mean_confidence={conf:.3f}")
84
+ parts.append("\n".join(lines))
85
+ except Exception as exc:
86
+ logger.debug("devtorch proxy[azure_openai]: theta_summary failed — %s", exc)
87
+
88
+ try:
89
+ bundle = gcc_repo.context_bundle(
90
+ k_tokens=8000,
91
+ policy={"sensitivity_policy": "default"},
92
+ )
93
+ artifacts = bundle.get("artifacts", [])
94
+ if artifacts:
95
+ lines = ["[DevTorch context bundle]"]
96
+ for art in artifacts:
97
+ path = art.get("path", "?")
98
+ reason = art.get("reason", "")
99
+ lines.append(f" - {path}: {reason}" if reason else f" - {path}")
100
+ parts.append("\n".join(lines))
101
+ except Exception as exc:
102
+ logger.debug("devtorch proxy[azure_openai]: context_bundle failed — %s", exc)
103
+
104
+ return "\n\n".join(parts)
105
+
106
+
107
+ # ---------------------------------------------------------------------------
108
+ # Capture helper
109
+ # ---------------------------------------------------------------------------
110
+
111
+ def _schedule_capture(gcc_repo: Any, response_text: str, session_id: str) -> None:
112
+ async def _do_capture() -> None:
113
+ try:
114
+ from devtorch_core.wrapper.base import CaptureOrchestrator
115
+ orchestrator = CaptureOrchestrator(gcc_repo)
116
+ orchestrator.capture(
117
+ response_text=response_text,
118
+ thinking_blocks=[],
119
+ session_id=session_id,
120
+ )
121
+ except Exception as exc:
122
+ logger.warning("devtorch proxy[azure_openai]: capture failed — %s", exc)
123
+
124
+ try:
125
+ loop = asyncio.get_event_loop()
126
+ if loop.is_running():
127
+ asyncio.ensure_future(_do_capture())
128
+ else:
129
+ loop.run_until_complete(_do_capture())
130
+ except Exception as exc:
131
+ logger.warning("devtorch proxy[azure_openai]: could not schedule capture — %s", exc)
132
+
133
+
134
+ # ---------------------------------------------------------------------------
135
+ # SSE accumulator: extract assistant text from streamed chunks
136
+ # ---------------------------------------------------------------------------
137
+
138
+ def _extract_text_from_sse(raw: str) -> str:
139
+ """
140
+ Parse SSE stream and concatenate content delta text for capture.
141
+ """
142
+ parts: list[str] = []
143
+ for line in raw.splitlines():
144
+ line = line.strip()
145
+ if not line.startswith("data:"):
146
+ continue
147
+ data_str = line[5:].strip()
148
+ if data_str == "[DONE]":
149
+ break
150
+ try:
151
+ obj = json.loads(data_str)
152
+ for choice in obj.get("choices", []):
153
+ delta = choice.get("delta", {})
154
+ content = delta.get("content", "")
155
+ if content:
156
+ parts.append(content)
157
+ except Exception:
158
+ pass
159
+ return "".join(parts)
160
+
161
+
162
+ # ---------------------------------------------------------------------------
163
+ # Route handler
164
+ # ---------------------------------------------------------------------------
165
+
166
+ async def proxy_azure_openai(path: str, request: "Any", gcc_repo: Any) -> "Any":
167
+ """
168
+ Proxy /azure-openai/{path} → Azure OpenAI endpoint.
169
+ """
170
+ try:
171
+ from fastapi import Response
172
+ from fastapi.responses import StreamingResponse
173
+ import httpx
174
+ except ImportError:
175
+ raise RuntimeError("proxy deps not installed: pip install devtorch[proxy]")
176
+
177
+ # -----------------------------------------------------------------------
178
+ # 1. Read request body
179
+ # -----------------------------------------------------------------------
180
+ try:
181
+ body_bytes = await request.body()
182
+ body: dict = json.loads(body_bytes) if body_bytes else {}
183
+ except Exception:
184
+ body = {}
185
+ body_bytes = b""
186
+
187
+ # -----------------------------------------------------------------------
188
+ # 2. Resolve endpoint and inject RACP
189
+ # -----------------------------------------------------------------------
190
+ endpoint, api_version = _resolve_endpoint(request)
191
+ if not endpoint:
192
+ return Response(
193
+ content=json.dumps(
194
+ {"error": "missing Azure OpenAI endpoint", "detail": "Set AZURE_OPENAI_ENDPOINT or X-Azure-OpenAI-Endpoint header"}
195
+ ).encode(),
196
+ status_code=400,
197
+ media_type="application/json",
198
+ )
199
+
200
+ modified_body = body
201
+ try:
202
+ if gcc_repo is not None and gcc_repo.is_initialized():
203
+ prefix = _build_racp_prefix(gcc_repo)
204
+ if prefix:
205
+ messages = list(body.get("messages", []))
206
+ racp_msg = {"role": "system", "content": prefix}
207
+ if messages and messages[0].get("role") == "system":
208
+ existing_content = messages[0].get("content", "")
209
+ messages[0] = {
210
+ "role": "system",
211
+ "content": prefix + "\n\n" + existing_content,
212
+ }
213
+ else:
214
+ messages.insert(0, racp_msg)
215
+ modified_body = {**body, "messages": messages}
216
+ except Exception as exc:
217
+ logger.warning("devtorch proxy[azure_openai]: RACP injection failed — %s", exc)
218
+ modified_body = body
219
+
220
+ send_body = json.dumps(modified_body).encode() if modified_body else body_bytes
221
+
222
+ # -----------------------------------------------------------------------
223
+ # 3. Build forwarding headers
224
+ # -----------------------------------------------------------------------
225
+ forward_headers = {
226
+ k: v for k, v in request.headers.items()
227
+ if k.lower() not in (
228
+ "host",
229
+ "content-length",
230
+ "transfer-encoding",
231
+ "x-azure-openai-endpoint",
232
+ "x-azure-openai-api-version",
233
+ )
234
+ }
235
+ try:
236
+ host = endpoint.split("//", 1)[1].split("/", 1)[0]
237
+ except Exception:
238
+ host = endpoint
239
+ forward_headers["host"] = host
240
+ if send_body:
241
+ forward_headers["content-length"] = str(len(send_body))
242
+
243
+ upstream_url = f"{endpoint}/{path}" if path else endpoint
244
+ if request.url.query:
245
+ upstream_url = f"{upstream_url}?{request.url.query}"
246
+ else:
247
+ upstream_url = f"{upstream_url}?api-version={api_version}"
248
+
249
+ session_id = str(uuid.uuid4())[:8]
250
+ is_streaming = bool(modified_body.get("stream", False))
251
+
252
+ # -----------------------------------------------------------------------
253
+ # 4 & 5. Stream or buffer
254
+ # -----------------------------------------------------------------------
255
+ if is_streaming:
256
+ async def _stream_generator():
257
+ buffer_parts: list[bytes] = []
258
+ try:
259
+ async with httpx.AsyncClient(timeout=300.0) as client:
260
+ async with client.stream(
261
+ method=request.method,
262
+ url=upstream_url,
263
+ headers=forward_headers,
264
+ content=send_body,
265
+ ) as upstream_resp:
266
+ async for chunk in upstream_resp.aiter_bytes():
267
+ buffer_parts.append(chunk)
268
+ yield chunk
269
+ except Exception as exc:
270
+ logger.warning("devtorch proxy[azure_openai]: streaming error — %s", exc)
271
+
272
+ raw_text = b"".join(buffer_parts).decode("utf-8", errors="replace")
273
+ extracted = _extract_text_from_sse(raw_text)
274
+ _schedule_capture(gcc_repo, extracted or raw_text, session_id)
275
+
276
+ return StreamingResponse(
277
+ _stream_generator(),
278
+ media_type="text/event-stream",
279
+ )
280
+ else:
281
+ try:
282
+ async with httpx.AsyncClient(timeout=300.0) as client:
283
+ upstream_resp = await client.request(
284
+ method=request.method,
285
+ url=upstream_url,
286
+ headers=forward_headers,
287
+ content=send_body,
288
+ )
289
+
290
+ try:
291
+ resp_json = upstream_resp.json()
292
+ text_parts: list[str] = []
293
+ for choice in resp_json.get("choices", []):
294
+ msg = choice.get("message", {})
295
+ content = msg.get("content", "")
296
+ if content:
297
+ text_parts.append(content)
298
+ resp_text = "\n".join(text_parts) or upstream_resp.text
299
+ except Exception:
300
+ resp_text = upstream_resp.text
301
+
302
+ _schedule_capture(gcc_repo, resp_text, session_id)
303
+
304
+ excluded = {"transfer-encoding", "content-encoding", "content-length"}
305
+ resp_headers = {
306
+ k: v for k, v in upstream_resp.headers.items()
307
+ if k.lower() not in excluded
308
+ }
309
+ return Response(
310
+ content=upstream_resp.content,
311
+ status_code=upstream_resp.status_code,
312
+ headers=resp_headers,
313
+ media_type=upstream_resp.headers.get("content-type", "application/json"),
314
+ )
315
+ except Exception as exc:
316
+ logger.warning("devtorch proxy[azure_openai]: upstream request failed — %s", exc)
317
+ try:
318
+ async with httpx.AsyncClient(timeout=300.0) as client:
319
+ fallback_resp = await client.request(
320
+ method=request.method,
321
+ url=upstream_url,
322
+ headers=forward_headers,
323
+ content=body_bytes,
324
+ )
325
+ return Response(
326
+ content=fallback_resp.content,
327
+ status_code=fallback_resp.status_code,
328
+ media_type=fallback_resp.headers.get("content-type", "application/json"),
329
+ )
330
+ except Exception as exc2:
331
+ logger.error("devtorch proxy[azure_openai]: fallback also failed — %s", exc2)
332
+ return Response(
333
+ content=json.dumps({"error": "proxy error", "detail": str(exc2)}).encode(),
334
+ status_code=502,
335
+ media_type="application/json",
336
+ )