deepparallel 0.5.7__tar.gz → 0.7.0__tar.gz
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-0.5.7 → deepparallel-0.7.0}/PKG-INFO +2 -1
- {deepparallel-0.5.7 → deepparallel-0.7.0}/deepparallel/__init__.py +1 -1
- {deepparallel-0.5.7 → deepparallel-0.7.0}/deepparallel/agent.py +19 -5
- {deepparallel-0.5.7 → deepparallel-0.7.0}/deepparallel/branding.py +80 -0
- {deepparallel-0.5.7 → deepparallel-0.7.0}/deepparallel/cli.py +240 -24
- {deepparallel-0.5.7 → deepparallel-0.7.0}/deepparallel/licensing.py +89 -24
- deepparallel-0.7.0/deepparallel/mesh.py +165 -0
- deepparallel-0.7.0/deepparallel/research/compound_discovery.py +321 -0
- {deepparallel-0.5.7 → deepparallel-0.7.0}/deepparallel/serve.py +150 -2
- deepparallel-0.7.0/deepparallel/session.py +132 -0
- deepparallel-0.7.0/deepparallel/tui/__init__.py +7 -0
- deepparallel-0.7.0/deepparallel/tui/app.py +156 -0
- deepparallel-0.7.0/deepparallel/tui/renderer.py +136 -0
- deepparallel-0.7.0/deepparallel/tui/widgets/animations.py +171 -0
- deepparallel-0.7.0/deepparallel/tui/widgets/confirm.py +85 -0
- {deepparallel-0.5.7 → deepparallel-0.7.0}/deepparallel.egg-info/PKG-INFO +2 -1
- {deepparallel-0.5.7 → deepparallel-0.7.0}/deepparallel.egg-info/SOURCES.txt +11 -0
- {deepparallel-0.5.7 → deepparallel-0.7.0}/deepparallel.egg-info/requires.txt +1 -0
- {deepparallel-0.5.7 → deepparallel-0.7.0}/pyproject.toml +2 -1
- {deepparallel-0.5.7 → deepparallel-0.7.0}/tests/test_cli.py +5 -0
- deepparallel-0.7.0/tests/test_cli_licensing.py +297 -0
- {deepparallel-0.5.7 → deepparallel-0.7.0}/tests/test_crowe_gateway_backend.py +0 -2
- {deepparallel-0.5.7 → deepparallel-0.7.0}/tests/test_crowe_payment_required.py +0 -1
- deepparallel-0.7.0/tests/test_licensing_files.py +128 -0
- {deepparallel-0.5.7 → deepparallel-0.7.0}/tests/test_serve.py +22 -16
- deepparallel-0.7.0/tests/test_serve_session.py +40 -0
- {deepparallel-0.5.7 → deepparallel-0.7.0}/tests/test_tools_mcp.py +1 -1
- {deepparallel-0.5.7 → deepparallel-0.7.0}/README.md +0 -0
- {deepparallel-0.5.7 → deepparallel-0.7.0}/deepparallel/backend.py +0 -0
- {deepparallel-0.5.7 → deepparallel-0.7.0}/deepparallel/cockpit.py +0 -0
- {deepparallel-0.5.7 → deepparallel-0.7.0}/deepparallel/cockpit_observe.py +0 -0
- {deepparallel-0.5.7 → deepparallel-0.7.0}/deepparallel/cockpit_panel.py +0 -0
- {deepparallel-0.5.7 → deepparallel-0.7.0}/deepparallel/cockpit_sim.py +0 -0
- {deepparallel-0.5.7 → deepparallel-0.7.0}/deepparallel/config.py +0 -0
- {deepparallel-0.5.7 → deepparallel-0.7.0}/deepparallel/crowe_id.py +0 -0
- {deepparallel-0.5.7 → deepparallel-0.7.0}/deepparallel/dsml.py +0 -0
- {deepparallel-0.5.7 → deepparallel-0.7.0}/deepparallel/fusion.py +0 -0
- {deepparallel-0.5.7 → deepparallel-0.7.0}/deepparallel/memory.py +0 -0
- {deepparallel-0.5.7 → deepparallel-0.7.0}/deepparallel/registry.json +0 -0
- {deepparallel-0.5.7 → deepparallel-0.7.0}/deepparallel/renderer.py +0 -0
- {deepparallel-0.5.7 → deepparallel-0.7.0}/deepparallel/research/__init__.py +0 -0
- {deepparallel-0.5.7 → deepparallel-0.7.0}/deepparallel/research/conduit.py +0 -0
- {deepparallel-0.5.7 → deepparallel-0.7.0}/deepparallel/research/provider.py +0 -0
- {deepparallel-0.5.7 → deepparallel-0.7.0}/deepparallel/routing.example.json +0 -0
- {deepparallel-0.5.7 → deepparallel-0.7.0}/deepparallel/routing.py +0 -0
- {deepparallel-0.5.7 → deepparallel-0.7.0}/deepparallel/supply_chain.py +0 -0
- {deepparallel-0.5.7 → deepparallel-0.7.0}/deepparallel/system_prompt.txt +0 -0
- {deepparallel-0.5.7 → deepparallel-0.7.0}/deepparallel/tools/__init__.py +0 -0
- {deepparallel-0.5.7 → deepparallel-0.7.0}/deepparallel/tools/codeast.py +0 -0
- {deepparallel-0.5.7 → deepparallel-0.7.0}/deepparallel/tools/edit.py +0 -0
- {deepparallel-0.5.7 → deepparallel-0.7.0}/deepparallel/tools/files.py +0 -0
- {deepparallel-0.5.7 → deepparallel-0.7.0}/deepparallel/tools/git_ops.py +0 -0
- {deepparallel-0.5.7 → deepparallel-0.7.0}/deepparallel/tools/mcp.py +0 -0
- {deepparallel-0.5.7 → deepparallel-0.7.0}/deepparallel/tools/memory.py +0 -0
- {deepparallel-0.5.7 → deepparallel-0.7.0}/deepparallel/tools/registry.py +0 -0
- {deepparallel-0.5.7 → deepparallel-0.7.0}/deepparallel/tools/sandbox.py +0 -0
- {deepparallel-0.5.7 → deepparallel-0.7.0}/deepparallel/tools/search.py +0 -0
- {deepparallel-0.5.7 → deepparallel-0.7.0}/deepparallel/tools/shell.py +0 -0
- {deepparallel-0.5.7 → deepparallel-0.7.0}/deepparallel/tools/vision.py +0 -0
- {deepparallel-0.5.7 → deepparallel-0.7.0}/deepparallel/tools/web.py +0 -0
- {deepparallel-0.5.7 → deepparallel-0.7.0}/deepparallel/userinput.py +0 -0
- {deepparallel-0.5.7 → deepparallel-0.7.0}/deepparallel.egg-info/dependency_links.txt +0 -0
- {deepparallel-0.5.7 → deepparallel-0.7.0}/deepparallel.egg-info/entry_points.txt +0 -0
- {deepparallel-0.5.7 → deepparallel-0.7.0}/deepparallel.egg-info/top_level.txt +0 -0
- {deepparallel-0.5.7 → deepparallel-0.7.0}/setup.cfg +0 -0
- {deepparallel-0.5.7 → deepparallel-0.7.0}/tests/test_agent.py +0 -0
- {deepparallel-0.5.7 → deepparallel-0.7.0}/tests/test_backend.py +0 -0
- {deepparallel-0.5.7 → deepparallel-0.7.0}/tests/test_backend_chat.py +0 -0
- {deepparallel-0.5.7 → deepparallel-0.7.0}/tests/test_backend_stream.py +0 -0
- {deepparallel-0.5.7 → deepparallel-0.7.0}/tests/test_branding.py +0 -0
- {deepparallel-0.5.7 → deepparallel-0.7.0}/tests/test_cockpit.py +0 -0
- {deepparallel-0.5.7 → deepparallel-0.7.0}/tests/test_cockpit_panel.py +0 -0
- {deepparallel-0.5.7 → deepparallel-0.7.0}/tests/test_cockpit_sim.py +0 -0
- {deepparallel-0.5.7 → deepparallel-0.7.0}/tests/test_config.py +0 -0
- {deepparallel-0.5.7 → deepparallel-0.7.0}/tests/test_config_file.py +0 -0
- {deepparallel-0.5.7 → deepparallel-0.7.0}/tests/test_crowe_backend.py +0 -0
- {deepparallel-0.5.7 → deepparallel-0.7.0}/tests/test_crowe_id_auth.py +0 -0
- {deepparallel-0.5.7 → deepparallel-0.7.0}/tests/test_dsml.py +0 -0
- {deepparallel-0.5.7 → deepparallel-0.7.0}/tests/test_fusion.py +0 -0
- {deepparallel-0.5.7 → deepparallel-0.7.0}/tests/test_git_ops.py +0 -0
- {deepparallel-0.5.7 → deepparallel-0.7.0}/tests/test_issuer_signer.py +0 -0
- {deepparallel-0.5.7 → deepparallel-0.7.0}/tests/test_licensing.py +0 -0
- {deepparallel-0.5.7 → deepparallel-0.7.0}/tests/test_memory.py +0 -0
- {deepparallel-0.5.7 → deepparallel-0.7.0}/tests/test_renderer.py +0 -0
- {deepparallel-0.5.7 → deepparallel-0.7.0}/tests/test_research.py +0 -0
- {deepparallel-0.5.7 → deepparallel-0.7.0}/tests/test_research_provider.py +0 -0
- {deepparallel-0.5.7 → deepparallel-0.7.0}/tests/test_routing.py +0 -0
- {deepparallel-0.5.7 → deepparallel-0.7.0}/tests/test_spinner_color.py +0 -0
- {deepparallel-0.5.7 → deepparallel-0.7.0}/tests/test_supply_chain.py +0 -0
- {deepparallel-0.5.7 → deepparallel-0.7.0}/tests/test_tool_registry.py +0 -0
- {deepparallel-0.5.7 → deepparallel-0.7.0}/tests/test_tools_codeast.py +0 -0
- {deepparallel-0.5.7 → deepparallel-0.7.0}/tests/test_tools_edit.py +0 -0
- {deepparallel-0.5.7 → deepparallel-0.7.0}/tests/test_tools_files.py +0 -0
- {deepparallel-0.5.7 → deepparallel-0.7.0}/tests/test_tools_sandbox.py +0 -0
- {deepparallel-0.5.7 → deepparallel-0.7.0}/tests/test_tools_search.py +0 -0
- {deepparallel-0.5.7 → deepparallel-0.7.0}/tests/test_tools_shell.py +0 -0
- {deepparallel-0.5.7 → deepparallel-0.7.0}/tests/test_tools_vision.py +0 -0
- {deepparallel-0.5.7 → deepparallel-0.7.0}/tests/test_tools_web.py +0 -0
- {deepparallel-0.5.7 → deepparallel-0.7.0}/tests/test_userinput.py +0 -0
- {deepparallel-0.5.7 → deepparallel-0.7.0}/tests/test_userinput_paste.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: deepparallel
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.7.0
|
|
4
4
|
Summary: DeepParallel - a multi-model agentic coding CLI with cross-model Guardian review, served via Crowe Logic.
|
|
5
5
|
Author-email: Michael Crowe <michael@crowelogic.com>
|
|
6
6
|
License: Apache-2.0
|
|
@@ -16,6 +16,7 @@ Requires-Dist: python-dotenv>=1.0.0
|
|
|
16
16
|
Requires-Dist: tree-sitter>=0.25.0
|
|
17
17
|
Requires-Dist: tree-sitter-language-pack>=1.8.0
|
|
18
18
|
Requires-Dist: cryptography>=42.0.0
|
|
19
|
+
Requires-Dist: textual>=0.52.0
|
|
19
20
|
Provides-Extra: dev
|
|
20
21
|
Requires-Dist: pytest>=8.0.0; extra == "dev"
|
|
21
22
|
Requires-Dist: ruff>=0.6.0; extra == "dev"
|
|
@@ -255,16 +255,29 @@ def _supply_chain_note(name: str, args: dict) -> str | None:
|
|
|
255
255
|
return None
|
|
256
256
|
|
|
257
257
|
|
|
258
|
-
def _approved(name, args, interactive, auto_approve, renderer, guardian=None) -> bool:
|
|
258
|
+
def _approved(name, args, interactive, auto_approve, renderer, guardian=None, mesh_fn=None) -> bool:
|
|
259
259
|
forced = name in _GATED_PATH_TOOLS and _outside_cwd(args)
|
|
260
260
|
sc_note = _supply_chain_note(name, args) if name in _EDIT_TOOLS else None
|
|
261
|
-
#
|
|
262
|
-
|
|
261
|
+
# The verification mesh reviews every edit. A BUG verdict blocks the action
|
|
262
|
+
# and overrides auto-approve, exactly like a hallucinated dependency does.
|
|
263
|
+
mesh_result = None
|
|
264
|
+
if mesh_fn is not None and name in _EDIT_TOOLS:
|
|
265
|
+
try:
|
|
266
|
+
mesh_result = mesh_fn(_guardian_review_content(name, args))
|
|
267
|
+
except Exception: # noqa: BLE001 - the gate is best-effort, never fatal
|
|
268
|
+
mesh_result = None
|
|
269
|
+
blocked = bool(mesh_result and mesh_result.get("severity") == "bug")
|
|
270
|
+
# A hallucinated dependency or a BUG verdict overrides auto-approve.
|
|
271
|
+
if auto_approve and not forced and not sc_note and not blocked:
|
|
263
272
|
return True
|
|
264
273
|
if not interactive:
|
|
265
274
|
return False
|
|
266
275
|
title, detail = _describe(name, args)
|
|
267
|
-
if
|
|
276
|
+
if mesh_result is not None:
|
|
277
|
+
from deepparallel.mesh import format_panel
|
|
278
|
+
|
|
279
|
+
detail = f"{detail}\n\nVerification mesh:\n{format_panel(mesh_result)}"
|
|
280
|
+
elif guardian is not None and name in _EDIT_TOOLS:
|
|
268
281
|
verdict = _guardian_verdict(guardian, name, args)
|
|
269
282
|
if verdict:
|
|
270
283
|
detail = f"{detail}\n\nGuardian: {verdict}"
|
|
@@ -307,6 +320,7 @@ def run_agent(
|
|
|
307
320
|
max_steps: int | None = None,
|
|
308
321
|
stream: bool = False,
|
|
309
322
|
guardian=None,
|
|
323
|
+
mesh_fn=None,
|
|
310
324
|
on_event=None,
|
|
311
325
|
) -> str:
|
|
312
326
|
steps = max_steps if max_steps is not None else settings.max_steps
|
|
@@ -346,7 +360,7 @@ def run_agent(
|
|
|
346
360
|
elif "__parse_error__" in args:
|
|
347
361
|
result = json.dumps({"error": "invalid JSON arguments"})
|
|
348
362
|
elif meta.dangerous and not _approved(
|
|
349
|
-
name, args, interactive, auto_approve, renderer, guardian
|
|
363
|
+
name, args, interactive, auto_approve, renderer, guardian, mesh_fn
|
|
350
364
|
):
|
|
351
365
|
result = json.dumps({"error": "denied by user"})
|
|
352
366
|
else:
|
|
@@ -173,6 +173,86 @@ def info(msg: str) -> None:
|
|
|
173
173
|
console.print(f"[{DIM}]{msg}[/]")
|
|
174
174
|
|
|
175
175
|
|
|
176
|
+
def upgrade_card(feature: str, current_tier: str, needed_tier: str, unlocks: str, url: str) -> None:
|
|
177
|
+
body = Text()
|
|
178
|
+
body.append(f"{feature} ", style=f"bold {DP_ACCENT}")
|
|
179
|
+
body.append("is a ", style="white")
|
|
180
|
+
body.append(needed_tier, style=f"bold {CROWE_ACCENT}")
|
|
181
|
+
body.append(" feature. You are on ", style="white")
|
|
182
|
+
body.append(f"{current_tier}", style=AMBER_HEX)
|
|
183
|
+
body.append(".\n\n", style="white")
|
|
184
|
+
body.append("Unlocks ", style=DIM)
|
|
185
|
+
body.append(f"{unlocks}\n\n", style="white")
|
|
186
|
+
body.append(f"{ARROW} ", style=CROWE_ACCENT)
|
|
187
|
+
body.append("dp upgrade", style=f"bold {CROWE_ACCENT}")
|
|
188
|
+
body.append(f" {url}", style=DIM)
|
|
189
|
+
console.print(
|
|
190
|
+
_gutter(
|
|
191
|
+
Panel(
|
|
192
|
+
body,
|
|
193
|
+
title=Text(f"{MARK} Upgrade to {needed_tier}", style=DP_ACCENT),
|
|
194
|
+
title_align="left",
|
|
195
|
+
border_style=AMBER_HEX,
|
|
196
|
+
box=box.ROUNDED,
|
|
197
|
+
)
|
|
198
|
+
)
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def license_panel(
|
|
203
|
+
title: str,
|
|
204
|
+
header_lines: list[tuple[str, str]],
|
|
205
|
+
rows: list[tuple[bool, str]],
|
|
206
|
+
footer_lines: list[str] | None = None,
|
|
207
|
+
) -> None:
|
|
208
|
+
body = Text()
|
|
209
|
+
for label, value in header_lines:
|
|
210
|
+
body.append(f"{label:<9}", style=DIM)
|
|
211
|
+
body.append(f"{value}\n", style="white")
|
|
212
|
+
if rows:
|
|
213
|
+
body.append("\n")
|
|
214
|
+
for unlocked, desc in rows:
|
|
215
|
+
if unlocked:
|
|
216
|
+
body.append(f"{CHECK} ", style=GREEN_HEX)
|
|
217
|
+
body.append(f"{desc}\n", style="white")
|
|
218
|
+
else:
|
|
219
|
+
body.append(f"{CROSS} ", style=RED_HEX)
|
|
220
|
+
body.append(f"{desc}\n", style=DIM)
|
|
221
|
+
if footer_lines:
|
|
222
|
+
body.append("\n")
|
|
223
|
+
for line in footer_lines:
|
|
224
|
+
body.append(f"{line}\n", style=DIM)
|
|
225
|
+
console.print(
|
|
226
|
+
_gutter(
|
|
227
|
+
Panel(
|
|
228
|
+
body,
|
|
229
|
+
title=Text(f"{MARK} {title}", style=DP_ACCENT),
|
|
230
|
+
title_align="left",
|
|
231
|
+
border_style=DP_ACCENT,
|
|
232
|
+
box=box.ROUNDED,
|
|
233
|
+
)
|
|
234
|
+
)
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def first_run_banner() -> None:
|
|
239
|
+
body = Text()
|
|
240
|
+
body.append("You're on ", style=DIM)
|
|
241
|
+
body.append("Free", style=f"bold {AMBER_HEX}")
|
|
242
|
+
body.append(". Unlock Guardian, fusion, and the ", style=DIM)
|
|
243
|
+
body.append("--deep", style="white")
|
|
244
|
+
body.append(" council: ", style=DIM)
|
|
245
|
+
body.append("dp upgrade", style=f"bold {CROWE_ACCENT}")
|
|
246
|
+
console.print(_gutter(Panel(body, border_style=DP_ACCENT, box=box.ROUNDED)))
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def upgrade_link(url: str, opened: bool) -> None:
|
|
250
|
+
if opened:
|
|
251
|
+
console.print(f"[{DIM}]Opening your browser to DeepParallel pricing:[/] {url}")
|
|
252
|
+
else:
|
|
253
|
+
console.print(f"[{DIM}]Upgrade at[/] {url}")
|
|
254
|
+
|
|
255
|
+
|
|
176
256
|
def error(msg: str) -> None:
|
|
177
257
|
console.print(f"[bold red]error[/] {msg}")
|
|
178
258
|
|
|
@@ -58,6 +58,27 @@ from deepparallel.cockpit import Cockpit
|
|
|
58
58
|
from deepparallel.cockpit_observe import make_observer
|
|
59
59
|
|
|
60
60
|
|
|
61
|
+
_soft_cards_shown: set[str] = set()
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def require_feature(feature: str, *, hard: bool, exit_code: int = 3) -> bool:
|
|
65
|
+
have = licensing.resolve_tier()
|
|
66
|
+
if licensing.check_feature(feature, have)[0]:
|
|
67
|
+
return True
|
|
68
|
+
if hard or feature not in _soft_cards_shown:
|
|
69
|
+
branding.upgrade_card(
|
|
70
|
+
feature,
|
|
71
|
+
have.label,
|
|
72
|
+
licensing.feature_tier(feature).label,
|
|
73
|
+
licensing.feature_unlocks(feature),
|
|
74
|
+
licensing.upgrade_url(),
|
|
75
|
+
)
|
|
76
|
+
if hard:
|
|
77
|
+
sys.exit(exit_code)
|
|
78
|
+
_soft_cards_shown.add(feature)
|
|
79
|
+
return False
|
|
80
|
+
|
|
81
|
+
|
|
61
82
|
def _build_messages(history: list[tuple[str, str]], system: str, user_msg: str) -> list[dict]:
|
|
62
83
|
msgs: list[dict] = [{"role": "system", "content": system}]
|
|
63
84
|
for role, content in history:
|
|
@@ -82,9 +103,7 @@ def _effective_backend(base: Backend, settings: Settings, mode: str) -> Backend:
|
|
|
82
103
|
"""
|
|
83
104
|
if mode not in ("reason", "escalate"):
|
|
84
105
|
return base
|
|
85
|
-
|
|
86
|
-
if not ok:
|
|
87
|
-
branding.info(msg)
|
|
106
|
+
if not require_feature("fusion", hard=False):
|
|
88
107
|
return base
|
|
89
108
|
reasoner = backend_for_deployment(settings, settings.reasoner_deployment)
|
|
90
109
|
if mode == "reason":
|
|
@@ -100,10 +119,7 @@ def _wrap_fusion(backend: Backend, settings: Settings) -> Backend:
|
|
|
100
119
|
def _run_deep(settings: Settings, messages: list[dict]) -> None:
|
|
101
120
|
"""Heavy multi-model fan-out + judge, one-shot. Chains are shown with
|
|
102
121
|
generic labels (no raw model names) per the brand rule. Paid (Pro+)."""
|
|
103
|
-
|
|
104
|
-
if not ok:
|
|
105
|
-
branding.error(msg)
|
|
106
|
-
sys.exit(2)
|
|
122
|
+
require_feature("deep", hard=True, exit_code=2)
|
|
107
123
|
chains = [
|
|
108
124
|
(f"candidate-{i + 1}", backend_for_deployment(settings, dep))
|
|
109
125
|
for i, dep in enumerate(settings.parallel_deployments)
|
|
@@ -135,10 +151,7 @@ def _run_deep(settings: Settings, messages: list[dict]) -> None:
|
|
|
135
151
|
|
|
136
152
|
|
|
137
153
|
def _run_dual(settings: Settings, messages: list[dict], dual: str, synth: bool) -> None:
|
|
138
|
-
|
|
139
|
-
if not ok:
|
|
140
|
-
branding.error(msg)
|
|
141
|
-
sys.exit(2)
|
|
154
|
+
require_feature("dual", hard=True, exit_code=2)
|
|
142
155
|
names = [s.strip() for s in dual.split(",") if s.strip()] if dual else []
|
|
143
156
|
if len(names) >= 2:
|
|
144
157
|
la, ra, lname, rname = names[0], names[1], names[0], names[1]
|
|
@@ -170,13 +183,26 @@ def _build_guardian(settings: Settings) -> Backend | None:
|
|
|
170
183
|
"""
|
|
171
184
|
if not settings.guardian_enabled:
|
|
172
185
|
return None
|
|
173
|
-
|
|
174
|
-
if not ok:
|
|
175
|
-
branding.info(msg)
|
|
186
|
+
if not require_feature("guardian", hard=False):
|
|
176
187
|
return None
|
|
177
188
|
return backend_for_deployment(settings, settings.guardian_deployment)
|
|
178
189
|
|
|
179
190
|
|
|
191
|
+
def _build_mesh(settings: Settings):
|
|
192
|
+
"""The verification mesh: a parallel reviewer panel that gates every edit.
|
|
193
|
+
|
|
194
|
+
Supersedes the single guardian when available. Paid (Pro+) feature; returns
|
|
195
|
+
a callable ``mesh_fn(content) -> result`` or None on Free / when disabled.
|
|
196
|
+
"""
|
|
197
|
+
if not settings.guardian_enabled:
|
|
198
|
+
return None
|
|
199
|
+
if not licensing.check_feature("mesh")[0]:
|
|
200
|
+
return None
|
|
201
|
+
from deepparallel import mesh
|
|
202
|
+
|
|
203
|
+
return lambda content: mesh.mesh_review(settings, content)
|
|
204
|
+
|
|
205
|
+
|
|
180
206
|
def _system_prompt() -> str:
|
|
181
207
|
"""System prompt with the long-term memory index injected (recall tier 1)."""
|
|
182
208
|
return load_system_prompt() + memory.index_block()
|
|
@@ -293,7 +319,8 @@ def _agent_repl(backend: Backend, settings: Settings, renderer: Renderer) -> Non
|
|
|
293
319
|
"""Interactive agentic loop: tools enabled, conversation persists. The dial
|
|
294
320
|
(/fast //fuse //escalate //deep) swaps the active fusion mode live."""
|
|
295
321
|
registry = get_registry()
|
|
296
|
-
|
|
322
|
+
mesh_fn = _build_mesh(settings)
|
|
323
|
+
guardian = None if mesh_fn else _build_guardian(settings)
|
|
297
324
|
system = _system_prompt()
|
|
298
325
|
messages: list[dict] = [{"role": "system", "content": system}]
|
|
299
326
|
mode = settings.fusion_mode if settings.fusion_mode in ("reason", "escalate") else "off"
|
|
@@ -385,6 +412,7 @@ def _agent_repl(backend: Backend, settings: Settings, renderer: Renderer) -> Non
|
|
|
385
412
|
auto_approve=auto,
|
|
386
413
|
stream=True,
|
|
387
414
|
guardian=guardian,
|
|
415
|
+
mesh_fn=mesh_fn,
|
|
388
416
|
on_event=observer if cockpit_on else None,
|
|
389
417
|
)
|
|
390
418
|
if mode in ("reason", "escalate"):
|
|
@@ -421,6 +449,33 @@ def _chat_loop(settings: Settings) -> None:
|
|
|
421
449
|
_stream_repl(_wrap_fusion(backend, settings), settings)
|
|
422
450
|
|
|
423
451
|
|
|
452
|
+
_LICENSE_COMMANDS = {"activate", "deactivate", "upgrade", "account", "whoami"}
|
|
453
|
+
|
|
454
|
+
|
|
455
|
+
def _is_interactive() -> bool:
|
|
456
|
+
return sys.stdout.isatty() and not _bool_env("DEEPPARALLEL_PLAIN", False)
|
|
457
|
+
|
|
458
|
+
|
|
459
|
+
def _first_run_marker() -> Path:
|
|
460
|
+
return licensing.license_file_path().parent / ".first_run_shown"
|
|
461
|
+
|
|
462
|
+
|
|
463
|
+
def _maybe_first_run_nudge() -> None:
|
|
464
|
+
if not _is_interactive():
|
|
465
|
+
return
|
|
466
|
+
if licensing.resolve_tier() is not licensing.Tier.FREE:
|
|
467
|
+
return
|
|
468
|
+
marker = _first_run_marker()
|
|
469
|
+
if marker.exists():
|
|
470
|
+
return
|
|
471
|
+
try:
|
|
472
|
+
marker.parent.mkdir(parents=True, exist_ok=True)
|
|
473
|
+
marker.write_text("shown\n")
|
|
474
|
+
except OSError:
|
|
475
|
+
pass
|
|
476
|
+
branding.first_run_banner()
|
|
477
|
+
|
|
478
|
+
|
|
424
479
|
@click.group(
|
|
425
480
|
invoke_without_command=True,
|
|
426
481
|
context_settings={"help_option_names": ["-h", "--help"]},
|
|
@@ -441,6 +496,8 @@ def main(ctx: click.Context, temperature: float | None, assume_yes: bool) -> Non
|
|
|
441
496
|
if assume_yes:
|
|
442
497
|
settings = replace(settings, auto_approve=True)
|
|
443
498
|
ctx.obj["settings"] = settings
|
|
499
|
+
if ctx.invoked_subcommand not in _LICENSE_COMMANDS:
|
|
500
|
+
_maybe_first_run_nudge()
|
|
444
501
|
if ctx.invoked_subcommand is None:
|
|
445
502
|
_chat_loop(settings)
|
|
446
503
|
|
|
@@ -463,6 +520,14 @@ def chat(ctx: click.Context, no_tools: bool, assume_yes: bool, fuse: str | None)
|
|
|
463
520
|
_chat_loop(settings)
|
|
464
521
|
|
|
465
522
|
|
|
523
|
+
@main.command()
|
|
524
|
+
def tui() -> None:
|
|
525
|
+
"""Launch the DeepParallel TUI (persistent multi-panel terminal interface)."""
|
|
526
|
+
from deepparallel.tui.app import run_tui
|
|
527
|
+
|
|
528
|
+
run_tui()
|
|
529
|
+
|
|
530
|
+
|
|
466
531
|
@main.command()
|
|
467
532
|
@click.option("--no-tools", is_flag=True, help="Disable tools (plain chat).")
|
|
468
533
|
@click.option("--yes", "-y", "assume_yes", is_flag=True, help="Auto-approve tool actions.")
|
|
@@ -518,6 +583,7 @@ def run(
|
|
|
518
583
|
renderer,
|
|
519
584
|
interactive=False,
|
|
520
585
|
auto_approve=settings.auto_approve,
|
|
586
|
+
mesh_fn=_build_mesh(settings),
|
|
521
587
|
)
|
|
522
588
|
except Exception as e: # noqa: BLE001 - surface as friendly message
|
|
523
589
|
branding.error(_translate_error(e))
|
|
@@ -598,10 +664,7 @@ def review(ctx: click.Context, as_diff: bool, path: str | None) -> None:
|
|
|
598
664
|
"""
|
|
599
665
|
settings: Settings = ctx.obj["settings"]
|
|
600
666
|
if not settings.byok:
|
|
601
|
-
|
|
602
|
-
if not ok:
|
|
603
|
-
branding.error(msg)
|
|
604
|
-
sys.exit(3)
|
|
667
|
+
require_feature("review", hard=True, exit_code=3)
|
|
605
668
|
if as_diff:
|
|
606
669
|
content = sys.stdin.read()
|
|
607
670
|
elif path:
|
|
@@ -614,10 +677,25 @@ def review(ctx: click.Context, as_diff: bool, path: str | None) -> None:
|
|
|
614
677
|
branding.error("provide a PATH or --diff (with a diff on stdin)")
|
|
615
678
|
sys.exit(3)
|
|
616
679
|
_require_ready(settings) # validates creds / exits if missing
|
|
680
|
+
glyphs = {"safe": branding.CHECK, "risky": "!", "bug": branding.CROSS}
|
|
681
|
+
mesh_ok, _ = licensing.check_feature("mesh")
|
|
682
|
+
if mesh_ok and settings.guardian_enabled:
|
|
683
|
+
# Pro: the full verification mesh, one independent reviewer per lens.
|
|
684
|
+
from deepparallel import mesh as _mesh
|
|
685
|
+
|
|
686
|
+
result = _mesh.mesh_review(settings, content)
|
|
687
|
+
for r in result["lenses"]:
|
|
688
|
+
g = glyphs.get(r["severity"], "?")
|
|
689
|
+
console.print(f" {g} [dim]{r['label']}[/] {r['verdict'] or '(no verdict)'}")
|
|
690
|
+
verdict = result["verdict"]
|
|
691
|
+
glyph = glyphs.get(result["severity"], "?")
|
|
692
|
+
console.print(f"[bold]{glyph} {result['severity'].upper()}[/] {verdict or '(no verdict)'}")
|
|
693
|
+
sys.exit(verdict_exit_code(verdict))
|
|
694
|
+
# Free / BYOK: a single independent reviewer.
|
|
617
695
|
guardian = backend_for_deployment(settings, settings.guardian_deployment)
|
|
618
696
|
verdict = guardian_review(guardian, content[:8000])
|
|
619
697
|
severity = verdict_severity(verdict)
|
|
620
|
-
glyph =
|
|
698
|
+
glyph = glyphs.get(severity, "?")
|
|
621
699
|
console.print(f"[bold]{glyph} {severity.upper()}[/] {verdict or '(no verdict)'}")
|
|
622
700
|
sys.exit(verdict_exit_code(verdict))
|
|
623
701
|
|
|
@@ -648,10 +726,7 @@ def audit(ctx: click.Context, path: str) -> None:
|
|
|
648
726
|
seen) in source files and manifests. Exit code: 0 clean, 2 if a likely
|
|
649
727
|
hallucinated dependency is found - so it can gate a commit or PR. Paid (Pro+).
|
|
650
728
|
"""
|
|
651
|
-
|
|
652
|
-
if not ok:
|
|
653
|
-
branding.error(msg)
|
|
654
|
-
sys.exit(3)
|
|
729
|
+
require_feature("audit", hard=True, exit_code=3)
|
|
655
730
|
try:
|
|
656
731
|
content = Path(path).expanduser().read_text(encoding="utf-8")
|
|
657
732
|
except OSError as e:
|
|
@@ -726,6 +801,47 @@ def research_conduit() -> None:
|
|
|
726
801
|
)
|
|
727
802
|
|
|
728
803
|
|
|
804
|
+
@research.command("compound-discovery")
|
|
805
|
+
@click.option("--target", default="EGFR", help="Biological target (gene symbol).")
|
|
806
|
+
@click.option(
|
|
807
|
+
"--compounds",
|
|
808
|
+
default="erlotinib,gefitinib,osimertinib",
|
|
809
|
+
help="Comma-separated compound names.",
|
|
810
|
+
)
|
|
811
|
+
@click.option("--dry-run", is_flag=True, help="Print the MCP tool call manifest.")
|
|
812
|
+
@click.option("--output", default=None, help="Output report path (Markdown + JSON).")
|
|
813
|
+
def research_compound_discovery(
|
|
814
|
+
target: str, compounds: str, dry_run: bool, output: str | None
|
|
815
|
+
) -> None:
|
|
816
|
+
"""PubChem compound discovery workflow across 10 stages.
|
|
817
|
+
|
|
818
|
+
Generates a full intelligence report: target assays, compound profiling,
|
|
819
|
+
bioactivity, safety, interactions, 3D structure, cross-references, and
|
|
820
|
+
analog discovery. Uses PubChem MCP tools when available; --dry-run prints
|
|
821
|
+
the tool call manifest without executing.
|
|
822
|
+
"""
|
|
823
|
+
from deepparallel.research.compound_discovery import (
|
|
824
|
+
generate_markdown_report,
|
|
825
|
+
run_dry_run,
|
|
826
|
+
run_workflow,
|
|
827
|
+
)
|
|
828
|
+
|
|
829
|
+
compound_list = [c.strip() for c in compounds.split(",")]
|
|
830
|
+
if dry_run:
|
|
831
|
+
console.print(run_dry_run(target, compound_list))
|
|
832
|
+
return
|
|
833
|
+
result = run_workflow(target, compound_list)
|
|
834
|
+
report = generate_markdown_report(result)
|
|
835
|
+
print(report)
|
|
836
|
+
if output:
|
|
837
|
+
path = Path(output)
|
|
838
|
+
path.write_text(report)
|
|
839
|
+
json_path = path.with_suffix(".json")
|
|
840
|
+
json_path.write_text(result.to_json())
|
|
841
|
+
branding.info(f"Report written to {path}")
|
|
842
|
+
branding.info(f"JSON data written to {json_path}")
|
|
843
|
+
|
|
844
|
+
|
|
729
845
|
@main.command(name="tools")
|
|
730
846
|
def tools_cmd() -> None:
|
|
731
847
|
"""List the agent tools available to DeepParallel."""
|
|
@@ -787,5 +903,105 @@ def doctor(ctx: click.Context) -> None:
|
|
|
787
903
|
sys.exit(1)
|
|
788
904
|
|
|
789
905
|
|
|
906
|
+
def _format_exp(payload: dict) -> str:
|
|
907
|
+
exp = int(payload.get("exp") or 0)
|
|
908
|
+
if not exp:
|
|
909
|
+
return "no expiry"
|
|
910
|
+
import datetime
|
|
911
|
+
|
|
912
|
+
return datetime.datetime.fromtimestamp(exp).strftime("%Y-%m-%d")
|
|
913
|
+
|
|
914
|
+
|
|
915
|
+
def _show_account(settings: Settings) -> None:
|
|
916
|
+
source, payload = licensing.active_license()
|
|
917
|
+
tier = licensing.resolve_tier()
|
|
918
|
+
if source == "env":
|
|
919
|
+
src_label = "DEEPPARALLEL_LICENSE env var"
|
|
920
|
+
elif source == "file":
|
|
921
|
+
src_label = str(licensing.license_file_path())
|
|
922
|
+
else:
|
|
923
|
+
src_label = "none"
|
|
924
|
+
if source != "none" and payload is None:
|
|
925
|
+
src_label += " (invalid or expired)"
|
|
926
|
+
header = [("Tier", tier.label), ("Source", src_label)]
|
|
927
|
+
if payload and payload.get("email"):
|
|
928
|
+
header.append(("Account", str(payload["email"])))
|
|
929
|
+
if payload:
|
|
930
|
+
header.append(("Expires", _format_exp(payload)))
|
|
931
|
+
rows = [(unlocked, desc) for _f, desc, _need, unlocked in licensing.features_matrix(tier)]
|
|
932
|
+
footer = None
|
|
933
|
+
if tier is licensing.Tier.FREE:
|
|
934
|
+
footer = ["Unlock Guardian, fusion, and the --deep council: dp upgrade"]
|
|
935
|
+
branding.license_panel("DeepParallel account", header, rows, footer)
|
|
936
|
+
|
|
937
|
+
|
|
938
|
+
@main.command()
|
|
939
|
+
@click.argument("key", required=True)
|
|
940
|
+
@click.pass_context
|
|
941
|
+
def activate(ctx: click.Context, key: str) -> None:
|
|
942
|
+
payload = licensing.verify_token(key.strip())
|
|
943
|
+
if not payload:
|
|
944
|
+
branding.error(
|
|
945
|
+
"that license key is invalid or expired. Copy it exactly from your "
|
|
946
|
+
"purchase email, or run dp upgrade to buy one."
|
|
947
|
+
)
|
|
948
|
+
sys.exit(2)
|
|
949
|
+
tier = licensing.tier_from_payload(payload)
|
|
950
|
+
try:
|
|
951
|
+
path = licensing.write_license(key)
|
|
952
|
+
except OSError as e:
|
|
953
|
+
branding.error(f"could not write the license file: {e}")
|
|
954
|
+
sys.exit(1)
|
|
955
|
+
header = [("Tier", tier.label)]
|
|
956
|
+
if payload.get("email"):
|
|
957
|
+
header.append(("Account", str(payload["email"])))
|
|
958
|
+
header.append(("Expires", _format_exp(payload)))
|
|
959
|
+
header.append(("License", str(path)))
|
|
960
|
+
rows = [(unlocked, desc) for _f, desc, _need, unlocked in licensing.features_matrix(tier)]
|
|
961
|
+
branding.license_panel("License activated", header, rows)
|
|
962
|
+
|
|
963
|
+
|
|
964
|
+
@main.command()
|
|
965
|
+
@click.pass_context
|
|
966
|
+
def account(ctx: click.Context) -> None:
|
|
967
|
+
_show_account(ctx.obj["settings"])
|
|
968
|
+
|
|
969
|
+
|
|
970
|
+
@main.command()
|
|
971
|
+
@click.pass_context
|
|
972
|
+
def whoami(ctx: click.Context) -> None:
|
|
973
|
+
_show_account(ctx.obj["settings"])
|
|
974
|
+
|
|
975
|
+
|
|
976
|
+
@main.command()
|
|
977
|
+
@click.option("--yes", "-y", "assume_yes", is_flag=True, help="Skip the confirmation prompt.")
|
|
978
|
+
@click.pass_context
|
|
979
|
+
def deactivate(ctx: click.Context, assume_yes: bool) -> None:
|
|
980
|
+
path = licensing.license_file_path()
|
|
981
|
+
if not path.exists():
|
|
982
|
+
branding.info("no stored license; already on Free.")
|
|
983
|
+
return
|
|
984
|
+
if not assume_yes and not click.confirm(f"Remove the license at {path} and revert to Free?"):
|
|
985
|
+
branding.info("kept the current license.")
|
|
986
|
+
return
|
|
987
|
+
if licensing.remove_license():
|
|
988
|
+
branding.info("license removed; you are now on Free.")
|
|
989
|
+
else:
|
|
990
|
+
branding.error("could not remove the license file.")
|
|
991
|
+
sys.exit(1)
|
|
992
|
+
|
|
993
|
+
|
|
994
|
+
@main.command()
|
|
995
|
+
def upgrade() -> None:
|
|
996
|
+
import webbrowser
|
|
997
|
+
|
|
998
|
+
url = licensing.upgrade_url()
|
|
999
|
+
try:
|
|
1000
|
+
opened = bool(webbrowser.open(url))
|
|
1001
|
+
except Exception:
|
|
1002
|
+
opened = False
|
|
1003
|
+
branding.upgrade_link(url, opened)
|
|
1004
|
+
|
|
1005
|
+
|
|
790
1006
|
if __name__ == "__main__":
|
|
791
1007
|
main()
|