deepparallel 0.5.1__tar.gz → 0.5.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. {deepparallel-0.5.1 → deepparallel-0.5.2}/PKG-INFO +1 -1
  2. {deepparallel-0.5.1 → deepparallel-0.5.2}/deepparallel/__init__.py +1 -1
  3. {deepparallel-0.5.1 → deepparallel-0.5.2}/deepparallel/agent.py +33 -1
  4. {deepparallel-0.5.1 → deepparallel-0.5.2}/deepparallel/supply_chain.py +19 -8
  5. deepparallel-0.5.2/deepparallel/system_prompt.txt +14 -0
  6. {deepparallel-0.5.1 → deepparallel-0.5.2}/deepparallel/tools/mcp.py +52 -27
  7. {deepparallel-0.5.1 → deepparallel-0.5.2}/deepparallel/tools/web.py +9 -3
  8. {deepparallel-0.5.1 → deepparallel-0.5.2}/deepparallel.egg-info/PKG-INFO +1 -1
  9. {deepparallel-0.5.1 → deepparallel-0.5.2}/pyproject.toml +1 -1
  10. {deepparallel-0.5.1 → deepparallel-0.5.2}/tests/test_supply_chain.py +18 -0
  11. {deepparallel-0.5.1 → deepparallel-0.5.2}/tests/test_tools_mcp.py +35 -0
  12. {deepparallel-0.5.1 → deepparallel-0.5.2}/tests/test_tools_web.py +15 -0
  13. deepparallel-0.5.1/deepparallel/system_prompt.txt +0 -7
  14. {deepparallel-0.5.1 → deepparallel-0.5.2}/README.md +0 -0
  15. {deepparallel-0.5.1 → deepparallel-0.5.2}/deepparallel/backend.py +0 -0
  16. {deepparallel-0.5.1 → deepparallel-0.5.2}/deepparallel/branding.py +0 -0
  17. {deepparallel-0.5.1 → deepparallel-0.5.2}/deepparallel/cli.py +0 -0
  18. {deepparallel-0.5.1 → deepparallel-0.5.2}/deepparallel/config.py +0 -0
  19. {deepparallel-0.5.1 → deepparallel-0.5.2}/deepparallel/crowe_id.py +0 -0
  20. {deepparallel-0.5.1 → deepparallel-0.5.2}/deepparallel/dsml.py +0 -0
  21. {deepparallel-0.5.1 → deepparallel-0.5.2}/deepparallel/fusion.py +0 -0
  22. {deepparallel-0.5.1 → deepparallel-0.5.2}/deepparallel/licensing.py +0 -0
  23. {deepparallel-0.5.1 → deepparallel-0.5.2}/deepparallel/registry.json +0 -0
  24. {deepparallel-0.5.1 → deepparallel-0.5.2}/deepparallel/renderer.py +0 -0
  25. {deepparallel-0.5.1 → deepparallel-0.5.2}/deepparallel/research/__init__.py +0 -0
  26. {deepparallel-0.5.1 → deepparallel-0.5.2}/deepparallel/research/conduit.py +0 -0
  27. {deepparallel-0.5.1 → deepparallel-0.5.2}/deepparallel/research/provider.py +0 -0
  28. {deepparallel-0.5.1 → deepparallel-0.5.2}/deepparallel/routing.example.json +0 -0
  29. {deepparallel-0.5.1 → deepparallel-0.5.2}/deepparallel/routing.py +0 -0
  30. {deepparallel-0.5.1 → deepparallel-0.5.2}/deepparallel/serve.py +0 -0
  31. {deepparallel-0.5.1 → deepparallel-0.5.2}/deepparallel/tools/__init__.py +0 -0
  32. {deepparallel-0.5.1 → deepparallel-0.5.2}/deepparallel/tools/codeast.py +0 -0
  33. {deepparallel-0.5.1 → deepparallel-0.5.2}/deepparallel/tools/edit.py +0 -0
  34. {deepparallel-0.5.1 → deepparallel-0.5.2}/deepparallel/tools/files.py +0 -0
  35. {deepparallel-0.5.1 → deepparallel-0.5.2}/deepparallel/tools/registry.py +0 -0
  36. {deepparallel-0.5.1 → deepparallel-0.5.2}/deepparallel/tools/sandbox.py +0 -0
  37. {deepparallel-0.5.1 → deepparallel-0.5.2}/deepparallel/tools/search.py +0 -0
  38. {deepparallel-0.5.1 → deepparallel-0.5.2}/deepparallel/tools/shell.py +0 -0
  39. {deepparallel-0.5.1 → deepparallel-0.5.2}/deepparallel/tools/vision.py +0 -0
  40. {deepparallel-0.5.1 → deepparallel-0.5.2}/deepparallel/userinput.py +0 -0
  41. {deepparallel-0.5.1 → deepparallel-0.5.2}/deepparallel.egg-info/SOURCES.txt +0 -0
  42. {deepparallel-0.5.1 → deepparallel-0.5.2}/deepparallel.egg-info/dependency_links.txt +0 -0
  43. {deepparallel-0.5.1 → deepparallel-0.5.2}/deepparallel.egg-info/entry_points.txt +0 -0
  44. {deepparallel-0.5.1 → deepparallel-0.5.2}/deepparallel.egg-info/requires.txt +0 -0
  45. {deepparallel-0.5.1 → deepparallel-0.5.2}/deepparallel.egg-info/top_level.txt +0 -0
  46. {deepparallel-0.5.1 → deepparallel-0.5.2}/setup.cfg +0 -0
  47. {deepparallel-0.5.1 → deepparallel-0.5.2}/tests/test_agent.py +0 -0
  48. {deepparallel-0.5.1 → deepparallel-0.5.2}/tests/test_backend.py +0 -0
  49. {deepparallel-0.5.1 → deepparallel-0.5.2}/tests/test_backend_chat.py +0 -0
  50. {deepparallel-0.5.1 → deepparallel-0.5.2}/tests/test_backend_stream.py +0 -0
  51. {deepparallel-0.5.1 → deepparallel-0.5.2}/tests/test_branding.py +0 -0
  52. {deepparallel-0.5.1 → deepparallel-0.5.2}/tests/test_cli.py +0 -0
  53. {deepparallel-0.5.1 → deepparallel-0.5.2}/tests/test_config.py +0 -0
  54. {deepparallel-0.5.1 → deepparallel-0.5.2}/tests/test_crowe_backend.py +0 -0
  55. {deepparallel-0.5.1 → deepparallel-0.5.2}/tests/test_crowe_gateway_backend.py +0 -0
  56. {deepparallel-0.5.1 → deepparallel-0.5.2}/tests/test_crowe_id_auth.py +0 -0
  57. {deepparallel-0.5.1 → deepparallel-0.5.2}/tests/test_crowe_payment_required.py +0 -0
  58. {deepparallel-0.5.1 → deepparallel-0.5.2}/tests/test_dsml.py +0 -0
  59. {deepparallel-0.5.1 → deepparallel-0.5.2}/tests/test_fusion.py +0 -0
  60. {deepparallel-0.5.1 → deepparallel-0.5.2}/tests/test_issuer_signer.py +0 -0
  61. {deepparallel-0.5.1 → deepparallel-0.5.2}/tests/test_licensing.py +0 -0
  62. {deepparallel-0.5.1 → deepparallel-0.5.2}/tests/test_renderer.py +0 -0
  63. {deepparallel-0.5.1 → deepparallel-0.5.2}/tests/test_research.py +0 -0
  64. {deepparallel-0.5.1 → deepparallel-0.5.2}/tests/test_research_provider.py +0 -0
  65. {deepparallel-0.5.1 → deepparallel-0.5.2}/tests/test_routing.py +0 -0
  66. {deepparallel-0.5.1 → deepparallel-0.5.2}/tests/test_serve.py +0 -0
  67. {deepparallel-0.5.1 → deepparallel-0.5.2}/tests/test_spinner_color.py +0 -0
  68. {deepparallel-0.5.1 → deepparallel-0.5.2}/tests/test_tool_registry.py +0 -0
  69. {deepparallel-0.5.1 → deepparallel-0.5.2}/tests/test_tools_codeast.py +0 -0
  70. {deepparallel-0.5.1 → deepparallel-0.5.2}/tests/test_tools_edit.py +0 -0
  71. {deepparallel-0.5.1 → deepparallel-0.5.2}/tests/test_tools_files.py +0 -0
  72. {deepparallel-0.5.1 → deepparallel-0.5.2}/tests/test_tools_sandbox.py +0 -0
  73. {deepparallel-0.5.1 → deepparallel-0.5.2}/tests/test_tools_search.py +0 -0
  74. {deepparallel-0.5.1 → deepparallel-0.5.2}/tests/test_tools_shell.py +0 -0
  75. {deepparallel-0.5.1 → deepparallel-0.5.2}/tests/test_tools_vision.py +0 -0
  76. {deepparallel-0.5.1 → deepparallel-0.5.2}/tests/test_userinput.py +0 -0
  77. {deepparallel-0.5.1 → deepparallel-0.5.2}/tests/test_userinput_paste.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: deepparallel
