"""Assistance orchestrator. The piece that turns raw user intent + collected context into a well-formed request for the LLM adapter, then turns the adapter response into the final terminal output the user sees. Key responsibilities: - Coordinate context collector, memory (Profile 0 via recall with activation_context), rule-based risk classifier, and LLMAdapter. - Wire memory influence into every assistance cycle and `--explain-context`. - Own the `cya retrospect` flow that feeds higher-order memory back into the system. - Keep the CLI surface thin. **Memory note (Profile 0):** This module is the primary consumer of the stable memory ports defined in src/cya/memory/__init__.py. All calls use activation_context derived from the current working directory + git root. See MemoryVision.md "Profile 0 Baseline" and CYA-WP-0005 for the evolution path to Profiles 1–3. See workplan CYA-WP-0001 (core) + CYA-WP-0003 (memory wiring + retrospect) + CYA-WP-0005 (profile formalization). """ from __future__ import annotations from pathlib import Path from rich.console import Console from rich.panel import Panel from cya.context.collector import collect, render_explanation from cya.memory import ( recall_preferences, remember_retrospection_outcome, KIND_RETROSPECTION, KIND_INTERACTION_GOAL, ) 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]") # T03 (memory wiring): consult after context (so safety can see it in future T04 0002), # before risk/LLM. Real T02 prefs now available; graceful. # T03 (0003): pass activation_context so directory/project-bound memory is automatically # activated based on cwd + git root. memory = {} try: act_ctx = {"cwd": str(Path.cwd())} if envelope and getattr(envelope, "git", None): git_info = envelope.git or {} if git_info.get("workdir"): act_ctx["git_root"] = git_info["workdir"] memory = recall_preferences(".", activation_context=act_ctx) except Exception: memory = {"error": "recall failed (graceful degradation)"} if explain_context and memory.get("items"): try: prov = memory.get("provenance", [{}])[0] # Show a couple of activated items for transparency (T03 0003) sample = ", ".join(i.get("key", "?") for i in memory.get("items", [])[:3]) act_note = "" if prov.get("activation_context"): act_note = f" | ctx: {prov['activation_context']}" console.print( Panel( f"Phase: {memory.get('phase')} | {len(memory.get('items', []))} items | {prov.get('source', 'local')}{act_note}\n" f"Sample activated: {sample}", title="Memory Activated (T03)", border_style="blue", padding=(0, 1), ) ) except Exception: pass # 2. Risk classification + mandatory confirmation (T03 safety; T04 memory signals) assessment = classify(user_request, envelope, memory=memory) 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() ctx = (envelope.to_dict() if envelope else {}) or {} ctx["memory"] = memory # T03: memory now in context passed to LLM (for personalization + explain) llm_request = AssistanceRequest( user_request=user_request, context=ctx, ) llm_response = adapter.complete(llm_request) # 4. Render final user-facing artifact (T06 responsibility; T03 memory surface) mem_line = "" if memory.get("items"): mem_line = f"\n[dim]Memory activated: {len(memory.get('items', []))} items (phase {memory.get('phase')})[/dim]" console.print( Panel( f"[bold]Suggestion:[/bold]\n{llm_response.suggestion}\n\n" f"[dim]{llm_response.explanation}\n" f"Rationale: {llm_response.rationale}{mem_line}[/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)." ) def run_retrospection(scope: str = ".", limit: int = 8) -> None: """Guided retrospection session (T04 of CYA-WP-0003). Helps the user review recent memory usage in the given scope, reflect, and record new interaction goals or preferences. These are stored using the retrospection-aware memory helper. """ console.print( Panel( "[bold cyan]Retrospection Session[/bold cyan]\n\n" f"Scope: [green]{scope}[/green]\n" "We will look at recent memory items and help you reflect.\n" "Your answers will be stored as durable retrospection memory.", title="cya retrospect", border_style="magenta", padding=(1, 2), ) ) # Recall recent items, with bias toward retrospection kinds if present try: recent = recall_preferences( scope, limit=limit, kinds=[KIND_RETROSPECTION, KIND_INTERACTION_GOAL, "preference"], ) items = recent.get("items", []) except Exception as e: console.print(f"[red]Could not load memory: {e}[/red]") return if not items: console.print( "[yellow]No memory items found in this scope yet.[/yellow]\n" "You can create some with normal usage or explicit remembers." ) return console.print( Panel( "\n".join( f"• [bold]{item.get('key')}[/bold]: {item.get('value')}" for item in items[:5] ), title=f"Recent Memory in {scope} (showing up to 5)", border_style="blue", ) ) # Simple guided reflection console.print("\n[bold]Reflection time[/bold]") what_worked = typer.prompt( "What worked well in recent assistance? (short answer or 'skip')", default="", show_default=False, ) if what_worked and what_worked.lower() not in ("skip", "s", ""): remember_retrospection_outcome( "what_worked", what_worked, scope=scope ) console.print("[green]Recorded.[/green]") what_to_change = typer.prompt( "What should change in future interactions? (e.g. 'be more concise', 'always show alternatives')", default="", show_default=False, ) if what_to_change and what_to_change.lower() not in ("skip", "s", ""): remember_retrospection_outcome( "interaction_goal", what_to_change, scope=scope ) console.print("[green]Recorded as interaction goal.[/green]") safety_note = typer.prompt( "Any standing safety or preference rules for this project? (optional)", default="", show_default=False, ) if safety_note and safety_note.lower() not in ("skip", "s", ""): remember_retrospection_outcome( "safety_preference", safety_note, scope=scope ) console.print("[green]Recorded as safety preference.[/green]") console.print( Panel( "Thank you. Your reflections have been stored as retrospection memory.\n" "They will be preferentially activated in future sessions in this scope.\n\n" "You can review them anytime with:\n" f" [bold]cya --explain-context \"...\"[/bold] (in this directory)\n" f" or inspect the JSON files in [cyan]~/.config/cya/memory/[/cyan]", title="Retrospection Complete", border_style="green", padding=(1, 2), ) ) __all__ = ["handle_request", "run_retrospection"]