Files
rune/scripts/research_router.py
T
Nick b6d1a08750 feat(research): R1-R5 — event-driven coordinator, stage-scoped methodology, OUTWARD router, structured artifact gate, MORE_RESEARCH immediate re-dispatch
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.
2026-09-08 12:31:53 +01:00

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))