deepparallel 0.2.0__py3-none-any.whl

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/config.py ADDED
@@ -0,0 +1,158 @@
1
+ """Runtime configuration and model-spec loading.
2
+
3
+ Pure resolution from os.environ and packaged data files. No network calls,
4
+ no printing, and no .env loading here (cli.py loads .env once at startup).
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import json
10
+ import os
11
+ from dataclasses import dataclass
12
+ from pathlib import Path
13
+
14
+ _PKG_DIR = Path(__file__).parent
15
+ REGISTRY_PATH = _PKG_DIR / "registry.json"
16
+ SYSTEM_PROMPT_PATH = _PKG_DIR / "system_prompt.txt"
17
+
18
+
19
+ @dataclass(frozen=True)
20
+ class Settings:
21
+ backend: str
22
+ azure_endpoint: str | None
23
+ azure_api_key: str | None
24
+ deployment: str
25
+ api_version: str
26
+ foundry_base_url: str | None
27
+ foundry_api_key: str | None
28
+ foundry_model: str
29
+ temperature: float
30
+ max_tokens: int
31
+ show_thinking: bool
32
+ tools_enabled: bool
33
+ auto_approve: bool
34
+ max_steps: int
35
+ shell_timeout: int
36
+ fusion_mode: str
37
+ reasoner_deployment: str
38
+ parallel_deployments: tuple[str, ...]
39
+ judge_deployment: str
40
+ guardian_enabled: bool
41
+ guardian_deployment: str
42
+
43
+
44
+ @dataclass(frozen=True)
45
+ class ModelSpec:
46
+ name: str
47
+ label: str
48
+ deployment: str
49
+ base_family: str
50
+ context_window: int
51
+ license: str
52
+ license_url: str
53
+
54
+
55
+ def _bool_env(name: str, default: bool = False) -> bool:
56
+ raw = os.environ.get(name)
57
+ if raw is None:
58
+ return default
59
+ return raw.strip().lower() in {"1", "true", "yes", "on"}
60
+
61
+
62
+ def _float_env(name: str, default: float) -> float:
63
+ raw = os.environ.get(name)
64
+ if raw is None:
65
+ return default
66
+ try:
67
+ return float(raw)
68
+ except ValueError:
69
+ return default
70
+
71
+
72
+ def _list_env(name: str, default: list[str]) -> tuple[str, ...]:
73
+ raw = os.environ.get(name)
74
+ if raw is None:
75
+ return tuple(default)
76
+ items = [s.strip() for s in raw.split(",") if s.strip()]
77
+ return tuple(items) if items else tuple(default)
78
+
79
+
80
+ def _int_env(name: str, default: int) -> int:
81
+ raw = os.environ.get(name)
82
+ if raw is None:
83
+ return default
84
+ try:
85
+ return int(raw)
86
+ except ValueError:
87
+ return default
88
+
89
+
90
+ def resolve_settings() -> Settings:
91
+ backend = os.environ.get("DEEPPARALLEL_BACKEND", "azure").strip().lower()
92
+ if backend not in {"azure", "foundry"}:
93
+ backend = "azure"
94
+ return Settings(
95
+ backend=backend,
96
+ azure_endpoint=os.environ.get("AZURE_CORE_ENDPOINT"),
97
+ azure_api_key=os.environ.get("AZURE_CORE_API_KEY"),
98
+ deployment=os.environ.get("DEEPPARALLEL_DEPLOYMENT", "DeepSeek-V4-Pro"),
99
+ api_version=os.environ.get("DEEPPARALLEL_API_VERSION", "2024-08-01-preview"),
100
+ foundry_base_url=os.environ.get("FOUNDRY_BASE_URL"),
101
+ foundry_api_key=os.environ.get("FOUNDRY_API_KEY"),
102
+ foundry_model=os.environ.get("DEEPPARALLEL_FOUNDRY_MODEL", "DeepSeek-V3-1"),
103
+ temperature=_float_env("DEEPPARALLEL_TEMPERATURE", 0.4),
104
+ max_tokens=_int_env("DEEPPARALLEL_MAX_TOKENS", 2048),
105
+ show_thinking=_bool_env("DEEPPARALLEL_THINK", False),
106
+ tools_enabled=_bool_env("DEEPPARALLEL_TOOLS", True),
107
+ auto_approve=_bool_env("DEEPPARALLEL_AUTO_APPROVE", False),
108
+ max_steps=_int_env("DEEPPARALLEL_MAX_STEPS", 12),
109
+ shell_timeout=_int_env("DEEPPARALLEL_SHELL_TIMEOUT", 120),
110
+ fusion_mode=(os.environ.get("DEEPPARALLEL_FUSION", "off").strip().lower() or "off"),
111
+ reasoner_deployment=os.environ.get("DEEPPARALLEL_REASONER_DEPLOYMENT", "DeepSeek-R1-0528"),
112
+ parallel_deployments=_list_env(
113
+ "DEEPPARALLEL_PARALLEL_MODELS",
114
+ ["DeepSeek-V4-Pro", "DeepSeek-R1-0528", "DeepSeek-V4-Flash"],
115
+ ),
116
+ judge_deployment=os.environ.get(
117
+ "DEEPPARALLEL_JUDGE_DEPLOYMENT",
118
+ os.environ.get("DEEPPARALLEL_DEPLOYMENT", "DeepSeek-V4-Pro"),
119
+ ),
120
+ guardian_enabled=_bool_env("DEEPPARALLEL_GUARDIAN", True),
121
+ # A fast, independent reviewer by default (NOT the reasoner: a reasoning
122
+ # model spends a small token budget on hidden thinking and returns no
123
+ # verdict). Differs from the primary author model for a real 2nd opinion.
124
+ guardian_deployment=os.environ.get("DEEPPARALLEL_GUARDIAN_DEPLOYMENT", "DeepSeek-V4-Flash"),
125
+ )
126
+
127
+
128
+ def missing_required(settings: Settings) -> list[str]:
129
+ """Names of required env vars unset for the active backend, in display order."""
130
+ missing: list[str] = []
131
+ if settings.backend == "azure":
132
+ if not settings.azure_endpoint:
133
+ missing.append("AZURE_CORE_ENDPOINT")
134
+ if not settings.azure_api_key:
135
+ missing.append("AZURE_CORE_API_KEY")
136
+ else:
137
+ if not settings.foundry_base_url:
138
+ missing.append("FOUNDRY_BASE_URL")
139
+ if not settings.foundry_api_key:
140
+ missing.append("FOUNDRY_API_KEY")
141
+ return missing
142
+
143
+
144
+ def load_model_spec() -> ModelSpec:
145
+ entry = json.loads(REGISTRY_PATH.read_text())["models"][0]
146
+ return ModelSpec(
147
+ name=entry["name"],
148
+ label=entry["label"],
149
+ deployment=entry["deployment"],
150
+ base_family=entry["base_family"],
151
+ context_window=entry["context_window"],
152
+ license=entry["license"],
153
+ license_url=entry["license_url"],
154
+ )
155
+
156
+
157
+ def load_system_prompt() -> str:
158
+ return SYSTEM_PROMPT_PATH.read_text().strip()
deepparallel/fusion.py ADDED
@@ -0,0 +1,225 @@
1
+ """Fusion backends: stack DeepParallel with other models for stronger output.
2
+
3
+ All strategies compose deepparallel's own `Backend` instances (secondary backends
4
+ pointed at other deployments), so there is no foundry dependency. Modes:
5
+ - ReasonAnswerBackend: a reasoner produces a thinking trace, injected into the
6
+ primary's answer call.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import difflib
12
+ from concurrent.futures import ThreadPoolExecutor, as_completed
13
+ from typing import Callable, Iterator
14
+
15
+ _REASON_NOTE = (
16
+ "\n\nA separate reasoning model analyzed this request. "
17
+ "Consider its reasoning, then give your own best answer:\n"
18
+ )
19
+
20
+
21
+ def _augment(messages: list[dict], reasoning: str) -> list[dict]:
22
+ if not reasoning:
23
+ return messages
24
+ aug = [dict(m) for m in messages]
25
+ note = _REASON_NOTE + reasoning
26
+ if aug and aug[0].get("role") == "system":
27
+ aug[0] = {**aug[0], "content": (aug[0].get("content") or "") + note}
28
+ else:
29
+ aug.insert(0, {"role": "system", "content": note.strip()})
30
+ return aug
31
+
32
+
33
+ class ReasonAnswerBackend:
34
+ """Run the reasoner first, inject its trace, then answer with the primary."""
35
+
36
+ label = "DeepParallel (fusion: reason)"
37
+
38
+ def __init__(self, primary, reasoner):
39
+ self._primary = primary
40
+ self._reasoner = reasoner
41
+
42
+ def check(self) -> tuple[bool, str]:
43
+ return self._primary.check()
44
+
45
+ def _reason(self, messages, temperature, max_tokens) -> str:
46
+ try:
47
+ msg = self._reasoner.chat(messages, [], temperature, max_tokens)
48
+ except Exception: # noqa: BLE001 - reasoning is best-effort; fall back
49
+ return ""
50
+ return (msg.get("reasoning_content") or msg.get("content") or "").strip()
51
+
52
+ def chat(self, messages, tools, temperature, max_tokens) -> dict:
53
+ reasoning = self._reason(messages, temperature, max_tokens)
54
+ return self._primary.chat(_augment(messages, reasoning), tools, temperature, max_tokens)
55
+
56
+ def stream_chat(self, messages, temperature, max_tokens) -> Iterator[tuple[str, str]]:
57
+ reasoning = self._reason(messages, temperature, max_tokens)
58
+ yield from self._primary.stream_chat(_augment(messages, reasoning), temperature, max_tokens)
59
+
60
+
61
+ _DOUBT = (
62
+ "not sure",
63
+ "uncertain",
64
+ "not certain",
65
+ "unclear",
66
+ "cannot determine",
67
+ "can't determine",
68
+ "don't have enough",
69
+ "do not have enough",
70
+ "more information",
71
+ "insufficient information",
72
+ )
73
+
74
+
75
+ class EscalationBackend:
76
+ """Answer with the primary; only escalate to the reasoner when the answer
77
+ signals uncertainty. Cheapest fusion mode on average."""
78
+
79
+ label = "DeepParallel (fusion: escalate)"
80
+
81
+ def __init__(self, primary, reasoner):
82
+ self._primary = primary
83
+ self._reasoner = reasoner
84
+
85
+ def check(self) -> tuple[bool, str]:
86
+ return self._primary.check()
87
+
88
+ @staticmethod
89
+ def _uncertain(content: str) -> bool:
90
+ low = content.lower()
91
+ return any(phrase in low for phrase in _DOUBT)
92
+
93
+ def chat(self, messages, tools, temperature, max_tokens) -> dict:
94
+ msg = self._primary.chat(messages, tools, temperature, max_tokens)
95
+ if msg.get("tool_calls"):
96
+ return msg # mid-loop tool turn; never escalate
97
+ if self._uncertain(msg.get("content") or ""):
98
+ return ReasonAnswerBackend(self._primary, self._reasoner).chat(
99
+ messages, tools, temperature, max_tokens
100
+ )
101
+ return msg
102
+
103
+ def stream_chat(self, messages, temperature, max_tokens) -> Iterator[tuple[str, str]]:
104
+ yield from self._primary.stream_chat(messages, temperature, max_tokens)
105
+
106
+
107
+ def _last_user(messages: list[dict]) -> str:
108
+ for m in reversed(messages):
109
+ if m.get("role") == "user":
110
+ return m.get("content") or ""
111
+ return ""
112
+
113
+
114
+ def _chain_answer(backend, messages, temperature, max_tokens) -> str:
115
+ try:
116
+ msg = backend.chat(messages, [], temperature, max_tokens)
117
+ except Exception as e: # noqa: BLE001 - one chain failing must not sink the run
118
+ return f"[error: {type(e).__name__}: {e}]"
119
+ return (msg.get("content") or msg.get("reasoning_content") or "").strip()
120
+
121
+
122
+ def _fallback(results: list[dict]) -> str:
123
+ valid = [r["content"] for r in results if not r["content"].startswith("[error")]
124
+ return max(valid, key=len) if valid else "All reasoning chains failed."
125
+
126
+
127
+ def _judge(messages, results, judge, temperature, max_tokens) -> str:
128
+ question = _last_user(messages)
129
+ blocks = "\n\n".join(
130
+ f"Candidate {i + 1} ({r['name']}):\n{r['content']}" for i, r in enumerate(results)
131
+ )
132
+ judge_messages = [
133
+ {
134
+ "role": "system",
135
+ "content": (
136
+ "You are a careful judge. Synthesize the single best, correct answer "
137
+ "from the candidate answers below. Resolve disagreements with sound "
138
+ "reasoning. Output only the final answer."
139
+ ),
140
+ },
141
+ {
142
+ "role": "user",
143
+ "content": f"Question:\n{question}\n\nCandidate answers:\n{blocks}\n\nFinal answer:",
144
+ },
145
+ ]
146
+ try:
147
+ msg = judge.chat(judge_messages, [], temperature, max_tokens)
148
+ except Exception: # noqa: BLE001 - degrade gracefully if the judge fails
149
+ return _fallback(results)
150
+ return (msg.get("content") or "").strip() or _fallback(results)
151
+
152
+
153
+ def deep_query(
154
+ messages: list[dict],
155
+ chains: list[tuple[str, object]],
156
+ judge,
157
+ temperature: float,
158
+ max_tokens: int,
159
+ on_event: Callable[[str, str], None] | None = None,
160
+ ) -> dict:
161
+ """Fan a prompt out to several model chains in parallel, then synthesize a
162
+ final answer with a judge. Heavy and slow; for one-shot --deep runs."""
163
+ order = {name: i for i, (name, _) in enumerate(chains)}
164
+ results: list[dict] = []
165
+ with ThreadPoolExecutor(max_workers=min(8, max(1, len(chains)))) as pool:
166
+ futures = {
167
+ pool.submit(_chain_answer, backend, messages, temperature, max_tokens): name
168
+ for name, backend in chains
169
+ }
170
+ for future in as_completed(futures):
171
+ name = futures[future]
172
+ content = future.result()
173
+ results.append({"name": name, "content": content})
174
+ if on_event:
175
+ on_event(name, content)
176
+ results.sort(key=lambda r: order.get(r["name"], 0))
177
+ answer = _judge(messages, results, judge, temperature, max_tokens)
178
+ return {"answer": answer, "chains": results, "agreement": _agreement(results)}
179
+
180
+
181
+ def _agreement(results: list[dict]) -> dict:
182
+ """Cross-model agreement (NOT correctness) from pairwise answer similarity.
183
+
184
+ Zero extra model cost; uses difflib over the candidate answers.
185
+ """
186
+ answers = [r["content"] for r in results if not r["content"].startswith("[error")]
187
+ if len(answers) < 2:
188
+ return {"level": "n/a", "score": 1.0 if answers else 0.0}
189
+ ratios = [
190
+ difflib.SequenceMatcher(None, answers[i], answers[j]).ratio()
191
+ for i in range(len(answers))
192
+ for j in range(i + 1, len(answers))
193
+ ]
194
+ score = round(sum(ratios) / len(ratios), 2)
195
+ level = "high" if score >= 0.6 else "medium" if score >= 0.3 else "low"
196
+ return {"level": level, "score": score}
197
+
198
+
199
+ def dual_query(
200
+ messages: list[dict],
201
+ left: tuple[str, object],
202
+ right: tuple[str, object],
203
+ temperature: float,
204
+ max_tokens: int,
205
+ judge=None,
206
+ ) -> dict:
207
+ """Run two models on the same prompt (in parallel) for side-by-side
208
+ comparison, optionally synthesizing a merged answer with a judge."""
209
+ pairs = [left, right]
210
+ answers: dict[str, str] = {}
211
+ with ThreadPoolExecutor(max_workers=2) as pool:
212
+ futures = {
213
+ pool.submit(_chain_answer, backend, messages, temperature, max_tokens): name
214
+ for name, backend in pairs
215
+ }
216
+ for future in as_completed(futures):
217
+ answers[futures[future]] = future.result()
218
+ results = [
219
+ {"name": left[0], "content": answers[left[0]]},
220
+ {"name": right[0], "content": answers[right[0]]},
221
+ ]
222
+ synthesis = None
223
+ if judge is not None:
224
+ synthesis = _judge(messages, results, judge, temperature, max_tokens)
225
+ return {"left": results[0], "right": results[1], "synthesis": synthesis}
@@ -0,0 +1,108 @@
1
+ """License tiers and offline verification for DeepParallel.
2
+
3
+ A license key is a signed token `body.signature` where body is
4
+ url-safe-base64(JSON payload) and signature is an Ed25519 signature of the body.
5
+ The CLI embeds the issuer's PUBLIC key and verifies offline, so a key cannot be
6
+ forged without the private issuance key (held server-side, used at Stripe
7
+ fulfillment time).
8
+
9
+ Open-core split:
10
+ - FREE: the full single-model agent and all tools (bring your own key).
11
+ - PRO/TEAM: the fusion layer - Guardian edit-review, fusion modes, and the
12
+ --deep multi-model council - which is the paid value.
13
+
14
+ Enforcement here is the client-side gate; hosted inference is additionally
15
+ gated server-side at the gateway.
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import base64
21
+ import json
22
+ import os
23
+ import time
24
+ from enum import IntEnum
25
+ from pathlib import Path
26
+
27
+ # Issuer public key (Ed25519, raw, base64). The matching private key is the
28
+ # issuance secret and is never shipped.
29
+ _EMBEDDED_PUBKEY = "I0YYNFwGGUZGD6h15leBEPBQ197Snik+njzxkVf/Owg="
30
+
31
+ # Paid features -> minimum tier required.
32
+ _FEATURE_TIER: dict[str, "Tier"] = {}
33
+
34
+
35
+ class Tier(IntEnum):
36
+ FREE = 0
37
+ PRO = 1
38
+ TEAM = 2
39
+
40
+ @property
41
+ def label(self) -> str:
42
+ return {0: "Free", 1: "Pro", 2: "Team"}[int(self)]
43
+
44
+
45
+ def _b64url_decode(s: str) -> bytes:
46
+ return base64.urlsafe_b64decode(s + "=" * (-len(s) % 4))
47
+
48
+
49
+ def verify_token(token: str, pubkey_b64: str | None = None) -> dict | None:
50
+ """Return the payload dict if the token is validly signed and unexpired."""
51
+ if not token or "." not in token:
52
+ return None
53
+ try:
54
+ from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey
55
+ except ImportError:
56
+ return None
57
+ body, _, sig = token.partition(".")
58
+ try:
59
+ pub = Ed25519PublicKey.from_public_bytes(_b64url_decode(pubkey_b64 or _EMBEDDED_PUBKEY))
60
+ pub.verify(_b64url_decode(sig), body.encode())
61
+ except Exception: # noqa: BLE001 - any failure (bad sig/key/encoding) = invalid
62
+ return None
63
+ try:
64
+ payload = json.loads(_b64url_decode(body))
65
+ except (ValueError, json.JSONDecodeError):
66
+ return None
67
+ exp = payload.get("exp", 0)
68
+ if exp and time.time() > exp:
69
+ return None
70
+ return payload
71
+
72
+
73
+ def _license_file_token() -> str | None:
74
+ path = Path(
75
+ os.environ.get("DEEPPARALLEL_LICENSE_FILE", "~/.config/deepparallel/license")
76
+ ).expanduser()
77
+ try:
78
+ return path.read_text().strip() or None
79
+ except OSError:
80
+ return None
81
+
82
+
83
+ def resolve_tier() -> Tier:
84
+ """Resolve the active tier from the license env var or file. FREE on absence/failure."""
85
+ token = os.environ.get("DEEPPARALLEL_LICENSE") or _license_file_token()
86
+ if not token:
87
+ return Tier.FREE
88
+ payload = verify_token(token)
89
+ if not payload:
90
+ return Tier.FREE
91
+ name = str(payload.get("tier", "")).upper()
92
+ return getattr(Tier, name, Tier.FREE) if name in Tier.__members__ else Tier.FREE
93
+
94
+
95
+ def tier_allows(have: Tier, need: Tier) -> bool:
96
+ return int(have) >= int(need)
97
+
98
+
99
+ def check_feature(feature: str, have: Tier | None = None) -> tuple[bool, str]:
100
+ """Return (allowed, message). Paid features need PRO+; message nudges upgrade."""
101
+ have = resolve_tier() if have is None else have
102
+ need = _FEATURE_TIER.get(feature, Tier.PRO)
103
+ if tier_allows(have, need):
104
+ return True, ""
105
+ return False, (
106
+ f"{feature} requires DeepParallel {need.label} (you are on {have.label}). "
107
+ f"Upgrade at https://deepparallel.dev/pricing"
108
+ )
@@ -0,0 +1,13 @@
1
+ {
2
+ "models": [
3
+ {
4
+ "name": "crowelm-deepparallel",
5
+ "label": "DeepParallel",
6
+ "deployment": "DeepSeek-V4-Pro",
7
+ "base_family": "CroweLM",
8
+ "context_window": 131072,
9
+ "license": "Crowe Logic Service Terms",
10
+ "license_url": "https://crowelogic.com"
11
+ }
12
+ ]
13
+ }