annator / src /core /command_router.py
techprotrade's picture
Add src directory
734b5b4 verified
Raw
History Blame Contribute Delete
11.9 kB
from __future__ import annotations
from dataclasses import dataclass
from pathlib import Path
from typing import Any
from scripts.orchestrator import PDFEditorOrchestrator
@dataclass(frozen=True)
class TeamMember:
id: str
name: str
title: str
agent_key: str
icon: str
color: str
system_prompt: str
TEAM_MEMBERS: tuple[TeamMember, ...] = (
TeamMember(
id="lead",
name="Mike",
title="Command Lead",
agent_key="layout_analysis",
icon="🧭",
color="#5b7cfa",
system_prompt=(
"You are a command lead for a PDF AI team. Break the user's goal into a "
"clear execution direction for the rest of the team. Be concise."
),
),
TeamMember(
id="text",
name="Iris",
title="Text Extraction Specialist",
agent_key="text_extraction",
icon="📝",
color="#2ec9b7",
system_prompt=(
"You decide how text should be extracted from PDFs. Focus on OCR, scanned "
"documents, text quality, and extraction risks."
),
),
TeamMember(
id="tables",
name="David",
title="Table Analyst",
agent_key="table_detection",
icon="📊",
color="#f59e0b",
system_prompt=(
"You analyze structured content in PDFs. Focus on tables, statements, rows, "
"columns, and downstream data extraction needs."
),
),
TeamMember(
id="image",
name="Alex",
title="Image Quality Engineer",
agent_key="image_processing",
icon="🖼️",
color="#a78bfa",
system_prompt=(
"You assess PDF page image quality. Focus on scan quality, blur, contrast, "
"and whether OCR or cleanup is needed first."
),
),
TeamMember(
id="format",
name="Emma",
title="Formatting Editor",
agent_key="format_correction",
icon="✏️",
color="#3ecf8e",
system_prompt=(
"You refine extracted PDF content. Focus on edits, formatting corrections, "
"text insertion needs, cleanup, and output clarity."
),
),
TeamMember(
id="language",
name="Sarah",
title="Language Reviewer",
agent_key="language_detection",
icon="🌐",
color="#ec4899",
system_prompt=(
"You detect PDF language and review multilingual requirements. Focus on OCR "
"language choice and language-sensitive processing."
),
),
TeamMember(
id="quality",
name="Bob",
title="Quality Validator",
agent_key="content_validation",
icon="✅",
color="#f87171",
system_prompt=(
"You validate whether the PDF workflow will produce reliable results. Focus "
"on risks, verification, and failure prevention."
),
),
TeamMember(
id="export",
name="Nora",
title="Export Coordinator",
agent_key="export_rendering",
icon="📦",
color="#06b6d4",
system_prompt=(
"You prepare the final delivery plan for a PDF workflow. Focus on output, "
"artifacts, export expectations, and handoff."
),
),
)
class CommandRouter:
"""Atoms-like AI team planner for PDF workflows."""
def __init__(self, project_root: Path | None = None, provider: str = "lmstudio"):
self.project_root = Path(project_root) if project_root else Path.cwd()
self.orchestrator = PDFEditorOrchestrator(provider=provider)
def describe_team(self) -> list[dict[str, Any]]:
mapping = self.orchestrator.describe_agent_models()
members = []
for member in TEAM_MEMBERS:
llm = mapping.get(member.agent_key, {})
members.append(
{
"id": member.id,
"name": member.name,
"title": member.title,
"agent_key": member.agent_key,
"icon": member.icon,
"color": member.color,
"requested_provider": llm.get("requested_provider", ""),
"provider": llm.get("provider", ""),
"model": llm.get("model", ""),
"base_url": llm.get("base_url", ""),
}
)
return members
def plan_command(self, payload: dict[str, Any]) -> dict[str, Any]:
brief = (payload.get("brief") or "").strip()
if not brief:
return {"success": False, "error": "Command brief is required."}
selected_file = str(payload.get("input_pdf") or "").strip()
output_dir = str(payload.get("output_dir") or "").strip()
recommendation = self._build_recommendation(brief)
team = self.describe_team()
insights = [self._member_plan(member, brief, selected_file) for member in TEAM_MEMBERS]
return {
"success": True,
"brief": brief,
"input_pdf": selected_file,
"output_dir": output_dir,
"team": team,
"insights": insights,
"recommendation": recommendation,
"available_pipelines": self.orchestrator.list_available_pipelines(),
}
def _member_plan(
self, member: TeamMember, brief: str, selected_file: str
) -> dict[str, Any]:
llm = self.orchestrator.agent_llms[member.agent_key]
prompt = (
f"User command:\n{brief}\n\n"
f"Input PDF: {selected_file or 'not specified'}\n\n"
"Respond in exactly 3 short bullet points:\n"
"- Focus: your main responsibility\n"
"- Action: what you would do next\n"
"- Handoff: what the next role should receive"
)
raw = llm.complete(
prompt,
system=member.system_prompt,
max_tokens=220,
temperature=0.4,
)
focus, action, handoff = self._parse_member_response(raw, member)
return {
"member_id": member.id,
"name": member.name,
"title": member.title,
"agent_key": member.agent_key,
"focus": focus,
"action": action,
"handoff": handoff,
"raw": raw,
}
def _parse_member_response(
self, raw: str, member: TeamMember
) -> tuple[str, str, str]:
if not raw or raw.startswith("Error:"):
return self._fallback_member_response(member)
lines = []
for line in raw.splitlines():
cleaned = line.strip().lstrip("-").strip()
if cleaned:
lines.append(cleaned)
if len(lines) < 3:
return self._fallback_member_response(member)
return lines[0], lines[1], lines[2]
def _fallback_member_response(self, member: TeamMember) -> tuple[str, str, str]:
fallback = {
"layout_analysis": (
"Define the overall PDF processing direction.",
"Decide whether the command needs OCR, editing, annotation, or validation first.",
"Pass a prioritized flow to the rest of the team.",
),
"text_extraction": (
"Check whether text must be extracted from scanned pages.",
"Recommend OCR when the command depends on searchable text.",
"Pass extracted-text requirements to editing and validation.",
),
"table_detection": (
"Check whether the command targets statement or table structure.",
"Highlight whether row/column preservation matters.",
"Pass structured-data risks to the editor and validator.",
),
"image_processing": (
"Check scan quality and OCR readiness.",
"Recommend cleanup when blur, noise, or low contrast will block OCR.",
"Pass image-quality constraints to OCR and validation.",
),
"format_correction": (
"Plan text cleanup and edit operations.",
"Translate the user command into concrete text or region edits.",
"Pass cleaned content requirements to export.",
),
"language_detection": (
"Choose the right language handling path.",
"Recommend OCR language and multilingual handling.",
"Pass language settings to OCR and quality review.",
),
"content_validation": (
"Define what must be checked before delivery.",
"Flag accuracy, truncation, and corruption risks.",
"Pass final QA criteria to export.",
),
"export_rendering": (
"Decide final output expectations.",
"Prepare final PDF, manifest, and delivery artifacts.",
"Return the deliverables to the user.",
),
}
return fallback.get(
member.agent_key,
(
"Own this part of the command.",
"Produce the next useful step.",
"Hand it to the next role.",
),
)
def _build_recommendation(self, brief: str) -> dict[str, Any]:
text = brief.lower()
wants_ocr = any(
token in text
for token in ["ocr", "scan", "scanned", "searchable", "extract text", "text layer"]
)
wants_edit = any(
token in text
for token in [
"edit",
"replace",
"insert",
"delete page",
"reorder",
"rotate",
"fix text",
"cleanup",
]
)
wants_annotation = any(
token in text
for token in ["highlight", "comment", "annotate", "redact", "mark", "rectangle"]
)
recommended_agents = ["inspector"]
if wants_ocr:
recommended_agents.append("ocr")
if wants_edit:
recommended_agents.append("editor")
if wants_annotation:
recommended_agents.append("annotator")
recommended_agents.extend(["validator", "exporter"])
deduped_agents = []
for agent in recommended_agents:
if agent not in deduped_agents:
deduped_agents.append(agent)
if wants_edit and wants_annotation:
pipeline_name = "full_document"
elif wants_edit:
pipeline_name = "text_edit"
elif wants_annotation:
pipeline_name = "annotation_review"
elif wants_ocr:
pipeline_name = "ocr_preflight"
else:
pipeline_name = "full_document"
reasons = []
if wants_ocr:
reasons.append("Command viitab OCR-ile või skännitud PDF-ile.")
if wants_edit:
reasons.append("Command sisaldab muutmist või sisu parandamist.")
if wants_annotation:
reasons.append("Command sisaldab märkimist, kommentaare või redaktsiooni.")
if not reasons:
reasons.append("Vaikimisi soovitan täielikku ülevaate + valideerimise töövoogu.")
return {
"pipeline_name": pipeline_name,
"recommended_agents": deduped_agents,
"reasons": reasons,
}