deepparallel 0.4.1__tar.gz → 0.4.2__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.4.1 → deepparallel-0.4.2}/PKG-INFO +7 -2
- {deepparallel-0.4.1 → deepparallel-0.4.2}/README.md +6 -1
- {deepparallel-0.4.1 → deepparallel-0.4.2}/deepparallel/__init__.py +1 -1
- {deepparallel-0.4.1 → deepparallel-0.4.2}/deepparallel/agent.py +2 -0
- {deepparallel-0.4.1 → deepparallel-0.4.2}/deepparallel/cli.py +21 -4
- {deepparallel-0.4.1 → deepparallel-0.4.2}/deepparallel/renderer.py +52 -61
- deepparallel-0.4.2/deepparallel/system_prompt.txt +4 -0
- deepparallel-0.4.2/deepparallel/tools/shell.py +69 -0
- {deepparallel-0.4.1 → deepparallel-0.4.2}/deepparallel.egg-info/PKG-INFO +7 -2
- {deepparallel-0.4.1 → deepparallel-0.4.2}/pyproject.toml +1 -1
- {deepparallel-0.4.1 → deepparallel-0.4.2}/tests/test_renderer.py +18 -4
- deepparallel-0.4.2/tests/test_tools_shell.py +48 -0
- deepparallel-0.4.1/deepparallel/system_prompt.txt +0 -4
- deepparallel-0.4.1/deepparallel/tools/shell.py +0 -38
- deepparallel-0.4.1/tests/test_tools_shell.py +0 -24
- {deepparallel-0.4.1 → deepparallel-0.4.2}/deepparallel/backend.py +0 -0
- {deepparallel-0.4.1 → deepparallel-0.4.2}/deepparallel/branding.py +0 -0
- {deepparallel-0.4.1 → deepparallel-0.4.2}/deepparallel/config.py +0 -0
- {deepparallel-0.4.1 → deepparallel-0.4.2}/deepparallel/fusion.py +0 -0
- {deepparallel-0.4.1 → deepparallel-0.4.2}/deepparallel/licensing.py +0 -0
- {deepparallel-0.4.1 → deepparallel-0.4.2}/deepparallel/registry.json +0 -0
- {deepparallel-0.4.1 → deepparallel-0.4.2}/deepparallel/research/__init__.py +0 -0
- {deepparallel-0.4.1 → deepparallel-0.4.2}/deepparallel/research/conduit.py +0 -0
- {deepparallel-0.4.1 → deepparallel-0.4.2}/deepparallel/supply_chain.py +0 -0
- {deepparallel-0.4.1 → deepparallel-0.4.2}/deepparallel/tools/__init__.py +0 -0
- {deepparallel-0.4.1 → deepparallel-0.4.2}/deepparallel/tools/codeast.py +0 -0
- {deepparallel-0.4.1 → deepparallel-0.4.2}/deepparallel/tools/edit.py +0 -0
- {deepparallel-0.4.1 → deepparallel-0.4.2}/deepparallel/tools/files.py +0 -0
- {deepparallel-0.4.1 → deepparallel-0.4.2}/deepparallel/tools/registry.py +0 -0
- {deepparallel-0.4.1 → deepparallel-0.4.2}/deepparallel/tools/sandbox.py +0 -0
- {deepparallel-0.4.1 → deepparallel-0.4.2}/deepparallel/tools/search.py +0 -0
- {deepparallel-0.4.1 → deepparallel-0.4.2}/deepparallel/tools/vision.py +0 -0
- {deepparallel-0.4.1 → deepparallel-0.4.2}/deepparallel/tools/web.py +0 -0
- {deepparallel-0.4.1 → deepparallel-0.4.2}/deepparallel.egg-info/SOURCES.txt +0 -0
- {deepparallel-0.4.1 → deepparallel-0.4.2}/deepparallel.egg-info/dependency_links.txt +0 -0
- {deepparallel-0.4.1 → deepparallel-0.4.2}/deepparallel.egg-info/entry_points.txt +0 -0
- {deepparallel-0.4.1 → deepparallel-0.4.2}/deepparallel.egg-info/requires.txt +0 -0
- {deepparallel-0.4.1 → deepparallel-0.4.2}/deepparallel.egg-info/top_level.txt +0 -0
- {deepparallel-0.4.1 → deepparallel-0.4.2}/setup.cfg +0 -0
- {deepparallel-0.4.1 → deepparallel-0.4.2}/tests/test_agent.py +0 -0
- {deepparallel-0.4.1 → deepparallel-0.4.2}/tests/test_backend.py +0 -0
- {deepparallel-0.4.1 → deepparallel-0.4.2}/tests/test_backend_chat.py +0 -0
- {deepparallel-0.4.1 → deepparallel-0.4.2}/tests/test_backend_stream.py +0 -0
- {deepparallel-0.4.1 → deepparallel-0.4.2}/tests/test_branding.py +0 -0
- {deepparallel-0.4.1 → deepparallel-0.4.2}/tests/test_cli.py +0 -0
- {deepparallel-0.4.1 → deepparallel-0.4.2}/tests/test_config.py +0 -0
- {deepparallel-0.4.1 → deepparallel-0.4.2}/tests/test_fusion.py +0 -0
- {deepparallel-0.4.1 → deepparallel-0.4.2}/tests/test_issuer_signer.py +0 -0
- {deepparallel-0.4.1 → deepparallel-0.4.2}/tests/test_licensing.py +0 -0
- {deepparallel-0.4.1 → deepparallel-0.4.2}/tests/test_research.py +0 -0
- {deepparallel-0.4.1 → deepparallel-0.4.2}/tests/test_supply_chain.py +0 -0
- {deepparallel-0.4.1 → deepparallel-0.4.2}/tests/test_tool_registry.py +0 -0
- {deepparallel-0.4.1 → deepparallel-0.4.2}/tests/test_tools_codeast.py +0 -0
- {deepparallel-0.4.1 → deepparallel-0.4.2}/tests/test_tools_edit.py +0 -0
- {deepparallel-0.4.1 → deepparallel-0.4.2}/tests/test_tools_files.py +0 -0
- {deepparallel-0.4.1 → deepparallel-0.4.2}/tests/test_tools_sandbox.py +0 -0
- {deepparallel-0.4.1 → deepparallel-0.4.2}/tests/test_tools_search.py +0 -0
- {deepparallel-0.4.1 → deepparallel-0.4.2}/tests/test_tools_vision.py +0 -0
- {deepparallel-0.4.1 → deepparallel-0.4.2}/tests/test_tools_web.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: deepparallel
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.2
|
|
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
|
|
@@ -115,7 +115,12 @@ compose hosted backends (no GPU/weight-merging), so they are API-call stacking.
|
|
|
115
115
|
- Read-only (run automatically): `read_file`, `list_dir`, `glob`, `grep`,
|
|
116
116
|
`ast_symbols`, `ast_show_symbol`, `web_fetch`, `web_search`, `analyze_image`.
|
|
117
117
|
- Mutating / executing (require confirmation): `write_file`, `edit_file`,
|
|
118
|
-
`ast_replace_symbol`, `run_shell`, `run_code
|
|
118
|
+
`ast_replace_symbol`, `run_shell`, `run_code`, `open_path` (open a file, folder,
|
|
119
|
+
or URL in the OS default app - e.g. an HTML report in the browser).
|
|
120
|
+
|
|
121
|
+
In interactive chat, type `/auto` to toggle auto-approve (tools run without
|
|
122
|
+
asking; edits outside the project and unknown dependencies still confirm), or
|
|
123
|
+
start with `deepparallel --yes`.
|
|
119
124
|
|
|
120
125
|
`web_search` needs `DEEPPARALLEL_SEARCH_API_KEY`; `analyze_image` works out of the
|
|
121
126
|
box on a multimodal deployment (override with `DEEPPARALLEL_VISION_DEPLOYMENT`).
|
|
@@ -91,7 +91,12 @@ compose hosted backends (no GPU/weight-merging), so they are API-call stacking.
|
|
|
91
91
|
- Read-only (run automatically): `read_file`, `list_dir`, `glob`, `grep`,
|
|
92
92
|
`ast_symbols`, `ast_show_symbol`, `web_fetch`, `web_search`, `analyze_image`.
|
|
93
93
|
- Mutating / executing (require confirmation): `write_file`, `edit_file`,
|
|
94
|
-
`ast_replace_symbol`, `run_shell`, `run_code
|
|
94
|
+
`ast_replace_symbol`, `run_shell`, `run_code`, `open_path` (open a file, folder,
|
|
95
|
+
or URL in the OS default app - e.g. an HTML report in the browser).
|
|
96
|
+
|
|
97
|
+
In interactive chat, type `/auto` to toggle auto-approve (tools run without
|
|
98
|
+
asking; edits outside the project and unknown dependencies still confirm), or
|
|
99
|
+
start with `deepparallel --yes`.
|
|
95
100
|
|
|
96
101
|
`web_search` needs `DEEPPARALLEL_SEARCH_API_KEY`; `analyze_image` works out of the
|
|
97
102
|
box on a multimodal deployment (override with `DEEPPARALLEL_VISION_DEPLOYMENT`).
|
|
@@ -71,6 +71,8 @@ def _summarize_result(name: str, result: str) -> str:
|
|
|
71
71
|
return "edited"
|
|
72
72
|
if name == "ast_replace_symbol":
|
|
73
73
|
return "replaced"
|
|
74
|
+
if name == "open_path":
|
|
75
|
+
return f"opened {obj.get('opened', '')}" if "opened" in obj else "ok"
|
|
74
76
|
if name == "run_shell":
|
|
75
77
|
lines = (obj.get("stdout") or "").count("\n")
|
|
76
78
|
return f"rc {obj.get('return_code')} · {_plural(lines, 'line', 'lines')}"
|
|
@@ -289,8 +289,10 @@ def _agent_repl(backend: Backend, settings: Settings, renderer: Renderer) -> Non
|
|
|
289
289
|
messages: list[dict] = [{"role": "system", "content": system}]
|
|
290
290
|
mode = settings.fusion_mode if settings.fusion_mode in ("reason", "escalate") else "off"
|
|
291
291
|
deep_next = False
|
|
292
|
+
auto = settings.auto_approve
|
|
292
293
|
while True:
|
|
293
|
-
|
|
294
|
+
bits = ([mode] if mode != "off" else []) + (["auto"] if auto else [])
|
|
295
|
+
tag = f"[{' · '.join(bits)}] " if bits else ""
|
|
294
296
|
try:
|
|
295
297
|
user_msg = console.input(f"{tag}{branding.user_prefix()}› ").strip()
|
|
296
298
|
except (EOFError, KeyboardInterrupt):
|
|
@@ -302,9 +304,19 @@ def _agent_repl(backend: Backend, settings: Settings, renderer: Renderer) -> Non
|
|
|
302
304
|
break
|
|
303
305
|
if user_msg == "/help":
|
|
304
306
|
branding.info(
|
|
305
|
-
"/quit · /reset · /info · /tools · /fast //fuse //escalate //deep · prompt"
|
|
307
|
+
"/quit · /reset · /info · /tools · /auto · /fast //fuse //escalate //deep · prompt"
|
|
306
308
|
)
|
|
307
309
|
continue
|
|
310
|
+
if user_msg in {"/auto", "/yes"}:
|
|
311
|
+
auto = not auto
|
|
312
|
+
if auto:
|
|
313
|
+
branding.info(
|
|
314
|
+
"auto-approve ON - tools run without asking. Edits outside this "
|
|
315
|
+
"project and unknown dependencies still confirm. /auto to turn off."
|
|
316
|
+
)
|
|
317
|
+
else:
|
|
318
|
+
branding.info("auto-approve OFF - mutating tools will ask first.")
|
|
319
|
+
continue
|
|
308
320
|
if user_msg == "/reset":
|
|
309
321
|
messages = [{"role": "system", "content": system}]
|
|
310
322
|
branding.info("conversation cleared.")
|
|
@@ -334,7 +346,7 @@ def _agent_repl(backend: Backend, settings: Settings, renderer: Renderer) -> Non
|
|
|
334
346
|
settings,
|
|
335
347
|
renderer,
|
|
336
348
|
interactive=True,
|
|
337
|
-
auto_approve=
|
|
349
|
+
auto_approve=auto,
|
|
338
350
|
stream=True,
|
|
339
351
|
guardian=guardian,
|
|
340
352
|
)
|
|
@@ -376,14 +388,19 @@ def _chat_loop(settings: Settings) -> None:
|
|
|
376
388
|
)
|
|
377
389
|
@click.version_option(__version__, prog_name="deepparallel")
|
|
378
390
|
@click.option("--temperature", "-t", default=None, type=float, help="Sampling temperature.")
|
|
391
|
+
@click.option(
|
|
392
|
+
"--yes", "-y", "assume_yes", is_flag=True, help="Auto-approve tool actions (toggle with /auto)."
|
|
393
|
+
)
|
|
379
394
|
@click.pass_context
|
|
380
|
-
def main(ctx: click.Context, temperature: float | None) -> None:
|
|
395
|
+
def main(ctx: click.Context, temperature: float | None, assume_yes: bool) -> None:
|
|
381
396
|
"""DeepParallel - a focused agentic CLI for the DeepParallel model."""
|
|
382
397
|
load_dotenv()
|
|
383
398
|
ctx.ensure_object(dict)
|
|
384
399
|
settings = resolve_settings()
|
|
385
400
|
if temperature is not None:
|
|
386
401
|
settings = replace(settings, temperature=temperature)
|
|
402
|
+
if assume_yes:
|
|
403
|
+
settings = replace(settings, auto_approve=True)
|
|
387
404
|
ctx.obj["settings"] = settings
|
|
388
405
|
if ctx.invoked_subcommand is None:
|
|
389
406
|
_chat_loop(settings)
|
|
@@ -13,7 +13,6 @@ incrementally (no redrawing Live regions) so output never ghosts.
|
|
|
13
13
|
from __future__ import annotations
|
|
14
14
|
|
|
15
15
|
import sys
|
|
16
|
-
import threading
|
|
17
16
|
import time
|
|
18
17
|
from abc import ABC, abstractmethod
|
|
19
18
|
from typing import Iterable
|
|
@@ -132,8 +131,6 @@ class RichRenderer(Renderer):
|
|
|
132
131
|
self._console = console or branding.console
|
|
133
132
|
self._input_fn = input_fn or self._console.input
|
|
134
133
|
self._cur: str | None = None
|
|
135
|
-
self._timer_stop: threading.Event | None = None
|
|
136
|
-
self._timer_thread: threading.Thread | None = None
|
|
137
134
|
|
|
138
135
|
def welcome(self, backend_label, *, version="", tool_count=0, fusion_modes=()) -> None:
|
|
139
136
|
animate = self._console.is_terminal and _REVEAL_SECONDS > 0
|
|
@@ -155,12 +152,14 @@ class RichRenderer(Renderer):
|
|
|
155
152
|
self._console.print(branding.build_transcript_markdown(self._console, text))
|
|
156
153
|
|
|
157
154
|
def answer_stream(self, chunks: Iterable[str]) -> str:
|
|
158
|
-
"""Stream the answer. On a real terminal, render Markdown
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
155
|
+
"""Stream the answer. On a real terminal, render Markdown block by block,
|
|
156
|
+
append-only: each complete block (a paragraph/heading/list ended by a
|
|
157
|
+
blank line, or a closed code fence) is rendered once as it settles. This
|
|
158
|
+
never moves the cursor up, so it cannot ghost or stack the way a growing
|
|
159
|
+
Live region does - it works the same on any terminal and at any answer
|
|
160
|
+
length. On a pipe / non-tty, fall back to raw inline streaming."""
|
|
162
161
|
if self._console.is_terminal:
|
|
163
|
-
return self.
|
|
162
|
+
return self._stream_markdown_blocks(chunks)
|
|
164
163
|
return self._stream_inline(chunks)
|
|
165
164
|
|
|
166
165
|
def _stream_inline(self, chunks: Iterable[str]) -> str:
|
|
@@ -185,71 +184,63 @@ class RichRenderer(Renderer):
|
|
|
185
184
|
self._console.print()
|
|
186
185
|
return "".join(parts)
|
|
187
186
|
|
|
188
|
-
def
|
|
189
|
-
from rich.
|
|
187
|
+
def _stream_markdown_blocks(self, chunks: Iterable[str]) -> str:
|
|
188
|
+
from rich.markdown import Markdown
|
|
189
|
+
from rich.padding import Padding
|
|
190
190
|
|
|
191
|
-
|
|
191
|
+
full: list[str] = []
|
|
192
|
+
buf = "" # text not yet split into complete lines
|
|
193
|
+
block: list[str] = [] # lines of the block currently accumulating
|
|
194
|
+
in_fence = False
|
|
192
195
|
started = False
|
|
193
|
-
last_draw = 0.0
|
|
194
|
-
live = Live(
|
|
195
|
-
console=self._console,
|
|
196
|
-
auto_refresh=False, # we drive refreshes; deterministic, no bg thread
|
|
197
|
-
vertical_overflow="visible", # let answers taller than the screen scroll
|
|
198
|
-
)
|
|
199
|
-
try:
|
|
200
|
-
for c in chunks:
|
|
201
|
-
parts.append(c)
|
|
202
|
-
if not started:
|
|
203
|
-
if not "".join(parts).strip():
|
|
204
|
-
continue # hold the panel back until real content arrives
|
|
205
|
-
started = True
|
|
206
|
-
live.start()
|
|
207
|
-
now = time.monotonic()
|
|
208
|
-
if now - last_draw >= 0.06: # throttle to ~16 fps
|
|
209
|
-
live.update(self._answer_panel("".join(parts)), refresh=True)
|
|
210
|
-
last_draw = now
|
|
211
|
-
if started: # final frame: the complete, settled answer
|
|
212
|
-
live.update(self._answer_panel("".join(parts)), refresh=True)
|
|
213
|
-
finally:
|
|
214
|
-
if started:
|
|
215
|
-
live.stop()
|
|
216
|
-
return "".join(parts)
|
|
217
196
|
|
|
218
|
-
|
|
219
|
-
|
|
197
|
+
def emit() -> None:
|
|
198
|
+
nonlocal block, started
|
|
199
|
+
text = "\n".join(block).strip("\n")
|
|
200
|
+
block = []
|
|
201
|
+
if not text.strip():
|
|
202
|
+
return
|
|
203
|
+
if not started:
|
|
204
|
+
started = True
|
|
205
|
+
self._console.print(f"[{branding.DP_ACCENT}]{branding.MARK}[/]", highlight=False)
|
|
206
|
+
self._console.print(
|
|
207
|
+
Padding(Markdown(_balance_fences(text)), (0, 0, 0, branding.GUTTER))
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
for c in chunks:
|
|
211
|
+
full.append(c)
|
|
212
|
+
buf += c
|
|
213
|
+
while "\n" in buf: # only act on complete lines; keep the partial
|
|
214
|
+
line, buf = buf.split("\n", 1)
|
|
215
|
+
if line.strip().startswith("```"):
|
|
216
|
+
block.append(line)
|
|
217
|
+
if in_fence: # this line closes the fence -> block is complete
|
|
218
|
+
in_fence = False
|
|
219
|
+
emit()
|
|
220
|
+
else:
|
|
221
|
+
in_fence = True
|
|
222
|
+
elif in_fence:
|
|
223
|
+
block.append(line)
|
|
224
|
+
elif line.strip() == "": # blank line ends a prose block
|
|
225
|
+
emit()
|
|
226
|
+
else:
|
|
227
|
+
block.append(line)
|
|
228
|
+
if buf.strip(): # trailing partial line at end of stream
|
|
229
|
+
block.append(buf)
|
|
230
|
+
emit() # flush whatever block remains (fence balanced if still open)
|
|
231
|
+
return "".join(full)
|
|
220
232
|
|
|
221
233
|
def reasoning(self, text: str) -> None:
|
|
222
234
|
self._console.print(branding.build_reasoning_panel(self._console, text))
|
|
223
235
|
|
|
224
236
|
def tool_start(self, name: str, args_preview: str) -> None:
|
|
237
|
+
# Append-only: render the running card and stop. A per-tick elapsed timer
|
|
238
|
+
# needs carriage-return overwrites, which some terminals don't honor and
|
|
239
|
+
# then flood with hundreds of lines; the final card carries the duration.
|
|
225
240
|
self._cur = name
|
|
226
241
|
branding.render_tool_card(self._console, name, args_preview, status="running")
|
|
227
|
-
if self._console.is_terminal:
|
|
228
|
-
self._timer_stop = threading.Event()
|
|
229
|
-
start = time.monotonic()
|
|
230
|
-
fh = self._console.file
|
|
231
|
-
|
|
232
|
-
def tick() -> None:
|
|
233
|
-
while not self._timer_stop.wait(0.5): # type: ignore[union-attr]
|
|
234
|
-
fh.write(f"\r {name}... {time.monotonic() - start:.0f}s ")
|
|
235
|
-
fh.flush()
|
|
236
|
-
|
|
237
|
-
self._timer_thread = threading.Thread(target=tick, daemon=True)
|
|
238
|
-
self._timer_thread.start()
|
|
239
|
-
|
|
240
|
-
def _stop_timer(self) -> None:
|
|
241
|
-
if self._timer_stop is None:
|
|
242
|
-
return
|
|
243
|
-
self._timer_stop.set()
|
|
244
|
-
if self._timer_thread is not None:
|
|
245
|
-
self._timer_thread.join(timeout=1.0)
|
|
246
|
-
self._timer_stop = None
|
|
247
|
-
self._timer_thread = None
|
|
248
|
-
self._console.file.write("\r" + " " * 48 + "\r") # clear the timer line
|
|
249
|
-
self._console.file.flush()
|
|
250
242
|
|
|
251
243
|
def tool_result(self, ok: bool, summary: str, duration_s: float) -> None:
|
|
252
|
-
self._stop_timer()
|
|
253
244
|
branding.render_tool_card(
|
|
254
245
|
self._console,
|
|
255
246
|
self._cur or "",
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
You are DeepParallel, a precise and capable coding assistant served via Crowe Logic.
|
|
2
|
+
Answer clearly and directly. When a problem benefits from step-by-step reasoning, reason carefully before giving the final answer. Be concise unless asked for depth.
|
|
3
|
+
|
|
4
|
+
You can use tools to read, search, analyze, edit, open, and run code. Use them when they help; do not call them speculatively. When the user asks to "open" a file (an HTML report, image, PDF, or folder) for viewing, use open_path to launch it in the default app rather than read_file, which only returns text contents. When asked to run something with different parameters, prefer non-destructive approaches (command-line arguments, environment variables, or a temporary copy) over editing the user's source files. Only edit a source file when changing that file is the actual goal, and explain what you changed.
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"""Shell execution tools."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import shutil
|
|
7
|
+
import subprocess
|
|
8
|
+
import sys
|
|
9
|
+
|
|
10
|
+
from deepparallel.tools import tool
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@tool(dangerous=True)
|
|
14
|
+
def run_shell(command: str, working_directory: str = "", timeout_seconds: int = 120) -> str:
|
|
15
|
+
"""Run a shell command and capture its output.
|
|
16
|
+
|
|
17
|
+
:param command: Command to execute via the shell.
|
|
18
|
+
:param working_directory: Directory to run in (default: current).
|
|
19
|
+
:param timeout_seconds: Maximum execution time in seconds.
|
|
20
|
+
"""
|
|
21
|
+
timeout_seconds = min(int(timeout_seconds), 600)
|
|
22
|
+
try:
|
|
23
|
+
r = subprocess.run(
|
|
24
|
+
command,
|
|
25
|
+
shell=True,
|
|
26
|
+
capture_output=True,
|
|
27
|
+
text=True,
|
|
28
|
+
timeout=timeout_seconds,
|
|
29
|
+
cwd=working_directory or None,
|
|
30
|
+
)
|
|
31
|
+
except subprocess.TimeoutExpired:
|
|
32
|
+
return json.dumps(
|
|
33
|
+
{"error": f"Command timed out after {timeout_seconds}s", "return_code": -1}
|
|
34
|
+
)
|
|
35
|
+
stdout = r.stdout or ""
|
|
36
|
+
if len(stdout) > 50000:
|
|
37
|
+
stdout = stdout[:50000] + "\n... (output truncated at 50KB)"
|
|
38
|
+
return json.dumps(
|
|
39
|
+
{"stdout": stdout, "stderr": (r.stderr or "")[:10000], "return_code": r.returncode}
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _opener_argv(path: str) -> list[str]:
|
|
44
|
+
if sys.platform == "darwin":
|
|
45
|
+
return ["open", path]
|
|
46
|
+
if sys.platform.startswith("win"):
|
|
47
|
+
return ["cmd", "/c", "start", "", path]
|
|
48
|
+
return ["xdg-open", path]
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@tool(dangerous=True)
|
|
52
|
+
def open_path(path: str) -> str:
|
|
53
|
+
"""Open a file, directory, or URL in the operating system's default app.
|
|
54
|
+
|
|
55
|
+
Use this when the user asks to "open" something for viewing - an HTML report
|
|
56
|
+
in the browser, a PNG/PDF in the image/document viewer, a folder in the file
|
|
57
|
+
manager. This launches the real app; it does not return the file contents
|
|
58
|
+
(use read_file for text or analyze_image to inspect an image).
|
|
59
|
+
|
|
60
|
+
:param path: File path, directory, or URL to open.
|
|
61
|
+
"""
|
|
62
|
+
argv = _opener_argv(path)
|
|
63
|
+
if shutil.which(argv[0]) is None:
|
|
64
|
+
return json.dumps({"error": f"no opener available ({argv[0]} not found)"})
|
|
65
|
+
try:
|
|
66
|
+
subprocess.Popen(argv, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
|
67
|
+
except Exception as e: # noqa: BLE001 - surface launch failure to the model
|
|
68
|
+
return json.dumps({"error": f"{type(e).__name__}: {e}"})
|
|
69
|
+
return json.dumps({"opened": path})
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: deepparallel
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.2
|
|
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
|
|
@@ -115,7 +115,12 @@ compose hosted backends (no GPU/weight-merging), so they are API-call stacking.
|
|
|
115
115
|
- Read-only (run automatically): `read_file`, `list_dir`, `glob`, `grep`,
|
|
116
116
|
`ast_symbols`, `ast_show_symbol`, `web_fetch`, `web_search`, `analyze_image`.
|
|
117
117
|
- Mutating / executing (require confirmation): `write_file`, `edit_file`,
|
|
118
|
-
`ast_replace_symbol`, `run_shell`, `run_code
|
|
118
|
+
`ast_replace_symbol`, `run_shell`, `run_code`, `open_path` (open a file, folder,
|
|
119
|
+
or URL in the OS default app - e.g. an HTML report in the browser).
|
|
120
|
+
|
|
121
|
+
In interactive chat, type `/auto` to toggle auto-approve (tools run without
|
|
122
|
+
asking; edits outside the project and unknown dependencies still confirm), or
|
|
123
|
+
start with `deepparallel --yes`.
|
|
119
124
|
|
|
120
125
|
`web_search` needs `DEEPPARALLEL_SEARCH_API_KEY`; `analyze_image` works out of the
|
|
121
126
|
box on a multimodal deployment (override with `DEEPPARALLEL_VISION_DEPLOYMENT`).
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "deepparallel"
|
|
7
|
-
version = "0.4.
|
|
7
|
+
version = "0.4.2"
|
|
8
8
|
description = "DeepParallel - a multi-model agentic coding CLI with cross-model Guardian review, served via Crowe Logic."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = { text = "Apache-2.0" }
|
|
@@ -95,7 +95,8 @@ def test_rich_tool_timer_path_renders_card(monkeypatch):
|
|
|
95
95
|
out = buf.getvalue()
|
|
96
96
|
assert "run_shell" in out
|
|
97
97
|
assert "✓" in out and "rc 0" in out
|
|
98
|
-
|
|
98
|
+
# append-only: no ticking timer thread is spawned
|
|
99
|
+
assert not hasattr(r, "_timer_thread")
|
|
99
100
|
|
|
100
101
|
|
|
101
102
|
def test_rich_error_uses_cross():
|
|
@@ -123,15 +124,28 @@ def test_rich_confirm_shows_detail():
|
|
|
123
124
|
assert "added line" in out
|
|
124
125
|
|
|
125
126
|
|
|
126
|
-
def
|
|
127
|
+
def test_rich_answer_stream_returns_text_and_renders_markdown():
|
|
127
128
|
buf = io.StringIO()
|
|
128
129
|
con = Console(no_color=True, width=80, file=buf, force_terminal=True, highlight=False)
|
|
129
130
|
r = RichRenderer(console=con)
|
|
130
131
|
full = r.answer_stream(iter(["Hel", "lo ", "world"]))
|
|
131
132
|
out = buf.getvalue()
|
|
132
133
|
assert full == "Hello world" # full text preserved for history
|
|
133
|
-
assert "Hello world" in out # rendered
|
|
134
|
-
assert "◆" in out
|
|
134
|
+
assert "Hello world" in out # rendered as a markdown block
|
|
135
|
+
assert "◆" in out # answer marker printed once
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def test_rich_answer_stream_code_block_renders_atomically():
|
|
139
|
+
# a code block with an internal blank line must not be split mid-block
|
|
140
|
+
buf = io.StringIO()
|
|
141
|
+
con = Console(no_color=True, width=80, file=buf, force_terminal=True, highlight=False)
|
|
142
|
+
r = RichRenderer(console=con)
|
|
143
|
+
code = "intro\n\n```python\na = 1\n\nb = 2\n```\n\ndone"
|
|
144
|
+
full = r.answer_stream(iter([code]))
|
|
145
|
+
out = buf.getvalue()
|
|
146
|
+
assert full == code
|
|
147
|
+
assert "a = 1" in out and "b = 2" in out and "intro" in out and "done" in out
|
|
148
|
+
assert "```" not in out # fence rendered, not shown literally
|
|
135
149
|
|
|
136
150
|
|
|
137
151
|
def test_rich_answer_stream_renders_markdown_formatting():
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import json
|
|
2
|
+
|
|
3
|
+
import deepparallel.tools.shell as shell_mod
|
|
4
|
+
from deepparallel.tools import get_registry
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def test_run_shell_captures_stdout_and_rc():
|
|
8
|
+
out = json.loads(shell_mod.run_shell("echo hello"))
|
|
9
|
+
assert out["return_code"] == 0
|
|
10
|
+
assert "hello" in out["stdout"]
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def test_run_shell_respects_cwd(tmp_path):
|
|
14
|
+
out = json.loads(shell_mod.run_shell("pwd", working_directory=str(tmp_path)))
|
|
15
|
+
assert str(tmp_path) in out["stdout"]
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def test_run_shell_timeout_returns_error():
|
|
19
|
+
out = json.loads(shell_mod.run_shell("sleep 5", timeout_seconds=1))
|
|
20
|
+
assert "timed out" in out["error"]
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def test_run_shell_is_dangerous():
|
|
24
|
+
assert get_registry().get("run_shell").dangerous is True
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def test_open_path_launches_default_opener(monkeypatch):
|
|
28
|
+
calls = {}
|
|
29
|
+
monkeypatch.setattr(shell_mod.shutil, "which", lambda _: "/usr/bin/open")
|
|
30
|
+
|
|
31
|
+
def fake_popen(argv, **kw):
|
|
32
|
+
calls["argv"] = argv
|
|
33
|
+
return object()
|
|
34
|
+
|
|
35
|
+
monkeypatch.setattr(shell_mod.subprocess, "Popen", fake_popen)
|
|
36
|
+
out = json.loads(shell_mod.open_path("/tmp/report.html"))
|
|
37
|
+
assert out["opened"] == "/tmp/report.html"
|
|
38
|
+
assert "/tmp/report.html" in calls["argv"]
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def test_open_path_reports_missing_opener(monkeypatch):
|
|
42
|
+
monkeypatch.setattr(shell_mod.shutil, "which", lambda _: None)
|
|
43
|
+
out = json.loads(shell_mod.open_path("/tmp/x.png"))
|
|
44
|
+
assert "error" in out
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def test_open_path_is_dangerous():
|
|
48
|
+
assert get_registry().get("open_path").dangerous is True
|
|
@@ -1,4 +0,0 @@
|
|
|
1
|
-
You are DeepParallel, a precise and capable coding assistant served via Crowe Logic.
|
|
2
|
-
Answer clearly and directly. When a problem benefits from step-by-step reasoning, reason carefully before giving the final answer. Be concise unless asked for depth.
|
|
3
|
-
|
|
4
|
-
You can use tools to read, search, analyze, edit, and run code. Use them when they help; do not call them speculatively. When asked to run something with different parameters, prefer non-destructive approaches (command-line arguments, environment variables, or a temporary copy) over editing the user's source files. Only edit a source file when changing that file is the actual goal, and explain what you changed.
|
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
"""Shell execution tool."""
|
|
2
|
-
|
|
3
|
-
from __future__ import annotations
|
|
4
|
-
|
|
5
|
-
import json
|
|
6
|
-
import subprocess
|
|
7
|
-
|
|
8
|
-
from deepparallel.tools import tool
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
@tool(dangerous=True)
|
|
12
|
-
def run_shell(command: str, working_directory: str = "", timeout_seconds: int = 120) -> str:
|
|
13
|
-
"""Run a shell command and capture its output.
|
|
14
|
-
|
|
15
|
-
:param command: Command to execute via the shell.
|
|
16
|
-
:param working_directory: Directory to run in (default: current).
|
|
17
|
-
:param timeout_seconds: Maximum execution time in seconds.
|
|
18
|
-
"""
|
|
19
|
-
timeout_seconds = min(int(timeout_seconds), 600)
|
|
20
|
-
try:
|
|
21
|
-
r = subprocess.run(
|
|
22
|
-
command,
|
|
23
|
-
shell=True,
|
|
24
|
-
capture_output=True,
|
|
25
|
-
text=True,
|
|
26
|
-
timeout=timeout_seconds,
|
|
27
|
-
cwd=working_directory or None,
|
|
28
|
-
)
|
|
29
|
-
except subprocess.TimeoutExpired:
|
|
30
|
-
return json.dumps(
|
|
31
|
-
{"error": f"Command timed out after {timeout_seconds}s", "return_code": -1}
|
|
32
|
-
)
|
|
33
|
-
stdout = r.stdout or ""
|
|
34
|
-
if len(stdout) > 50000:
|
|
35
|
-
stdout = stdout[:50000] + "\n... (output truncated at 50KB)"
|
|
36
|
-
return json.dumps(
|
|
37
|
-
{"stdout": stdout, "stderr": (r.stderr or "")[:10000], "return_code": r.returncode}
|
|
38
|
-
)
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
import json
|
|
2
|
-
|
|
3
|
-
import deepparallel.tools.shell as shell_mod
|
|
4
|
-
from deepparallel.tools import get_registry
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
def test_run_shell_captures_stdout_and_rc():
|
|
8
|
-
out = json.loads(shell_mod.run_shell("echo hello"))
|
|
9
|
-
assert out["return_code"] == 0
|
|
10
|
-
assert "hello" in out["stdout"]
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
def test_run_shell_respects_cwd(tmp_path):
|
|
14
|
-
out = json.loads(shell_mod.run_shell("pwd", working_directory=str(tmp_path)))
|
|
15
|
-
assert str(tmp_path) in out["stdout"]
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
def test_run_shell_timeout_returns_error():
|
|
19
|
-
out = json.loads(shell_mod.run_shell("sleep 5", timeout_seconds=1))
|
|
20
|
-
assert "timed out" in out["error"]
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
def test_run_shell_is_dangerous():
|
|
24
|
-
assert get_registry().get("run_shell").dangerous is True
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|