fix: normalize optional fields in tool invocation identity (#4289)
This commit is contained in:
@@ -12,9 +12,12 @@ VERIFIER_PROMPT = (
|
||||
"URLs. Judge the report only against that supplied evidence; do not reject or approve claims "
|
||||
"based on your own memory. Check that material numeric and time-sensitive claims are supported "
|
||||
"by the evidence, that citations use supplied URLs, that the report is internally consistent, "
|
||||
"and that uncertainty is appropriately caveated. Treat information published on or before the "
|
||||
"research cutoff as potentially available. Mark unsupported claims separately from claims that "
|
||||
"the evidence directly contradicts."
|
||||
"and that uncertainty is appropriately caveated. The payload includes allowed_source_urls; "
|
||||
"compare citation URL strings exactly against that list. If a citation exactly matches an "
|
||||
"allowed URL, accept the URL even when it contains a tracking parameter or another allowed "
|
||||
"variant exists. Treat information published on or before the research cutoff as potentially "
|
||||
"available. Mark unsupported claims separately from claims that the evidence directly "
|
||||
"contradicts."
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -17,7 +17,9 @@ WRITER_PROMPT = (
|
||||
REVISION_PROMPT = (
|
||||
f"{WRITER_PROMPT} You are revising an existing report after evidence verification. Address "
|
||||
"every verification issue, remove claims that cannot be supported, preserve valid analysis, "
|
||||
"and return a complete replacement report rather than a patch or commentary."
|
||||
"and return a complete replacement report rather than a patch or commentary. Copy replacement "
|
||||
"citation URLs verbatim from each verification issue's source_urls; do not change path casing, "
|
||||
"language segments, or query parameters."
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -237,18 +237,31 @@ class FinancialResearchManager:
|
||||
search_results: Sequence[FinancialSearchEvidence],
|
||||
) -> VerificationResult:
|
||||
self.printer.update_item("verifying", "Verifying report...")
|
||||
input_data = json.dumps(
|
||||
result = await Runner.run(
|
||||
verifier_agent,
|
||||
self._verification_input(query, report, search_results),
|
||||
)
|
||||
self.printer.mark_item_done("verifying")
|
||||
return result.final_output_as(VerificationResult)
|
||||
|
||||
def _verification_input(
|
||||
self,
|
||||
query: str,
|
||||
report: FinancialReportData,
|
||||
search_results: Sequence[FinancialSearchEvidence],
|
||||
) -> str:
|
||||
return json.dumps(
|
||||
{
|
||||
"original_query": query,
|
||||
"research_cutoff": self.research_cutoff,
|
||||
"report": report.model_dump(mode="json"),
|
||||
"evidence": [item.model_dump(mode="json") for item in search_results],
|
||||
"allowed_source_urls": sorted(
|
||||
{source.url for item in search_results for source in item.sources}
|
||||
),
|
||||
},
|
||||
ensure_ascii=False,
|
||||
)
|
||||
result = await Runner.run(verifier_agent, input_data)
|
||||
self.printer.mark_item_done("verifying")
|
||||
return result.final_output_as(VerificationResult)
|
||||
|
||||
def _report_input(
|
||||
self,
|
||||
|
||||
@@ -79,6 +79,8 @@ DEFAULT_AUTO_SKIP = {
|
||||
"examples/sandbox/docker/mounts/gcs_mount_read_write.py",
|
||||
"examples/sandbox/docker/mounts/s3_files_mount_read_write.py",
|
||||
"examples/sandbox/docker/mounts/s3_mount_read_write.py",
|
||||
# Blaxel 0.3.2 still imports an MCP v1 module that was removed in MCP v2.
|
||||
"examples/sandbox/extensions/blaxel_runner.py",
|
||||
"examples/sandbox/extensions/daytona/usaspending_text2sql/setup_db.py",
|
||||
"examples/sandbox/extensions/temporal/temporal_sandbox_agent.py",
|
||||
# Temporarily disabled due to credential issues.
|
||||
|
||||
@@ -72,15 +72,16 @@ def _as_mapping(value: Any) -> Mapping[str, Any] | None:
|
||||
return None
|
||||
|
||||
|
||||
def _normalize_value(value: Any) -> Any:
|
||||
def _normalize_value(value: Any, *, exclude_none: bool = False) -> Any:
|
||||
mapping = _as_mapping(value)
|
||||
if mapping is not None:
|
||||
return {
|
||||
str(key): _normalize_value(item)
|
||||
str(key): _normalize_value(item, exclude_none=exclude_none)
|
||||
for key, item in sorted(mapping.items(), key=lambda pair: str(pair[0]))
|
||||
if not (exclude_none and item is None)
|
||||
}
|
||||
if isinstance(value, Sequence) and not isinstance(value, str | bytes | bytearray):
|
||||
return [_normalize_value(item) for item in value]
|
||||
return [_normalize_value(item, exclude_none=exclude_none) for item in value]
|
||||
if value is None or isinstance(value, str | int | float | bool):
|
||||
return value
|
||||
return str(value)
|
||||
@@ -197,8 +198,12 @@ def tool_invocation_identity_and_scope(
|
||||
if field_name not in mapping:
|
||||
continue
|
||||
value = mapping[field_name]
|
||||
if value is None:
|
||||
continue
|
||||
semantic_payload[field_name] = (
|
||||
_normalize_arguments(value) if field_name == "arguments" else _normalize_value(value)
|
||||
_normalize_arguments(value)
|
||||
if field_name == "arguments"
|
||||
else _normalize_value(value, exclude_none=True)
|
||||
)
|
||||
|
||||
return (
|
||||
|
||||
@@ -279,6 +279,30 @@ def test_financial_report_input_includes_cutoff_and_evidence() -> None:
|
||||
}
|
||||
|
||||
|
||||
def test_financial_verification_input_lists_exact_allowed_source_urls() -> None:
|
||||
manager = object.__new__(FinancialResearchManager)
|
||||
manager.research_cutoff = "2026-07-11"
|
||||
source_url = "https://example.com/report?utm_source=openai"
|
||||
evidence = FinancialSearchEvidence(
|
||||
query="company annual report",
|
||||
reason="Ground annual metrics",
|
||||
summary="Revenue increased.",
|
||||
sources=[FinancialSource(title="Annual report", url=source_url)],
|
||||
retrieved_at="2026-07-11",
|
||||
)
|
||||
report = FinancialReportData(
|
||||
short_summary="Summary",
|
||||
markdown_report=f"Revenue increased ([source]({source_url})).",
|
||||
follow_up_questions=[],
|
||||
)
|
||||
|
||||
payload = json.loads(manager._verification_input("Analyze the company", report, [evidence]))
|
||||
|
||||
assert payload["allowed_source_urls"] == [source_url]
|
||||
assert payload["report"] == report.model_dump(mode="json")
|
||||
assert payload["evidence"] == [evidence.model_dump(mode="json")]
|
||||
|
||||
|
||||
def test_sandbox_basic_direct_run_imports_external_docker_sdk(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
tmp_path: Path,
|
||||
|
||||
@@ -11,6 +11,7 @@ def test_default_auto_skip_excludes_prerequisite_bound_examples() -> None:
|
||||
"examples/sandbox/docker/mounts/gcs_mount_read_write.py",
|
||||
"examples/sandbox/docker/mounts/s3_files_mount_read_write.py",
|
||||
"examples/sandbox/docker/mounts/s3_mount_read_write.py",
|
||||
"examples/sandbox/extensions/blaxel_runner.py",
|
||||
"examples/sandbox/extensions/daytona/usaspending_text2sql/setup_db.py",
|
||||
"examples/sandbox/extensions/temporal/temporal_sandbox_agent.py",
|
||||
"examples/sandbox/extensions/vercel_runner.py",
|
||||
|
||||
@@ -79,6 +79,41 @@ def test_canonical_shell_identity_ignores_stripped_provider_metadata() -> None:
|
||||
assert tool_invocation_identity(provider_call) == tool_invocation_identity(persisted_call)
|
||||
|
||||
|
||||
def test_canonical_shell_identity_treats_optional_nulls_as_omitted() -> None:
|
||||
provider_call = {
|
||||
"type": "shell_call",
|
||||
"call_id": "shell_0",
|
||||
"action": {
|
||||
"commands": ["echo safe"],
|
||||
"max_output_length": None,
|
||||
"timeout_ms": None,
|
||||
},
|
||||
"environment": None,
|
||||
}
|
||||
normalized_call = dict(provider_call)
|
||||
normalized_call["action"] = {"commands": ["echo safe"]}
|
||||
normalized_call.pop("environment")
|
||||
|
||||
assert tool_invocation_identity(provider_call) == tool_invocation_identity(normalized_call)
|
||||
|
||||
|
||||
def test_canonical_function_identity_preserves_null_argument_values() -> None:
|
||||
call_with_null = {
|
||||
"type": "function_call",
|
||||
"call_id": "call_0",
|
||||
"name": "lookup",
|
||||
"arguments": '{"value": null}',
|
||||
}
|
||||
call_without_value = {
|
||||
"type": "function_call",
|
||||
"call_id": "call_0",
|
||||
"name": "lookup",
|
||||
"arguments": "{}",
|
||||
}
|
||||
|
||||
assert tool_invocation_identity(call_with_null) != tool_invocation_identity(call_without_value)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize("fallback_type", ["custom", "function"])
|
||||
async def test_completed_apply_patch_fallback_run_state_round_trip(fallback_type: str) -> None:
|
||||
|
||||
Reference in New Issue
Block a user