deepparallel 0.2.0__tar.gz → 0.4.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {deepparallel-0.2.0 → deepparallel-0.4.0}/PKG-INFO +30 -3
- {deepparallel-0.2.0 → deepparallel-0.4.0}/README.md +27 -2
- {deepparallel-0.2.0 → deepparallel-0.4.0}/deepparallel/__init__.py +1 -1
- {deepparallel-0.2.0 → deepparallel-0.4.0}/deepparallel/agent.py +43 -3
- {deepparallel-0.2.0 → deepparallel-0.4.0}/deepparallel/backend.py +18 -2
- {deepparallel-0.2.0 → deepparallel-0.4.0}/deepparallel/cli.py +89 -1
- {deepparallel-0.2.0 → deepparallel-0.4.0}/deepparallel/config.py +1 -1
- {deepparallel-0.2.0 → deepparallel-0.4.0}/deepparallel/licensing.py +24 -0
- deepparallel-0.4.0/deepparallel/research/__init__.py +6 -0
- deepparallel-0.4.0/deepparallel/research/conduit.py +156 -0
- deepparallel-0.4.0/deepparallel/supply_chain.py +130 -0
- {deepparallel-0.2.0 → deepparallel-0.4.0}/deepparallel.egg-info/PKG-INFO +30 -3
- {deepparallel-0.2.0 → deepparallel-0.4.0}/deepparallel.egg-info/SOURCES.txt +6 -0
- {deepparallel-0.2.0 → deepparallel-0.4.0}/deepparallel.egg-info/requires.txt +3 -0
- {deepparallel-0.2.0 → deepparallel-0.4.0}/pyproject.toml +2 -1
- {deepparallel-0.2.0 → deepparallel-0.4.0}/tests/test_agent.py +118 -0
- {deepparallel-0.2.0 → deepparallel-0.4.0}/tests/test_backend_chat.py +25 -0
- {deepparallel-0.2.0 → deepparallel-0.4.0}/tests/test_cli.py +34 -0
- {deepparallel-0.2.0 → deepparallel-0.4.0}/tests/test_config.py +1 -1
- deepparallel-0.4.0/tests/test_issuer_signer.py +54 -0
- {deepparallel-0.2.0 → deepparallel-0.4.0}/tests/test_licensing.py +24 -0
- deepparallel-0.4.0/tests/test_research.py +27 -0
- deepparallel-0.4.0/tests/test_supply_chain.py +71 -0
- {deepparallel-0.2.0 → deepparallel-0.4.0}/deepparallel/branding.py +0 -0
- {deepparallel-0.2.0 → deepparallel-0.4.0}/deepparallel/fusion.py +0 -0
- {deepparallel-0.2.0 → deepparallel-0.4.0}/deepparallel/registry.json +0 -0
- {deepparallel-0.2.0 → deepparallel-0.4.0}/deepparallel/renderer.py +0 -0
- {deepparallel-0.2.0 → deepparallel-0.4.0}/deepparallel/system_prompt.txt +0 -0
- {deepparallel-0.2.0 → deepparallel-0.4.0}/deepparallel/tools/__init__.py +0 -0
- {deepparallel-0.2.0 → deepparallel-0.4.0}/deepparallel/tools/codeast.py +0 -0
- {deepparallel-0.2.0 → deepparallel-0.4.0}/deepparallel/tools/edit.py +0 -0
- {deepparallel-0.2.0 → deepparallel-0.4.0}/deepparallel/tools/files.py +0 -0
- {deepparallel-0.2.0 → deepparallel-0.4.0}/deepparallel/tools/registry.py +0 -0
- {deepparallel-0.2.0 → deepparallel-0.4.0}/deepparallel/tools/sandbox.py +0 -0
- {deepparallel-0.2.0 → deepparallel-0.4.0}/deepparallel/tools/search.py +0 -0
- {deepparallel-0.2.0 → deepparallel-0.4.0}/deepparallel/tools/shell.py +0 -0
- {deepparallel-0.2.0 → deepparallel-0.4.0}/deepparallel/tools/vision.py +0 -0
- {deepparallel-0.2.0 → deepparallel-0.4.0}/deepparallel/tools/web.py +0 -0
- {deepparallel-0.2.0 → deepparallel-0.4.0}/deepparallel.egg-info/dependency_links.txt +0 -0
- {deepparallel-0.2.0 → deepparallel-0.4.0}/deepparallel.egg-info/entry_points.txt +0 -0
- {deepparallel-0.2.0 → deepparallel-0.4.0}/deepparallel.egg-info/top_level.txt +0 -0
- {deepparallel-0.2.0 → deepparallel-0.4.0}/setup.cfg +0 -0
- {deepparallel-0.2.0 → deepparallel-0.4.0}/tests/test_backend.py +0 -0
- {deepparallel-0.2.0 → deepparallel-0.4.0}/tests/test_backend_stream.py +0 -0
- {deepparallel-0.2.0 → deepparallel-0.4.0}/tests/test_branding.py +0 -0
- {deepparallel-0.2.0 → deepparallel-0.4.0}/tests/test_fusion.py +0 -0
- {deepparallel-0.2.0 → deepparallel-0.4.0}/tests/test_renderer.py +0 -0
- {deepparallel-0.2.0 → deepparallel-0.4.0}/tests/test_tool_registry.py +0 -0
- {deepparallel-0.2.0 → deepparallel-0.4.0}/tests/test_tools_codeast.py +0 -0
- {deepparallel-0.2.0 → deepparallel-0.4.0}/tests/test_tools_edit.py +0 -0
- {deepparallel-0.2.0 → deepparallel-0.4.0}/tests/test_tools_files.py +0 -0
- {deepparallel-0.2.0 → deepparallel-0.4.0}/tests/test_tools_sandbox.py +0 -0
- {deepparallel-0.2.0 → deepparallel-0.4.0}/tests/test_tools_search.py +0 -0
- {deepparallel-0.2.0 → deepparallel-0.4.0}/tests/test_tools_shell.py +0 -0
- {deepparallel-0.2.0 → deepparallel-0.4.0}/tests/test_tools_vision.py +0 -0
- {deepparallel-0.2.0 → deepparallel-0.4.0}/tests/test_tools_web.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: deepparallel
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: DeepParallel - a multi-model agentic coding CLI with cross-model Guardian review, served via Crowe Logic.
|
|
5
5
|
Author-email: Michael Crowe <michael@crowelogic.com>
|
|
6
6
|
License: Apache-2.0
|
|
@@ -19,13 +19,27 @@ Requires-Dist: cryptography>=42.0.0
|
|
|
19
19
|
Provides-Extra: dev
|
|
20
20
|
Requires-Dist: pytest>=8.0.0; extra == "dev"
|
|
21
21
|
Requires-Dist: ruff>=0.6.0; extra == "dev"
|
|
22
|
+
Provides-Extra: research
|
|
23
|
+
Requires-Dist: numpy>=1.24; extra == "research"
|
|
22
24
|
|
|
23
25
|
# DeepParallel
|
|
24
26
|
|
|
25
|
-
A
|
|
27
|
+
A multi-model agentic coding CLI with cross-model Guardian review. Many models
|
|
28
|
+
check the work; you ship with a second opinion. Served via Crowe Logic.
|
|
26
29
|
|
|
27
30
|
## Install
|
|
28
31
|
|
|
32
|
+
uv tool install deepparallel # recommended
|
|
33
|
+
# or
|
|
34
|
+
pipx install deepparallel --python python3.13
|
|
35
|
+
|
|
36
|
+
Then run `deepparallel` (or the short alias `dp`).
|
|
37
|
+
|
|
38
|
+
Note: on systems with a broken Homebrew Python 3.14, bare `pipx install` may
|
|
39
|
+
fail to find a usable interpreter - use `uv tool install` or pin `--python`.
|
|
40
|
+
|
|
41
|
+
### From source
|
|
42
|
+
|
|
29
43
|
uv venv
|
|
30
44
|
uv pip install -p .venv -e .
|
|
31
45
|
|
|
@@ -53,6 +67,19 @@ Set the backend env vars (a `.env` file in the working directory is loaded autom
|
|
|
53
67
|
deepparallel doctor # diagnose config + reachability
|
|
54
68
|
deepparallel run --no-tools "..." # plain chat, no tools
|
|
55
69
|
deepparallel run --yes "..." # auto-approve tool actions
|
|
70
|
+
deepparallel review <file|--diff> # cross-model review as a CI gate (Pro)
|
|
71
|
+
deepparallel audit <file> # supply-chain gate: catch hallucinated deps (Pro)
|
|
72
|
+
deepparallel research conduit # latent-relay research demo (needs [research] extra)
|
|
73
|
+
|
|
74
|
+
## Supply-chain gate
|
|
75
|
+
|
|
76
|
+
LLMs invent package names that don't exist — and attackers register those names
|
|
77
|
+
("slopsquatting"). `deepparallel audit <file>` extracts the dependencies a file
|
|
78
|
+
introduces (imports, requirements.txt, package.json) and checks each against the
|
|
79
|
+
real registry (PyPI / npm), flagging any the registry has never seen. Exit 0
|
|
80
|
+
clean, 2 if a likely-hallucinated package is found — so it gates a commit or PR.
|
|
81
|
+
The Guardian also runs this automatically on every edit: a hallucinated import
|
|
82
|
+
forces a confirmation even under `--yes`.
|
|
56
83
|
|
|
57
84
|
## Fusion (stacking models for stronger output)
|
|
58
85
|
|
|
@@ -107,7 +134,7 @@ timeboxed local subprocess.
|
|
|
107
134
|
| `DEEPPARALLEL_API_VERSION` | Azure API version | `2024-08-01-preview` |
|
|
108
135
|
| `FOUNDRY_BASE_URL` / `FOUNDRY_API_KEY` | control-plane transport | (required for foundry) |
|
|
109
136
|
| `DEEPPARALLEL_TEMPERATURE` | default sampling temperature | `0.4` |
|
|
110
|
-
| `DEEPPARALLEL_MAX_TOKENS` | response cap | `
|
|
137
|
+
| `DEEPPARALLEL_MAX_TOKENS` | response cap (large enough to write whole files) | `8192` |
|
|
111
138
|
| `DEEPPARALLEL_THINK` | surface reasoning stream | `0` (answer-only) |
|
|
112
139
|
| `DEEPPARALLEL_TOOLS` | enable agent tools | `1` (on) |
|
|
113
140
|
| `DEEPPARALLEL_AUTO_APPROVE` | auto-approve mutating tools | `0` (off) |
|
|
@@ -1,9 +1,21 @@
|
|
|
1
1
|
# DeepParallel
|
|
2
2
|
|
|
3
|
-
A
|
|
3
|
+
A multi-model agentic coding CLI with cross-model Guardian review. Many models
|
|
4
|
+
check the work; you ship with a second opinion. Served via Crowe Logic.
|
|
4
5
|
|
|
5
6
|
## Install
|
|
6
7
|
|
|
8
|
+
uv tool install deepparallel # recommended
|
|
9
|
+
# or
|
|
10
|
+
pipx install deepparallel --python python3.13
|
|
11
|
+
|
|
12
|
+
Then run `deepparallel` (or the short alias `dp`).
|
|
13
|
+
|
|
14
|
+
Note: on systems with a broken Homebrew Python 3.14, bare `pipx install` may
|
|
15
|
+
fail to find a usable interpreter - use `uv tool install` or pin `--python`.
|
|
16
|
+
|
|
17
|
+
### From source
|
|
18
|
+
|
|
7
19
|
uv venv
|
|
8
20
|
uv pip install -p .venv -e .
|
|
9
21
|
|
|
@@ -31,6 +43,19 @@ Set the backend env vars (a `.env` file in the working directory is loaded autom
|
|
|
31
43
|
deepparallel doctor # diagnose config + reachability
|
|
32
44
|
deepparallel run --no-tools "..." # plain chat, no tools
|
|
33
45
|
deepparallel run --yes "..." # auto-approve tool actions
|
|
46
|
+
deepparallel review <file|--diff> # cross-model review as a CI gate (Pro)
|
|
47
|
+
deepparallel audit <file> # supply-chain gate: catch hallucinated deps (Pro)
|
|
48
|
+
deepparallel research conduit # latent-relay research demo (needs [research] extra)
|
|
49
|
+
|
|
50
|
+
## Supply-chain gate
|
|
51
|
+
|
|
52
|
+
LLMs invent package names that don't exist — and attackers register those names
|
|
53
|
+
("slopsquatting"). `deepparallel audit <file>` extracts the dependencies a file
|
|
54
|
+
introduces (imports, requirements.txt, package.json) and checks each against the
|
|
55
|
+
real registry (PyPI / npm), flagging any the registry has never seen. Exit 0
|
|
56
|
+
clean, 2 if a likely-hallucinated package is found — so it gates a commit or PR.
|
|
57
|
+
The Guardian also runs this automatically on every edit: a hallucinated import
|
|
58
|
+
forces a confirmation even under `--yes`.
|
|
34
59
|
|
|
35
60
|
## Fusion (stacking models for stronger output)
|
|
36
61
|
|
|
@@ -85,7 +110,7 @@ timeboxed local subprocess.
|
|
|
85
110
|
| `DEEPPARALLEL_API_VERSION` | Azure API version | `2024-08-01-preview` |
|
|
86
111
|
| `FOUNDRY_BASE_URL` / `FOUNDRY_API_KEY` | control-plane transport | (required for foundry) |
|
|
87
112
|
| `DEEPPARALLEL_TEMPERATURE` | default sampling temperature | `0.4` |
|
|
88
|
-
| `DEEPPARALLEL_MAX_TOKENS` | response cap | `
|
|
113
|
+
| `DEEPPARALLEL_MAX_TOKENS` | response cap (large enough to write whole files) | `8192` |
|
|
89
114
|
| `DEEPPARALLEL_THINK` | surface reasoning stream | `0` (answer-only) |
|
|
90
115
|
| `DEEPPARALLEL_TOOLS` | enable agent tools | `1` (on) |
|
|
91
116
|
| `DEEPPARALLEL_AUTO_APPROVE` | auto-approve mutating tools | `0` (off) |
|
|
@@ -18,6 +18,17 @@ _MAX_TOOL_RESULT = 50_000
|
|
|
18
18
|
_GATED_PATH_TOOLS = ("write_file", "edit_file")
|
|
19
19
|
_EDIT_TOOLS = ("write_file", "edit_file", "ast_replace_symbol")
|
|
20
20
|
|
|
21
|
+
# Shown back to the model when a tool call arrives on a response that the API
|
|
22
|
+
# cut off at the token limit (finish_reason=length). The call's arguments, and
|
|
23
|
+
# any file body inside them, are incomplete, so we never apply it.
|
|
24
|
+
_TRUNCATION_HINT = (
|
|
25
|
+
"Your previous output was cut off by the response token limit "
|
|
26
|
+
"(finish_reason=length), so this tool call is incomplete and was NOT applied. "
|
|
27
|
+
"Write large files in smaller pieces: create the file with write_file using "
|
|
28
|
+
"the first portion, then append the rest with edit_file (or split it across "
|
|
29
|
+
"files). Do not emit an entire large file in one tool call."
|
|
30
|
+
)
|
|
31
|
+
|
|
21
32
|
|
|
22
33
|
def _parse_args(raw: str) -> dict:
|
|
23
34
|
if not raw:
|
|
@@ -191,9 +202,30 @@ def _guardian_verdict(guardian, name: str, args: dict) -> str | None:
|
|
|
191
202
|
return guardian_review(guardian, _guardian_review_content(name, args))
|
|
192
203
|
|
|
193
204
|
|
|
205
|
+
def _supply_chain_note(name: str, args: dict) -> str | None:
|
|
206
|
+
"""Best-effort: flag hallucinated/slopsquatted deps an edit introduces."""
|
|
207
|
+
content = args.get("new_source") or args.get("new_string") or args.get("content") or ""
|
|
208
|
+
path = args.get("file_path", "")
|
|
209
|
+
if not content or not path:
|
|
210
|
+
return None
|
|
211
|
+
try:
|
|
212
|
+
from deepparallel import supply_chain
|
|
213
|
+
|
|
214
|
+
result = supply_chain.audit(content, path)
|
|
215
|
+
except Exception: # noqa: BLE001 - supply-chain check is best-effort
|
|
216
|
+
return None
|
|
217
|
+
if result["hallucinated"]:
|
|
218
|
+
return "not found on the registry (possible hallucination): " + ", ".join(
|
|
219
|
+
result["hallucinated"]
|
|
220
|
+
)
|
|
221
|
+
return None
|
|
222
|
+
|
|
223
|
+
|
|
194
224
|
def _approved(name, args, interactive, auto_approve, renderer, guardian=None) -> bool:
|
|
195
225
|
forced = name in _GATED_PATH_TOOLS and _outside_cwd(args)
|
|
196
|
-
if
|
|
226
|
+
sc_note = _supply_chain_note(name, args) if name in _EDIT_TOOLS else None
|
|
227
|
+
# A hallucinated dependency overrides auto-approve: always surface it.
|
|
228
|
+
if auto_approve and not forced and not sc_note:
|
|
197
229
|
return True
|
|
198
230
|
if not interactive:
|
|
199
231
|
return False
|
|
@@ -202,6 +234,8 @@ def _approved(name, args, interactive, auto_approve, renderer, guardian=None) ->
|
|
|
202
234
|
verdict = _guardian_verdict(guardian, name, args)
|
|
203
235
|
if verdict:
|
|
204
236
|
detail = f"{detail}\n\nGuardian: {verdict}"
|
|
237
|
+
if sc_note:
|
|
238
|
+
detail = f"{detail}\n\nSupply-chain: {sc_note}"
|
|
205
239
|
return renderer.confirm(title, detail)
|
|
206
240
|
|
|
207
241
|
|
|
@@ -250,6 +284,7 @@ def run_agent(
|
|
|
250
284
|
msg = backend.chat(messages, schemas, settings.temperature, settings.max_tokens)
|
|
251
285
|
streamed = False
|
|
252
286
|
tool_calls = msg.get("tool_calls")
|
|
287
|
+
truncated = bool(msg.get("_truncated"))
|
|
253
288
|
if not tool_calls:
|
|
254
289
|
content = msg.get("content") or ""
|
|
255
290
|
messages.append({"role": "assistant", "content": content})
|
|
@@ -260,13 +295,18 @@ def run_agent(
|
|
|
260
295
|
messages.append(
|
|
261
296
|
{"role": "assistant", "content": msg.get("content"), "tool_calls": tool_calls}
|
|
262
297
|
)
|
|
263
|
-
|
|
298
|
+
# A length-truncated response cuts off the final tool call mid-arguments,
|
|
299
|
+
# so its file body is incomplete. Refuse it and tell the model to chunk.
|
|
300
|
+
last_idx = len(tool_calls) - 1
|
|
301
|
+
for i, tc in enumerate(tool_calls):
|
|
264
302
|
name = tc["function"]["name"]
|
|
265
303
|
args = _parse_args(tc["function"].get("arguments", ""))
|
|
266
304
|
meta = registry.get(name)
|
|
267
305
|
renderer.tool_start(name, _preview(args))
|
|
268
306
|
start = time.monotonic()
|
|
269
|
-
if
|
|
307
|
+
if truncated and i == last_idx:
|
|
308
|
+
result = json.dumps({"error": _TRUNCATION_HINT})
|
|
309
|
+
elif meta is None:
|
|
270
310
|
result = json.dumps({"error": f"unknown tool: {name}"})
|
|
271
311
|
elif "__parse_error__" in args:
|
|
272
312
|
result = json.dumps({"error": "invalid JSON arguments"})
|
|
@@ -56,6 +56,7 @@ def parse_sse_stream(lines: Iterator[str]):
|
|
|
56
56
|
"""
|
|
57
57
|
content_parts: list[str] = []
|
|
58
58
|
acc: dict[int, dict] = {}
|
|
59
|
+
finish_reason: str | None = None
|
|
59
60
|
for raw in lines:
|
|
60
61
|
line = raw.strip()
|
|
61
62
|
if not line or not line.startswith("data:"):
|
|
@@ -70,6 +71,8 @@ def parse_sse_stream(lines: Iterator[str]):
|
|
|
70
71
|
choices = obj.get("choices") or []
|
|
71
72
|
if not choices:
|
|
72
73
|
continue
|
|
74
|
+
if choices[0].get("finish_reason"):
|
|
75
|
+
finish_reason = choices[0]["finish_reason"]
|
|
73
76
|
delta = choices[0].get("delta") or {}
|
|
74
77
|
reasoning = delta.get("reasoning_content")
|
|
75
78
|
if reasoning:
|
|
@@ -95,6 +98,7 @@ def parse_sse_stream(lines: Iterator[str]):
|
|
|
95
98
|
"role": "assistant",
|
|
96
99
|
"content": "".join(content_parts) or None,
|
|
97
100
|
"tool_calls": tool_calls,
|
|
101
|
+
"_truncated": finish_reason == "length",
|
|
98
102
|
}
|
|
99
103
|
|
|
100
104
|
|
|
@@ -103,6 +107,18 @@ def _host(url: str) -> str:
|
|
|
103
107
|
return f"{p.scheme}://{p.netloc}" if p.netloc else url
|
|
104
108
|
|
|
105
109
|
|
|
110
|
+
def _message_from_choice(choice: dict) -> dict:
|
|
111
|
+
"""Extract the assistant message and flag output-token truncation.
|
|
112
|
+
|
|
113
|
+
`finish_reason == "length"` means the model was cut off mid-output. For a
|
|
114
|
+
tool call that carries file content, that means the arguments (and any file
|
|
115
|
+
body inside them) are incomplete and must not be applied blindly.
|
|
116
|
+
"""
|
|
117
|
+
msg = dict(choice.get("message") or {})
|
|
118
|
+
msg["_truncated"] = choice.get("finish_reason") == "length"
|
|
119
|
+
return msg
|
|
120
|
+
|
|
121
|
+
|
|
106
122
|
class Backend(Protocol):
|
|
107
123
|
label: str
|
|
108
124
|
|
|
@@ -172,7 +188,7 @@ class AzureBackend:
|
|
|
172
188
|
headers = {"api-key": self._api_key, "content-type": "application/json"}
|
|
173
189
|
r = httpx.post(self._url, json=payload, headers=headers, timeout=_STREAM_TIMEOUT)
|
|
174
190
|
r.raise_for_status()
|
|
175
|
-
return r.json()["choices"][0]
|
|
191
|
+
return _message_from_choice(r.json()["choices"][0])
|
|
176
192
|
|
|
177
193
|
def stream_chat_tools(self, messages, tools, temperature, max_tokens):
|
|
178
194
|
payload = {
|
|
@@ -246,7 +262,7 @@ class FoundryBackend:
|
|
|
246
262
|
}
|
|
247
263
|
r = httpx.post(self._url, json=payload, headers=headers, timeout=_STREAM_TIMEOUT)
|
|
248
264
|
r.raise_for_status()
|
|
249
|
-
return r.json()["choices"][0]
|
|
265
|
+
return _message_from_choice(r.json()["choices"][0])
|
|
250
266
|
|
|
251
267
|
def stream_chat_tools(self, messages, tools, temperature, max_tokens):
|
|
252
268
|
payload = {
|
|
@@ -43,7 +43,7 @@ from deepparallel.config import (
|
|
|
43
43
|
missing_required,
|
|
44
44
|
resolve_settings,
|
|
45
45
|
)
|
|
46
|
-
from deepparallel import licensing
|
|
46
|
+
from deepparallel import licensing, supply_chain
|
|
47
47
|
from deepparallel.fusion import (
|
|
48
48
|
EscalationBackend,
|
|
49
49
|
ReasonAnswerBackend,
|
|
@@ -504,6 +504,94 @@ def review(ctx: click.Context, as_diff: bool, path: str | None) -> None:
|
|
|
504
504
|
sys.exit(verdict_exit_code(verdict))
|
|
505
505
|
|
|
506
506
|
|
|
507
|
+
@main.command()
|
|
508
|
+
@click.argument("path", required=True)
|
|
509
|
+
@click.pass_context
|
|
510
|
+
def audit(ctx: click.Context, path: str) -> None:
|
|
511
|
+
"""Supply-chain gate: check a file's dependencies against the real registry.
|
|
512
|
+
|
|
513
|
+
Flags hallucinated / slopsquatted packages (names PyPI or npm has never
|
|
514
|
+
seen) in source files and manifests. Exit code: 0 clean, 2 if a likely
|
|
515
|
+
hallucinated dependency is found - so it can gate a commit or PR. Paid (Pro+).
|
|
516
|
+
"""
|
|
517
|
+
ok, msg = licensing.check_feature("audit")
|
|
518
|
+
if not ok:
|
|
519
|
+
branding.error(msg)
|
|
520
|
+
sys.exit(3)
|
|
521
|
+
try:
|
|
522
|
+
content = Path(path).expanduser().read_text(encoding="utf-8")
|
|
523
|
+
except OSError as e:
|
|
524
|
+
branding.error(f"cannot read {path}: {e}")
|
|
525
|
+
sys.exit(3)
|
|
526
|
+
result = supply_chain.audit(content, path)
|
|
527
|
+
findings = result["findings"]
|
|
528
|
+
if not findings:
|
|
529
|
+
console.print(f"[{branding.DIM}]no external dependencies found in {path}[/]")
|
|
530
|
+
sys.exit(0)
|
|
531
|
+
for f in findings:
|
|
532
|
+
glyph = {"ok": branding.CHECK, "missing": branding.CROSS}.get(f["status"], "?")
|
|
533
|
+
color = {"ok": branding.GREEN_HEX, "missing": branding.RED_HEX}.get(
|
|
534
|
+
f["status"], branding.DIM
|
|
535
|
+
)
|
|
536
|
+
console.print(
|
|
537
|
+
f" [{color}]{glyph}[/] {f['name']} [{branding.DIM}]({f['ecosystem']}) · {f['status']}[/]"
|
|
538
|
+
)
|
|
539
|
+
if result["hallucinated"]:
|
|
540
|
+
console.print(
|
|
541
|
+
f"[bold {branding.RED_HEX}]{branding.CROSS} {len(result['hallucinated'])} "
|
|
542
|
+
f"possibly hallucinated:[/] {', '.join(result['hallucinated'])}"
|
|
543
|
+
)
|
|
544
|
+
sys.exit(2)
|
|
545
|
+
console.print(f"[{branding.GREEN_HEX}]{branding.CHECK} all dependencies verified[/]")
|
|
546
|
+
sys.exit(0)
|
|
547
|
+
|
|
548
|
+
|
|
549
|
+
@main.group()
|
|
550
|
+
def research() -> None:
|
|
551
|
+
"""Runnable demonstrators of the ideas behind DeepParallel."""
|
|
552
|
+
|
|
553
|
+
|
|
554
|
+
@research.command("conduit")
|
|
555
|
+
def research_conduit() -> None:
|
|
556
|
+
"""Conduit: relay a thought between models as a hidden state vs. as a word.
|
|
557
|
+
|
|
558
|
+
Frozen synthetic models relay a continuous meaning down a chain of agents two
|
|
559
|
+
ways and we measure how much survives each hop. The latent channel holds the
|
|
560
|
+
meaning; the word channel (one emitted token per hop) collapses. The same
|
|
561
|
+
result on real open-weight models: crowelogic.com/research/conduit
|
|
562
|
+
"""
|
|
563
|
+
try:
|
|
564
|
+
from deepparallel.research import conduit as _conduit
|
|
565
|
+
except ImportError:
|
|
566
|
+
branding.error(
|
|
567
|
+
"this demo needs numpy. Install it with: pip install 'deepparallel[research]'"
|
|
568
|
+
)
|
|
569
|
+
sys.exit(3)
|
|
570
|
+
console.print(f"[{branding.DIM}]running the latent relay (frozen synthetic models)...[/]")
|
|
571
|
+
r = _conduit.demonstrate()
|
|
572
|
+
console.print(
|
|
573
|
+
f"\n[bold]Conduit[/] · meaning retained after K relay hops "
|
|
574
|
+
f"[{branding.DIM}](cosine; 1.0 = perfect)[/]"
|
|
575
|
+
)
|
|
576
|
+
console.print(
|
|
577
|
+
f"[{branding.DIM}] meaning = {r['meaning_dim']}-dim continuous "
|
|
578
|
+
f"word channel = 1 token of {r['vocab']}[/]\n"
|
|
579
|
+
)
|
|
580
|
+
console.print(
|
|
581
|
+
f" [{branding.DIM}]{'hops':>5} {'latent relay':>13} {'word relay':>11} {'gain':>6}[/]"
|
|
582
|
+
)
|
|
583
|
+
for K in r["hops"]:
|
|
584
|
+
lat, wrd = r["latent"][K], r["word"][K]
|
|
585
|
+
console.print(
|
|
586
|
+
f" {K:>5} [{branding.GREEN_HEX}]{lat:>13.3f}[/] "
|
|
587
|
+
f"[{branding.DIM}]{wrd:>11.3f}[/] [{branding.AMBER_HEX}]{lat - wrd:>+6.3f}[/]"
|
|
588
|
+
)
|
|
589
|
+
console.print(
|
|
590
|
+
f"\n[{branding.DIM}] the full hidden state relays more meaning than the single word "
|
|
591
|
+
f"the model would have said.[/]"
|
|
592
|
+
)
|
|
593
|
+
|
|
594
|
+
|
|
507
595
|
@main.command(name="tools")
|
|
508
596
|
def tools_cmd() -> None:
|
|
509
597
|
"""List the agent tools available to DeepParallel."""
|
|
@@ -101,7 +101,7 @@ def resolve_settings() -> Settings:
|
|
|
101
101
|
foundry_api_key=os.environ.get("FOUNDRY_API_KEY"),
|
|
102
102
|
foundry_model=os.environ.get("DEEPPARALLEL_FOUNDRY_MODEL", "DeepSeek-V3-1"),
|
|
103
103
|
temperature=_float_env("DEEPPARALLEL_TEMPERATURE", 0.4),
|
|
104
|
-
max_tokens=_int_env("DEEPPARALLEL_MAX_TOKENS",
|
|
104
|
+
max_tokens=_int_env("DEEPPARALLEL_MAX_TOKENS", 8192),
|
|
105
105
|
show_thinking=_bool_env("DEEPPARALLEL_THINK", False),
|
|
106
106
|
tools_enabled=_bool_env("DEEPPARALLEL_TOOLS", True),
|
|
107
107
|
auto_approve=_bool_env("DEEPPARALLEL_AUTO_APPROVE", False),
|
|
@@ -70,6 +70,30 @@ def verify_token(token: str, pubkey_b64: str | None = None) -> dict | None:
|
|
|
70
70
|
return payload
|
|
71
71
|
|
|
72
72
|
|
|
73
|
+
def sign_token(payload: dict, private_key_b64: str) -> str:
|
|
74
|
+
"""Sign a license payload into a `body.signature` token (issuer-side).
|
|
75
|
+
|
|
76
|
+
Requires the private issuance key; verify_token() checks it with the public
|
|
77
|
+
key. Used by the license-issuer (Stripe webhook) and the admin CLI.
|
|
78
|
+
"""
|
|
79
|
+
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
|
|
80
|
+
|
|
81
|
+
priv = Ed25519PrivateKey.from_private_bytes(_b64url_decode_std(private_key_b64))
|
|
82
|
+
body = base64.urlsafe_b64encode(json.dumps(payload).encode()).decode().rstrip("=")
|
|
83
|
+
sig = base64.urlsafe_b64encode(priv.sign(body.encode())).decode().rstrip("=")
|
|
84
|
+
return f"{body}.{sig}"
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _b64url_decode_std(s: str) -> bytes:
|
|
88
|
+
"""Decode standard OR url-safe base64 (keys are emitted as standard b64)."""
|
|
89
|
+
s = s.strip()
|
|
90
|
+
pad = "=" * (-len(s) % 4)
|
|
91
|
+
try:
|
|
92
|
+
return base64.b64decode(s + pad)
|
|
93
|
+
except Exception: # noqa: BLE001
|
|
94
|
+
return base64.urlsafe_b64decode(s + pad)
|
|
95
|
+
|
|
96
|
+
|
|
73
97
|
def _license_file_token() -> str | None:
|
|
74
98
|
path = Path(
|
|
75
99
|
os.environ.get("DEEPPARALLEL_LICENSE_FILE", "~/.config/deepparallel/license")
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
"""Conduit: latent-state relay between frozen models (runnable demonstrator).
|
|
2
|
+
|
|
3
|
+
The idea behind DeepParallel's multi-model approach, made concrete. When agents
|
|
4
|
+
collaborate by writing words and reading them back, every hand-off squeezes a
|
|
5
|
+
continuous thought through a single emitted token (~log2(vocab) bits). Conduit
|
|
6
|
+
relays the model's hidden state directly through a tiny connector instead.
|
|
7
|
+
|
|
8
|
+
This is a dependency-light demonstration of the mechanism and the information
|
|
9
|
+
loss it avoids: two FROZEN synthetic transformers of different hidden sizes (so
|
|
10
|
+
the connector is genuinely required) relay a continuous meaning vector down a
|
|
11
|
+
chain of agents two ways, and we measure how much meaning survives each hop:
|
|
12
|
+
|
|
13
|
+
latent : h_i -> connector(W) -> next model (continuous)
|
|
14
|
+
word : h_i -> argmax token -> re-embed (one discrete symbol)
|
|
15
|
+
|
|
16
|
+
Needs numpy (`pip install deepparallel[research]`). The full research, including
|
|
17
|
+
the same result on real open-weight models, is at crowelogic.com/research/conduit.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import numpy as np
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _gelu(x):
|
|
26
|
+
return 0.5 * x * (1.0 + np.tanh(0.7978845608 * (x + 0.044715 * x**3)))
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _ln(x):
|
|
30
|
+
return (x - x.mean(-1, keepdims=True)) / (x.std(-1, keepdims=True) + 1e-5)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _softmax(x):
|
|
34
|
+
e = np.exp(x - x.max(-1, keepdims=True))
|
|
35
|
+
return e / e.sum(-1, keepdims=True)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class _FrozenLM:
|
|
39
|
+
"""A tiny frozen decoder-only transformer with its own hidden size + vocab."""
|
|
40
|
+
|
|
41
|
+
def __init__(self, vocab, d, n_layers, seed):
|
|
42
|
+
self.vocab, self.d = vocab, d
|
|
43
|
+
rng = np.random.default_rng(seed)
|
|
44
|
+
s = 1.0 / np.sqrt(d)
|
|
45
|
+
self.E = rng.standard_normal((vocab, d)) * 0.02
|
|
46
|
+
self.U = rng.standard_normal((d, vocab)) * s
|
|
47
|
+
self.layers = [
|
|
48
|
+
{
|
|
49
|
+
"Wq": rng.standard_normal((d, d)) * s,
|
|
50
|
+
"Wk": rng.standard_normal((d, d)) * s,
|
|
51
|
+
"Wv": rng.standard_normal((d, d)) * s,
|
|
52
|
+
"Wo": rng.standard_normal((d, d)) * s,
|
|
53
|
+
"W1": rng.standard_normal((d, 4 * d)) * s,
|
|
54
|
+
"W2": rng.standard_normal((4 * d, d)) * s,
|
|
55
|
+
}
|
|
56
|
+
for _ in range(n_layers)
|
|
57
|
+
]
|
|
58
|
+
|
|
59
|
+
def _forward(self, x):
|
|
60
|
+
T = x.shape[0]
|
|
61
|
+
mask = np.triu(np.full((T, T), -1e9), k=1)
|
|
62
|
+
for p in self.layers:
|
|
63
|
+
h = _ln(x)
|
|
64
|
+
q, k, v = h @ p["Wq"], h @ p["Wk"], h @ p["Wv"]
|
|
65
|
+
att = _softmax(q @ k.T / np.sqrt(self.d) + mask)
|
|
66
|
+
x = x + (att @ v) @ p["Wo"]
|
|
67
|
+
x = x + _gelu(_ln(x) @ p["W1"]) @ p["W2"]
|
|
68
|
+
return x
|
|
69
|
+
|
|
70
|
+
def hidden_at(self, in_embed):
|
|
71
|
+
return self._forward(in_embed.reshape(1, self.d))[-1]
|
|
72
|
+
|
|
73
|
+
def next_token(self, hidden):
|
|
74
|
+
return int(np.argmax(hidden @ self.U))
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _ridge(X, Y, lam=1e-2):
|
|
78
|
+
Xb = np.hstack([X, np.ones((X.shape[0], 1))])
|
|
79
|
+
return np.linalg.solve(Xb.T @ Xb + lam * np.eye(Xb.shape[1]), Xb.T @ Y)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _affine(W, x):
|
|
83
|
+
return np.hstack([x, np.ones((x.shape[0], 1))]) @ W
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class _Conduit:
|
|
87
|
+
def __init__(self, d_meaning, dims, vocab, seed=7):
|
|
88
|
+
self.dm = d_meaning
|
|
89
|
+
rng = np.random.default_rng(seed)
|
|
90
|
+
self.models = [
|
|
91
|
+
_FrozenLM(vocab, dims[0], 2, seed + 1),
|
|
92
|
+
_FrozenLM(vocab, dims[1], 2, seed + 2),
|
|
93
|
+
]
|
|
94
|
+
self.P = [rng.standard_normal((d_meaning, m.d)) * 0.5 for m in self.models]
|
|
95
|
+
self.conn, self.readout = {}, {}
|
|
96
|
+
|
|
97
|
+
def _encode(self, mi, z):
|
|
98
|
+
return self.models[mi].hidden_at(z @ self.P[mi])
|
|
99
|
+
|
|
100
|
+
def _relay(self, z, K, path):
|
|
101
|
+
mi = 0
|
|
102
|
+
h = self._encode(mi, z)
|
|
103
|
+
for hop in range(1, K + 1):
|
|
104
|
+
mj = hop % len(self.models)
|
|
105
|
+
if path == "latent":
|
|
106
|
+
in_embed = _affine(self.conn[(mi, mj)], h.reshape(1, -1))[0]
|
|
107
|
+
else:
|
|
108
|
+
tok = self.models[mi].next_token(h)
|
|
109
|
+
in_embed = self.models[mj].E[tok]
|
|
110
|
+
h = self.models[mj].hidden_at(in_embed)
|
|
111
|
+
mi = mj
|
|
112
|
+
return h, mi
|
|
113
|
+
|
|
114
|
+
def fit(self, n_train, fit_hops, seed=0):
|
|
115
|
+
rng = np.random.default_rng(seed)
|
|
116
|
+
Z = rng.standard_normal((n_train, self.dm))
|
|
117
|
+
n = len(self.models)
|
|
118
|
+
for i in range(n):
|
|
119
|
+
Hi = np.array([self._encode(i, z) for z in Z])
|
|
120
|
+
for j in range(n):
|
|
121
|
+
self.conn[(i, j)] = _ridge(Hi, Z @ self.P[j])
|
|
122
|
+
for path in ("latent", "word"):
|
|
123
|
+
finals = {0: [], 1: []}
|
|
124
|
+
targets = {0: [], 1: []}
|
|
125
|
+
for K in fit_hops:
|
|
126
|
+
for z in Z:
|
|
127
|
+
h, m = self._relay(z, K, path)
|
|
128
|
+
finals[m].append(h)
|
|
129
|
+
targets[m].append(z)
|
|
130
|
+
for m in finals:
|
|
131
|
+
if finals[m]:
|
|
132
|
+
self.readout[(path, m)] = _ridge(np.array(finals[m]), np.array(targets[m]))
|
|
133
|
+
|
|
134
|
+
def recover(self, z, K, path):
|
|
135
|
+
h, m = self._relay(z, K, path)
|
|
136
|
+
return _affine(self.readout[(path, m)], h.reshape(1, -1))[0]
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _cos(a, b):
|
|
140
|
+
return float(a @ b / (np.linalg.norm(a) * np.linalg.norm(b) + 1e-9))
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def demonstrate(hops=(1, 2, 4, 6, 8)) -> dict:
|
|
144
|
+
"""Run the relay both ways and return meaning retained per hop count."""
|
|
145
|
+
d_meaning, vocab = 16, 256
|
|
146
|
+
link = _Conduit(d_meaning, dims=[32, 48], vocab=vocab)
|
|
147
|
+
link.fit(n_train=800, fit_hops=hops)
|
|
148
|
+
rng = np.random.default_rng(123)
|
|
149
|
+
test = rng.standard_normal((300, d_meaning))
|
|
150
|
+
out = {"latent": {}, "word": {}, "meaning_dim": d_meaning, "vocab": vocab, "hops": list(hops)}
|
|
151
|
+
for path in ("latent", "word"):
|
|
152
|
+
for K in hops:
|
|
153
|
+
out[path][K] = round(
|
|
154
|
+
float(np.mean([_cos(z, link.recover(z, K, path)) for z in test])), 3
|
|
155
|
+
)
|
|
156
|
+
return out
|