"""Assistance orchestrator (T06). The piece that turns raw user intent + collected context into a well-formed request for the LLM adapter (T04), then turns the adapter response into the final terminal output the user sees. Responsibilities in this slice: - Own the end-to-end happy path after Typer argument parsing. - Coordinate context collector (T02), risk classifier (T03), and LLMAdapter (T04). - Keep the CLI surface (main.py) thin — it should only do argument parsing, help/version, and delegation to this orchestrator. - Be testable in isolation with the FakeLLMAdapter (critical for T07). This module is the natural home for future prompt framing, context packing with token awareness, safety charter injection, and response post-processing. See workplan CYA-WP-0001-T06. """ from __future__ import annotations from rich.console import Console from rich.panel import Panel from cya.context.collector import collect, render_explanation from cya.safety.risk import classify, get_user_confirmation from cya.llm.adapter import AssistanceRequest, FakeLLMAdapter console = Console() def handle_request( user_request: str, *, explain_context: bool = False, dry_run: bool = False, ) -> None: """Primary orchestrator entry point. This is what the CLI (and future tests / other front-ends) should call. It coordinates the full current flow: context → safety (with mandatory confirmation) → LLMAdapter → render """ # 1. Context (always cheap; needed for safety "affected" and for the adapter) try: envelope = collect(".") except Exception: envelope = None if explain_context and envelope: try: explanation = render_explanation(envelope) console.print( Panel( explanation, title="Context Envelope (T02)", border_style="green", padding=(1, 1), ) ) except Exception as exc: console.print(f"[red]Context explanation error: {exc}[/red]") # 2. Risk classification + mandatory confirmation (T03) assessment = classify(user_request, envelope) if assessment.requires_confirmation: from rich.table import Table table = Table( title=f"Risk Assessment — {assessment.level.value.upper()}", show_header=False, border_style="red", ) table.add_row("Rationale", assessment.rationale) if assessment.preview: table.add_row("Preview", assessment.preview) if assessment.affected_summary: table.add_row("Would affect", assessment.affected_summary) table.add_row("Rules", ", ".join(assessment.rules_triggered[:3])) console.print(table) if not get_user_confirmation(assessment): console.print("[yellow]Action cancelled by user. No changes made.[/yellow]") return if dry_run: console.print("[green]--dry-run acknowledged.[/green] No side-effects.") return # 3. Call through the single LLMAdapter boundary (T04) adapter = FakeLLMAdapter() llm_request = AssistanceRequest( user_request=user_request, context=envelope.to_dict() if envelope else None, ) llm_response = adapter.complete(llm_request) # 4. Render final user-facing artifact (T06 responsibility) console.print( Panel( f"[bold]Suggestion:[/bold]\n{llm_response.suggestion}\n\n" f"[dim]{llm_response.explanation}\n" f"Rationale: {llm_response.rationale}[/dim]", title="LLM Response (via T04 seam)", border_style="magenta", padding=(1, 1), ) ) console.print( "[green]✓[/green] Request processed by orchestrator (T02+T03+T04 coordinated by T06)." ) __all__ = ["handle_request"]