3
- Version: 0.5.1
3
+ Version: 0.5.2
4
4
  Summary: DeepParallel - a multi-model agentic coding CLI with cross-model Guardian review, served via Crowe Logic.
5
5
  Author-email: Michael Crowe <michael@crowelogic.com>
6
6
  License: Apache-2.0
@@ -1,3 +1,3 @@
1
1
  """DeepParallel CLI package."""
2
2
 
3
- __version__ = "0.5.1"
3
+ __version__ = "0.5.2"
@@ -204,6 +204,38 @@ def _guardian_verdict(guardian, name: str, args: dict) -> str | None:
204
204
  return guardian_review(guardian, _guardian_review_content(name, args))
205
205
 
206
206
 
207
+ def _local_module_names(target_path: str) -> set[str]:
208
+ """Module names that resolve to files in the workspace, so a sibling import
209
+ like `from compound_library import ...` is never flagged as a hallucinated
210
+ PyPI package. Scans the target file's directory and the cwd (capped)."""
211
+ names: set[str] = set()
212
+ roots = []
213
+ try:
214
+ roots.append(Path(target_path).expanduser().resolve().parent)
215
+ except Exception: # noqa: BLE001
216
+ pass
217
+ try:
218
+ roots.append(Path.cwd().resolve())
219
+ except Exception: # noqa: BLE001
220
+ pass
221
+ seen_roots: set[Path] = set()
222
+ for root in roots:
223
+ if root in seen_roots:
224
+ continue
225
+ seen_roots.add(root)
226
+ try:
227
+ for i, p in enumerate(root.rglob("*.py")):
228
+ names.add(p.stem)
229
+ if p.name == "__init__.py":
230
+ names.add(p.parent.name)
231
+ if i >= 5000:
232
+ break
233
+ except Exception: # noqa: BLE001
234
+ pass
235
+ names.discard("__init__")
236
+ return names
237
+
238
+
207
239
  def _supply_chain_note(name: str, args: dict) -> str | None:
