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, }