deepparallel 0.4.0__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.0 → deepparallel-0.4.2}/PKG-INFO +7 -2
- {deepparallel-0.4.0 → deepparallel-0.4.2}/README.md +6 -1
- {deepparallel-0.4.0 → deepparallel-0.4.2}/deepparallel/__init__.py +1 -1
- {deepparallel-0.4.0 → deepparallel-0.4.2}/deepparallel/agent.py +2 -0
- {deepparallel-0.4.0 → deepparallel-0.4.2}/deepparallel/cli.py +21 -4
- {deepparallel-0.4.0 → deepparallel-0.4.2}/deepparallel/renderer.py +73 -32
- deepparallel-0.4.2/deepparallel/system_prompt.txt +4 -0
- deepparallel-0.4.2/deepparallel/tools/shell.py +69 -0
- {deepparallel-0.4.0 → deepparallel-0.4.2}/deepparallel.egg-info/PKG-INFO +7 -2
- {deepparallel-0.4.0 → deepparallel-0.4.2}/pyproject.toml +1 -1
- {deepparallel-0.4.0 → deepparallel-0.4.2}/tests/test_renderer.py +47 -11
- deepparallel-0.4.2/tests/test_tools_shell.py +48 -0
- deepparallel-0.4.0/deepparallel/system_prompt.txt +0 -4
- deepparallel-0.4.0/deepparallel/tools/shell.py +0 -38
- deepparallel-0.4.0/tests/test_tools_shell.py +0 -24
- {deepparallel-0.4.0 → deepparallel-0.4.2}/deepparallel/backend.py +0 -0
- {deepparallel-0.4.0 → deepparallel-0.4.2}/deepparallel/branding.py +0 -0
- {deepparallel-0.4.0 → deepparallel-0.4.2}/deepparallel/config.py +0 -0
- {deepparallel-0.4.0 → deepparallel-0.4.2}/deepparallel/fusion.py +0 -0
- {deepparallel-0.4.0 → deepparallel-0.4.2}/deepparallel/licensing.py +0 -0
- {deepparallel-0.4.0 → deepparallel-0.4.2}/deepparallel/registry.json +0 -0
- {deepparallel-0.4.0 → deepparallel-0.4.2}/deepparallel/research/__init__.py +0 -0
- {deepparallel-0.4.0 → deepparallel-0.4.2}/deepparallel/research/conduit.py +0 -0
- {deepparallel-0.4.0 → deepparallel-0.4.2}/deepparallel/supply_chain.py +0 -0
- {deepparallel-0.4.0 → deepparallel-0.4.2}/deepparallel/tools/__init__.py +0 -0
- {deepparallel-0.4.0 → deepparallel-0.4.2}/deepparallel/tools/codeast.py +0 -0
- {deepparallel-0.4.0 → deepparallel-0.4.2}/deepparallel/tools/edit.py +0 -0
- {deepparallel-0.4.0 → deepparallel-0.4.2}/deepparallel/tools/files.py +0 -0
- {deepparallel-0.4.0 → deepparallel-0.4.2}/deepparallel/tools/registry.py +0 -0
- {deepparallel-0.4.0 → deepparallel-0.4.2}/deepparallel/tools/sandbox.py +0 -0
- {deepparallel-0.4.0 → deepparallel-0.4.2}/deepparallel/tools/search.py +0 -0
- {deepparallel-0.4.0 → deepparallel-0.4.2}/deepparallel/tools/vision.py +0 -0
- {deepparallel-0.4.0 → deepparallel-0.4.2}/deepparallel/tools/web.py +0 -0
- {deepparallel-0.4.0 → deepparallel-0.4.2}/deepparallel.egg-info/SOURCES.txt +0 -0
- {deepparallel-0.4.0 → deepparallel-0.4.2}/deepparallel.egg-info/dependency_links.txt +0 -0
- {deepparallel-0.4.0 → deepparallel-0.4.2}/deepparallel.egg-info/entry_points.txt +0 -0
- {deepparallel-0.4.0 → deepparallel-0.4.2}/deepparallel.egg-info/requires.txt +0 -0
- {deepparallel-0.4.0 → deepparallel-0.4.2}/deepparallel.egg-info/top_level.txt +0 -0
- {deepparallel-0.4.0 → deepparallel-0.4.2}/setup.cfg +0 -0
- {deepparallel-0.4.0 → deepparallel-0.4.2}/tests/test_agent.py +0 -0
- {deepparallel-0.4.0 → deepparallel-0.4.2}/tests/test_backend.py +0 -0
- {deepparallel-0.4.0 → deepparallel-0.4.2}/tests/test_backend_chat.py +0 -0
- {deepparallel-0.4.0 → deepparallel-0.4.2}/tests/test_backend_stream.py +0 -0
- {deepparallel-0.4.0 → deepparallel-0.4.2}/tests/test_branding.py +0 -0
- {deepparallel-0.4.0 → deepparallel-0.4.2}/tests/test_cli.py +0 -0
- {deepparallel-0.4.0 → deepparallel-0.4.2}/tests/test_config.py +0 -0
- {deepparallel-0.4.0 → deepparallel-0.4.2}/tests/test_fusion.py +0 -0
- {deepparallel-0.4.0 → deepparallel-0.4.2}/tests/test_issuer_signer.py +0 -0
- {deepparallel-0.4.0 → deepparallel-0.4.2}/tests/test_licensing.py +0 -0
- {deepparallel-0.4.0 → deepparallel-0.4.2}/tests/test_research.py +0 -0
- {deepparallel-0.4.0 → deepparallel-0.4.2}/tests/test_supply_chain.py +0 -0
- {deepparallel-0.4.0 → deepparallel-0.4.2}/tests/test_tool_registry.py +0 -0
- {deepparallel-0.4.0 → deepparallel-0.4.2}/tests/test_tools_codeast.py +0 -0
- {deepparallel-0.4.0 → deepparallel-0.4.2}/tests/test_tools_edit.py +0 -0
- {deepparallel-0.4.0 → deepparallel-0.4.2}/tests/test_tools_files.py +0 -0
- {deepparallel-0.4.0 → deepparallel-0.4.2}/tests/test_tools_sandbox.py +0 -0
- {deepparallel-0.4.0 → deepparallel-0.4.2}/tests/test_tools_search.py +0 -0
- {deepparallel-0.4.0 → deepparallel-0.4.2}/tests/test_tools_vision.py +0 -0
- {deepparallel-0.4.0 → 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
|
|
@@ -25,6 +24,15 @@ from deepparallel import branding
|
|
|
25
24
|
_REVEAL_SECONDS = 0.04 # per-line delay for the animated intro (tests set 0)
|
|
26
25
|
|
|
27
26
|
|
|
27
|
+
def _balance_fences(text: str) -> str:
|
|
28
|
+
"""Close a dangling ``` while streaming so a half-arrived code block renders
|
|
29
|
+
as code instead of leaking its fence as literal text. The closing fence is
|
|
30
|
+
cosmetic for the in-progress frame; the final frame has the real one."""
|
|
31
|
+
if text.count("```") % 2 == 1:
|
|
32
|
+
return text + "\n```"
|
|
33
|
+
return text
|
|
34
|
+
|
|
35
|
+
|
|
28
36
|
class Renderer(ABC):
|
|
29
37
|
@abstractmethod
|
|
30
38
|
def welcome(
|
|
@@ -123,8 +131,6 @@ class RichRenderer(Renderer):
|
|
|
123
131
|
self._console = console or branding.console
|
|
124
132
|
self._input_fn = input_fn or self._console.input
|
|
125
133
|
self._cur: str | None = None
|
|
126
|
-
self._timer_stop: threading.Event | None = None
|
|
127
|
-
self._timer_thread: threading.Thread | None = None
|
|
128
134
|
|
|
129
135
|
def welcome(self, backend_label, *, version="", tool_count=0, fusion_modes=()) -> None:
|
|
130
136
|
animate = self._console.is_terminal and _REVEAL_SECONDS > 0
|
|
@@ -146,10 +152,20 @@ class RichRenderer(Renderer):
|
|
|
146
152
|
self._console.print(branding.build_transcript_markdown(self._console, text))
|
|
147
153
|
|
|
148
154
|
def answer_stream(self, chunks: Iterable[str]) -> str:
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
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."""
|
|
161
|
+
if self._console.is_terminal:
|
|
162
|
+
return self._stream_markdown_blocks(chunks)
|
|
163
|
+
return self._stream_inline(chunks)
|
|
164
|
+
|
|
165
|
+
def _stream_inline(self, chunks: Iterable[str]) -> str:
|
|
166
|
+
# Raw token streaming for pipes / non-tty: no Live, never ghosts. The
|
|
167
|
+
# marker is printed only on the first VISIBLE character, so empty /
|
|
168
|
+
# whitespace-leading (tool-only) turns render no stray marker.
|
|
153
169
|
parts: list[str] = []
|
|
154
170
|
started = False
|
|
155
171
|
for c in chunks:
|
|
@@ -158,7 +174,7 @@ class RichRenderer(Renderer):
|
|
|
158
174
|
self._console.print(c, end="", soft_wrap=True, highlight=False, markup=False)
|
|
159
175
|
continue
|
|
160
176
|
if not "".join(parts).strip():
|
|
161
|
-
continue
|
|
177
|
+
continue
|
|
162
178
|
started = True
|
|
163
179
|
self._console.print(
|
|
164
180
|
f"[{branding.DP_ACCENT}]{branding.MARK}[/] ", end="", highlight=False
|
|
@@ -168,38 +184,63 @@ class RichRenderer(Renderer):
|
|
|
168
184
|
self._console.print()
|
|
169
185
|
return "".join(parts)
|
|
170
186
|
|
|
187
|
+
def _stream_markdown_blocks(self, chunks: Iterable[str]) -> str:
|
|
188
|
+
from rich.markdown import Markdown
|
|
189
|
+
from rich.padding import Padding
|
|
190
|
+
|
|
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
|
|
195
|
+
started = False
|
|
196
|
+
|
|
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)
|
|
232
|
+
|
|
171
233
|
def reasoning(self, text: str) -> None:
|
|
172
234
|
self._console.print(branding.build_reasoning_panel(self._console, text))
|
|
173
235
|
|
|
174
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.
|
|
175
240
|
self._cur = name
|
|
176
241
|
branding.render_tool_card(self._console, name, args_preview, status="running")
|
|
177
|
-
if self._console.is_terminal:
|
|
178
|
-
self._timer_stop = threading.Event()
|
|
179
|
-
start = time.monotonic()
|
|
180
|
-
fh = self._console.file
|
|
181
|
-
|
|
182
|
-
def tick() -> None:
|
|
183
|
-
while not self._timer_stop.wait(0.5): # type: ignore[union-attr]
|
|
184
|
-
fh.write(f"\r {name}... {time.monotonic() - start:.0f}s ")
|
|
185
|
-
fh.flush()
|
|
186
|
-
|
|
187
|
-
self._timer_thread = threading.Thread(target=tick, daemon=True)
|
|
188
|
-
self._timer_thread.start()
|
|
189
|
-
|
|
190
|
-
def _stop_timer(self) -> None:
|
|
191
|
-
if self._timer_stop is None:
|
|
192
|
-
return
|
|
193
|
-
self._timer_stop.set()
|
|
194
|
-
if self._timer_thread is not None:
|
|
195
|
-
self._timer_thread.join(timeout=1.0)
|
|
196
|
-
self._timer_stop = None
|
|
197
|
-
self._timer_thread = None
|
|
198
|
-
self._console.file.write("\r" + " " * 48 + "\r") # clear the timer line
|
|
199
|
-
self._console.file.flush()
|
|
200
242
|
|
|
201
243
|
def tool_result(self, ok: bool, summary: str, duration_s: float) -> None:
|
|
202
|
-
self._stop_timer()
|
|
203
244
|
branding.render_tool_card(
|
|
204
245
|
self._console,
|
|
205
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,14 +124,40 @@ 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
|
-
|
|
133
|
-
assert
|
|
132
|
+
out = buf.getvalue()
|
|
133
|
+
assert full == "Hello world" # full text preserved for history
|
|
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
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def test_rich_answer_stream_renders_markdown_formatting():
|
|
152
|
+
buf = io.StringIO()
|
|
153
|
+
con = Console(no_color=True, width=80, file=buf, force_terminal=True, highlight=False)
|
|
154
|
+
r = RichRenderer(console=con)
|
|
155
|
+
full = r.answer_stream(iter(["# Big\n\n", "body **bold**"]))
|
|
156
|
+
out = buf.getvalue()
|
|
157
|
+
assert full == "# Big\n\nbody **bold**"
|
|
158
|
+
# markdown is rendered, not shown raw: the heading hash is consumed
|
|
159
|
+
assert "Big" in out and "body" in out
|
|
160
|
+
assert "# Big" not in out
|
|
134
161
|
|
|
135
162
|
|
|
136
163
|
def test_rich_answer_stream_empty_prints_nothing():
|
|
@@ -139,20 +166,29 @@ def test_rich_answer_stream_empty_prints_nothing():
|
|
|
139
166
|
r = RichRenderer(console=con)
|
|
140
167
|
full = r.answer_stream(iter([]))
|
|
141
168
|
assert full == ""
|
|
142
|
-
assert buf.getvalue() == "" # tool-only turns
|
|
169
|
+
assert buf.getvalue() == "" # tool-only turns never start the live panel
|
|
143
170
|
|
|
144
171
|
|
|
145
|
-
def
|
|
172
|
+
def test_rich_answer_stream_whitespace_leading_holds_panel():
|
|
146
173
|
buf = io.StringIO()
|
|
147
174
|
con = Console(no_color=True, width=80, file=buf, force_terminal=True, highlight=False)
|
|
148
175
|
r = RichRenderer(console=con)
|
|
149
176
|
full = r.answer_stream(iter(["\n", " ", "\n", "Hello"]))
|
|
150
177
|
out = buf.getvalue()
|
|
151
178
|
assert full == "\n \nHello" # full text preserved for history
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
179
|
+
assert "Hello" in out and "◆" in out
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def test_rich_answer_stream_inline_when_not_a_terminal():
|
|
183
|
+
# pipes / CI: raw inline streaming, no Live panel (no border characters)
|
|
184
|
+
buf = io.StringIO()
|
|
185
|
+
con = Console(no_color=True, width=80, file=buf, highlight=False) # not a terminal
|
|
186
|
+
r = RichRenderer(console=con)
|
|
187
|
+
full = r.answer_stream(iter(["Hel", "lo"]))
|
|
188
|
+
out = buf.getvalue()
|
|
189
|
+
assert full == "Hello"
|
|
190
|
+
assert "Hello" in out
|
|
191
|
+
assert "─" not in out and "answer" not in out # no panel chrome
|
|
156
192
|
|
|
157
193
|
|
|
158
194
|
# ---------------------------------------------------------------- FakeRenderer
|
|
@@ -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
|