"""A single, explicit structured-result transport; never repair action data.""" from __future__ import annotations import json from copy import deepcopy FUNCTION_NAME = "return_structured_output" class ResponseContractError(ValueError): """Only a program-owned reason survives, never response text or values.""" def __init__(self, reason: str): self.reason = reason super().__init__(reason) def _single_action_items(items: dict, schema: dict) -> bool: """Recognize the framework's closed, mutually exclusive action objects.""" def resolve(node): seen = set() while isinstance(node, dict) and "$ref" in node: ref = node["$ref"] if not isinstance(ref, str) or not ref.startswith("#/$defs/") or ref in seen: return {} seen.add(ref) node = schema.get("$defs", {}).get(ref.removeprefix("#/$defs/"), {}) return node if isinstance(node, dict) else {} action = resolve(items) branches = action.get("anyOf", action.get("oneOf", [action])) if not isinstance(branches, list) or not branches: return False names = [] for branch in branches: branch = resolve(branch) properties = branch.get("properties") if (branch.get("type") != "object" or branch.get("additionalProperties") is not False or not isinstance(properties, dict) or len(properties) != 1 or branch.get("required") != list(properties)): return False names.extend(properties) return len(set(names)) == len(names) def request_contract(output_format) -> tuple[dict, str]: schema = deepcopy(output_format.model_json_schema()) action = schema.get("properties", {}).get("action") or {} single_action = action.get("type") == "array" and _single_action_items(action.get("items", {}), schema) if action.get("type") == "array": # The pinned framework exports nonstandard `min_items`; our response # contract also rejects an empty array. Export that actual contract in # standard JSON Schema rather than relying on prose alone. action.pop("min_items", None) action["minItems"] = max(1, action.get("minItems", 0)) if single_action: # Each closed branch already requires exactly its own action key. # State the same invariant beside the array item for the provider; # retain the original union and every parameter schema unchanged. action["items"].update(minProperties=1, maxProperties=1) function = {"type": "function", "function": { "name": FUNCTION_NAME, "description": "Submit the complete structured result for this step; this function does not execute a browser action.", "parameters": schema, }} instruction = ( "本次结果只通过 return_structured_output 函数的 JSON 参数提交,必须符合该函数的完整 Schema。" "原有 JSON 输出指引指这个参数对象;不要仅返回自然语言总结、Markdown或单个内层动作。" "过程说明保持简短,字符串中的引号、换行按 JSON 规则转义;动作参数按现场依据完整填写。" ) if action.get("type") == "array": instruction += "action 是必填的非空数组,每个元素是 Schema 中的一种完整动作对象;不能省略action或返回空数组。" if single_action: instruction += "每个数组元素只能包含一个Schema声明的动作名键,参数放在该动作名下;多个动作必须拆成多个数组元素,不能把两个动作放进同一个对象。" return function, instruction def attach_instruction(messages: list[dict], instruction: str) -> list[dict]: """Preserve conversation order, images and the caller's original objects.""" prepared = list(messages) index = next((i for i, row in enumerate(prepared) if row.get("role") == "system"), None) if index is None: return [{"role": "system", "content": instruction}, *prepared] original = prepared[index] content = original.get("content") if isinstance(content, str): prepared[index] = {**original, "content": content + "\n\n" + instruction} elif isinstance(content, list): prepared[index] = {**original, "content": [*content, {"type": "text", "text": instruction}]} else: return [{"role": "system", "content": instruction}, *prepared] return prepared def _unique_object(pairs): result = {} for key, value in pairs: if key in result: raise ResponseContractError("duplicate_argument_key") result[key] = value return result def _finite_json(_value): raise ResponseContractError("nonfinite_arguments") def validated_tool_result(message, output_format): """Validate the one requested call, ignoring prose instead of executing it. No JSON extraction, quote repair, missing-field defaults or action inference is performed. The caller accounts usage before invoking this function. """ calls = getattr(message, "tool_calls", None) if not calls: raise ResponseContractError("missing_tool_call") if not isinstance(calls, list) or len(calls) != 1: raise ResponseContractError("multiple_tool_calls") call = calls[0] function = getattr(call, "function", None) if getattr(call, "type", None) != "function" or getattr(function, "name", None) != FUNCTION_NAME: raise ResponseContractError("unexpected_tool_call") arguments = getattr(function, "arguments", None) if not isinstance(arguments, str) or not arguments.strip(): raise ResponseContractError("missing_arguments") try: parsed = json.loads(arguments, object_pairs_hook=_unique_object, parse_constant=_finite_json) except ResponseContractError: raise except (ValueError, UnicodeError, RecursionError): raise ResponseContractError("malformed_arguments") from None if not isinstance(parsed, dict): raise ResponseContractError("arguments_not_object") # Validation against the original complete model remains authoritative. result = output_format.model_validate_json(arguments) if isinstance(getattr(result, "action", None), list) and not result.action: raise ResponseContractError("empty_actions") return result