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.
58 lines
2.6 KiB
Python
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")
|