can-you-assist/src/cya/orchestrator.py

269 lines
9.6 KiB
Python
Raw Normal View History

"""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 13.
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"]