deepparallel 0.2.0__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.
- deepparallel/__init__.py +3 -0
- deepparallel/agent.py +286 -0
- deepparallel/backend.py +302 -0
- deepparallel/branding.py +211 -0
- deepparallel/cli.py +569 -0
- deepparallel/config.py +158 -0
- deepparallel/fusion.py +225 -0
- deepparallel/licensing.py +108 -0
- deepparallel/registry.json +13 -0
- deepparallel/renderer.py +222 -0
- deepparallel/system_prompt.txt +4 -0
- deepparallel/tools/__init__.py +27 -0
- deepparallel/tools/codeast.py +171 -0
- deepparallel/tools/edit.py +29 -0
- deepparallel/tools/files.py +74 -0
- deepparallel/tools/registry.py +149 -0
- deepparallel/tools/sandbox.py +110 -0
- deepparallel/tools/search.py +38 -0
- deepparallel/tools/shell.py +38 -0
- deepparallel/tools/vision.py +54 -0
- deepparallel/tools/web.py +76 -0
- deepparallel-0.2.0.dist-info/METADATA +128 -0
- deepparallel-0.2.0.dist-info/RECORD +26 -0
- deepparallel-0.2.0.dist-info/WHEEL +5 -0
- deepparallel-0.2.0.dist-info/entry_points.txt +3 -0
- deepparallel-0.2.0.dist-info/top_level.txt +1 -0
deepparallel/cli.py
ADDED
|
@@ -0,0 +1,569 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""DeepParallel CLI.
|
|
3
|
+
|
|
4
|
+
Commands:
|
|
5
|
+
deepparallel interactive agent chat (default)
|
|
6
|
+
deepparallel chat interactive agent chat (explicit)
|
|
7
|
+
deepparallel run "prompt" one-shot; answer to stdout
|
|
8
|
+
deepparallel tools list available agent tools
|
|
9
|
+
deepparallel info model + backend status
|
|
10
|
+
deepparallel models list registered model
|
|
11
|
+
deepparallel doctor diagnose config + reachability
|
|
12
|
+
|
|
13
|
+
Flags --no-tools (plain chat, no tools) and --yes (auto-approve tool actions)
|
|
14
|
+
apply to chat and run.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import sys
|
|
20
|
+
from dataclasses import replace
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
|
|
23
|
+
import click
|
|
24
|
+
import httpx
|
|
25
|
+
from dotenv import load_dotenv
|
|
26
|
+
from rich import box
|
|
27
|
+
from rich.table import Table
|
|
28
|
+
|
|
29
|
+
from deepparallel import __version__, branding
|
|
30
|
+
from deepparallel.agent import (
|
|
31
|
+
guardian_review,
|
|
32
|
+
run_agent,
|
|
33
|
+
verdict_exit_code,
|
|
34
|
+
verdict_severity,
|
|
35
|
+
)
|
|
36
|
+
from deepparallel.backend import Backend, backend_for_deployment, resolve_backend
|
|
37
|
+
from deepparallel.branding import console
|
|
38
|
+
from deepparallel.config import (
|
|
39
|
+
Settings,
|
|
40
|
+
_bool_env,
|
|
41
|
+
load_model_spec,
|
|
42
|
+
load_system_prompt,
|
|
43
|
+
missing_required,
|
|
44
|
+
resolve_settings,
|
|
45
|
+
)
|
|
46
|
+
from deepparallel import licensing
|
|
47
|
+
from deepparallel.fusion import (
|
|
48
|
+
EscalationBackend,
|
|
49
|
+
ReasonAnswerBackend,
|
|
50
|
+
deep_query,
|
|
51
|
+
dual_query,
|
|
52
|
+
)
|
|
53
|
+
from deepparallel.renderer import PlainRenderer, Renderer, RichRenderer
|
|
54
|
+
from deepparallel.tools import get_registry
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _build_messages(history: list[tuple[str, str]], system: str, user_msg: str) -> list[dict]:
|
|
58
|
+
msgs: list[dict] = [{"role": "system", "content": system}]
|
|
59
|
+
for role, content in history:
|
|
60
|
+
msgs.append({"role": role, "content": content})
|
|
61
|
+
msgs.append({"role": "user", "content": user_msg})
|
|
62
|
+
return msgs
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _apply_flags(settings: Settings, no_tools: bool, assume_yes: bool) -> Settings:
|
|
66
|
+
if no_tools:
|
|
67
|
+
settings = replace(settings, tools_enabled=False)
|
|
68
|
+
if assume_yes:
|
|
69
|
+
settings = replace(settings, auto_approve=True)
|
|
70
|
+
return settings
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _effective_backend(base: Backend, settings: Settings, mode: str) -> Backend:
|
|
74
|
+
"""Wrap the base backend in a fusion strategy for the given mode.
|
|
75
|
+
|
|
76
|
+
Fusion is a paid (Pro+) feature; on Free it degrades to the single-model
|
|
77
|
+
backend with a one-time upgrade nudge.
|
|
78
|
+
"""
|
|
79
|
+
if mode not in ("reason", "escalate"):
|
|
80
|
+
return base
|
|
81
|
+
ok, msg = licensing.check_feature("fusion")
|
|
82
|
+
if not ok:
|
|
83
|
+
branding.info(msg)
|
|
84
|
+
return base
|
|
85
|
+
reasoner = backend_for_deployment(settings, settings.reasoner_deployment)
|
|
86
|
+
if mode == "reason":
|
|
87
|
+
return ReasonAnswerBackend(base, reasoner)
|
|
88
|
+
return EscalationBackend(base, reasoner)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _wrap_fusion(backend: Backend, settings: Settings) -> Backend:
|
|
92
|
+
"""Wrap per settings.fusion_mode (used by the non-interactive paths)."""
|
|
93
|
+
return _effective_backend(backend, settings, settings.fusion_mode)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _run_deep(settings: Settings, messages: list[dict]) -> None:
|
|
97
|
+
"""Heavy multi-model fan-out + judge, one-shot. Chains are shown with
|
|
98
|
+
generic labels (no raw model names) per the brand rule. Paid (Pro+)."""
|
|
99
|
+
ok, msg = licensing.check_feature("deep")
|
|
100
|
+
if not ok:
|
|
101
|
+
branding.error(msg)
|
|
102
|
+
sys.exit(2)
|
|
103
|
+
chains = [
|
|
104
|
+
(f"candidate-{i + 1}", backend_for_deployment(settings, dep))
|
|
105
|
+
for i, dep in enumerate(settings.parallel_deployments)
|
|
106
|
+
]
|
|
107
|
+
judge = backend_for_deployment(settings, settings.judge_deployment)
|
|
108
|
+
|
|
109
|
+
def on_event(name: str, content: str) -> None:
|
|
110
|
+
sys.stderr.write(f"> {name}: {content[:80]}\n")
|
|
111
|
+
sys.stderr.flush()
|
|
112
|
+
|
|
113
|
+
try:
|
|
114
|
+
result = deep_query(
|
|
115
|
+
messages, chains, judge, settings.temperature, settings.max_tokens, on_event
|
|
116
|
+
)
|
|
117
|
+
except Exception as e: # noqa: BLE001 - surface as friendly message
|
|
118
|
+
branding.error(_translate_error(e))
|
|
119
|
+
sys.exit(1)
|
|
120
|
+
ag = result.get("agreement") or {}
|
|
121
|
+
if ag.get("level") and ag["level"] != "n/a":
|
|
122
|
+
sys.stderr.write(
|
|
123
|
+
f"consensus: {ag['level']} ({ag.get('score')}) [agreement, not correctness]\n"
|
|
124
|
+
)
|
|
125
|
+
if ag["level"] == "low":
|
|
126
|
+
sys.stderr.write("models diverged - candidates:\n")
|
|
127
|
+
for c in result.get("chains", []):
|
|
128
|
+
sys.stderr.write(f" {c['name']}: {c['content'][:120]}\n")
|
|
129
|
+
sys.stderr.flush()
|
|
130
|
+
sys.stdout.write(result["answer"].rstrip("\n") + "\n")
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _run_dual(settings: Settings, messages: list[dict], dual: str, synth: bool) -> None:
|
|
134
|
+
ok, msg = licensing.check_feature("dual")
|
|
135
|
+
if not ok:
|
|
136
|
+
branding.error(msg)
|
|
137
|
+
sys.exit(2)
|
|
138
|
+
names = [s.strip() for s in dual.split(",") if s.strip()] if dual else []
|
|
139
|
+
if len(names) >= 2:
|
|
140
|
+
la, ra, lname, rname = names[0], names[1], names[0], names[1]
|
|
141
|
+
else:
|
|
142
|
+
la, ra, lname, rname = (
|
|
143
|
+
settings.deployment,
|
|
144
|
+
settings.reasoner_deployment,
|
|
145
|
+
"primary",
|
|
146
|
+
"reasoner",
|
|
147
|
+
)
|
|
148
|
+
left = (lname, backend_for_deployment(settings, la))
|
|
149
|
+
right = (rname, backend_for_deployment(settings, ra))
|
|
150
|
+
judge = backend_for_deployment(settings, settings.judge_deployment) if synth else None
|
|
151
|
+
try:
|
|
152
|
+
out = dual_query(messages, left, right, settings.temperature, settings.max_tokens, judge)
|
|
153
|
+
except Exception as e: # noqa: BLE001 - surface as friendly message
|
|
154
|
+
branding.error(_translate_error(e))
|
|
155
|
+
sys.exit(1)
|
|
156
|
+
sys.stdout.write(f"## {out['left']['name']}\n{out['left']['content']}\n\n")
|
|
157
|
+
sys.stdout.write(f"## {out['right']['name']}\n{out['right']['content']}\n")
|
|
158
|
+
if out["synthesis"]:
|
|
159
|
+
sys.stdout.write(f"\n## synthesis\n{out['synthesis']}\n")
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def _build_guardian(settings: Settings) -> Backend | None:
|
|
163
|
+
"""A second model that reviews edits before they apply (interactive only).
|
|
164
|
+
|
|
165
|
+
Guardian is a paid (Pro+) feature; disabled on Free.
|
|
166
|
+
"""
|
|
167
|
+
if not settings.guardian_enabled:
|
|
168
|
+
return None
|
|
169
|
+
ok, msg = licensing.check_feature("guardian")
|
|
170
|
+
if not ok:
|
|
171
|
+
branding.info(msg)
|
|
172
|
+
return None
|
|
173
|
+
return backend_for_deployment(settings, settings.guardian_deployment)
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def _make_renderer(*, force_plain: bool, assume_yes: bool = False) -> Renderer:
|
|
177
|
+
plain = force_plain or _bool_env("DEEPPARALLEL_PLAIN", False) or not sys.stdout.isatty()
|
|
178
|
+
if plain:
|
|
179
|
+
return PlainRenderer(assume_yes=assume_yes)
|
|
180
|
+
return RichRenderer()
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def _require_ready(settings: Settings) -> Backend:
|
|
184
|
+
missing = missing_required(settings)
|
|
185
|
+
if missing:
|
|
186
|
+
branding.error(f"missing required env: {', '.join(missing)} (backend={settings.backend})")
|
|
187
|
+
sys.exit(1)
|
|
188
|
+
return resolve_backend(settings)
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def _translate_error(e: Exception) -> str:
|
|
192
|
+
if isinstance(e, httpx.HTTPStatusError):
|
|
193
|
+
code = e.response.status_code
|
|
194
|
+
if code == 401:
|
|
195
|
+
return "authentication failed (401) - check the API key."
|
|
196
|
+
if code == 404:
|
|
197
|
+
return "model endpoint not found (404) - check the backend configuration."
|
|
198
|
+
return f"backend returned HTTP {code}."
|
|
199
|
+
return f"{e.__class__.__name__}: {e}"
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def _print_info(settings: Settings) -> None:
|
|
203
|
+
spec = load_model_spec()
|
|
204
|
+
table = Table(box=box.SIMPLE_HEAVY, show_header=False, pad_edge=False)
|
|
205
|
+
table.add_column("k", style="grey50")
|
|
206
|
+
table.add_column("v", style="white")
|
|
207
|
+
table.add_row("model", spec.label)
|
|
208
|
+
table.add_row("backend", settings.backend)
|
|
209
|
+
table.add_row("context", str(spec.context_window))
|
|
210
|
+
table.add_row("license", f"{spec.license} · {spec.license_url}")
|
|
211
|
+
console.print(table)
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def _stream_once(backend: Backend, settings: Settings, messages: list[dict]) -> None:
|
|
215
|
+
"""Plain-chat one-shot (no tools): stream tokens to stdout."""
|
|
216
|
+
try:
|
|
217
|
+
for channel, text in backend.stream_chat(
|
|
218
|
+
messages, settings.temperature, settings.max_tokens
|
|
219
|
+
):
|
|
220
|
+
if channel == "thinking" and not settings.show_thinking:
|
|
221
|
+
continue
|
|
222
|
+
sys.stdout.write(text)
|
|
223
|
+
sys.stdout.flush()
|
|
224
|
+
sys.stdout.write("\n")
|
|
225
|
+
except Exception as e: # noqa: BLE001 - surface as friendly message
|
|
226
|
+
branding.error(_translate_error(e))
|
|
227
|
+
sys.exit(1)
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def _stream_repl(backend: Backend, settings: Settings) -> None:
|
|
231
|
+
"""Plain-chat interactive loop (no tools)."""
|
|
232
|
+
system = load_system_prompt()
|
|
233
|
+
history: list[tuple[str, str]] = []
|
|
234
|
+
while True:
|
|
235
|
+
try:
|
|
236
|
+
user_msg = console.input(branding.user_prefix() + "› ").strip()
|
|
237
|
+
except (EOFError, KeyboardInterrupt):
|
|
238
|
+
console.print()
|
|
239
|
+
break
|
|
240
|
+
if not user_msg:
|
|
241
|
+
continue
|
|
242
|
+
if user_msg in {"/quit", "/exit", ":q"}:
|
|
243
|
+
break
|
|
244
|
+
if user_msg == "/help":
|
|
245
|
+
branding.info("/quit · /reset · /info · anything else is a prompt")
|
|
246
|
+
continue
|
|
247
|
+
if user_msg == "/reset":
|
|
248
|
+
history.clear()
|
|
249
|
+
branding.info("conversation cleared.")
|
|
250
|
+
continue
|
|
251
|
+
if user_msg == "/info":
|
|
252
|
+
_print_info(settings)
|
|
253
|
+
continue
|
|
254
|
+
|
|
255
|
+
messages = _build_messages(history, system, user_msg)
|
|
256
|
+
console.print(branding.model_prefix() + "›", end=" ")
|
|
257
|
+
reply: list[str] = []
|
|
258
|
+
try:
|
|
259
|
+
for channel, text in backend.stream_chat(
|
|
260
|
+
messages, settings.temperature, settings.max_tokens
|
|
261
|
+
):
|
|
262
|
+
if channel == "thinking":
|
|
263
|
+
if settings.show_thinking:
|
|
264
|
+
branding.thinking(text)
|
|
265
|
+
continue
|
|
266
|
+
console.print(text, end="", soft_wrap=True, highlight=False)
|
|
267
|
+
reply.append(text)
|
|
268
|
+
console.print()
|
|
269
|
+
except Exception as e: # noqa: BLE001 - surface as friendly message
|
|
270
|
+
console.print()
|
|
271
|
+
branding.error(_translate_error(e))
|
|
272
|
+
continue
|
|
273
|
+
|
|
274
|
+
history.append(("user", user_msg))
|
|
275
|
+
history.append(("assistant", "".join(reply)))
|
|
276
|
+
|
|
277
|
+
branding.attribution_footer()
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
_DIAL = {"/fast": "off", "/fuse": "reason", "/escalate": "escalate"}
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def _agent_repl(backend: Backend, settings: Settings, renderer: Renderer) -> None:
|
|
284
|
+
"""Interactive agentic loop: tools enabled, conversation persists. The dial
|
|
285
|
+
(/fast //fuse //escalate //deep) swaps the active fusion mode live."""
|
|
286
|
+
registry = get_registry()
|
|
287
|
+
guardian = _build_guardian(settings)
|
|
288
|
+
system = load_system_prompt()
|
|
289
|
+
messages: list[dict] = [{"role": "system", "content": system}]
|
|
290
|
+
mode = settings.fusion_mode if settings.fusion_mode in ("reason", "escalate") else "off"
|
|
291
|
+
deep_next = False
|
|
292
|
+
while True:
|
|
293
|
+
tag = f"[{mode}] " if mode != "off" else ""
|
|
294
|
+
try:
|
|
295
|
+
user_msg = console.input(f"{tag}{branding.user_prefix()}› ").strip()
|
|
296
|
+
except (EOFError, KeyboardInterrupt):
|
|
297
|
+
console.print()
|
|
298
|
+
break
|
|
299
|
+
if not user_msg:
|
|
300
|
+
continue
|
|
301
|
+
if user_msg in {"/quit", "/exit", ":q"}:
|
|
302
|
+
break
|
|
303
|
+
if user_msg == "/help":
|
|
304
|
+
branding.info(
|
|
305
|
+
"/quit · /reset · /info · /tools · /fast //fuse //escalate //deep · prompt"
|
|
306
|
+
)
|
|
307
|
+
continue
|
|
308
|
+
if user_msg == "/reset":
|
|
309
|
+
messages = [{"role": "system", "content": system}]
|
|
310
|
+
branding.info("conversation cleared.")
|
|
311
|
+
continue
|
|
312
|
+
if user_msg == "/info":
|
|
313
|
+
_print_info(settings)
|
|
314
|
+
continue
|
|
315
|
+
if user_msg in _DIAL:
|
|
316
|
+
mode = _DIAL[user_msg]
|
|
317
|
+
branding.info(f"mode: {mode}")
|
|
318
|
+
continue
|
|
319
|
+
if user_msg == "/deep":
|
|
320
|
+
deep_next = True
|
|
321
|
+
branding.info("next prompt runs as a multi-model deep query")
|
|
322
|
+
continue
|
|
323
|
+
|
|
324
|
+
messages.append({"role": "user", "content": user_msg})
|
|
325
|
+
if deep_next:
|
|
326
|
+
deep_next = False
|
|
327
|
+
_run_deep(settings, list(messages))
|
|
328
|
+
continue
|
|
329
|
+
try:
|
|
330
|
+
run_agent(
|
|
331
|
+
_effective_backend(backend, settings, mode),
|
|
332
|
+
registry,
|
|
333
|
+
messages,
|
|
334
|
+
settings,
|
|
335
|
+
renderer,
|
|
336
|
+
interactive=True,
|
|
337
|
+
auto_approve=settings.auto_approve,
|
|
338
|
+
stream=True,
|
|
339
|
+
guardian=guardian,
|
|
340
|
+
)
|
|
341
|
+
if mode in ("reason", "escalate"):
|
|
342
|
+
branding.info("reasoned by CroweLM Reason · answered by DeepParallel")
|
|
343
|
+
except Exception as e: # noqa: BLE001 - surface as friendly message
|
|
344
|
+
renderer.error(_translate_error(e))
|
|
345
|
+
|
|
346
|
+
branding.attribution_footer()
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
def _chat_loop(settings: Settings) -> None:
|
|
350
|
+
backend = _require_ready(settings)
|
|
351
|
+
ok, msg = backend.check()
|
|
352
|
+
if settings.tools_enabled:
|
|
353
|
+
renderer = _make_renderer(force_plain=False, assume_yes=settings.auto_approve)
|
|
354
|
+
renderer.welcome(
|
|
355
|
+
msg,
|
|
356
|
+
version=__version__,
|
|
357
|
+
tool_count=len(get_registry().list_all()),
|
|
358
|
+
fusion_modes=("reason", "escalate", "deep", "dual"),
|
|
359
|
+
)
|
|
360
|
+
if not ok:
|
|
361
|
+
renderer.error(msg)
|
|
362
|
+
sys.exit(1)
|
|
363
|
+
_agent_repl(backend, settings, renderer) # raw backend; dial re-wraps live
|
|
364
|
+
else:
|
|
365
|
+
if not ok:
|
|
366
|
+
branding.welcome(backend_label=msg)
|
|
367
|
+
branding.error(msg)
|
|
368
|
+
sys.exit(1)
|
|
369
|
+
branding.welcome(backend_label=msg)
|
|
370
|
+
_stream_repl(_wrap_fusion(backend, settings), settings)
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
@click.group(
|
|
374
|
+
invoke_without_command=True,
|
|
375
|
+
context_settings={"help_option_names": ["-h", "--help"]},
|
|
376
|
+
)
|
|
377
|
+
@click.version_option(__version__, prog_name="deepparallel")
|
|
378
|
+
@click.option("--temperature", "-t", default=None, type=float, help="Sampling temperature.")
|
|
379
|
+
@click.pass_context
|
|
380
|
+
def main(ctx: click.Context, temperature: float | None) -> None:
|
|
381
|
+
"""DeepParallel - a focused agentic CLI for the DeepParallel model."""
|
|
382
|
+
load_dotenv()
|
|
383
|
+
ctx.ensure_object(dict)
|
|
384
|
+
settings = resolve_settings()
|
|
385
|
+
if temperature is not None:
|
|
386
|
+
settings = replace(settings, temperature=temperature)
|
|
387
|
+
ctx.obj["settings"] = settings
|
|
388
|
+
if ctx.invoked_subcommand is None:
|
|
389
|
+
_chat_loop(settings)
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
@main.command()
|
|
393
|
+
@click.option("--no-tools", is_flag=True, help="Disable tools (plain chat).")
|
|
394
|
+
@click.option("--yes", "-y", "assume_yes", is_flag=True, help="Auto-approve tool actions.")
|
|
395
|
+
@click.option(
|
|
396
|
+
"--fuse",
|
|
397
|
+
type=click.Choice(["reason", "escalate", "off"]),
|
|
398
|
+
default=None,
|
|
399
|
+
help="Stack a reasoner with the answerer.",
|
|
400
|
+
)
|
|
401
|
+
@click.pass_context
|
|
402
|
+
def chat(ctx: click.Context, no_tools: bool, assume_yes: bool, fuse: str | None) -> None:
|
|
403
|
+
"""Start an interactive chat session."""
|
|
404
|
+
settings = _apply_flags(ctx.obj["settings"], no_tools, assume_yes)
|
|
405
|
+
if fuse is not None:
|
|
406
|
+
settings = replace(settings, fusion_mode=fuse)
|
|
407
|
+
_chat_loop(settings)
|
|
408
|
+
|
|
409
|
+
|
|
410
|
+
@main.command()
|
|
411
|
+
@click.option("--no-tools", is_flag=True, help="Disable tools (plain chat).")
|
|
412
|
+
@click.option("--yes", "-y", "assume_yes", is_flag=True, help="Auto-approve tool actions.")
|
|
413
|
+
@click.option(
|
|
414
|
+
"--fuse",
|
|
415
|
+
type=click.Choice(["reason", "escalate", "off"]),
|
|
416
|
+
default=None,
|
|
417
|
+
help="Stack a reasoner with the answerer.",
|
|
418
|
+
)
|
|
419
|
+
@click.option("--deep", is_flag=True, help="Heavy multi-model fan-out + judge (slow).")
|
|
420
|
+
@click.option(
|
|
421
|
+
"--dual",
|
|
422
|
+
default=None,
|
|
423
|
+
help="Compare two deployments 'A,B' (default: primary vs reasoner).",
|
|
424
|
+
)
|
|
425
|
+
@click.option("--synth", is_flag=True, help="With --dual, also synthesize a merged answer.")
|
|
426
|
+
@click.argument("prompt", nargs=-1, required=True)
|
|
427
|
+
@click.pass_context
|
|
428
|
+
def run(
|
|
429
|
+
ctx: click.Context,
|
|
430
|
+
no_tools: bool,
|
|
431
|
+
assume_yes: bool,
|
|
432
|
+
fuse: str | None,
|
|
433
|
+
deep: bool,
|
|
434
|
+
dual: str | None,
|
|
435
|
+
synth: bool,
|
|
436
|
+
prompt: tuple[str, ...],
|
|
437
|
+
) -> None:
|
|
438
|
+
"""Run a single prompt; the answer goes to stdout."""
|
|
439
|
+
settings = _apply_flags(ctx.obj["settings"], no_tools, assume_yes)
|
|
440
|
+
if fuse is not None:
|
|
441
|
+
settings = replace(settings, fusion_mode=fuse)
|
|
442
|
+
backend = _require_ready(settings)
|
|
443
|
+
system = load_system_prompt()
|
|
444
|
+
messages = _build_messages([], system, " ".join(prompt))
|
|
445
|
+
if deep:
|
|
446
|
+
_run_deep(settings, messages)
|
|
447
|
+
return
|
|
448
|
+
if dual is not None:
|
|
449
|
+
_run_dual(settings, messages, dual, synth)
|
|
450
|
+
return
|
|
451
|
+
backend = _wrap_fusion(backend, settings)
|
|
452
|
+
if not settings.tools_enabled:
|
|
453
|
+
_stream_once(backend, settings, messages)
|
|
454
|
+
return
|
|
455
|
+
renderer = PlainRenderer(assume_yes=settings.auto_approve)
|
|
456
|
+
try:
|
|
457
|
+
run_agent(
|
|
458
|
+
backend,
|
|
459
|
+
get_registry(),
|
|
460
|
+
messages,
|
|
461
|
+
settings,
|
|
462
|
+
renderer,
|
|
463
|
+
interactive=False,
|
|
464
|
+
auto_approve=settings.auto_approve,
|
|
465
|
+
)
|
|
466
|
+
except Exception as e: # noqa: BLE001 - surface as friendly message
|
|
467
|
+
branding.error(_translate_error(e))
|
|
468
|
+
sys.exit(1)
|
|
469
|
+
|
|
470
|
+
|
|
471
|
+
@main.command()
|
|
472
|
+
@click.option("--diff", "as_diff", is_flag=True, help="Read a unified diff from stdin.")
|
|
473
|
+
@click.argument("path", required=False)
|
|
474
|
+
@click.pass_context
|
|
475
|
+
def review(ctx: click.Context, as_diff: bool, path: str | None) -> None:
|
|
476
|
+
"""Independent cross-model review of a code change (the Guardian, as a gate).
|
|
477
|
+
|
|
478
|
+
Reviews a file (PATH) or a unified diff (--diff, from stdin) with a second
|
|
479
|
+
model and prints a verdict. Exit code: 0 safe, 1 risky, 2 bug - so it can
|
|
480
|
+
gate a commit or PR. Paid (Pro+).
|
|
481
|
+
"""
|
|
482
|
+
settings: Settings = ctx.obj["settings"]
|
|
483
|
+
ok, msg = licensing.check_feature("review")
|
|
484
|
+
if not ok:
|
|
485
|
+
branding.error(msg)
|
|
486
|
+
sys.exit(3)
|
|
487
|
+
if as_diff:
|
|
488
|
+
content = sys.stdin.read()
|
|
489
|
+
elif path:
|
|
490
|
+
try:
|
|
491
|
+
content = Path(path).expanduser().read_text(encoding="utf-8")
|
|
492
|
+
except OSError as e:
|
|
493
|
+
branding.error(f"cannot read {path}: {e}")
|
|
494
|
+
sys.exit(3)
|
|
495
|
+
else:
|
|
496
|
+
branding.error("provide a PATH or --diff (with a diff on stdin)")
|
|
497
|
+
sys.exit(3)
|
|
498
|
+
_require_ready(settings) # validates creds / exits if missing
|
|
499
|
+
guardian = backend_for_deployment(settings, settings.guardian_deployment)
|
|
500
|
+
verdict = guardian_review(guardian, content[:8000])
|
|
501
|
+
severity = verdict_severity(verdict)
|
|
502
|
+
glyph = {"safe": branding.CHECK, "risky": "!", "bug": branding.CROSS}.get(severity, "?")
|
|
503
|
+
console.print(f"[bold]{glyph} {severity.upper()}[/] {verdict or '(no verdict)'}")
|
|
504
|
+
sys.exit(verdict_exit_code(verdict))
|
|
505
|
+
|
|
506
|
+
|
|
507
|
+
@main.command(name="tools")
|
|
508
|
+
def tools_cmd() -> None:
|
|
509
|
+
"""List the agent tools available to DeepParallel."""
|
|
510
|
+
table = Table(box=box.SIMPLE_HEAVY)
|
|
511
|
+
table.add_column("tool", style="bold")
|
|
512
|
+
table.add_column("danger")
|
|
513
|
+
table.add_column("description")
|
|
514
|
+
for m in get_registry().list_all():
|
|
515
|
+
table.add_row(m.name, "!" if m.dangerous else "", m.description.splitlines()[0])
|
|
516
|
+
console.print(table)
|
|
517
|
+
|
|
518
|
+
|
|
519
|
+
@main.command()
|
|
520
|
+
@click.pass_context
|
|
521
|
+
def info(ctx: click.Context) -> None:
|
|
522
|
+
"""Show model and backend status."""
|
|
523
|
+
settings: Settings = ctx.obj["settings"]
|
|
524
|
+
_print_info(settings)
|
|
525
|
+
missing = missing_required(settings)
|
|
526
|
+
if missing:
|
|
527
|
+
console.print(f"[bold red]config incomplete[/] · missing {', '.join(missing)}")
|
|
528
|
+
else:
|
|
529
|
+
ok, msg = resolve_backend(settings).check()
|
|
530
|
+
console.print(
|
|
531
|
+
f"[bright_green]backend ok[/] · {msg}" if ok else f"[bold red]backend down[/] · {msg}"
|
|
532
|
+
)
|
|
533
|
+
branding.attribution_footer()
|
|
534
|
+
|
|
535
|
+
|
|
536
|
+
@main.command()
|
|
537
|
+
def models() -> None:
|
|
538
|
+
"""List registered models (single-model CLI)."""
|
|
539
|
+
spec = load_model_spec()
|
|
540
|
+
table = Table(box=box.SIMPLE_HEAVY)
|
|
541
|
+
table.add_column("name", style="bold")
|
|
542
|
+
table.add_column("label")
|
|
543
|
+
table.add_column("family")
|
|
544
|
+
table.add_row(spec.name, spec.label, spec.base_family)
|
|
545
|
+
console.print(table)
|
|
546
|
+
branding.attribution_footer()
|
|
547
|
+
|
|
548
|
+
|
|
549
|
+
@main.command()
|
|
550
|
+
@click.pass_context
|
|
551
|
+
def doctor(ctx: click.Context) -> None:
|
|
552
|
+
"""Diagnose configuration and backend reachability."""
|
|
553
|
+
settings: Settings = ctx.obj["settings"]
|
|
554
|
+
console.print(f"[bold]backend[/]: {settings.backend}")
|
|
555
|
+
missing = missing_required(settings)
|
|
556
|
+
if missing:
|
|
557
|
+
branding.error(f"missing env: {', '.join(missing)}")
|
|
558
|
+
sys.exit(1)
|
|
559
|
+
console.print("[bright_green]env ok[/]")
|
|
560
|
+
ok, msg = resolve_backend(settings).check()
|
|
561
|
+
if ok:
|
|
562
|
+
console.print(f"[bright_green]reachable[/] · {msg}")
|
|
563
|
+
else:
|
|
564
|
+
branding.error(msg)
|
|
565
|
+
sys.exit(1)
|
|
566
|
+
|
|
567
|
+
|
|
568
|
+
if __name__ == "__main__":
|
|
569
|
+
main()
|