208
240
  """Best-effort: flag hallucinated/slopsquatted deps an edit introduces."""
209
241
  content = args.get("new_source") or args.get("new_string") or args.get("content") or ""
@@ -213,7 +245,7 @@ def _supply_chain_note(name: str, args: dict) -> str | None:
213
245
  try:
214
246
  from deepparallel import supply_chain
215
247
 
216
- result = supply_chain.audit(content, path)
248
+ result = supply_chain.audit(content, path, _local_module_names(path))
217
249
  except Exception: # noqa: BLE001 - supply-chain check is best-effort
218
250
  return None
219
251
  if result["hallucinated"]:
@@ -44,23 +44,30 @@ _IMPORT_RE = re.compile(r"^\s*(?:import\s+([a-zA-Z0-9_.]+)|from\s+([a-zA-Z0-9_.]
44
44
  _REQ_RE = re.compile(r"^\s*([A-Za-z0-9][A-Za-z0-9._-]*)")
45
45
 
46
46
 
47
- def extract_dependencies(content: str, filename: str) -> list[dict]:
48
- """Return [{name, ecosystem, raw}] introduced by this content."""
47
+ def extract_dependencies(
48
+ content: str, filename: str, local_modules: set[str] | None = None
49
+ ) -> list[dict]:
50
+ """Return [{name, ecosystem, raw}] introduced by this content.
51
+
52
+ `local_modules` names resolve to files in the workspace (sibling modules,
53
+ local packages) and are never treated as third-party dependencies.
54
+ """
49
55
  fn = filename.rsplit("/", 1)[-1].lower()
50
56
  if fn == "package.json":
51
57
  return _from_package_json(content)
52
58
  if fn in ("requirements.txt",) or fn.startswith("requirements"):
53
59
  return _from_requirements(content)
54
60
  if fn.endswith(".py"):
55
- return _from_python(content)
61
+ return _from_python(content, local_modules)
56
62
  return []
57
63
 
58
64
 
59
- def _from_python(content: str) -> list[dict]:
65
+ def _from_python(content: str, local: set[str] | None = None) -> list[dict]:
66
+ local = local or set()
60
67
  out, seen = [], set()
61
68
  for imp, frm in _IMPORT_RE.findall(content):
62
69
  mod = (imp or frm).split(".")[0]
63
- if not mod or mod.startswith("_") or mod in _STDLIB or mod in seen:
70
+ if not mod or mod.startswith("_") or mod in _STDLIB or mod in seen or mod in local:
64
71
  continue
65
72
  seen.add(mod)
66
73
  dist = _PYPI_ALIASES.get(mod, mod)
@@ -117,11 +124,15 @@ def check_exists(name: str, ecosystem: str) -> bool | None:
117
124
  return None
118
125
 
119
126
 
120
- def audit(content: str, filename: str) -> dict:
121
- """Audit a change's dependencies. Returns findings + the hallucinated list."""
127
+ def audit(content: str, filename: str, local_modules: set[str] | None = None) -> dict:
128
+ """Audit a change's dependencies. Returns findings + the hallucinated list.
129
+
130
+ `local_modules` are workspace-local module names (sibling files, local
131
+ packages) that must not be checked against PyPI/npm.
132
+ """
122
133
  findings = []
123
134
  hallucinated = []
124
- for dep in extract_dependencies(content, filename):
135
+ for dep in extract_dependencies(content, filename, local_modules):
125
136
  exists = check_exists(dep["name"], dep["ecosystem"])
126
137
  status = "ok" if exists is True else "missing" if exists is False else "unknown"
127
138
  findings.append({"name": dep["name"], "ecosystem": dep["ecosystem"], "status": status})
@@ -0,0 +1,14 @@
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.
8
+
9
+ Engineering discipline — how you build:
10
+ - Build the smallest unit that can be verified, and verify it before scaling up. Before generating a multi-file system, write the single most fundamental primitive and run it (or a one-line check) to confirm it works. Never emit hundreds of lines across several files before executing anything — a wrong assumption then costs many rounds of debugging instead of one.
11
+ - Ground before you generate. For domain work (chemistry, biology, finance, an API or library you are not certain of), prefer retrieving real data or references over inventing them. Use mcp_search with a single keyword (e.g. "pubchem") to find a domain MCP server, web_fetch for documentation, and read_file/grep for local truth. Reach for a database or a reference before reconstructing it from memory.
12
+ - Treat warnings as signal, not noise. A Guardian "risky/bug" verdict, a supply-chain flag, a parser or valence error, or a low similarity-to-reference score is evidence that something is wrong. Investigate and fix the cause; never rationalize it away or approve through it.
13
+ - Validate against known-good references. When generating structured artifacts (molecules, schemas, configs, queries), check a sample against a known-correct example before trusting the whole batch. If your output does not resemble the references you expect, the generator is wrong, not the references.
14
+ - Label honestly. Never emit an output whose name, ID, or label does not match what it actually is. If you cannot represent something correctly, say so rather than silently substituting a near-miss.
@@ -10,6 +10,7 @@ from __future__ import annotations
10
10
 
11
11
  import json
12
12
  import os
13
+ import re
13
14
  import subprocess
14
15
  import sys
15
16
  import threading
@@ -159,6 +160,35 @@ def _reap_idle() -> None:
159
160
  del _pool[key]
160
161
 
161
162
 
163
+ def _registry_query(query: str, limit: int) -> list[dict]:
164
+ """One registry search call; returns raw server entries (possibly empty)."""
165
+ r = httpx.get(
166
+ _REGISTRY_API,
167
+ params={"search": query, "limit": limit},
168
+ timeout=_TIMEOUT,
169
+ headers={"user-agent": _UA},
170
+ )
171
+ r.raise_for_status()
172
+ return r.json().get("servers", [])
173
+
174
+
175
+ def _format_server(entry: dict) -> dict:
176
+ server = entry.get("server", entry)
177
+ return {
178
+ "name": server.get("name", "unknown"),
179
+ "description": server.get("description", ""),
180
+ "version": server.get("version", ""),
181
+ "packages": [
182
+ {
183
+ "type": p.get("registryType", ""),
184
+ "package": p.get("identifier", ""),
185
+ "transport": p.get("transport", {}).get("type", "unknown"),
186
+ }
187
+ for p in server.get("packages", [])
188
+ ],
189
+ }
190
+
191
+
162
192
  @tool(dangerous=False)
163
193
  def mcp_search(query: str, limit: int = 10) -> str:
164
194
  """Search the MCP server registry (5,800+ servers) for a capability.
@@ -166,39 +196,34 @@ def mcp_search(query: str, limit: int = 10) -> str:
166
196
  Returns matching server names, descriptions, and package install info.
167
197
  Use this first to discover what exists, then mcp_list_tools to connect.
168
198
 
169
- :param query: Capability to search for (e.g. "postgres", "slack", "github").
199
+ :param query: A single capability keyword works best (e.g. "pubchem",
200
+ "postgres", "slack"); multi-word phrases are split and merged automatically.
170
201
  :param limit: Maximum number of results (max 50).
171
202
  """
172
203
  limit = min(int(limit), 50)
173
204
  try:
174
- r = httpx.get(
175
- _REGISTRY_API,
176
- params={"search": query, "limit": limit},
177
- timeout=_TIMEOUT,
178
- headers={"user-agent": _UA},
179
- )
180
- r.raise_for_status()
181
- data = r.json()
205
+ servers = _registry_query(query, limit)
206
+ # The registry matches substrings, not phrases or meaning: a natural-
207
+ # language query like "chemistry molecular docking" matches nothing,
208
+ # while the single word "chemistry" finds servers. When a multi-word
209
+ # query comes back empty, retry each significant word and merge, so the
210
+ # model discovers servers without having to guess the exact term.
211
+ if not servers:
212
+ words = [w for w in re.findall(r"[A-Za-z0-9]+", query.lower()) if len(w) > 2]
213
+ seen: set[str] = set()
214
+ merged: list[dict] = []
215
+ for word in dict.fromkeys(words):
216
+ for entry in _registry_query(word, limit):
217
+ name = entry.get("server", entry).get("name", "")
218
+ if name and name not in seen:
219
+ seen.add(name)
220
+ merged.append(entry)
221
+ if len(merged) >= limit:
222
+ break
223
+ servers = merged[:limit]
182
224
  except Exception as e: # noqa: BLE001 - surface registry failure to the model
183
225
  return json.dumps({"error": f"registry search failed: {type(e).__name__}: {e}"})
184
- results = []
185
- for entry in data.get("servers", []):
186
- server = entry.get("server", entry)
187
- results.append(
188
- {
189
- "name": server.get("name", "unknown"),
190
- "description": server.get("description", ""),
191
- "version": server.get("version", ""),
192
- "packages": [
193
- {
194
- "type": p.get("registryType", ""),
195
- "package": p.get("identifier", ""),
196
- "transport": p.get("transport", {}).get("type", "unknown"),
197
- }
198
- for p in server.get("packages", [])
199
- ],
200
- }
201
- )
226
+ results = [_format_server(entry) for entry in servers]
202
227
  return json.dumps({"query": query, "count": len(results), "servers": results})
203
228
 
204
229
 
@@ -46,10 +46,10 @@ def web_search(query: str, count: int = 5) -> str:
46
46
  :param query: The search query.
47
47
  :param count: Maximum number of results.
48
48
  """
49
- key = os.environ.get("DEEPPARALLEL_SEARCH_API_KEY")
49
+ key = (os.environ.get("DEEPPARALLEL_SEARCH_API_KEY") or "").strip()
50
50
  if not key:
51
51
  return json.dumps(
52
- {"error": "search not configured: set DEEPPARALLEL_SEARCH_API_KEY (Brave Search API)"}
52
+ {"error": "search not configured: set DEEPPARALLEL_SEARCH_API_KEY (Brave Search API key)"}
53
53
  )
54
54
  url = os.environ.get(
55
55
  "DEEPPARALLEL_SEARCH_URL", "https://api.search.brave.com/res/v1/web/search"
@@ -61,7 +61,13 @@ def web_search(query: str, count: int = 5) -> str:
61
61
  headers={"X-Subscription-Token": key, "accept": "application/json"},
62
62
  timeout=_TIMEOUT,
63
63
  )
64
- r.raise_for_status()
64
+ if r.status_code >= 400:
65
+ # Surface the provider's error body: a bare "422" hides the reason
66
+ # (missing key header, over-long query, plan limit). The body tells
67
+ # the model and the user exactly what to fix.
68
+ return json.dumps(
69
+ {"error": f"search failed: HTTP {r.status_code}: {(r.text or '')[:300]}"}
70
+ )
65
71
  data = r.json()
66
72
  except Exception as e: # noqa: BLE001 - surface search failure to the model
67
73
  return json.dumps({"error": f"search failed: {type(e).__name__}: {e}"})
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: deepparallel
3
- Version: 0.5.1
3
+ Version: 0.5.2
4
4
  Summary: DeepParallel - a multi-model agentic coding CLI with cross-model Guardian review, served via Crowe Logic.
5
5
  Author-email: Michael Crowe <michael@crowelogic.com>
6
6
  License: Apache-2.0
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "deepparallel"
7
- version = "0.5.1"
7
+ version = "0.5.2"
8
8
  description = "DeepParallel - a multi-model agentic coding CLI with cross-model Guardian review, served via Crowe Logic."
9
9
  readme = "README.md"
10
10
  license = { text = "Apache-2.0" }
@@ -69,3 +69,21 @@ def test_check_pypi_existence(monkeypatch):
69
69
  def test_audit_empty_when_no_deps():
70
70
  out = sc.audit("x = 1\nprint(x)\n", "app.py")
71
71
  assert out["findings"] == [] and out["hallucinated"] == []
72
+
73
+
74
+ def test_audit_skips_workspace_local_modules(monkeypatch):
75
+ # 200 for requests, 404 for everything else (so a local module would be
76
+ # falsely flagged if it were checked).
77
+ def fake_get(url, **kw):
78
+ code = 200 if "/requests/" in url else 404
79
+ return httpx.Response(code, request=httpx.Request("GET", url))
80
+
81
+ monkeypatch.setattr(httpx, "get", fake_get)
82
+ code = "from compound_library import build\nimport receptor_analysis\nimport requests\n"
83
+ out = sc.audit(code, "scripts/workflow.py",
84
+ local_modules={"compound_library", "receptor_analysis"})
85
+ names = {f["name"] for f in out["findings"]}
86
+ assert "compound_library" not in names # local, never checked
87
+ assert "receptor_analysis" not in names
88
+ assert "requests" in names
89
+ assert out["hallucinated"] == [] # the false positive is gone
@@ -45,3 +45,38 @@ def test_call_tool_rejects_bad_arguments_json():
45
45
  def test_stop_server_not_running():
46
46
  out = json.loads(mcp_mod.mcp_stop_server("never-started"))
47
47
  assert out["note"] == "never-started was not running"
48
+
49
+
50
+ def test_mcp_search_multiword_falls_back_to_keywords(monkeypatch):
51
+ # The registry matches substrings: a phrase returns nothing, but a single
52
+ # keyword finds a server. mcp_search should split and merge automatically.
53
+ calls = []
54
+
55
+ def fake_query(query, limit):
56
+ calls.append(query)
57
+ if " " in query:
58
+ return [] # phrase matches nothing, like the real registry
59
+ if query == "pubchem":
60
+ return [{"server": {"name": "io.github.cyanheads/pubchem-mcp-server",
61
+ "description": "Search compounds.", "packages": []}}]
62
+ return []
63
+
64
+ monkeypatch.setattr(mcp_mod, "_registry_query", fake_query)
65
+ out = json.loads(mcp_mod.mcp_search("pubchem chembl bioassay"))
66
+ assert out["count"] >= 1
67
+ assert any("pubchem" in s["name"] for s in out["servers"])
68
+ assert "pubchem chembl bioassay" in calls # tried the phrase first
69
+ assert "pubchem" in calls # then fell back to the keyword
70
+
71
+
72
+ def test_mcp_search_single_word_skips_fallback(monkeypatch):
73
+ calls = []
74
+
75
+ def fake_query(query, limit):
76
+ calls.append(query)
77
+ return [{"server": {"name": "ai.waystation/postgres", "packages": []}}]
78
+
79
+ monkeypatch.setattr(mcp_mod, "_registry_query", fake_query)
80
+ out = json.loads(mcp_mod.mcp_search("postgres"))
81
+ assert out["count"] == 1
82
+ assert calls == ["postgres"] # primary hit, no fallback fan-out
@@ -80,3 +80,18 @@ def test_web_search_parses_results(monkeypatch):
80
80
 
81
81
  def test_web_search_is_non_dangerous():
82
82
  assert get_registry().get("web_search").dangerous is False
83
+
84
+
85
+ def test_web_search_surfaces_http_error_body(monkeypatch):
86
+ monkeypatch.setenv("DEEPPARALLEL_SEARCH_API_KEY", "k")
87
+ body = '{"error":{"detail":"x-subscription-token Field required"}}'
88
+ monkeypatch.setattr(httpx, "get", lambda url, **kw: _Resp(text=body, status=422))
89
+ out = json.loads(web_mod.web_search("anything"))
90
+ assert "HTTP 422" in out["error"]
91
+ assert "x-subscription-token" in out["error"] # the real reason, not a bare 422
92
+
93
+
94
+ def test_web_search_blank_key_treated_as_unconfigured(monkeypatch):
95
+ monkeypatch.setenv("DEEPPARALLEL_SEARCH_API_KEY", " ")
96
+ out = json.loads(web_mod.web_search("anything"))
97
+ assert "DEEPPARALLEL_SEARCH_API_KEY" in out["error"]
@@ -1,7 +0,0 @@
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.
File without changes
File without changes