"""Erneut beantragte Vorschläge (→ Konzept 4.5): ohne Modell, nur Snapshot neu bauen, Freigabe abwarten, ausführen und das Ergebnis in den Chat schreiben.""" from sqlalchemy import select from app.approvals import service as approvals from app.approvals.models import ApprovalRequest from app.core.crypto import Vault from app.core.errors import Invalid from app.data_sources.connectors.git_repo import RepoCache from app.data_sources.models import DataSource from app.mcp import service as mcp from app.tasks import states from app.tasks.models import TaskRun from app.tasks.worker import RunContext, RunStopped, handler from app.tools import service as tools from app.tools.context import ToolContext from app.tools.models import DENY from app.tools.registry import ToolError from app.workspaces.models import Workspace TITLE = "**Erneut beantragte Änderung:** " @handler("proposal") def proposal(context: RunContext) -> str | None: with context.db.session() as session: run = session.get(TaskRun, context.run_id) state = dict(run.state or {}) old = session.get(ApprovalRequest, context.input["replaces"]) workspace = session.get(Workspace, context.workspace_id) sources = list(session.scalars(select(DataSource).where(DataSource.workspace_id == workspace.id, DataSource.status == "ok"))) verdict = tools.evaluate(session, workspace, old.tool, old.source_id) tool = tools.catalog(session).get(old.tool) mcp_targets = mcp.targets(session, Vault(context.settings.secret_key), context.settings, workspace.id) for source in sources: session.expunge(source) arguments, source_id, tool_name, old_id = dict(old.arguments), old.source_id, old.tool, old.id tool_context = ToolContext(settings=context.settings, vault=Vault(context.settings.secret_key), workspace_id=context.workspace_id, sources=sources, cache=RepoCache(context.settings), run_id=context.run_id, user_id=context.created_by, mcp=mcp_targets, db=context.db, chat_session_id=context.chat_session_id) if "approval_id" in state: try: output, _ = approvals.carry_out(context.db, tool_context, state["approval_id"], for_model=False) except approvals.Unclear as e: raise RunStopped(states.NEEDS_RECONCILIATION, "Ob die Änderung im Zielsystem angekommen ist, ist " "unklar. Ein Workspace-Admin muss den Abgleich starten.", detail=str(e)) from e return complete(context, TITLE + output) if tool is None or verdict.decision == DENY: return complete(context, TITLE + "nicht möglich, das Tool ist in diesem Workspace nicht mehr erlaubt.") try: # Quelle kann umbenannt worden sein: maßgeblich ist die ID aus dem alten Antrag if source_id is not None: arguments["source"] = tool_context.by_id(source_id).name built = tool.propose(tool_context, tool.validate(arguments)) except (ToolError, Invalid, ValueError) as e: return complete(context, TITLE + f"lässt sich gegen den aktuellen Stand nicht neu bauen: {e}") with context.db.session() as session: context.guard(session) approval = approvals.propose(session, run_id=context.run_id, workspace=session.get(Workspace, context.workspace_id), user_id=context.created_by, call_id=f"rerequest-{old_id}", tool=tool_name, proposal=built, verdict=verdict, hours=context.settings.approval_hours, replaces_id=old_id) session.get(TaskRun, context.run_id).state = {"approval_id": approval.id} approval_id, pending = approval.id, approval.status == approvals.PENDING session.flush() context.events.emit("approval", {"call_id": approval.call_id, "approval_id": approval_id}, session=session) if not pending: output, _ = approvals.carry_out(context.db, tool_context, approval_id, for_model=False) return complete(context, TITLE + output) return states.WAITING_APPROVAL def complete(context: RunContext, text: str) -> None: context.events.flush() with context.db.session() as session: context.guard(session) run = session.get(TaskRun, context.run_id) run.state = {} message = approvals.post_message(session, run, text) if message is not None: context.events.emit("message", {"message_id": message.id}, session=session) return None