semantic-code-review 0.24.2__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.
- semantic_code_review/__init__.py +5 -0
- semantic_code_review/augment/__init__.py +3 -0
- semantic_code_review/augment/agents.py +95 -0
- semantic_code_review/augment/console.py +440 -0
- semantic_code_review/augment/extra_review.py +237 -0
- semantic_code_review/augment/fold_summary.py +447 -0
- semantic_code_review/augment/hunks.py +264 -0
- semantic_code_review/augment/mcp_server.py +156 -0
- semantic_code_review/augment/overview.py +233 -0
- semantic_code_review/augment/pass_.py +171 -0
- semantic_code_review/augment/pipeline.py +571 -0
- semantic_code_review/augment/progress.py +223 -0
- semantic_code_review/augment/prompts.py +93 -0
- semantic_code_review/augment/schemas.py +408 -0
- semantic_code_review/augment/tools.py +475 -0
- semantic_code_review/augment/trace_adapter.py +369 -0
- semantic_code_review/backends/__init__.py +95 -0
- semantic_code_review/backends/_cli_driver.py +571 -0
- semantic_code_review/backends/anthropic_sdk.py +56 -0
- semantic_code_review/backends/base.py +104 -0
- semantic_code_review/backends/claude_cli.py +421 -0
- semantic_code_review/backends/google_sdk.py +73 -0
- semantic_code_review/backends/openai_compat.py +42 -0
- semantic_code_review/cache/__init__.py +3 -0
- semantic_code_review/cache/store.py +87 -0
- semantic_code_review/cli/__init__.py +70 -0
- semantic_code_review/cli/_shared.py +157 -0
- semantic_code_review/cli/augment.py +74 -0
- semantic_code_review/cli/config_cmd.py +266 -0
- semantic_code_review/cli/fetch.py +33 -0
- semantic_code_review/cli/lint.py +27 -0
- semantic_code_review/cli/pr.py +98 -0
- semantic_code_review/cli/review.py +104 -0
- semantic_code_review/cli/runs_cmd.py +17 -0
- semantic_code_review/cli/show.py +19 -0
- semantic_code_review/cli/strip.py +20 -0
- semantic_code_review/config.py +563 -0
- semantic_code_review/config_template.py +154 -0
- semantic_code_review/fetch/__init__.py +61 -0
- semantic_code_review/fetch/anchor.py +244 -0
- semantic_code_review/fetch/github.py +229 -0
- semantic_code_review/fetch/github_comments.py +404 -0
- semantic_code_review/fetch/local.py +443 -0
- semantic_code_review/fetch/run_source.py +70 -0
- semantic_code_review/format/__init__.py +3 -0
- semantic_code_review/format/emit.py +163 -0
- semantic_code_review/format/lint.py +74 -0
- semantic_code_review/format/parse.py +574 -0
- semantic_code_review/format/sidecar.py +19 -0
- semantic_code_review/format/strip.py +20 -0
- semantic_code_review/git_ops.py +252 -0
- semantic_code_review/paths.py +61 -0
- semantic_code_review/review/__init__.py +3 -0
- semantic_code_review/review/comments.py +176 -0
- semantic_code_review/review/github.py +284 -0
- semantic_code_review/review/github_graphql.py +433 -0
- semantic_code_review/review/pr_flow.py +299 -0
- semantic_code_review/review/runner.py +407 -0
- semantic_code_review/review/server.py +1043 -0
- semantic_code_review/structural/__init__.py +41 -0
- semantic_code_review/structural/diff.py +102 -0
- semantic_code_review/structural/parse.py +330 -0
- semantic_code_review/structural/symbols.py +43 -0
- semantic_code_review/viewer/__init__.py +3 -0
- semantic_code_review/viewer/assets/annotations.ts +547 -0
- semantic_code_review/viewer/assets/boot.ts +273 -0
- semantic_code_review/viewer/assets/comment_store.ts +121 -0
- semantic_code_review/viewer/assets/comments.ts +624 -0
- semantic_code_review/viewer/assets/console.ts +426 -0
- semantic_code_review/viewer/assets/console_render.ts +213 -0
- semantic_code_review/viewer/assets/console_selection.ts +102 -0
- semantic_code_review/viewer/assets/data_store.ts +156 -0
- semantic_code_review/viewer/assets/file_rows.ts +49 -0
- semantic_code_review/viewer/assets/folds.ts +560 -0
- semantic_code_review/viewer/assets/index.html +62 -0
- semantic_code_review/viewer/assets/post_modal.ts +383 -0
- semantic_code_review/viewer/assets/progress.ts +138 -0
- semantic_code_review/viewer/assets/render.ts +937 -0
- semantic_code_review/viewer/assets/sidebar.ts +429 -0
- semantic_code_review/viewer/assets/sse.ts +78 -0
- semantic_code_review/viewer/assets/text_highlight.ts +215 -0
- semantic_code_review/viewer/assets/types.d.ts +388 -0
- semantic_code_review/viewer/assets/vendor/LICENSE +29 -0
- semantic_code_review/viewer/assets/vendor/VENDOR.md +55 -0
- semantic_code_review/viewer/assets/vendor/github-dark.min.css +10 -0
- semantic_code_review/viewer/assets/vendor/github.min.css +10 -0
- semantic_code_review/viewer/assets/vendor/highlight.min.js +1244 -0
- semantic_code_review/viewer/assets/vendor/mermaid.LICENSE +21 -0
- semantic_code_review/viewer/assets/vendor/mermaid.min.js +3587 -0
- semantic_code_review/viewer/assets/vendor/refresh.sh +65 -0
- semantic_code_review/viewer/assets/viewer.css +1202 -0
- semantic_code_review/viewer/assets/viewer.js +10588 -0
- semantic_code_review/viewer/build_json.py +546 -0
- semantic_code_review/viewer/hunk_layout.py +471 -0
- semantic_code_review-0.24.2.dist-info/METADATA +348 -0
- semantic_code_review-0.24.2.dist-info/RECORD +100 -0
- semantic_code_review-0.24.2.dist-info/WHEEL +5 -0
- semantic_code_review-0.24.2.dist-info/entry_points.txt +2 -0
- semantic_code_review-0.24.2.dist-info/licenses/LICENSE +21 -0
- semantic_code_review-0.24.2.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
"""Translate a pydantic-ai `AgentRunResult` to the per-iteration trace
|
|
2
|
+
JSON shape `run_agentic` (now retired) used to write.
|
|
3
|
+
|
|
4
|
+
Every consumer of `trace/*.json` reads this shape (the viewer's
|
|
5
|
+
trace tab, ad-hoc tooling, support diagnostics). Keeping it stable
|
|
6
|
+
across the SDK / CLI migration means downstream code doesn't notice
|
|
7
|
+
which loop driver produced the run.
|
|
8
|
+
|
|
9
|
+
The shape:
|
|
10
|
+
|
|
11
|
+
{
|
|
12
|
+
"model": str,
|
|
13
|
+
"system": str,
|
|
14
|
+
"tools": [tool_name, ...],
|
|
15
|
+
"submit_tool": str, # name of the structured-output sink
|
|
16
|
+
"iterations": [
|
|
17
|
+
{
|
|
18
|
+
"messages_sent": [{"role", "content"}, ...],
|
|
19
|
+
"response": {"model", "role", "stop_reason", "usage", "content"},
|
|
20
|
+
"tool_results": [{"type": "tool_result", "tool_use_id", "content"}, ...]
|
|
21
|
+
},
|
|
22
|
+
...
|
|
23
|
+
],
|
|
24
|
+
"result": {
|
|
25
|
+
"submit_args": dict, # the validated output_type instance, dumped
|
|
26
|
+
"tool_calls": [{"name", "input", "result_len"}, ...],
|
|
27
|
+
"input_tokens": int,
|
|
28
|
+
"output_tokens": int,
|
|
29
|
+
"cache_read_tokens": int,
|
|
30
|
+
},
|
|
31
|
+
"error": { # present iff the run failed before submit
|
|
32
|
+
"type": str, # exception class name
|
|
33
|
+
"message": str, # str(exc)
|
|
34
|
+
},
|
|
35
|
+
}
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
from __future__ import annotations
|
|
39
|
+
|
|
40
|
+
import json
|
|
41
|
+
from pathlib import Path
|
|
42
|
+
from typing import Any
|
|
43
|
+
|
|
44
|
+
from pydantic_ai import CachePoint
|
|
45
|
+
from pydantic_ai.messages import (
|
|
46
|
+
ModelRequest,
|
|
47
|
+
ModelResponse,
|
|
48
|
+
RetryPromptPart,
|
|
49
|
+
SystemPromptPart,
|
|
50
|
+
TextPart,
|
|
51
|
+
ToolCallPart,
|
|
52
|
+
ToolReturnPart,
|
|
53
|
+
UserPromptPart,
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
#: Cap for the fallback `repr` on unrecognised parts. Long thinking
|
|
57
|
+
#: blocks can be tens of KB; trimming keeps traces inspectable.
|
|
58
|
+
_FALLBACK_REPR_CAP = 5000
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _trace_safe_user_content(content: Any) -> Any:
|
|
62
|
+
"""Render a UserPromptPart.content value into a JSON-safe shape.
|
|
63
|
+
|
|
64
|
+
A list-form user prompt can contain `CachePoint` markers (used for
|
|
65
|
+
Anthropic prompt caching) that aren't JSON-serialisable. We render
|
|
66
|
+
them as a sentinel dict so a human reading the trace can still see
|
|
67
|
+
where the cache breakpoints fell, without breaking `json.dumps`.
|
|
68
|
+
"""
|
|
69
|
+
if isinstance(content, str):
|
|
70
|
+
return content
|
|
71
|
+
if isinstance(content, list):
|
|
72
|
+
out: list[Any] = []
|
|
73
|
+
for item in content:
|
|
74
|
+
if isinstance(item, CachePoint):
|
|
75
|
+
out.append({"type": "cache-point", "ttl": item.ttl})
|
|
76
|
+
else:
|
|
77
|
+
out.append(item)
|
|
78
|
+
return out
|
|
79
|
+
return content
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _request_to_sent(req: ModelRequest) -> list[dict[str, Any]]:
|
|
83
|
+
"""Translate a ModelRequest's parts into the legacy `messages_sent` shape.
|
|
84
|
+
|
|
85
|
+
System prompts are intentionally dropped — the trace records the
|
|
86
|
+
system prompt once at the top level. Anything else we don't have
|
|
87
|
+
explicit handling for (FilePart, InstructionPart, …) falls through
|
|
88
|
+
to a generic dump so the trace doesn't silently lose information.
|
|
89
|
+
"""
|
|
90
|
+
out: list[dict[str, Any]] = []
|
|
91
|
+
for part in req.parts:
|
|
92
|
+
if isinstance(part, SystemPromptPart):
|
|
93
|
+
continue
|
|
94
|
+
if isinstance(part, UserPromptPart):
|
|
95
|
+
out.append({"role": "user", "content": _trace_safe_user_content(part.content)})
|
|
96
|
+
elif isinstance(part, ToolReturnPart):
|
|
97
|
+
out.append(
|
|
98
|
+
{
|
|
99
|
+
"role": "user",
|
|
100
|
+
"content": [
|
|
101
|
+
{
|
|
102
|
+
"type": "tool_result",
|
|
103
|
+
"tool_use_id": part.tool_call_id,
|
|
104
|
+
"content": _stringify(part.content),
|
|
105
|
+
}
|
|
106
|
+
],
|
|
107
|
+
}
|
|
108
|
+
)
|
|
109
|
+
elif isinstance(part, RetryPromptPart):
|
|
110
|
+
out.append(
|
|
111
|
+
{
|
|
112
|
+
"role": "user",
|
|
113
|
+
"content": [
|
|
114
|
+
{
|
|
115
|
+
"type": "tool_result",
|
|
116
|
+
"tool_use_id": part.tool_call_id or "",
|
|
117
|
+
"content": _stringify(part.content),
|
|
118
|
+
"is_error": True,
|
|
119
|
+
}
|
|
120
|
+
],
|
|
121
|
+
}
|
|
122
|
+
)
|
|
123
|
+
else:
|
|
124
|
+
out.append({"role": "unknown", "content": _fallback_part(part)})
|
|
125
|
+
return out
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def _response_content(resp: ModelResponse) -> list[dict[str, Any]]:
|
|
129
|
+
"""Flatten ModelResponse parts into the legacy assistant `content` blocks.
|
|
130
|
+
|
|
131
|
+
Parts the legacy shape doesn't have a slot for (ThinkingPart,
|
|
132
|
+
BuiltinToolCallPart, …) fall through to a generic dump so a
|
|
133
|
+
misbehaving model run's trace still shows what the model emitted.
|
|
134
|
+
"""
|
|
135
|
+
out: list[dict[str, Any]] = []
|
|
136
|
+
for part in resp.parts:
|
|
137
|
+
if isinstance(part, TextPart):
|
|
138
|
+
out.append({"type": "text", "text": part.content})
|
|
139
|
+
elif isinstance(part, ToolCallPart):
|
|
140
|
+
try:
|
|
141
|
+
input_ = part.args_as_dict()
|
|
142
|
+
except Exception as e: # noqa: BLE001 — args may be unparseable JSON
|
|
143
|
+
# The malformed args themselves are exactly what we
|
|
144
|
+
# want to see when a tool-output validation fails.
|
|
145
|
+
input_ = {
|
|
146
|
+
"_raw": getattr(part, "args", None),
|
|
147
|
+
"_parse_error": f"{type(e).__name__}: {e}",
|
|
148
|
+
}
|
|
149
|
+
out.append(
|
|
150
|
+
{
|
|
151
|
+
"type": "tool_use",
|
|
152
|
+
"id": part.tool_call_id or "",
|
|
153
|
+
"name": part.tool_name,
|
|
154
|
+
"input": input_,
|
|
155
|
+
}
|
|
156
|
+
)
|
|
157
|
+
else:
|
|
158
|
+
out.append(_fallback_part(part))
|
|
159
|
+
return out
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def _fallback_part(part: Any) -> dict[str, Any]:
|
|
163
|
+
"""Generic dump for message parts we don't render specifically.
|
|
164
|
+
|
|
165
|
+
Carries the class name and a truncated `repr` so trace readers can
|
|
166
|
+
see at minimum what kind of part the model emitted, and (for short
|
|
167
|
+
parts) its content. Common targets: ThinkingPart from extended-
|
|
168
|
+
thinking models, BuiltinToolCallPart / BuiltinToolReturnPart from
|
|
169
|
+
server-side tool surfaces, FilePart / InstructionPart from
|
|
170
|
+
multimodal prompts.
|
|
171
|
+
"""
|
|
172
|
+
text = repr(part)
|
|
173
|
+
if len(text) > _FALLBACK_REPR_CAP:
|
|
174
|
+
text = text[:_FALLBACK_REPR_CAP] + "…(truncated)"
|
|
175
|
+
return {"type": type(part).__name__, "repr": text}
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def _response_to_dict(resp: ModelResponse) -> dict[str, Any]:
|
|
179
|
+
usage = resp.usage
|
|
180
|
+
return {
|
|
181
|
+
"id": resp.provider_response_id or "",
|
|
182
|
+
"model": resp.model_name or "",
|
|
183
|
+
"role": "assistant",
|
|
184
|
+
"stop_reason": resp.finish_reason or "",
|
|
185
|
+
"usage": {
|
|
186
|
+
"input_tokens": (usage.input_tokens or 0) if usage else 0,
|
|
187
|
+
"output_tokens": (usage.output_tokens or 0) if usage else 0,
|
|
188
|
+
"cache_creation_input_tokens": (usage.cache_write_tokens or 0) if usage else 0,
|
|
189
|
+
"cache_read_input_tokens": (usage.cache_read_tokens or 0) if usage else 0,
|
|
190
|
+
},
|
|
191
|
+
"content": _response_content(resp),
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def _tool_returns_in(req: ModelRequest) -> list[dict[str, Any]]:
|
|
196
|
+
"""Tool results that follow a response (carried by the next request)."""
|
|
197
|
+
out: list[dict[str, Any]] = []
|
|
198
|
+
for part in req.parts:
|
|
199
|
+
if isinstance(part, ToolReturnPart):
|
|
200
|
+
out.append(
|
|
201
|
+
{
|
|
202
|
+
"type": "tool_result",
|
|
203
|
+
"tool_use_id": part.tool_call_id,
|
|
204
|
+
"content": _stringify(part.content),
|
|
205
|
+
}
|
|
206
|
+
)
|
|
207
|
+
elif isinstance(part, RetryPromptPart):
|
|
208
|
+
out.append(
|
|
209
|
+
{
|
|
210
|
+
"type": "tool_result",
|
|
211
|
+
"tool_use_id": part.tool_call_id or "",
|
|
212
|
+
"content": _stringify(part.content),
|
|
213
|
+
"is_error": True,
|
|
214
|
+
}
|
|
215
|
+
)
|
|
216
|
+
return out
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def _stringify(content: Any) -> str:
|
|
220
|
+
if isinstance(content, str):
|
|
221
|
+
return content
|
|
222
|
+
try:
|
|
223
|
+
return json.dumps(content, ensure_ascii=False)
|
|
224
|
+
except TypeError:
|
|
225
|
+
return str(content)
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def write_pydantic_ai_trace(
|
|
229
|
+
result: Any,
|
|
230
|
+
*,
|
|
231
|
+
trace_path: Path,
|
|
232
|
+
model: str,
|
|
233
|
+
system: str,
|
|
234
|
+
tool_names: list[str],
|
|
235
|
+
submit_tool: str,
|
|
236
|
+
) -> None:
|
|
237
|
+
"""Render an `AgentRunResult` into the legacy trace shape and write it."""
|
|
238
|
+
submit_args = _submit_args_from_output(getattr(result, "output", None))
|
|
239
|
+
write_partial_trace(
|
|
240
|
+
list(result.all_messages()),
|
|
241
|
+
trace_path=trace_path,
|
|
242
|
+
model=model,
|
|
243
|
+
system=system,
|
|
244
|
+
tool_names=tool_names,
|
|
245
|
+
submit_tool=submit_tool,
|
|
246
|
+
submit_args=submit_args,
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def write_partial_trace(
|
|
251
|
+
messages: list[Any],
|
|
252
|
+
*,
|
|
253
|
+
trace_path: Path,
|
|
254
|
+
model: str,
|
|
255
|
+
system: str,
|
|
256
|
+
tool_names: list[str],
|
|
257
|
+
submit_tool: str,
|
|
258
|
+
submit_args: dict[str, Any] | None = None,
|
|
259
|
+
error: BaseException | None = None,
|
|
260
|
+
) -> None:
|
|
261
|
+
"""Write a trace for a (possibly partial) message history.
|
|
262
|
+
|
|
263
|
+
Distinct entry point from ``write_pydantic_ai_trace`` because the
|
|
264
|
+
caller drives ``agent.iter()`` rather than ``agent.run()`` — on a
|
|
265
|
+
`UsageLimitExceeded` or similar mid-run failure there is no
|
|
266
|
+
`AgentRunResult` to extract `output`/`all_messages()` from, but
|
|
267
|
+
the partial message history is still on the `AgentRun`.
|
|
268
|
+
"""
|
|
269
|
+
requests = [m for m in messages if isinstance(m, ModelRequest)]
|
|
270
|
+
responses = [m for m in messages if isinstance(m, ModelResponse)]
|
|
271
|
+
|
|
272
|
+
iterations: list[dict[str, Any]] = []
|
|
273
|
+
cumulative: list[dict[str, Any]] = []
|
|
274
|
+
for idx, resp in enumerate(responses):
|
|
275
|
+
if idx < len(requests):
|
|
276
|
+
cumulative.extend(_request_to_sent(requests[idx]))
|
|
277
|
+
sent_snapshot = [dict(m) for m in cumulative]
|
|
278
|
+
response_dict = _response_to_dict(resp)
|
|
279
|
+
cumulative.append({"role": "assistant", "content": response_dict["content"]})
|
|
280
|
+
if idx + 1 < len(requests):
|
|
281
|
+
tool_results = _tool_returns_in(requests[idx + 1])
|
|
282
|
+
else:
|
|
283
|
+
tool_results = []
|
|
284
|
+
iterations.append(
|
|
285
|
+
{
|
|
286
|
+
"messages_sent": sent_snapshot,
|
|
287
|
+
"response": response_dict,
|
|
288
|
+
"tool_results": tool_results,
|
|
289
|
+
}
|
|
290
|
+
)
|
|
291
|
+
|
|
292
|
+
# Trailing request with no matching response — happens on the
|
|
293
|
+
# failure path when the agent loop raised before the model
|
|
294
|
+
# replied (e.g. usage-limit fired on the first call, or the
|
|
295
|
+
# provider returned an error). Surface the prompt anyway so the
|
|
296
|
+
# diagnostic trace shows what was sent.
|
|
297
|
+
if len(requests) > len(responses):
|
|
298
|
+
cumulative.extend(_request_to_sent(requests[len(responses)]))
|
|
299
|
+
iterations.append(
|
|
300
|
+
{
|
|
301
|
+
"messages_sent": [dict(m) for m in cumulative],
|
|
302
|
+
"response": None,
|
|
303
|
+
"tool_results": [],
|
|
304
|
+
}
|
|
305
|
+
)
|
|
306
|
+
|
|
307
|
+
input_t = output_t = cache_t = 0
|
|
308
|
+
for resp in responses:
|
|
309
|
+
u = resp.usage
|
|
310
|
+
if u is None:
|
|
311
|
+
continue
|
|
312
|
+
input_t += u.input_tokens or 0
|
|
313
|
+
output_t += u.output_tokens or 0
|
|
314
|
+
cache_t += u.cache_read_tokens or 0
|
|
315
|
+
|
|
316
|
+
if submit_args is None:
|
|
317
|
+
submit_args = {}
|
|
318
|
+
|
|
319
|
+
tool_calls: list[dict[str, Any]] = []
|
|
320
|
+
# Map tool_call_id -> result content length (taken from subsequent
|
|
321
|
+
# ToolReturnParts) so the legacy `result_len` field stays populated.
|
|
322
|
+
result_lens: dict[str, int] = {}
|
|
323
|
+
for req in requests:
|
|
324
|
+
for part in req.parts:
|
|
325
|
+
if isinstance(part, ToolReturnPart):
|
|
326
|
+
result_lens[part.tool_call_id] = len(_stringify(part.content))
|
|
327
|
+
for resp in responses:
|
|
328
|
+
for part in resp.parts:
|
|
329
|
+
if isinstance(part, ToolCallPart):
|
|
330
|
+
tool_calls.append(
|
|
331
|
+
{
|
|
332
|
+
"name": part.tool_name,
|
|
333
|
+
"input": part.args_as_dict(),
|
|
334
|
+
"result_len": result_lens.get(part.tool_call_id or "", 0),
|
|
335
|
+
}
|
|
336
|
+
)
|
|
337
|
+
|
|
338
|
+
trace: dict[str, Any] = {
|
|
339
|
+
"model": model,
|
|
340
|
+
"system": system,
|
|
341
|
+
"tools": tool_names,
|
|
342
|
+
"submit_tool": submit_tool,
|
|
343
|
+
"iterations": iterations,
|
|
344
|
+
"result": {
|
|
345
|
+
"submit_args": submit_args,
|
|
346
|
+
"tool_calls": tool_calls,
|
|
347
|
+
"input_tokens": input_t,
|
|
348
|
+
"output_tokens": output_t,
|
|
349
|
+
"cache_read_tokens": cache_t,
|
|
350
|
+
},
|
|
351
|
+
}
|
|
352
|
+
if error is not None:
|
|
353
|
+
trace["error"] = {"type": type(error).__name__, "message": str(error)}
|
|
354
|
+
trace_path.parent.mkdir(parents=True, exist_ok=True)
|
|
355
|
+
trace_path.write_text(json.dumps(trace, indent=2, ensure_ascii=False), encoding="utf-8")
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
def _submit_args_from_output(output: Any) -> dict[str, Any]:
|
|
359
|
+
if output is None:
|
|
360
|
+
return {}
|
|
361
|
+
if hasattr(output, "model_dump"):
|
|
362
|
+
return output.model_dump(by_alias=True)
|
|
363
|
+
return {}
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
def submit_args_from_result(result: Any) -> dict[str, Any]:
|
|
367
|
+
"""Extract the validated output as a dict that `apply_*_to_diff` can consume."""
|
|
368
|
+
output = result.output
|
|
369
|
+
return output.model_dump(by_alias=True)
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"""Backend registry — name → adapter dispatch.
|
|
2
|
+
|
|
3
|
+
The CLI calls `get(name, config=...)` to obtain a `Backend` instance,
|
|
4
|
+
then `.resolve(model=...)` for the `Client` that drives the augment
|
|
5
|
+
pipeline. `resolve_auto(config=...)` picks a backend when the user
|
|
6
|
+
did not pass `--backend`.
|
|
7
|
+
|
|
8
|
+
The registry maps `BackendType` to a `Backend` subclass. Builtins
|
|
9
|
+
(`claude-api`, `claude-cli`, `gemini-api`, plus the
|
|
10
|
+
openai-compat presets) auto-register at import time; user-defined
|
|
11
|
+
`[backends.<name>]` entries reuse the same dispatch by virtue of
|
|
12
|
+
sharing a `BackendType`.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import typer
|
|
18
|
+
|
|
19
|
+
from ..config import BackendType, ScrConfig
|
|
20
|
+
from .anthropic_sdk import AnthropicSdkBackend
|
|
21
|
+
from .base import Backend
|
|
22
|
+
from .claude_cli import ClaudeCliBackend
|
|
23
|
+
from .google_sdk import GoogleSdkBackend
|
|
24
|
+
from .openai_compat import OpenAICompatBackend
|
|
25
|
+
|
|
26
|
+
_HANDLERS: dict[BackendType, type[Backend]] = {
|
|
27
|
+
BackendType.ANTHROPIC_SDK: AnthropicSdkBackend,
|
|
28
|
+
BackendType.CLAUDE_CLI: ClaudeCliBackend,
|
|
29
|
+
BackendType.GOOGLE_SDK: GoogleSdkBackend,
|
|
30
|
+
BackendType.OPENAI_COMPAT: OpenAICompatBackend,
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def get(name: str, *, config: ScrConfig) -> Backend:
|
|
35
|
+
"""Return the adapter registered for `name`.
|
|
36
|
+
|
|
37
|
+
Raises `typer.BadParameter` with a list of valid choices if the
|
|
38
|
+
name is unknown. "auto" must be resolved by the caller via
|
|
39
|
+
`resolve_auto` first — passing it here is an error.
|
|
40
|
+
"""
|
|
41
|
+
bdef = config.backends.get(name)
|
|
42
|
+
if bdef is None:
|
|
43
|
+
valid = sorted(["auto", *config.backends.keys()])
|
|
44
|
+
raise typer.BadParameter(f"unknown backend {name!r}; expected one of: {', '.join(valid)}.")
|
|
45
|
+
cls = _HANDLERS.get(bdef.type)
|
|
46
|
+
if cls is None:
|
|
47
|
+
raise typer.BadParameter(f"backend {name!r} has unknown type {bdef.type!r}")
|
|
48
|
+
return cls(name, bdef)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def resolve_auto(*, config: ScrConfig) -> str:
|
|
52
|
+
"""Walk registered backends and return the first name that can satisfy auto.
|
|
53
|
+
|
|
54
|
+
Determinism: candidates are sorted by `(auto_priority, name)`.
|
|
55
|
+
Each candidate's `supports_auto()` is evaluated only once, in
|
|
56
|
+
that order.
|
|
57
|
+
"""
|
|
58
|
+
candidates: list[tuple[int, str]] = []
|
|
59
|
+
for name, bdef in config.backends.items():
|
|
60
|
+
cls = _HANDLERS.get(bdef.type)
|
|
61
|
+
if cls is None:
|
|
62
|
+
continue
|
|
63
|
+
priority = cls.auto_priority
|
|
64
|
+
if priority is None:
|
|
65
|
+
continue
|
|
66
|
+
adapter = cls(name, bdef)
|
|
67
|
+
if adapter.supports_auto():
|
|
68
|
+
candidates.append((priority, name))
|
|
69
|
+
if not candidates:
|
|
70
|
+
raise typer.BadParameter(
|
|
71
|
+
"No Anthropic credentials available: set ANTHROPIC_API_KEY "
|
|
72
|
+
"(or ANTHROPIC_API_TOKEN in .env), install the `claude` CLI "
|
|
73
|
+
"for subscription-based fallback, or pass --backend=gemini-api "
|
|
74
|
+
"(Google SDK) to opt into a Gemini backend."
|
|
75
|
+
)
|
|
76
|
+
candidates.sort()
|
|
77
|
+
return candidates[0][1]
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def register_handler(btype: BackendType, cls: type[Backend]) -> None:
|
|
81
|
+
"""Register or replace the adapter class for a backend type.
|
|
82
|
+
|
|
83
|
+
Intended for tests that want to swap a real adapter for a stub
|
|
84
|
+
(e.g. avoid touching the network in `resolve_auto`). Production
|
|
85
|
+
code should not call this.
|
|
86
|
+
"""
|
|
87
|
+
_HANDLERS[btype] = cls
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
__all__ = [
|
|
91
|
+
"Backend",
|
|
92
|
+
"get",
|
|
93
|
+
"register_handler",
|
|
94
|
+
"resolve_auto",
|
|
95
|
+
]
|