From 77c2e37b95374f3b84838926876a524ccbe125bb Mon Sep 17 00:00:00 2001 From: mirivlad Date: Sun, 10 May 2026 23:57:50 +0800 Subject: [PATCH] Add structured response feedback --- app/api/server.py | 12 ++ app/api/static/index.html | 231 ++++++++++++++++++++++++++++-- app/runtime/runtime_controller.py | 139 ++++++++++++++---- tests/test_api_handlers.py | 20 ++- 4 files changed, 367 insertions(+), 35 deletions(-) diff --git a/app/api/server.py b/app/api/server.py index 4938dab..531ca2e 100644 --- a/app/api/server.py +++ b/app/api/server.py @@ -13,6 +13,12 @@ class CriticFeedbackRequest(BaseModel): feedback: str task_id: str | None = None session_id: str | None = None + feedback_type: str | None = None + severity: str | None = None + correction: str | None = None + remember: bool = True + retry: bool = False + assistant_answer: str | None = None correctness_override: float | None = None usefulness_override: float | None = None safety_override: float | None = None @@ -87,6 +93,12 @@ def critic_feedback(request: CriticFeedbackRequest) -> dict[str, object]: feedback=request.feedback, task_id=request.task_id, session_id=request.session_id, + feedback_type=request.feedback_type, + severity=request.severity, + correction=request.correction, + remember=request.remember, + retry=request.retry, + assistant_answer=request.assistant_answer, correctness_override=request.correctness_override, usefulness_override=request.usefulness_override, safety_override=request.safety_override, diff --git a/app/api/static/index.html b/app/api/static/index.html index b381c08..330f8b6 100644 --- a/app/api/static/index.html +++ b/app/api/static/index.html @@ -68,6 +68,14 @@ font: inherit; cursor: pointer; } + button.secondary { + background: #ffffff; + color: var(--accent); + border: 1px solid var(--border); + } + button.danger { + background: #b42318; + } .messages, .events { display: grid; gap: 12px; @@ -95,6 +103,48 @@ gap: 10px; margin-top: 12px; } + .feedback-actions { + display: flex; + gap: 8px; + flex-wrap: wrap; + margin-top: 10px; + } + dialog { + border: 1px solid var(--border); + border-radius: 8px; + padding: 0; + width: min(560px, calc(100vw - 32px)); + color: var(--text); + } + dialog::backdrop { + background: rgba(20, 18, 15, 0.4); + } + .modal-body { + display: grid; + gap: 12px; + padding: 18px; + background: var(--panel); + } + select { + width: 100%; + border: 1px solid var(--border); + background: #fff; + border-radius: 12px; + padding: 10px; + font: inherit; + } + label { + display: grid; + gap: 6px; + } + label.inline { + display: flex; + align-items: center; + gap: 8px; + } + label.inline input { + width: auto; + } @media (max-width: 860px) { .layout { grid-template-columns: 1fr; } } @@ -117,6 +167,53 @@
+ + + diff --git a/app/runtime/runtime_controller.py b/app/runtime/runtime_controller.py index 72cb18e..8c17205 100644 --- a/app/runtime/runtime_controller.py +++ b/app/runtime/runtime_controller.py @@ -394,13 +394,16 @@ class RuntimeController: feedback: str, task_id: str | None = None, session_id: str | None = None, + feedback_type: str | None = None, + severity: str | None = None, + correction: str | None = None, + remember: bool = True, + retry: bool = False, + assistant_answer: str | None = None, correctness_override: float | None = None, usefulness_override: float | None = None, safety_override: float | None = None, ) -> dict[str, object]: - if not self._memory_interface: - return {"status": "error", "message": "Memory not available"} - target_task_id = task_id target_session_id = session_id @@ -410,10 +413,9 @@ class RuntimeController: "message": "Either task_id or session_id must be provided", } - if not target_session_id and target_task_id: - state = self.task_state_store.get_task(target_task_id) - if state: - target_session_id = state.get("session_id") + state = self.task_state_store.get_task(target_task_id) if target_task_id else None + if not target_session_id and state: + target_session_id = state.get("session_id") if not target_task_id and target_session_id: recent_tasks = self.task_state_store.get_session_tasks(target_session_id, limit=1) @@ -426,8 +428,27 @@ class RuntimeController: final_weight = max(min_weight, min(max_weight, user_weight)) + task_input = state.get("task_input") if state else None + last_directive = state.get("last_directive") if state else None + feedback_type = feedback_type or "other" + severity = severity or "major" + + lesson = self._build_feedback_lesson( + feedback_type=feedback_type, + severity=severity, + feedback=feedback, + correction=correction, + task_input=task_input, + ) + metadata = { "feedback_text": feedback, + "feedback_type": feedback_type, + "severity": severity, + "correction": correction, + "assistant_answer": assistant_answer, + "task_input": task_input, + "last_directive": last_directive, "overrides": { "correctness": correctness_override, "usefulness": usefulness_override, @@ -436,7 +457,7 @@ class RuntimeController: "source": "user", } - feedback_text = f"User feedback: {feedback}" + feedback_text = lesson if correctness_override is not None: feedback_text += f" | Correctness corrected to: {correctness_override}" if usefulness_override is not None: @@ -444,21 +465,91 @@ class RuntimeController: if safety_override is not None: feedback_text += f" | Safety corrected to: {safety_override}" + retry_result = None + stored = False + store_error = None try: - self._memory_interface.insert( - text=feedback_text, - kind="critique", - source="user", - task_id=target_task_id, - session_id=target_session_id, - weight=final_weight, - metadata=metadata, - ) - return { - "status": "ok", - "message": "Feedback saved", - "task_id": target_task_id, - "session_id": target_session_id, - } + if remember and self._memory_interface: + self._memory_interface.insert( + text=feedback_text, + kind="critique", + source="user", + task_id=target_task_id, + session_id=target_session_id, + weight=final_weight, + metadata=metadata, + ) + stored = True + elif remember and not self._memory_interface: + store_error = "Memory not available" except Exception as e: - return {"status": "error", "message": str(e)} + store_error = str(e) + + if retry and task_input: + retry_input = self._build_retry_input( + task_input=task_input, + feedback=feedback, + feedback_type=feedback_type, + correction=correction, + ) + retry_task = UserTask( + session_id=target_session_id or "feedback-retry", + input=retry_input, + context={ + "feedback_retry": True, + "original_task_id": target_task_id, + "feedback_type": feedback_type, + "severity": severity, + "correction": correction, + }, + ) + retry_result = self.handle_task(retry_task) + + status = "ok" if stored or not remember else "error" + return { + "status": status, + "message": "Feedback saved" if stored else (store_error or "Feedback accepted"), + "stored": stored, + "task_id": target_task_id, + "session_id": target_session_id, + "lesson": lesson, + "retry_result": retry_result, + } + + def _build_feedback_lesson( + self, + feedback_type: str, + severity: str, + feedback: str, + correction: str | None, + task_input: str | None, + ) -> str: + parts = [ + "User critique lesson.", + f"Error type: {feedback_type}.", + f"Severity: {severity}.", + ] + if task_input: + parts.append(f"Original task: {task_input}") + if feedback: + parts.append(f"What was wrong: {feedback}") + if correction: + parts.append(f"Preferred correction: {correction}") + return " | ".join(parts) + + def _build_retry_input( + self, + task_input: str, + feedback: str, + feedback_type: str, + correction: str | None, + ) -> str: + retry_input = ( + f"Повтори задачу с учетом обратной связи.\n" + f"Исходная задача: {task_input}\n" + f"Тип ошибки: {feedback_type}\n" + f"Что было неверно: {feedback}\n" + ) + if correction: + retry_input += f"Как должно быть: {correction}\n" + return retry_input diff --git a/tests/test_api_handlers.py b/tests/test_api_handlers.py index 658d9e9..59c13fb 100644 --- a/tests/test_api_handlers.py +++ b/tests/test_api_handlers.py @@ -1,5 +1,6 @@ -from app.api.server import chat, health, resolve_permission, resolve_secret +from app.api.server import chat, critic_feedback, health, resolve_permission, resolve_secret from app.core.permission_resolution import PermissionResolutionRequest, SecretResolutionRequest +from app.api.server import CriticFeedbackRequest from app.core.contracts import UserTask @@ -25,3 +26,20 @@ def test_resolve_permission_handler_allows_completion() -> None: def test_resolve_secret_handler_requires_pending_request() -> None: body = resolve_secret(SecretResolutionRequest(task_id="missing", secret="x")) assert body["status"] == "failed" + + +def test_structured_feedback_can_be_accepted_without_memory_write() -> None: + initial = chat(UserTask(input="feedback target")) + body = critic_feedback( + CriticFeedbackRequest( + task_id=initial["task_id"], + feedback="wrong answer", + feedback_type="hallucination", + severity="major", + correction="check first", + remember=False, + ) + ) + assert body["status"] == "ok" + assert body["stored"] is False + assert "hallucination" in body["lesson"]