Prepare dependency generations before selecting them. Keep shipped tool bytes separate from writable additions, and store facts beside their entries. Validate proposed plugin sets before config publication. Restore the previous config if the facts write fails. Consolidate duplicate updater, backup, setup, and voice helpers. Repair launcher selection, dependency consumers, download ownership, update feeds, and native Windows process and file handling. Verification: 206 changed/prior-failing Python files reported 4630 passed, one failed, and 330 skipped. Fix the remaining Hindsight fixture boundary. The final targeted rerun reported 234 passed and two skipped. The store review regression batch reported 83 passed and one skipped. Desktop TypeScript checks, 56 selected Electron tests, 24 release tests, and the removed-import/compatibility guards passed. This is an integration checkpoint, not full audit acceptance. The complete Python suite has not run on this fixed tree. Crash-atomic plugin publication, generation cleanup, receipt correlation, and packaged lifecycle acceptance remain open in docs/pm-audit-status.md.
123 lines
5.1 KiB
Python
123 lines
5.1 KiB
Python
"""Parallel.ai web search (sync ``Parallel`` SDK) + async extract (``AsyncParallel``).
|
|
|
|
Env: ``PARALLEL_API_KEY`` (https://parallel.ai), optional
|
|
``PARALLEL_SEARCH_MODE`` = agentic (default) | fast | one-shot.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import asyncio
|
|
import logging
|
|
import os
|
|
from typing import Any, Dict, List
|
|
|
|
from plugins.web._common import (
|
|
SEARCH_LIMIT_CAP, BaseWebSearchProvider, cached_sdk_client, document, keyless_extract, keyless_search,
|
|
keyless_variant_schema, page_error, provider_env, run_extract_async, run_search, search_ok, use_keyless, web_hit,
|
|
)
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
_MISSING_KEY = "PARALLEL_API_KEY environment variable not set. Get your API key at https://parallel.ai"
|
|
|
|
|
|
def _client(slot: str, cls_name: str) -> Any:
|
|
def _factory(api_key: str) -> Any:
|
|
import parallel # deliberately lazy
|
|
return getattr(parallel, cls_name)(api_key=api_key)
|
|
|
|
# Mirrors the lazy-deps pattern used by the legacy implementation.
|
|
# Swallows benign ImportError from the pm helper itself; if the
|
|
# SDK is genuinely missing the subsequent ``from parallel import ...``
|
|
# raises ImportError that the caller can handle.
|
|
try:
|
|
from pm import ensure_import as _lazy_ensure
|
|
|
|
_lazy_ensure("parallel-web")
|
|
except ImportError:
|
|
pass
|
|
except Exception as exc: # noqa: BLE001 — surface install hint as ImportError
|
|
raise ImportError(str(exc))
|
|
return cached_sdk_client(slot, "PARALLEL_API_KEY", _MISSING_KEY, "parallel-web", _factory)
|
|
|
|
|
|
def _get_sync_client() -> Any:
|
|
return _client("_parallel_client", "Parallel")
|
|
|
|
|
|
def _get_async_client() -> Any:
|
|
return _client("_async_parallel_client", "AsyncParallel")
|
|
|
|
|
|
def _resolve_search_mode() -> str:
|
|
mode = os.getenv("PARALLEL_SEARCH_MODE", "agentic").lower().strip()
|
|
return mode if mode in {"fast", "one-shot", "agentic"} else "agentic"
|
|
|
|
|
|
class ParallelWebSearchProvider(BaseWebSearchProvider):
|
|
"""Parallel.ai search + async extract provider."""
|
|
|
|
NAME = "parallel"
|
|
DISPLAY_NAME = "Parallel"
|
|
KEY_ENV = "PARALLEL_API_KEY"
|
|
EXTRACT = True
|
|
KEYLESS = True
|
|
|
|
def search(self, query: str, limit: int = 5) -> Dict[str, Any]:
|
|
def _body() -> Dict[str, Any]:
|
|
if use_keyless("parallel", provider_env("PARALLEL_API_KEY")):
|
|
return keyless_search("Parallel", "parallel", query, limit, logger)
|
|
mode = _resolve_search_mode()
|
|
logger.info("Parallel search: '%s' (mode=%s, limit=%d)", query, mode, limit)
|
|
response = _get_sync_client().beta.search(search_queries=[query], objective=query, mode=mode, max_results=min(limit, SEARCH_LIMIT_CAP))
|
|
return search_ok([
|
|
web_hit(r.url or "", r.title or "", " ".join(r.excerpts or []), i + 1)
|
|
for i, r in enumerate(response.results or [])
|
|
])
|
|
|
|
return run_search("Parallel", logger, _body, sdk=True)
|
|
|
|
async def extract(self, urls: List[str], **kwargs: Any) -> List[Dict[str, Any]]:
|
|
async def _body() -> List[Dict[str, Any]]:
|
|
if use_keyless("parallel", provider_env("PARALLEL_API_KEY")):
|
|
# Keyless ring is blocking HTTP — hop off the event loop.
|
|
return await asyncio.to_thread(keyless_extract, "Parallel", "parallel", urls, logger)
|
|
logger.info("Parallel extract: %d URL(s)", len(urls))
|
|
response = await _get_async_client().beta.extract(urls=urls, full_content=True)
|
|
results = [document(r.url or "", r.title or "", r.full_content or "\n\n".join(r.excerpts or [])) for r in response.results or []]
|
|
return results + [
|
|
{**page_error(e.url or "", e.content or e.error_type or "extraction failed"), "metadata": {"sourceURL": e.url or ""}}
|
|
for e in response.errors or []
|
|
]
|
|
|
|
return await run_extract_async("Parallel", logger, urls, _body, sdk=True)
|
|
|
|
def get_setup_schema(self) -> Dict[str, Any]:
|
|
return keyless_variant_schema(
|
|
"Parallel", "PARALLEL_API_KEY", "https://parallel.ai",
|
|
free_tag="Objective-tuned search + page extraction on Parallel's anonymous free tier. Rate-limited under burst load.",
|
|
paid_tag="Objective-tuned search + parallel page extraction via the Parallel SDK. Unthrottled, guaranteed service.",
|
|
)
|
|
|
|
|
|
# ---- BEGIN PLUGIN-COMPAT (revert-scheduled; see COMPAT_MANIFEST.md) ----
|
|
# Names external plugins imported from this module before the Sep 2026 decomposition.
|
|
# Internal code MUST NOT use these (scripts/check_compat_pointers.py fails CI if it does).
|
|
# The whole block is removed by reverting the commit that added it.
|
|
|
|
|
|
_PLUGIN_COMPAT_LAZY = {
|
|
'WebSearchProvider': ('agent.web_search_provider', 'WebSearchProvider'),
|
|
}
|
|
|
|
|
|
def __getattr__(name): # PEP 562 — lazy so no import cycles
|
|
target = _PLUGIN_COMPAT_LAZY.get(name)
|
|
if target is None:
|
|
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
|
import importlib
|
|
from hermes_cli.plugin_compat import warn_once
|
|
warn_once(__name__, name, *target)
|
|
return getattr(importlib.import_module(target[0]), target[1])
|
|
# ---- END PLUGIN-COMPAT ----
|