R1: disposition routing wired into the research stage workflow — PROCEED lets research-clear fire, MORE_RESEARCH dispatches round+1 immediately (no sweep wait). The workflow completion IS the event. R2: 8 stage-scoped prompt templates (DEFINE through QUALIFY) — each gets a narrow prompt, frozen prior artifacts, explicit MUST NOT constraints. R3: research_router.py — classifies questions (academic/software/web), routes to OpenAlex/SemanticScholar/arXiv/GitHub/DuckDuckGo, doctrine queries include limitations/alternatives/failures. R4: structured artifact validated in the stage workflow before commit — receipt + URLs + code paths + disposition all checked. R5: MORE_RESEARCH dispatches round+1 from the workflow (no sweep wait); blocking unknowns passed to the next round. R0 red reproduction recorded: foreground slept 300s×N while child mimo worker was productive (18.6% CPU, 547MB, 8min CPU). Anti-pattern confirmed.
138 lines
4.0 KiB
Python
138 lines
4.0 KiB
Python
#!/usr/bin/env python3
|
|
"""research_router.py — provider-neutral OUTWARD source router (R3).
|
|
|
|
Classifies the research question type and produces a search strategy
|
|
with the appropriate source providers. Does NOT fetch — produces
|
|
queries for the mimo agent to execute.
|
|
|
|
Source classes:
|
|
academic: OpenAlex, Semantic Scholar, arXiv, Crossref
|
|
software: official docs, GitHub, release notes, benchmarks
|
|
web: SearXNG (self-hostable metasearch), or equivalent
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import re
|
|
import sys
|
|
|
|
QUESTION_TYPES = {
|
|
"academic": {
|
|
"indicators": [
|
|
"paper",
|
|
"research",
|
|
"state of the art",
|
|
"benchmark",
|
|
"architecture",
|
|
"algorithm",
|
|
"model",
|
|
"dataset",
|
|
"performance study",
|
|
"survey",
|
|
"empirical",
|
|
],
|
|
"providers": [
|
|
{"name": "OpenAlex", "url": "https://api.openalex.org/works?search={query}&per_page=5"},
|
|
{
|
|
"name": "Semantic Scholar",
|
|
"url": "https://api.semanticscholar.org/graph/v1/paper/search?query={query}&limit=5",
|
|
},
|
|
{"name": "arXiv", "url": "http://export.arxiv.org/api/query?search_query=all:{query}&max_results=5"},
|
|
],
|
|
"fallback": [
|
|
{
|
|
"name": "DuckDuckGo",
|
|
"url": "https://duckduckgo.com/html/?q={query}+site:arxiv.org+OR+site:semanticscholar.org",
|
|
}
|
|
],
|
|
},
|
|
"software": {
|
|
"indicators": [
|
|
"library",
|
|
"framework",
|
|
"API",
|
|
"documentation",
|
|
"install",
|
|
"configuration",
|
|
"bug",
|
|
"feature",
|
|
"release",
|
|
"changelog",
|
|
"migration",
|
|
"upgrade",
|
|
"deprecation",
|
|
"how to",
|
|
"setup",
|
|
],
|
|
"providers": [
|
|
{"name": "GitHub", "url": "https://github.com/search?q={query}&type=repositories"},
|
|
{"name": "Official docs", "url": "https://duckduckgo.com/html/?q={query}+site:docs.{domain}"},
|
|
],
|
|
"fallback": [{"name": "DuckDuckGo", "url": "https://duckduckgo.com/html/?q={query}"}],
|
|
},
|
|
"web": {
|
|
"indicators": [
|
|
"best practices",
|
|
"comparison",
|
|
"vs",
|
|
"alternative",
|
|
"production",
|
|
"performance",
|
|
"optimization",
|
|
"tutorial",
|
|
],
|
|
"providers": [
|
|
{"name": "DuckDuckGo", "url": "https://duckduckgo.com/html/?q={query}"},
|
|
{"name": "GitHub issues", "url": "https://github.com/search?q={query}&type=issues"},
|
|
],
|
|
"fallback": [],
|
|
},
|
|
}
|
|
|
|
DOCTRINE_QUERIES = [
|
|
"{query} best practices",
|
|
"{query} limitations",
|
|
"{query} vs alternatives",
|
|
"{query} production failures",
|
|
"{query} benchmark comparison",
|
|
]
|
|
|
|
|
|
def classify(question: str) -> str:
|
|
q = question.lower()
|
|
scores = {}
|
|
for qtype, config in QUESTION_TYPES.items():
|
|
score = sum(1 for ind in config["indicators"] if ind in q)
|
|
if score:
|
|
scores[qtype] = score
|
|
return max(scores, key=scores.get) if scores else "web"
|
|
|
|
|
|
def route(question: str) -> dict:
|
|
qtype = classify(question)
|
|
config = QUESTION_TYPES[qtype]
|
|
queries = [q.format(query=question) for q in DOCTRINE_QUERIES[:3]]
|
|
return {
|
|
"question_type": qtype,
|
|
"providers": config["providers"],
|
|
"fallback": config.get("fallback", []),
|
|
"queries": queries,
|
|
"doctrine": "search for: supporting evidence, competing approaches, "
|
|
"limitations, failure modes, negative results, benchmarks, "
|
|
"newer techniques, prior implementations",
|
|
}
|
|
|
|
|
|
def main_with(argv: list[str]) -> int:
|
|
if len(argv) < 2:
|
|
print("usage: research_router.py <question>", file=sys.stderr)
|
|
return 2
|
|
import json
|
|
|
|
print(json.dumps(route(" ".join(argv[1:])), indent=2))
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main_with(sys.argv))
|