deepparallel 0.4.2__tar.gz → 0.4.3__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.2 → deepparallel-0.4.3}/PKG-INFO +1 -1
- {deepparallel-0.4.2 → deepparallel-0.4.3}/deepparallel/__init__.py +1 -1
- {deepparallel-0.4.2 → deepparallel-0.4.3}/deepparallel/branding.py +98 -0
- {deepparallel-0.4.2 → deepparallel-0.4.3}/deepparallel/cli.py +19 -2
- {deepparallel-0.4.2 → deepparallel-0.4.3}/deepparallel/licensing.py +1 -1
- {deepparallel-0.4.2 → deepparallel-0.4.3}/deepparallel/renderer.py +33 -5
- deepparallel-0.4.3/deepparallel/serve.py +261 -0
- deepparallel-0.4.3/deepparallel/system_prompt.txt +7 -0
- deepparallel-0.4.3/deepparallel/userinput.py +61 -0
- {deepparallel-0.4.2 → deepparallel-0.4.3}/deepparallel.egg-info/PKG-INFO +1 -1
- {deepparallel-0.4.2 → deepparallel-0.4.3}/deepparallel.egg-info/SOURCES.txt +5 -1
- {deepparallel-0.4.2 → deepparallel-0.4.3}/pyproject.toml +1 -1
- {deepparallel-0.4.2 → deepparallel-0.4.3}/tests/test_renderer.py +64 -2
- deepparallel-0.4.3/tests/test_spinner_color.py +45 -0
- deepparallel-0.4.3/tests/test_userinput.py +40 -0
- deepparallel-0.4.2/deepparallel/system_prompt.txt +0 -4
- {deepparallel-0.4.2 → deepparallel-0.4.3}/README.md +0 -0
- {deepparallel-0.4.2 → deepparallel-0.4.3}/deepparallel/agent.py +0 -0
- {deepparallel-0.4.2 → deepparallel-0.4.3}/deepparallel/backend.py +0 -0
- {deepparallel-0.4.2 → deepparallel-0.4.3}/deepparallel/config.py +0 -0
- {deepparallel-0.4.2 → deepparallel-0.4.3}/deepparallel/fusion.py +0 -0
- {deepparallel-0.4.2 → deepparallel-0.4.3}/deepparallel/registry.json +0 -0
- {deepparallel-0.4.2 → deepparallel-0.4.3}/deepparallel/research/__init__.py +0 -0
- {deepparallel-0.4.2 → deepparallel-0.4.3}/deepparallel/research/conduit.py +0 -0
- {deepparallel-0.4.2 → deepparallel-0.4.3}/deepparallel/supply_chain.py +0 -0
- {deepparallel-0.4.2 → deepparallel-0.4.3}/deepparallel/tools/__init__.py +0 -0
- {deepparallel-0.4.2 → deepparallel-0.4.3}/deepparallel/tools/codeast.py +0 -0
- {deepparallel-0.4.2 → deepparallel-0.4.3}/deepparallel/tools/edit.py +0 -0
- {deepparallel-0.4.2 → deepparallel-0.4.3}/deepparallel/tools/files.py +0 -0
- {deepparallel-0.4.2 → deepparallel-0.4.3}/deepparallel/tools/registry.py +0 -0
- {deepparallel-0.4.2 → deepparallel-0.4.3}/deepparallel/tools/sandbox.py +0 -0
- {deepparallel-0.4.2 → deepparallel-0.4.3}/deepparallel/tools/search.py +0 -0
- {deepparallel-0.4.2 → deepparallel-0.4.3}/deepparallel/tools/shell.py +0 -0
- {deepparallel-0.4.2 → deepparallel-0.4.3}/deepparallel/tools/vision.py +0 -0
- {deepparallel-0.4.2 → deepparallel-0.4.3}/deepparallel/tools/web.py +0 -0
- {deepparallel-0.4.2 → deepparallel-0.4.3}/deepparallel.egg-info/dependency_links.txt +0 -0
- {deepparallel-0.4.2 → deepparallel-0.4.3}/deepparallel.egg-info/entry_points.txt +0 -0
- {deepparallel-0.4.2 → deepparallel-0.4.3}/deepparallel.egg-info/requires.txt +0 -0
- {deepparallel-0.4.2 → deepparallel-0.4.3}/deepparallel.egg-info/top_level.txt +0 -0
- {deepparallel-0.4.2 → deepparallel-0.4.3}/setup.cfg +0 -0
- {deepparallel-0.4.2 → deepparallel-0.4.3}/tests/test_agent.py +0 -0
- {deepparallel-0.4.2 → deepparallel-0.4.3}/tests/test_backend.py +0 -0
- {deepparallel-0.4.2 → deepparallel-0.4.3}/tests/test_backend_chat.py +0 -0
- {deepparallel-0.4.2 → deepparallel-0.4.3}/tests/test_backend_stream.py +0 -0
- {deepparallel-0.4.2 → deepparallel-0.4.3}/tests/test_branding.py +0 -0
- {deepparallel-0.4.2 → deepparallel-0.4.3}/tests/test_cli.py +0 -0
- {deepparallel-0.4.2 → deepparallel-0.4.3}/tests/test_config.py +0 -0
- {deepparallel-0.4.2 → deepparallel-0.4.3}/tests/test_fusion.py +0 -0
- {deepparallel-0.4.2 → deepparallel-0.4.3}/tests/test_issuer_signer.py +0 -0
- {deepparallel-0.4.2 → deepparallel-0.4.3}/tests/test_licensing.py +0 -0
- {deepparallel-0.4.2 → deepparallel-0.4.3}/tests/test_research.py +0 -0
- {deepparallel-0.4.2 → deepparallel-0.4.3}/tests/test_supply_chain.py +0 -0
- {deepparallel-0.4.2 → deepparallel-0.4.3}/tests/test_tool_registry.py +0 -0
- {deepparallel-0.4.2 → deepparallel-0.4.3}/tests/test_tools_codeast.py +0 -0
- {deepparallel-0.4.2 → deepparallel-0.4.3}/tests/test_tools_edit.py +0 -0
- {deepparallel-0.4.2 → deepparallel-0.4.3}/tests/test_tools_files.py +0 -0
- {deepparallel-0.4.2 → deepparallel-0.4.3}/tests/test_tools_sandbox.py +0 -0
- {deepparallel-0.4.2 → deepparallel-0.4.3}/tests/test_tools_search.py +0 -0
- {deepparallel-0.4.2 → deepparallel-0.4.3}/tests/test_tools_shell.py +0 -0
- {deepparallel-0.4.2 → deepparallel-0.4.3}/tests/test_tools_vision.py +0 -0
- {deepparallel-0.4.2 → deepparallel-0.4.3}/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.3
|
|
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
|
|
@@ -9,6 +9,8 @@ status colors follow the Crowe Logic CLI family patterns.
|
|
|
9
9
|
|
|
10
10
|
from __future__ import annotations
|
|
11
11
|
|
|
12
|
+
import math
|
|
13
|
+
|
|
12
14
|
from rich import box
|
|
13
15
|
from rich.console import Console, Group
|
|
14
16
|
from rich.markdown import Markdown
|
|
@@ -66,6 +68,91 @@ def thinking(text: str) -> None:
|
|
|
66
68
|
console.print(f"[{DIM}]{text}[/]", end="", soft_wrap=True, highlight=False)
|
|
67
69
|
|
|
68
70
|
|
|
71
|
+
_PULSE_BLOCKS = "▁▂▃▄▅▆▇█"
|
|
72
|
+
|
|
73
|
+
# Crest color drifts slowly through these stops while the pulse travels. On-brand:
|
|
74
|
+
# DeepParallel cyan -> a cooler teal-blue -> Crowe green, then loops. The drift is
|
|
75
|
+
# what makes the spinner "change colors" as it works, distinct from the per-lane
|
|
76
|
+
# brightness that creates the travelling crest.
|
|
77
|
+
_CREST_PALETTE = ("#3fd0d0", "#5fb8c4", "#8fa4bf", "#79bf94", "#6fbf73")
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _lerp_hex(a: str, b: str, t: float) -> str:
|
|
81
|
+
"""Linear blend between two #rrggbb colors; t in 0..1. Pure, for the crest drift."""
|
|
82
|
+
t = 0.0 if t < 0 else 1.0 if t > 1 else t
|
|
83
|
+
ar, ag, ab = int(a[1:3], 16), int(a[3:5], 16), int(a[5:7], 16)
|
|
84
|
+
br, bg, bb = int(b[1:3], 16), int(b[3:5], 16), int(b[5:7], 16)
|
|
85
|
+
r = round(ar + (br - ar) * t)
|
|
86
|
+
g = round(ag + (bg - ag) * t)
|
|
87
|
+
bl = round(ab + (bb - ab) * t)
|
|
88
|
+
return f"#{r:02x}{g:02x}{bl:02x}"
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _crest_color(phase: float) -> str:
|
|
92
|
+
"""The crest hue at a continuous phase, cycling smoothly around _CREST_PALETTE."""
|
|
93
|
+
n = len(_CREST_PALETTE)
|
|
94
|
+
pos = (phase % n + n) % n # wrap into 0..n
|
|
95
|
+
i = int(pos)
|
|
96
|
+
return _lerp_hex(_CREST_PALETTE[i], _CREST_PALETTE[(i + 1) % n], pos - i)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
class ThinkingSpinner:
|
|
100
|
+
"""The 'working' animation in DeepParallel's idiom: several reasoning lanes
|
|
101
|
+
pulsing in parallel with a bright crest travelling across them, anchored by
|
|
102
|
+
the ◆ mark - parallel chains computing toward one answer, in the cyan
|
|
103
|
+
identity. Distinct from a borrowed braille dot.
|
|
104
|
+
|
|
105
|
+
Stateful: each render advances one frame. A rich `Live` widget animates it
|
|
106
|
+
simply by re-rendering at its refresh rate (the same mechanism the built-in
|
|
107
|
+
Spinner relies on), so no timer thread of our own is needed.
|
|
108
|
+
"""
|
|
109
|
+
|
|
110
|
+
def __init__(
|
|
111
|
+
self,
|
|
112
|
+
label: str = "thinking",
|
|
113
|
+
*,
|
|
114
|
+
lanes: int = 5,
|
|
115
|
+
speed: float = 0.45,
|
|
116
|
+
spread: float = 0.7,
|
|
117
|
+
hue_speed: float = 0.06,
|
|
118
|
+
):
|
|
119
|
+
self._label = label
|
|
120
|
+
self._lanes = lanes
|
|
121
|
+
self._speed = speed # radians advanced per frame (animation tempo)
|
|
122
|
+
self._spread = spread # phase offset between lanes (crest travel)
|
|
123
|
+
self._hue_speed = hue_speed # palette stops advanced per frame (color drift)
|
|
124
|
+
self._tick = 0
|
|
125
|
+
|
|
126
|
+
def frame(self, tick: int) -> Text:
|
|
127
|
+
"""Build the frame at a given tick (pure; used for rendering + tests)."""
|
|
128
|
+
text = Text()
|
|
129
|
+
crest = _crest_color(tick * self._hue_speed) # this frame's drifting hue
|
|
130
|
+
text.append(f"{MARK} ", style=f"bold {crest}")
|
|
131
|
+
for i in range(self._lanes):
|
|
132
|
+
level = (math.sin(tick * self._speed - i * self._spread) + 1) / 2 # 0..1
|
|
133
|
+
# Crest cells take the live hue; mid cells a dimmed blend; troughs go dim.
|
|
134
|
+
if level > 0.72:
|
|
135
|
+
style = f"bold {crest}"
|
|
136
|
+
elif level > 0.4:
|
|
137
|
+
style = _lerp_hex("#3a4a4a", crest, 0.6)
|
|
138
|
+
else:
|
|
139
|
+
style = DIM
|
|
140
|
+
text.append(_PULSE_BLOCKS[round(level * (len(_PULSE_BLOCKS) - 1))], style=style)
|
|
141
|
+
text.append(f" {self._label}…", style=DIM)
|
|
142
|
+
return text
|
|
143
|
+
|
|
144
|
+
def __rich__(self) -> Text:
|
|
145
|
+
frame = self.frame(self._tick)
|
|
146
|
+
self._tick += 1
|
|
147
|
+
return frame
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def thinking_spinner(label: str = "thinking") -> ThinkingSpinner:
|
|
151
|
+
"""A fresh DeepParallel thinking animation (one per turn so it starts at
|
|
152
|
+
frame 0). Drive it inside a transient `rich.live.Live`."""
|
|
153
|
+
return ThinkingSpinner(label)
|
|
154
|
+
|
|
155
|
+
|
|
69
156
|
def info(msg: str) -> None:
|
|
70
157
|
console.print(f"[{DIM}]{msg}[/]")
|
|
71
158
|
|
|
@@ -179,6 +266,17 @@ def wordmark_lines() -> list[str]:
|
|
|
179
266
|
return _WORDMARK.split("\n")
|
|
180
267
|
|
|
181
268
|
|
|
269
|
+
def wordmark_width() -> int:
|
|
270
|
+
"""Column width of the block wordmark; below this it wraps and shatters."""
|
|
271
|
+
return max((len(line) for line in wordmark_lines()), default=0)
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def compact_wordmark() -> str:
|
|
275
|
+
"""A one-line banner for panes too narrow for the block wordmark. Rich
|
|
276
|
+
markup; always fits, so a split/narrow pane never shows shattered letters."""
|
|
277
|
+
return f"[bold {DP_ACCENT}]{MARK} DeepParallel[/]"
|
|
278
|
+
|
|
279
|
+
|
|
182
280
|
def status_text(
|
|
183
281
|
*, version: str, tool_count: int, fusion_modes: tuple[str, ...], backend_label: str
|
|
184
282
|
):
|
|
@@ -35,6 +35,7 @@ from deepparallel.agent import (
|
|
|
35
35
|
)
|
|
36
36
|
from deepparallel.backend import Backend, backend_for_deployment, resolve_backend
|
|
37
37
|
from deepparallel.branding import console
|
|
38
|
+
from deepparallel.userinput import read_user_input
|
|
38
39
|
from deepparallel.config import (
|
|
39
40
|
Settings,
|
|
40
41
|
_bool_env,
|
|
@@ -233,7 +234,7 @@ def _stream_repl(backend: Backend, settings: Settings) -> None:
|
|
|
233
234
|
history: list[tuple[str, str]] = []
|
|
234
235
|
while True:
|
|
235
236
|
try:
|
|
236
|
-
user_msg =
|
|
237
|
+
user_msg = read_user_input()
|
|
237
238
|
except (EOFError, KeyboardInterrupt):
|
|
238
239
|
console.print()
|
|
239
240
|
break
|
|
@@ -294,7 +295,7 @@ def _agent_repl(backend: Backend, settings: Settings, renderer: Renderer) -> Non
|
|
|
294
295
|
bits = ([mode] if mode != "off" else []) + (["auto"] if auto else [])
|
|
295
296
|
tag = f"[{' · '.join(bits)}] " if bits else ""
|
|
296
297
|
try:
|
|
297
|
-
user_msg =
|
|
298
|
+
user_msg = read_user_input(tag)
|
|
298
299
|
except (EOFError, KeyboardInterrupt):
|
|
299
300
|
console.print()
|
|
300
301
|
break
|
|
@@ -521,6 +522,22 @@ def review(ctx: click.Context, as_diff: bool, path: str | None) -> None:
|
|
|
521
522
|
sys.exit(verdict_exit_code(verdict))
|
|
522
523
|
|
|
523
524
|
|
|
525
|
+
@main.command()
|
|
526
|
+
@click.option("--host", default="127.0.0.1", help="Bind host.")
|
|
527
|
+
@click.option("--port", default=8013, type=int, help="Bind port.")
|
|
528
|
+
def serve(host: str, port: int) -> None:
|
|
529
|
+
"""Run the OpenAI-compatible gateway for Crowe Terminal / Crowe Code, etc.
|
|
530
|
+
|
|
531
|
+
Exposes /v1/chat/completions + /v1/models so any OpenAI-format client can use
|
|
532
|
+
the Crowe Logic model stack through one endpoint, with the Crowe Logic persona
|
|
533
|
+
injected and raw-model identity leaks scrubbed. Select a model by deployment
|
|
534
|
+
name (see /v1/models); fusion models plug in here later.
|
|
535
|
+
"""
|
|
536
|
+
from deepparallel.serve import run_server
|
|
537
|
+
|
|
538
|
+
run_server(host, port)
|
|
539
|
+
|
|
540
|
+
|
|
524
541
|
@main.command()
|
|
525
542
|
@click.argument("path", required=True)
|
|
526
543
|
@click.pass_context
|
|
@@ -26,7 +26,7 @@ from pathlib import Path
|
|
|
26
26
|
|
|
27
27
|
# Issuer public key (Ed25519, raw, base64). The matching private key is the
|
|
28
28
|
# issuance secret and is never shipped.
|
|
29
|
-
_EMBEDDED_PUBKEY = "
|
|
29
|
+
_EMBEDDED_PUBKEY = "BdxSRIB5F2K2bXf7c0UqsR6jC/cSRMSxojpzRpTCUQg="
|
|
30
30
|
|
|
31
31
|
# Paid features -> minimum tier required.
|
|
32
32
|
_FEATURE_TIER: dict[str, "Tier"] = {}
|
|
@@ -15,7 +15,7 @@ from __future__ import annotations
|
|
|
15
15
|
import sys
|
|
16
16
|
import time
|
|
17
17
|
from abc import ABC, abstractmethod
|
|
18
|
-
from typing import Iterable
|
|
18
|
+
from typing import Iterable, Iterator
|
|
19
19
|
|
|
20
20
|
from rich.console import Console
|
|
21
21
|
|
|
@@ -134,10 +134,15 @@ class RichRenderer(Renderer):
|
|
|
134
134
|
|
|
135
135
|
def welcome(self, backend_label, *, version="", tool_count=0, fusion_modes=()) -> None:
|
|
136
136
|
animate = self._console.is_terminal and _REVEAL_SECONDS > 0
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
137
|
+
if self._console.width < branding.wordmark_width():
|
|
138
|
+
# Narrow / split pane: the block wordmark would wrap mid-letter and
|
|
139
|
+
# shatter. Use a compact one-line mark that always fits.
|
|
140
|
+
self._console.print(branding.compact_wordmark(), highlight=False)
|
|
141
|
+
else:
|
|
142
|
+
for line in branding.wordmark_lines():
|
|
143
|
+
self._console.print(f"[{branding.DP_ACCENT}]{line}[/]", highlight=False)
|
|
144
|
+
if animate:
|
|
145
|
+
time.sleep(_REVEAL_SECONDS)
|
|
141
146
|
self._console.print()
|
|
142
147
|
self._console.print(
|
|
143
148
|
branding.status_text(
|
|
@@ -158,10 +163,33 @@ class RichRenderer(Renderer):
|
|
|
158
163
|
never moves the cursor up, so it cannot ghost or stack the way a growing
|
|
159
164
|
Live region does - it works the same on any terminal and at any answer
|
|
160
165
|
length. On a pipe / non-tty, fall back to raw inline streaming."""
|
|
166
|
+
chunks = self._spin_until_first(chunks)
|
|
161
167
|
if self._console.is_terminal:
|
|
162
168
|
return self._stream_markdown_blocks(chunks)
|
|
163
169
|
return self._stream_inline(chunks)
|
|
164
170
|
|
|
171
|
+
def _spin_until_first(self, chunks: Iterable[str]) -> Iterator[str]:
|
|
172
|
+
"""Show a transient 'thinking' spinner during the wait for the first
|
|
173
|
+
token, then yield the stream through unchanged. The spinner is stopped
|
|
174
|
+
and cleared (transient=True) before any answer text prints, so unlike a
|
|
175
|
+
growing Live region it can never overlap or stack with the answer."""
|
|
176
|
+
it = iter(chunks)
|
|
177
|
+
if not self._console.is_terminal:
|
|
178
|
+
yield from it
|
|
179
|
+
return
|
|
180
|
+
from rich.live import Live
|
|
181
|
+
|
|
182
|
+
spinner = branding.thinking_spinner()
|
|
183
|
+
first = None
|
|
184
|
+
with Live(spinner, console=self._console, transient=True, refresh_per_second=12):
|
|
185
|
+
for c in it:
|
|
186
|
+
first = c
|
|
187
|
+
break
|
|
188
|
+
if first is None:
|
|
189
|
+
return # empty stream (e.g. a tool-only turn): nothing to reveal
|
|
190
|
+
yield first
|
|
191
|
+
yield from it
|
|
192
|
+
|
|
165
193
|
def _stream_inline(self, chunks: Iterable[str]) -> str:
|
|
166
194
|
# Raw token streaming for pipes / non-tty: no Live, never ghosts. The
|
|
167
195
|
# marker is printed only on the first VISIBLE character, so empty /
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
# Copyright 2026, Crowe Logic Inc.
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
"""OpenAI-compatible gateway for DeepParallel.
|
|
5
|
+
|
|
6
|
+
`deepparallel serve` exposes /v1/chat/completions and /v1/models so any
|
|
7
|
+
OpenAI-format client (Crowe Terminal / Crowe Code, the Vercel AI SDK, etc.) can
|
|
8
|
+
talk to the Crowe Logic model stack through one endpoint. The gateway is where
|
|
9
|
+
the models are made to *act as Crowe Logic*:
|
|
10
|
+
|
|
11
|
+
* persona — a Crowe Logic system prompt is injected when the caller sends none;
|
|
12
|
+
* scrub — raw-model identity leaks ("DeepSeek", deployment names) are rewritten
|
|
13
|
+
to CroweLM / Crowe Logic in the streamed output.
|
|
14
|
+
|
|
15
|
+
v1 is passthrough: `model` selects an Azure deployment by name and we proxy its
|
|
16
|
+
stream. Fusion models (deepparallel-deep / -reason) plug in here later by routing
|
|
17
|
+
to the fusion stack instead of a single deployment — the wire contract and the
|
|
18
|
+
persona/scrub layer stay identical, so the IDE never has to change.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import json
|
|
24
|
+
import re
|
|
25
|
+
import time
|
|
26
|
+
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
|
27
|
+
|
|
28
|
+
from deepparallel.backend import backend_for_deployment
|
|
29
|
+
from deepparallel.config import load_system_prompt, resolve_settings
|
|
30
|
+
|
|
31
|
+
# Identity-leak scrub: the raw model occasionally self-identifies as DeepSeek
|
|
32
|
+
# despite the persona prompt. Rewrite those surfaces so the assistant reads as
|
|
33
|
+
# Crowe Logic. Order matters — specific deployment names before the bare brand.
|
|
34
|
+
_SCRUB: list[tuple[re.Pattern[str], str]] = [
|
|
35
|
+
(re.compile(r"DeepSeek[-\s]?V4[-\s]?Pro", re.I), "CroweLM Apex"),
|
|
36
|
+
(re.compile(r"DeepSeek[-\s]?V4[-\s]?Flash", re.I), "CroweLM Flash"),
|
|
37
|
+
(re.compile(r"DeepSeek[-\s]?R1[-\w]*", re.I), "CroweLM Reason"),
|
|
38
|
+
(re.compile(r"\bDeepSeek\b", re.I), "CroweLM"),
|
|
39
|
+
]
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
# Public model ids (what the IDE shows / sends) -> Azure deployment (internal).
|
|
43
|
+
# The listing must never leak raw deployment names; chat accepts either the
|
|
44
|
+
# alias or the raw name, so existing callers keep working.
|
|
45
|
+
_MODEL_ALIASES: dict[str, str] = {
|
|
46
|
+
"crowelm-apex": "DeepSeek-V4-Pro",
|
|
47
|
+
"crowelm-reason": "DeepSeek-R1-0528",
|
|
48
|
+
"crowelm-flash": "DeepSeek-V4-Flash",
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _resolve_deployment(model: str, settings) -> str:
|
|
53
|
+
"""Map a public model id to its Azure deployment, passing through raw names."""
|
|
54
|
+
return _MODEL_ALIASES.get(model, model or settings.deployment)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def scrub(text: str) -> str:
|
|
58
|
+
"""Rewrite raw-model identity leaks to the Crowe Logic brand."""
|
|
59
|
+
if not text:
|
|
60
|
+
return text
|
|
61
|
+
for pattern, repl in _SCRUB:
|
|
62
|
+
text = pattern.sub(repl, text)
|
|
63
|
+
return text
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def collapse_spacing(text: str) -> str:
|
|
67
|
+
"""Collapse runs of 3+ newlines to a single blank line, and trim trailing
|
|
68
|
+
spaces before a newline. Raw models often over-emit blank lines, which reads
|
|
69
|
+
as double-spaced output in the chat; this normalizes it on the way out."""
|
|
70
|
+
text = re.sub(r"[ \t]+\n", "\n", text)
|
|
71
|
+
return re.sub(r"\n{3,}", "\n\n", text)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class _ContentShaper:
|
|
75
|
+
"""Smooths streamed content delivery. It (1) re-chunks token bursts to word
|
|
76
|
+
boundaries so the client renders a steady word-by-word reveal, and (2)
|
|
77
|
+
collapses blank-line runs across chunk boundaries (spacing). Holds back only
|
|
78
|
+
the trailing partial word until the next chunk; flush() drains the rest."""
|
|
79
|
+
|
|
80
|
+
def __init__(self):
|
|
81
|
+
self._buf = ""
|
|
82
|
+
|
|
83
|
+
def feed(self, text: str) -> list[str]:
|
|
84
|
+
self._buf += text
|
|
85
|
+
boundary = max(self._buf.rfind(" "), self._buf.rfind("\n"))
|
|
86
|
+
if boundary == -1:
|
|
87
|
+
return [] # no complete word yet — keep buffering
|
|
88
|
+
ready, self._buf = self._buf[: boundary + 1], self._buf[boundary + 1 :]
|
|
89
|
+
return self._tokens(ready)
|
|
90
|
+
|
|
91
|
+
def flush(self) -> list[str]:
|
|
92
|
+
ready, self._buf = self._buf, ""
|
|
93
|
+
return self._tokens(ready)
|
|
94
|
+
|
|
95
|
+
def _tokens(self, ready: str) -> list[str]:
|
|
96
|
+
ready = collapse_spacing(ready)
|
|
97
|
+
return re.findall(r"\S+\s*|\s+", ready) if ready else []
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _ensure_persona(messages: list[dict]) -> list[dict]:
|
|
101
|
+
"""Inject the Crowe Logic persona as a system message when none is present,
|
|
102
|
+
so a bare client request still answers in-brand."""
|
|
103
|
+
if any(m.get("role") == "system" for m in messages):
|
|
104
|
+
return messages
|
|
105
|
+
return [{"role": "system", "content": load_system_prompt()}, *messages]
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _models_payload(settings) -> dict:
|
|
109
|
+
deployments = set(settings.parallel_deployments or (settings.deployment,))
|
|
110
|
+
aliased = set(_MODEL_ALIASES.values())
|
|
111
|
+
# Prefer branded aliases; never list a raw deployment name that has one.
|
|
112
|
+
ids = [pub for pub, dep in _MODEL_ALIASES.items() if dep in deployments]
|
|
113
|
+
ids += [dep for dep in deployments if dep not in aliased]
|
|
114
|
+
created = int(time.time())
|
|
115
|
+
return {
|
|
116
|
+
"object": "list",
|
|
117
|
+
"data": [
|
|
118
|
+
{"id": i, "object": "model", "created": created, "owned_by": "crowe-logic"} for i in ids
|
|
119
|
+
],
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _chunk(
|
|
124
|
+
model: str,
|
|
125
|
+
*,
|
|
126
|
+
content: str | None = None,
|
|
127
|
+
reasoning: str | None = None,
|
|
128
|
+
finish: str | None = None,
|
|
129
|
+
) -> str:
|
|
130
|
+
delta: dict = {}
|
|
131
|
+
if content is not None:
|
|
132
|
+
delta["content"] = content
|
|
133
|
+
if reasoning is not None:
|
|
134
|
+
# reasoning_content is the OpenAI/Azure convention the chat clients read
|
|
135
|
+
# into their "thinking" channel — so the reasoning shows in the IDE.
|
|
136
|
+
delta["reasoning_content"] = reasoning
|
|
137
|
+
body = {
|
|
138
|
+
"id": "dp-" + str(int(time.time() * 1000)),
|
|
139
|
+
"object": "chat.completion.chunk",
|
|
140
|
+
"created": int(time.time()),
|
|
141
|
+
"model": model,
|
|
142
|
+
"choices": [{"index": 0, "delta": delta, "finish_reason": finish}],
|
|
143
|
+
}
|
|
144
|
+
return "data: " + json.dumps(body) + "\n\n"
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def _make_handler(settings):
|
|
148
|
+
class Handler(BaseHTTPRequestHandler):
|
|
149
|
+
protocol_version = "HTTP/1.1"
|
|
150
|
+
|
|
151
|
+
def log_message(self, *_args): # quiet default logging
|
|
152
|
+
pass
|
|
153
|
+
|
|
154
|
+
def _json(self, code: int, obj: dict) -> None:
|
|
155
|
+
payload = json.dumps(obj).encode()
|
|
156
|
+
self.send_response(code)
|
|
157
|
+
self.send_header("Content-Type", "application/json")
|
|
158
|
+
self.send_header("Content-Length", str(len(payload)))
|
|
159
|
+
self.end_headers()
|
|
160
|
+
self.wfile.write(payload)
|
|
161
|
+
|
|
162
|
+
def do_GET(self):
|
|
163
|
+
if self.path.rstrip("/") == "/v1/models":
|
|
164
|
+
self._json(200, _models_payload(settings))
|
|
165
|
+
return
|
|
166
|
+
if self.path.rstrip("/") in ("/health", "/healthz"):
|
|
167
|
+
self._json(200, {"status": "ok"})
|
|
168
|
+
return
|
|
169
|
+
self._json(404, {"error": {"message": "not found"}})
|
|
170
|
+
|
|
171
|
+
def do_POST(self):
|
|
172
|
+
if self.path.rstrip("/") != "/v1/chat/completions":
|
|
173
|
+
self._json(404, {"error": {"message": "not found"}})
|
|
174
|
+
return
|
|
175
|
+
length = int(self.headers.get("Content-Length", 0))
|
|
176
|
+
try:
|
|
177
|
+
req = json.loads(self.rfile.read(length) or b"{}")
|
|
178
|
+
except json.JSONDecodeError:
|
|
179
|
+
self._json(400, {"error": {"message": "invalid JSON body"}})
|
|
180
|
+
return
|
|
181
|
+
|
|
182
|
+
model = req.get("model") or "crowelm-apex"
|
|
183
|
+
deployment = _resolve_deployment(model, settings)
|
|
184
|
+
messages = _ensure_persona(list(req.get("messages", [])))
|
|
185
|
+
temperature = req.get("temperature", 0.7)
|
|
186
|
+
max_tokens = req.get("max_tokens", 4096)
|
|
187
|
+
backend = backend_for_deployment(settings, deployment)
|
|
188
|
+
|
|
189
|
+
if req.get("stream"):
|
|
190
|
+
self._stream(backend, model, messages, temperature, max_tokens)
|
|
191
|
+
else:
|
|
192
|
+
self._complete(backend, model, messages, temperature, max_tokens)
|
|
193
|
+
|
|
194
|
+
def _stream(self, backend, model, messages, temperature, max_tokens):
|
|
195
|
+
self.send_response(200)
|
|
196
|
+
self.send_header("Content-Type", "text/event-stream")
|
|
197
|
+
self.send_header("Cache-Control", "no-cache")
|
|
198
|
+
self.send_header("Connection", "keep-alive")
|
|
199
|
+
self.end_headers()
|
|
200
|
+
shaper = _ContentShaper()
|
|
201
|
+
try:
|
|
202
|
+
for channel, text in backend.stream_chat(messages, temperature, max_tokens):
|
|
203
|
+
if channel == "thinking":
|
|
204
|
+
# reasoning passes through verbatim (scrubbed) so the IDE
|
|
205
|
+
# can render the trace as it arrives.
|
|
206
|
+
self._send(_chunk(model, reasoning=scrub(text)))
|
|
207
|
+
continue
|
|
208
|
+
for word in shaper.feed(scrub(text)):
|
|
209
|
+
self._send(_chunk(model, content=word))
|
|
210
|
+
for word in shaper.flush():
|
|
211
|
+
self._send(_chunk(model, content=word))
|
|
212
|
+
self._send(_chunk(model, finish="stop"))
|
|
213
|
+
self.wfile.write(b"data: [DONE]\n\n")
|
|
214
|
+
self.wfile.flush()
|
|
215
|
+
except BrokenPipeError:
|
|
216
|
+
pass # client disconnected mid-stream
|
|
217
|
+
|
|
218
|
+
def _send(self, frame: str) -> None:
|
|
219
|
+
self.wfile.write(frame.encode())
|
|
220
|
+
self.wfile.flush()
|
|
221
|
+
|
|
222
|
+
def _complete(self, backend, model, messages, temperature, max_tokens):
|
|
223
|
+
# backend.chat returns the assistant message dict directly
|
|
224
|
+
# ({"role","content",...}), not a wrapper.
|
|
225
|
+
result = backend.chat(messages, None, temperature, max_tokens)
|
|
226
|
+
content = (
|
|
227
|
+
collapse_spacing(scrub((result or {}).get("content") or ""))
|
|
228
|
+
if isinstance(result, dict)
|
|
229
|
+
else ""
|
|
230
|
+
)
|
|
231
|
+
self._json(
|
|
232
|
+
200,
|
|
233
|
+
{
|
|
234
|
+
"id": "dp-" + str(int(time.time() * 1000)),
|
|
235
|
+
"object": "chat.completion",
|
|
236
|
+
"created": int(time.time()),
|
|
237
|
+
"model": model,
|
|
238
|
+
"choices": [
|
|
239
|
+
{
|
|
240
|
+
"index": 0,
|
|
241
|
+
"message": {"role": "assistant", "content": content},
|
|
242
|
+
"finish_reason": "stop",
|
|
243
|
+
}
|
|
244
|
+
],
|
|
245
|
+
},
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
return Handler
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def run_server(host: str = "127.0.0.1", port: int = 8013) -> None:
|
|
252
|
+
settings = resolve_settings()
|
|
253
|
+
httpd = ThreadingHTTPServer((host, port), _make_handler(settings))
|
|
254
|
+
print(
|
|
255
|
+
f"DeepParallel gateway on http://{host}:{port}/v1 (model: {settings.deployment}, backend: {settings.backend})"
|
|
256
|
+
)
|
|
257
|
+
try:
|
|
258
|
+
httpd.serve_forever()
|
|
259
|
+
except KeyboardInterrupt:
|
|
260
|
+
print("\nshutting down")
|
|
261
|
+
httpd.shutdown()
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
You are DeepParallel, a precise coding assistant from Crowe Logic.
|
|
2
|
+
|
|
3
|
+
Voice: direct and concise. Lead with the answer, not a preamble — never open with "I'd be happy to", "Great question", or by restating the request. Give depth only when asked or when the problem genuinely needs it. Stop when the answer is complete; don't pad with summaries or follow-up offers unless they add real value.
|
|
4
|
+
|
|
5
|
+
Formatting: clean Markdown. Single blank lines between paragraphs — never double. Fenced code blocks with a language tag for code, inline backticks for identifiers and paths, tight lists (no blank line between items). Prefer a short prose answer over a bulleted list when a sentence will do.
|
|
6
|
+
|
|
7
|
+
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. When asked to run something with different parameters, prefer non-destructive approaches (CLI arguments, environment variables, or a temporary copy) over editing the user's source files. Only edit a source file when changing it is the actual goal, and explain what you changed.
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"""Interactive prompt input.
|
|
2
|
+
|
|
3
|
+
The REPL reads user turns through prompt_toolkit rather than the cooked
|
|
4
|
+
`input()` / `console.input()` line discipline. This matters for robustness:
|
|
5
|
+
`input()` reads whatever lands in the terminal's line buffer, so when another
|
|
6
|
+
process writes to the same TTY (a background agent, a split pane), that stray
|
|
7
|
+
output gets echoed into the buffer and read as if the user typed it - the REPL
|
|
8
|
+
then "answers" the noise. prompt_toolkit puts the terminal in raw mode and reads
|
|
9
|
+
key events directly, rendering and redrawing its own prompt line, so background
|
|
10
|
+
writes scroll above the prompt instead of becoming input.
|
|
11
|
+
|
|
12
|
+
Falls back to plain `input()` when stdin/stdout is not a TTY (pipes, tests),
|
|
13
|
+
where a full-screen line editor can't run anyway.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import sys
|
|
19
|
+
from html import escape
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
|
|
22
|
+
from deepparallel import branding
|
|
23
|
+
|
|
24
|
+
_HISTORY_PATH = Path.home() / ".deepparallel_history"
|
|
25
|
+
_session = None # lazily built PromptSession (None until first interactive read)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _get_session():
|
|
29
|
+
global _session
|
|
30
|
+
if _session is None:
|
|
31
|
+
from prompt_toolkit import PromptSession
|
|
32
|
+
from prompt_toolkit.history import FileHistory
|
|
33
|
+
|
|
34
|
+
_session = PromptSession(history=FileHistory(str(_HISTORY_PATH)))
|
|
35
|
+
return _session
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _prompt_html(tag: str):
|
|
39
|
+
"""Build the prompt as prompt_toolkit markup: an optional dim mode tag
|
|
40
|
+
(e.g. '[reason · auto] ') followed by the green 'you ›' prefix."""
|
|
41
|
+
from prompt_toolkit.formatted_text import HTML
|
|
42
|
+
|
|
43
|
+
tag_part = f'<style fg="ansibrightblack">{escape(tag)}</style>' if tag else ""
|
|
44
|
+
return HTML(f'{tag_part}<b><style fg="ansibrightgreen">you</style></b> › ')
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _interactive() -> bool:
|
|
48
|
+
return sys.stdin.isatty() and sys.stdout.isatty()
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def read_user_input(tag: str = "") -> str:
|
|
52
|
+
"""Read one line from the user. `tag` is an optional prefix shown dimmed
|
|
53
|
+
before the prompt (the active mode chips). Returns the stripped line.
|
|
54
|
+
|
|
55
|
+
Raises EOFError on Ctrl-D and KeyboardInterrupt on Ctrl-C, matching the
|
|
56
|
+
contract the REPL loops already handle.
|
|
57
|
+
"""
|
|
58
|
+
if not _interactive():
|
|
59
|
+
# Non-interactive: a raw-mode editor can't run. Mirror the old prompt.
|
|
60
|
+
return input(f"{tag}you {branding.ARROW} ").strip()
|
|
61
|
+
return _get_session().prompt(_prompt_html(tag)).strip()
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: deepparallel
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.3
|
|
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
|
|
@@ -10,8 +10,10 @@ deepparallel/fusion.py
|
|
|
10
10
|
deepparallel/licensing.py
|
|
11
11
|
deepparallel/registry.json
|
|
12
12
|
deepparallel/renderer.py
|
|
13
|
+
deepparallel/serve.py
|
|
13
14
|
deepparallel/supply_chain.py
|
|
14
15
|
deepparallel/system_prompt.txt
|
|
16
|
+
deepparallel/userinput.py
|
|
15
17
|
deepparallel.egg-info/PKG-INFO
|
|
16
18
|
deepparallel.egg-info/SOURCES.txt
|
|
17
19
|
deepparallel.egg-info/dependency_links.txt
|
|
@@ -42,6 +44,7 @@ tests/test_issuer_signer.py
|
|
|
42
44
|
tests/test_licensing.py
|
|
43
45
|
tests/test_renderer.py
|
|
44
46
|
tests/test_research.py
|
|
47
|
+
tests/test_spinner_color.py
|
|
45
48
|
tests/test_supply_chain.py
|
|
46
49
|
tests/test_tool_registry.py
|
|
47
50
|
tests/test_tools_codeast.py
|
|
@@ -51,4 +54,5 @@ tests/test_tools_sandbox.py
|
|
|
51
54
|
tests/test_tools_search.py
|
|
52
55
|
tests/test_tools_shell.py
|
|
53
56
|
tests/test_tools_vision.py
|
|
54
|
-
tests/test_tools_web.py
|
|
57
|
+
tests/test_tools_web.py
|
|
58
|
+
tests/test_userinput.py
|
|
@@ -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.3"
|
|
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" }
|
|
@@ -86,6 +86,23 @@ def test_rich_welcome_renders_wordmark_and_status(monkeypatch):
|
|
|
86
86
|
assert "DeepSeek" not in out
|
|
87
87
|
|
|
88
88
|
|
|
89
|
+
def test_rich_welcome_narrow_pane_uses_compact_banner(monkeypatch):
|
|
90
|
+
import deepparallel.renderer as rmod
|
|
91
|
+
|
|
92
|
+
monkeypatch.setattr(rmod, "_REVEAL_SECONDS", 0)
|
|
93
|
+
buf = io.StringIO()
|
|
94
|
+
# narrower than the 71-col block wordmark: it would wrap and shatter
|
|
95
|
+
con = Console(no_color=True, width=46, file=buf, force_terminal=True, highlight=False)
|
|
96
|
+
rmod.RichRenderer(console=con).welcome("Azure @ https://x", version="0.4.2", tool_count=15)
|
|
97
|
+
out = buf.getvalue()
|
|
98
|
+
assert "◆ DeepParallel" in out # compact one-line mark
|
|
99
|
+
# the block wordmark's first row must NOT appear (no shattered letters)
|
|
100
|
+
assert "████ █████ █████" not in out
|
|
101
|
+
# no line exceeds the pane width (nothing wraps/shatters)
|
|
102
|
+
visible = [line for line in out.replace("\x1b[1m", "").replace("\x1b[0m", "").split("\n")]
|
|
103
|
+
assert max((len(line) for line in visible), default=0) <= 46
|
|
104
|
+
|
|
105
|
+
|
|
89
106
|
def test_rich_tool_timer_path_renders_card(monkeypatch):
|
|
90
107
|
buf = io.StringIO()
|
|
91
108
|
con = Console(no_color=True, width=80, file=buf, force_terminal=True, highlight=False)
|
|
@@ -160,13 +177,17 @@ def test_rich_answer_stream_renders_markdown_formatting():
|
|
|
160
177
|
assert "# Big" not in out
|
|
161
178
|
|
|
162
179
|
|
|
163
|
-
def
|
|
180
|
+
def test_rich_answer_stream_empty_renders_no_answer_panel():
|
|
181
|
+
# A tool-only turn (empty content stream) shows only the transient thinking
|
|
182
|
+
# spinner, which clears itself - it must not render an answer body and must
|
|
183
|
+
# leave nothing permanent (Live transient erases its line on exit).
|
|
164
184
|
buf = io.StringIO()
|
|
165
185
|
con = Console(no_color=True, width=80, file=buf, force_terminal=True, highlight=False)
|
|
166
186
|
r = RichRenderer(console=con)
|
|
167
187
|
full = r.answer_stream(iter([]))
|
|
168
188
|
assert full == ""
|
|
169
|
-
|
|
189
|
+
out = buf.getvalue()
|
|
190
|
+
assert "\x1b[2K" in out or out == "" # transient spinner erased its own line
|
|
170
191
|
|
|
171
192
|
|
|
172
193
|
def test_rich_answer_stream_whitespace_leading_holds_panel():
|
|
@@ -237,3 +258,44 @@ def test_fake_confirm_scripted():
|
|
|
237
258
|
assert f.confirm("a", "d") is True
|
|
238
259
|
assert f.confirm("b", "d") is False
|
|
239
260
|
assert ("confirm", "a", "d") in f.calls
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
# ---------------------------------------------------- answer_stream + spinner
|
|
264
|
+
def test_rich_answer_stream_returns_full_text_with_spinner_path():
|
|
265
|
+
# forced terminal -> the transient thinking-spinner path runs; it must not
|
|
266
|
+
# corrupt the returned text or swallow content.
|
|
267
|
+
buf = io.StringIO()
|
|
268
|
+
con = Console(no_color=True, width=80, file=buf, force_terminal=True, highlight=False)
|
|
269
|
+
r = RichRenderer(console=con)
|
|
270
|
+
full = r.answer_stream(iter(["Hello ", "world", "\n\ndone"]))
|
|
271
|
+
assert full == "Hello world\n\ndone"
|
|
272
|
+
out = buf.getvalue()
|
|
273
|
+
assert "Hello world" in out and "done" in out
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def test_rich_answer_stream_non_terminal_shows_no_spinner():
|
|
277
|
+
r, buf = _rich() # StringIO console: is_terminal is False
|
|
278
|
+
full = r.answer_stream(iter(["a", "b", "c"]))
|
|
279
|
+
assert full == "abc"
|
|
280
|
+
assert "thinking" not in buf.getvalue()
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def test_thinking_spinner_animates_and_is_branded():
|
|
284
|
+
from deepparallel import branding
|
|
285
|
+
|
|
286
|
+
sp = branding.thinking_spinner("reasoning")
|
|
287
|
+
f0 = sp.frame(0).plain
|
|
288
|
+
f3 = sp.frame(3).plain
|
|
289
|
+
assert f0.startswith(branding.MARK) # anchored by the diamond mark
|
|
290
|
+
assert "reasoning…" in f0
|
|
291
|
+
assert any(b in f0 for b in branding._PULSE_BLOCKS) # parallel lanes present
|
|
292
|
+
assert f0 != f3 # the crest travels: distinct frames over time
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
def test_thinking_spinner_rich_protocol_advances_frame():
|
|
296
|
+
from deepparallel import branding
|
|
297
|
+
|
|
298
|
+
sp = branding.thinking_spinner()
|
|
299
|
+
a = sp.__rich__().plain
|
|
300
|
+
b = sp.__rich__().plain
|
|
301
|
+
assert a != b # each render is a new frame (Live animates by re-rendering)
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""The thinking spinner drifts its crest color over time while keeping the
|
|
2
|
+
travelling-pulse motion. These tests pin the color-drift behavior without needing
|
|
3
|
+
a live terminal, since `frame()` is pure."""
|
|
4
|
+
|
|
5
|
+
from deepparallel import branding
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def _crest_style(text):
|
|
9
|
+
"""The ◆ mark is always the live crest hue; return its style string."""
|
|
10
|
+
# first span covers the "◆ " mark
|
|
11
|
+
return str(text.spans[0].style)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def test_crest_color_drifts_across_ticks():
|
|
15
|
+
sp = branding.thinking_spinner()
|
|
16
|
+
early = _crest_style(sp.frame(0))
|
|
17
|
+
later = _crest_style(sp.frame(40))
|
|
18
|
+
assert early != later, "crest hue should change as the spinner works"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def test_crest_color_cycles_back():
|
|
22
|
+
# The continuous hue function loops every len(_CREST_PALETTE) phase units.
|
|
23
|
+
# (The spinner samples this at discrete ticks, which need not divide evenly,
|
|
24
|
+
# so the invariant lives on the pure function, not the integer-tick frame.)
|
|
25
|
+
n = len(branding._CREST_PALETTE)
|
|
26
|
+
assert branding._crest_color(0.0) == branding._crest_color(float(n))
|
|
27
|
+
assert branding._crest_color(1.3) == branding._crest_color(1.3 + n)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def test_frame_is_pure():
|
|
31
|
+
sp = branding.thinking_spinner()
|
|
32
|
+
assert str(sp.frame(7)) == str(sp.frame(7)) # same tick -> identical frame
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def test_lerp_hex_endpoints_and_midpoint():
|
|
36
|
+
assert branding._lerp_hex("#000000", "#ffffff", 0.0) == "#000000"
|
|
37
|
+
assert branding._lerp_hex("#000000", "#ffffff", 1.0) == "#ffffff"
|
|
38
|
+
assert branding._lerp_hex("#000000", "#ffffff", 0.5) == "#808080"
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def test_crest_color_always_valid_hex():
|
|
42
|
+
for tick in range(0, 200, 3):
|
|
43
|
+
c = branding._crest_color(tick * 0.06)
|
|
44
|
+
assert c.startswith("#") and len(c) == 7
|
|
45
|
+
int(c[1:], 16) # parses as hex
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import builtins
|
|
2
|
+
|
|
3
|
+
from deepparallel import userinput
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def test_non_interactive_falls_back_to_input_and_strips(monkeypatch):
|
|
7
|
+
monkeypatch.setattr(userinput, "_interactive", lambda: False)
|
|
8
|
+
monkeypatch.setattr(builtins, "input", lambda prompt="": " hello there ")
|
|
9
|
+
assert userinput.read_user_input() == "hello there"
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def test_non_interactive_prompt_includes_tag(monkeypatch):
|
|
13
|
+
captured = {}
|
|
14
|
+
|
|
15
|
+
def fake_input(prompt=""):
|
|
16
|
+
captured["prompt"] = prompt
|
|
17
|
+
return "do the thing\n"
|
|
18
|
+
|
|
19
|
+
monkeypatch.setattr(userinput, "_interactive", lambda: False)
|
|
20
|
+
monkeypatch.setattr(builtins, "input", fake_input)
|
|
21
|
+
out = userinput.read_user_input("[reason · auto] ")
|
|
22
|
+
assert out == "do the thing"
|
|
23
|
+
assert "[reason · auto]" in captured["prompt"]
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def test_interactive_path_uses_prompt_session(monkeypatch):
|
|
27
|
+
class FakeSession:
|
|
28
|
+
def prompt(self, _formatted):
|
|
29
|
+
return " typed input "
|
|
30
|
+
|
|
31
|
+
monkeypatch.setattr(userinput, "_interactive", lambda: True)
|
|
32
|
+
monkeypatch.setattr(userinput, "_get_session", lambda: FakeSession())
|
|
33
|
+
assert userinput.read_user_input("[deep] ") == "typed input"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def test_prompt_html_escapes_tag(monkeypatch):
|
|
37
|
+
# a tag must not be able to inject prompt_toolkit markup
|
|
38
|
+
html = userinput._prompt_html("<b>x</b> ")
|
|
39
|
+
assert "<b>" in html.value
|
|
40
|
+
assert "you" in html.value
|
|
@@ -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, 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.
|
|
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
|
|
File without changes
|