"""Plan der KI je Chat (→ große Aufgaben, Plan-Modus). update_plan ersetzt die Todo-Liste des Chats; sie wirkt nur in der Plattform (write_internal). Im Planungsmodus bekommt die KI nur lesende Tools und dieses Tool, bis ein Mensch „Plan umsetzen“ klickt. Der Plan geht in jeden Prompt, in die Arbeitsnotiz und in die Beschreibung des Pull Requests. """ from typing import Literal from pydantic import Field from app.chat.models import ChatSession from app.core.errors import Invalid from app.security import untrusted from app.tools.context import ToolContext from app.tools.registry import Args, Tool, register NAME = "update_plan" MARKS = {"done": "[x]", "in_progress": "[~]", "open": "[ ]"} EXECUTE_MESSAGE = ("Bitte setze den Plan jetzt um. Arbeite die offenen Punkte der Reihe nach ab und halte den " "Plan mit update_plan aktuell.") class Step(Args): text: str = Field(min_length=3, max_length=300, description="Konkreter Schritt, gern mit betroffenen Dateien.") status: Literal["open", "in_progress", "done"] = Field("open", description="open, in_progress oder done.") class UpdatePlanArgs(Args): steps: list[Step] = Field(min_length=1, max_length=30, description="Der vollständige Plan in Reihenfolge; ersetzt den bisherigen.") def update_plan(context: ToolContext, args: UpdatePlanArgs) -> str: if context.db is None or context.chat_session_id is None: raise Invalid("Ein Plan ist nur in einem Chat möglich.") steps = [{"text": " ".join(s.text.split()), "status": s.status} for s in args.steps] with context.db.session() as session: session.get(ChatSession, context.chat_session_id).plan = steps counts = {k: sum(s["status"] == k for s in steps) for k in MARKS} return (f"Plan gespeichert: {counts['done']} erledigt, {counts['in_progress']} in Arbeit, {counts['open']} offen. " "Er ist im Chat sichtbar.") register(Tool( NAME, "git", "Plan für eine größere Aufgabe anlegen oder aktualisieren: eine Liste konkreter Schritte mit Status. " "Immer den vollständigen Plan senden; erledigte Schritte als done markieren, den aktuellen als in_progress.", UpdatePlanArgs, "write_internal", run=update_plan, untrusted=False)) def as_text(plan: list[dict]) -> str: return "\n".join(f"- {MARKS.get(s.get('status'), '[ ]')} {s.get('text', '')}" for s in plan) def prompt_text(plan: list[dict]) -> str: """Plan im Prompt: vom Modell verfasst, also eingefasst und entschärft.""" return f"\n{untrusted.neutralize(as_text(plan))}\n" def prompt_note(plan: list[dict], plan_mode: bool, offered: bool) -> str: if not offered: return "" if plan_mode: note = ("\n\nPLANUNGSMODUS: Ändere nichts. Untersuche die Aufgabe mit den lesenden Tools und lege dann mit " "update_plan einen Plan aus konkreten Schritten an (betroffene Dateien, Reihenfolge, offene Fragen). " "Fasse den Plan in der Antwort kurz zusammen. Umgesetzt wird erst, wenn der Nutzer „Plan umsetzen“ " "klickt; bis dahin kannst du den Plan auf Rückfragen hin anpassen.") else: note = ("\n\nBei größeren Aufgaben (mehrere Dateien oder Schritte) lege zuerst mit update_plan einen Plan an " "und halte ihn aktuell.") if plan: note += ("\nAktueller Plan dieses Chats ([x] erledigt, [~] in Arbeit, [ ] offen); setze beim nächsten " f"offenen Punkt an:\n{prompt_text(plan)}") return note