generated from coulomb/repo-seed
feat(memory): complete CYA-WP-0006 Profile 1 production hardening
Add guided reflection capture with preview, cya memory reflections CLI, near-duplicate compaction, budget-capped surfacing, and expanded tests. Profile 1 is now documented as production-ready in README and MemoryVision.
This commit is contained in:
@@ -25,6 +25,7 @@ from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
import typer
|
||||
from rich.console import Console
|
||||
from rich.panel import Panel
|
||||
|
||||
@@ -32,11 +33,20 @@ from cya.context.collector import collect, render_explanation
|
||||
from cya.memory import (
|
||||
recall_preferences,
|
||||
remember_retrospection_outcome,
|
||||
remember_reflection,
|
||||
KIND_RETROSPECTION,
|
||||
KIND_INTERACTION_GOAL,
|
||||
KIND_REFLECTION,
|
||||
)
|
||||
from cya.memory.reflections import (
|
||||
REFLECTION_CAPTURE_PROMPTS,
|
||||
collect_lessons_from_answers,
|
||||
compact_reflections,
|
||||
find_duplicate_reflection_groups,
|
||||
format_reflection_surfacing,
|
||||
preview_lessons,
|
||||
save_reflection_lessons,
|
||||
session_provenance,
|
||||
)
|
||||
from cya.safety.risk import classify, get_user_confirmation
|
||||
from cya.llm.adapter import AssistanceRequest, FakeLLMAdapter
|
||||
|
||||
@@ -98,15 +108,21 @@ def handle_request(
|
||||
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']}"
|
||||
body = (
|
||||
f"Phase: {memory.get('phase')} | {len(memory.get('items', []))} items | "
|
||||
f"{prov.get('source', 'local')}{act_note}\n"
|
||||
f"Sample activated: {sample}"
|
||||
)
|
||||
refl_line = format_reflection_surfacing(memory, for_explain=True)
|
||||
if refl_line:
|
||||
body += f"\n\n[cyan]Reflections:[/cyan] {refl_line}"
|
||||
console.print(
|
||||
Panel(
|
||||
f"Phase: {memory.get('phase')} | {len(memory.get('items', []))} items | {prov.get('source', 'local')}{act_note}\n"
|
||||
f"Sample activated: {sample}",
|
||||
body,
|
||||
title="Memory Activated (T03)",
|
||||
border_style="blue",
|
||||
padding=(0, 1),
|
||||
@@ -156,11 +172,9 @@ def handle_request(
|
||||
mem_line = ""
|
||||
if memory.get("items"):
|
||||
mem_line = f"\n[dim]Memory activated: {len(memory.get('items', []))} items (phase {memory.get('phase')})[/dim]"
|
||||
# Minimal Profile 1 surface (T05 spike)
|
||||
reflections = [i for i in memory.get("items", []) if i.get("kind") == KIND_REFLECTION]
|
||||
if reflections:
|
||||
refl_text = "; ".join(str(i.get("value", ""))[:60] for i in reflections[:2])
|
||||
mem_line += f"\n[cyan]Verbal reflections activated: {len(reflections)} — {refl_text}[/cyan]"
|
||||
refl_line = format_reflection_surfacing(memory, for_explain=False)
|
||||
if refl_line:
|
||||
mem_line += f"\n[cyan]{refl_line}[/cyan]"
|
||||
|
||||
console.print(
|
||||
Panel(
|
||||
@@ -263,29 +277,15 @@ def run_retrospection(scope: str = ".", limit: int = 8) -> None:
|
||||
)
|
||||
console.print("[green]Recorded as safety preference.[/green]")
|
||||
|
||||
# Minimal Profile 1 spike (T05): optional verbal reflection / lesson capture
|
||||
capture_lesson = typer.prompt(
|
||||
"Capture any verbal lessons or reflections from this session? (y/n or short text)",
|
||||
default="n",
|
||||
show_default=False,
|
||||
)
|
||||
if capture_lesson and capture_lesson.lower() not in ("n", "no", "skip", "s", ""):
|
||||
lesson_text = capture_lesson if len(capture_lesson) > 3 else typer.prompt(
|
||||
"What is the key lesson? (1-2 sentences)",
|
||||
default="",
|
||||
show_default=False,
|
||||
)
|
||||
if lesson_text:
|
||||
remember_reflection(
|
||||
"verbal_lesson", lesson_text, scope=scope
|
||||
)
|
||||
console.print("[green]Recorded as verbal reflection (Profile 1).[/green]")
|
||||
_capture_reflection_lessons(scope)
|
||||
_offer_reflection_compaction(scope)
|
||||
|
||||
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 memory reflections --scope {scope}[/bold]\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",
|
||||
@@ -295,4 +295,91 @@ def run_retrospection(scope: str = ".", limit: int = 8) -> None:
|
||||
)
|
||||
|
||||
|
||||
def _capture_reflection_lessons(scope: str) -> None:
|
||||
"""Profile 1: guided verbal lesson capture with preview and confirmation."""
|
||||
want = typer.prompt(
|
||||
"Capture 1–3 verbal lessons from this session? (y/n)",
|
||||
default="n",
|
||||
show_default=False,
|
||||
)
|
||||
if not want or want.strip().lower() in ("n", "no", "skip", "s", ""):
|
||||
return
|
||||
|
||||
answers: dict[str, str] = {}
|
||||
for prompt_key, label in REFLECTION_CAPTURE_PROMPTS:
|
||||
answers[prompt_key] = typer.prompt(label, default="", show_default=False)
|
||||
|
||||
lessons = collect_lessons_from_answers(answers)
|
||||
if not lessons:
|
||||
console.print("[yellow]No lessons captured — nothing stored.[/yellow]")
|
||||
return
|
||||
|
||||
console.print(
|
||||
Panel(
|
||||
preview_lessons(lessons),
|
||||
title="Preview — verbal lessons to save",
|
||||
border_style="cyan",
|
||||
)
|
||||
)
|
||||
confirm = typer.prompt("Save these lessons? (y/n)", default="n", show_default=False)
|
||||
if confirm.strip().lower() not in ("y", "yes"):
|
||||
console.print("[yellow]Lessons discarded — nothing stored.[/yellow]")
|
||||
return
|
||||
|
||||
count = save_reflection_lessons(
|
||||
lessons,
|
||||
scope,
|
||||
provenance=session_provenance(scope),
|
||||
)
|
||||
console.print(f"[green]Saved {count} verbal reflection(s) (Profile 1).[/green]")
|
||||
|
||||
|
||||
def _offer_reflection_compaction(scope: str) -> None:
|
||||
"""Offer explicit merge of near-duplicate reflections in this scope."""
|
||||
groups = find_duplicate_reflection_groups(scope)
|
||||
if not groups:
|
||||
return
|
||||
|
||||
console.print(
|
||||
Panel(
|
||||
f"Found {len(groups)} group(s) of similar reflections in scope [green]{scope}[/green].\n"
|
||||
"Compaction is opt-in — nothing is deleted without your confirmation.",
|
||||
title="Reflection compaction available",
|
||||
border_style="yellow",
|
||||
)
|
||||
)
|
||||
do_compact = typer.prompt("Review and compact duplicates? (y/n)", default="n", show_default=False)
|
||||
if do_compact.strip().lower() not in ("y", "yes"):
|
||||
return
|
||||
|
||||
for group in groups:
|
||||
console.print("\n[bold]Similar reflections:[/bold]")
|
||||
for item in group:
|
||||
console.print(f" • {item.get('key')}: {item.get('value')}")
|
||||
|
||||
keep = typer.prompt(
|
||||
"Key to keep (or 'skip' this group)",
|
||||
default=group[0].get("key", ""),
|
||||
show_default=True,
|
||||
)
|
||||
if not keep or keep.strip().lower() in ("skip", "s"):
|
||||
continue
|
||||
|
||||
remove_keys = [i.get("key") for i in group if i.get("key") != keep]
|
||||
merge = typer.prompt(
|
||||
"Merged text (Enter to keep existing value of kept key)",
|
||||
default="",
|
||||
show_default=False,
|
||||
)
|
||||
result = compact_reflections(
|
||||
scope,
|
||||
keep_key=keep,
|
||||
remove_keys=[k for k in remove_keys if k],
|
||||
merged_value=merge.strip() or None,
|
||||
)
|
||||
console.print(
|
||||
f"[green]Compacted: kept {result['kept']}, removed {len(result['removed'])}[/green]"
|
||||
)
|
||||
|
||||
|
||||
__all__ = ["handle_request", "run_retrospection"]
|
||||
|
||||
Reference in New Issue
Block a user