|
# retoor <retoor@molodetz.nl>
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import logging
|
|
import re
|
|
from typing import Callable
|
|
|
|
from devplacepy import stealth
|
|
from devplacepy.config import INTERNAL_GATEWAY_URL, INTERNAL_MODEL
|
|
|
|
from .phases import PHASE_PLANNING
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
ENHANCE_TIMEOUT_SECONDS = 90.0
|
|
ENHANCE_MAX_TOKENS = 400
|
|
MAX_SUBQUERIES = 6
|
|
|
|
PLANNER_PROMPT = (
|
|
"You plan web research. Given a research question, produce a JSON object with one "
|
|
"key 'queries': an array of 3 to 6 concise, diverse web search queries that together "
|
|
"cover the question from multiple angles. Return ONLY the JSON object, no prose."
|
|
)
|
|
|
|
|
|
def _fallback(query: str) -> list[str]:
|
|
base = query.strip()
|
|
variants = [
|
|
base,
|
|
f"{base} overview",
|
|
f"{base} latest",
|
|
f"{base} explained",
|
|
]
|
|
seen: list[str] = []
|
|
for variant in variants:
|
|
if variant and variant not in seen:
|
|
seen.append(variant)
|
|
return seen[:MAX_SUBQUERIES]
|
|
|
|
|
|
def _parse(text: str) -> list[str]:
|
|
match = re.search(r"\{.*\}", text, re.DOTALL)
|
|
if not match:
|
|
return []
|
|
try:
|
|
payload = json.loads(match.group())
|
|
except (ValueError, TypeError):
|
|
return []
|
|
queries = payload.get("queries")
|
|
if not isinstance(queries, list):
|
|
return []
|
|
cleaned = [str(item).strip() for item in queries if str(item).strip()]
|
|
return cleaned[:MAX_SUBQUERIES]
|
|
|
|
|
|
def _noop(frame: dict) -> None:
|
|
return None
|
|
|
|
|
|
async def plan_queries(
|
|
query: str, api_key: str, emit: Callable[[dict], None] = _noop
|
|
) -> list[str]:
|
|
emit(
|
|
{
|
|
"type": "substep",
|
|
"phase": PHASE_PLANNING,
|
|
"message": "Drafting diverse search angles",
|
|
}
|
|
)
|
|
payload = {
|
|
"model": INTERNAL_MODEL,
|
|
"messages": [
|
|
{"role": "system", "content": PLANNER_PROMPT},
|
|
{"role": "user", "content": query},
|
|
],
|
|
"max_tokens": ENHANCE_MAX_TOKENS,
|
|
"temperature": 0.3,
|
|
}
|
|
headers = {
|
|
"Authorization": f"Bearer {api_key}",
|
|
"Content-Type": "application/json",
|
|
"X-App-Reference": "devplace-deepsearch-v-1-0-0",
|
|
}
|
|
try:
|
|
async with stealth.stealth_async_client(timeout=ENHANCE_TIMEOUT_SECONDS) as client:
|
|
response = await client.post(
|
|
INTERNAL_GATEWAY_URL, json=payload, headers=headers
|
|
)
|
|
if response.status_code >= 400:
|
|
raise RuntimeError(f"planner gateway returned {response.status_code}")
|
|
data = response.json()
|
|
content = (
|
|
(data.get("choices") or [{}])[0].get("message", {}).get("content") or ""
|
|
)
|
|
parsed = _parse(content)
|
|
if parsed:
|
|
emit(
|
|
{
|
|
"type": "substep",
|
|
"phase": PHASE_PLANNING,
|
|
"message": f"Planned {len(parsed)} search angles",
|
|
}
|
|
)
|
|
return parsed
|
|
except Exception as exc:
|
|
logger.warning("deepsearch query planner failed, using fallback: %s", exc)
|
|
fallback = _fallback(query)
|
|
emit(
|
|
{
|
|
"type": "substep",
|
|
"phase": PHASE_PLANNING,
|
|
"message": f"Planner unavailable, using {len(fallback)} heuristic angles",
|
|
}
|
|
)
|
|
return fallback
|