Files
Artemis-Eapil/artemis/llm/response_guard.py
T
chenmingxuan fac4ccb760 feat: prepare 2.2.0 with UI and execution fixes
Add dark theme, richer Markdown and readable plugin results.
Improve Android input evidence, response integrity and diagnostics.
Unify lifecycle, guidance receipts and evidence sources across clients.
Keep direct action dispatch without an added AI quota approval layer.
Align version metadata at 2.2.0 and bundle Helper 1.2.2.

Validation: 3063 backend tests before the version bump; 44 after.
Frontend: 317 tests and production build passed.
2026-10-26 00:44:13 +08:00

58 lines
2.6 KiB
Python

"""Validate final Responses state and original tool JSON before consumption."""
import json
from typing import Any
class ResponseIntegrityError(ValueError):
"""An unfinished response must not become an action or a formal verdict."""
def __init__(self, reason: str, *, status: str | None = None):
self.details = {"reason": reason}
if status in {"completed", "incomplete", "failed", "cancelled", "in_progress", "queued"}:
self.details["status"] = status
super().__init__(
f"模型响应未完整有效结束({reason});已丢弃本次输出,未执行本次动作或采纳结论。"
)
def ensure_response_complete(response: Any) -> None:
"""Check the raw message even when structured parsing already succeeded.
LangChain supplies ``model_provider=openai`` on Responses chunks, but may
synthesize ``chunk_position=last`` at EOF or repair partial tool JSON. Neither
is evidence of provider completion. Generic models without Responses metadata
remain usable; their original tool argument strings still require strict JSON.
"""
raw = response.get("raw") if isinstance(response, dict) and "raw" in response else response
metadata = getattr(raw, "response_metadata", None)
metadata = metadata if isinstance(metadata, dict) else {}
status = metadata.get("status")
if metadata.get("model_provider") == "openai" or metadata.get("object") == "response":
if status != "completed":
raise ResponseIntegrityError(
"missing_completion" if status is None else "response_not_completed", status=status
)
invalid_calls = getattr(raw, "invalid_tool_calls", None)
if isinstance(invalid_calls, list) and invalid_calls:
raise ResponseIntegrityError("invalid_tool_arguments")
chunks = getattr(raw, "tool_call_chunks", None)
arguments = [tool.get("args") for tool in chunks] if isinstance(chunks, list) else []
content = getattr(raw, "content", None)
if isinstance(content, list):
arguments.extend(
block.get("arguments")
for block in content
if isinstance(block, dict) and block.get("type") == "function_call"
)
for value in arguments:
if not isinstance(value, str):
raise ResponseIntegrityError("invalid_tool_arguments")
try:
parsed = json.loads(value)
except (ValueError, RecursionError) as error:
raise ResponseIntegrityError("invalid_tool_arguments") from error
if not isinstance(parsed, dict):
raise ResponseIntegrityError("invalid_tool_arguments")