mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
Python: Fix broken samples for GitHub Copilot, declarative, and Responses API (#4915)
* Python: Fix broken samples for GitHub Copilot, declarative, and Responses API - Add missing on_permission_request handler to github_copilot_basic and github_copilot_with_session samples (required by copilot SDK) - Increase timeout for remote MCP query in github_copilot_with_mcp sample - Soften session isolation claim in github_copilot_with_session sample - Fix inline_yaml sample: pass project_endpoint via client_kwargs instead of relying on YAML connection block (AzureAIClient expects project_endpoint, not endpoint) - Handle raw JSON schemas in Responses client _convert_response_format so declarative outputSchema works with the Responses API Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Improve raw JSON schema detection heuristic and add tests - Broaden raw schema detection to handle anyOf, oneOf, allOf, $ref, $defs keywords and JSON Schema primitive types, not just 'properties' - Apply same raw schema handling to azure-ai _shared.py for consistency - Add unit tests for both openai and azure-ai response_format conversion Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
Unverified
parent
cc0cfaaac8
commit
6b47cdbf52
@@ -571,4 +571,25 @@ def _convert_response_format(response_format: Mapping[str, Any]) -> dict[str, An
|
||||
if format_type in {"json_object", "text"}:
|
||||
return {"type": format_type}
|
||||
|
||||
# Handle raw JSON schemas (e.g. {"type": "object", "properties": {...}})
|
||||
# by wrapping them in the expected json_schema envelope.
|
||||
# Detect by checking for JSON Schema primitive types or known schema keywords.
|
||||
json_schema_keywords = {"properties", "anyOf", "oneOf", "allOf", "$ref", "$defs"}
|
||||
json_schema_primitive_types = {"object", "array", "string", "number", "integer", "boolean", "null"}
|
||||
if format_type in json_schema_primitive_types or (
|
||||
format_type is None and any(k in response_format for k in json_schema_keywords)
|
||||
):
|
||||
schema = dict(response_format)
|
||||
if schema.get("type") == "object" and "additionalProperties" not in schema:
|
||||
schema["additionalProperties"] = False
|
||||
# Pop title from schema since OpenAI strict mode rejects unknown keys;
|
||||
# use it as the schema name in the envelope instead.
|
||||
name = str(schema.pop("title", None) or "response")
|
||||
return {
|
||||
"type": "json_schema",
|
||||
"name": name,
|
||||
"schema": schema,
|
||||
"strict": True,
|
||||
}
|
||||
|
||||
raise IntegrationInvalidRequestException("Unsupported response_format provided for Azure AI client.")
|
||||
|
||||
@@ -404,6 +404,32 @@ def test_convert_response_format_json_schema_missing_schema_raises() -> None:
|
||||
_convert_response_format({"type": "json_schema", "json_schema": {}})
|
||||
|
||||
|
||||
def test_convert_response_format_raw_json_schema_with_properties() -> None:
|
||||
"""Test raw JSON schema with properties is wrapped in json_schema envelope."""
|
||||
result = _convert_response_format({"type": "object", "properties": {"x": {"type": "string"}}, "title": "MyOutput"})
|
||||
|
||||
assert result["type"] == "json_schema"
|
||||
assert result["name"] == "MyOutput"
|
||||
assert result["strict"] is True
|
||||
assert result["schema"]["additionalProperties"] is False
|
||||
assert "title" not in result["schema"]
|
||||
|
||||
|
||||
def test_convert_response_format_raw_json_schema_no_title() -> None:
|
||||
"""Test raw JSON schema without title defaults name to 'response'."""
|
||||
result = _convert_response_format({"type": "object", "properties": {"x": {"type": "string"}}})
|
||||
|
||||
assert result["name"] == "response"
|
||||
|
||||
|
||||
def test_convert_response_format_raw_json_schema_with_anyof() -> None:
|
||||
"""Test raw JSON schema with anyOf keyword is detected."""
|
||||
result = _convert_response_format({"anyOf": [{"type": "string"}, {"type": "number"}]})
|
||||
|
||||
assert result["type"] == "json_schema"
|
||||
assert result["strict"] is True
|
||||
|
||||
|
||||
def test_from_azure_ai_tools_mcp_approval_mode_always() -> None:
|
||||
"""Test from_azure_ai_tools converts MCP require_approval='always' to dict."""
|
||||
tools = [
|
||||
|
||||
@@ -636,6 +636,27 @@ class RawOpenAIChatClient( # type: ignore[misc]
|
||||
if format_type in {"json_object", "text"}:
|
||||
return {"type": format_type}
|
||||
|
||||
# Handle raw JSON schemas (e.g. {"type": "object", "properties": {...}})
|
||||
# by wrapping them in the expected json_schema envelope.
|
||||
# Detect by checking for JSON Schema primitive types or known schema keywords.
|
||||
json_schema_keywords = {"properties", "anyOf", "oneOf", "allOf", "$ref", "$defs"}
|
||||
json_schema_primitive_types = {"object", "array", "string", "number", "integer", "boolean", "null"}
|
||||
if format_type in json_schema_primitive_types or (
|
||||
format_type is None and any(k in response_format for k in json_schema_keywords)
|
||||
):
|
||||
schema = dict(response_format)
|
||||
if schema.get("type") == "object" and "additionalProperties" not in schema:
|
||||
schema["additionalProperties"] = False
|
||||
# Pop title from schema since OpenAI strict mode rejects unknown keys;
|
||||
# use it as the schema name in the envelope instead.
|
||||
name = str(schema.pop("title", None) or "response")
|
||||
return {
|
||||
"type": "json_schema",
|
||||
"name": name,
|
||||
"schema": schema,
|
||||
"strict": True,
|
||||
}
|
||||
|
||||
raise ChatClientInvalidRequestException("Unsupported response_format provided for Responses client.")
|
||||
|
||||
def _get_conversation_id(
|
||||
|
||||
@@ -1713,6 +1713,71 @@ def test_response_format_json_schema_missing_schema() -> None:
|
||||
client._prepare_response_and_text_format(response_format=response_format, text_config=None)
|
||||
|
||||
|
||||
def test_response_format_raw_json_schema_with_properties() -> None:
|
||||
"""Test raw JSON schema with properties is wrapped in json_schema envelope."""
|
||||
client = OpenAIChatClient(model="test-model", api_key="test-key")
|
||||
|
||||
response_format = {"type": "object", "properties": {"x": {"type": "string"}}, "title": "MyOutput"}
|
||||
|
||||
_, text_config = client._prepare_response_and_text_format(response_format=response_format, text_config=None)
|
||||
|
||||
assert text_config is not None
|
||||
fmt = text_config["format"]
|
||||
assert fmt["type"] == "json_schema"
|
||||
assert fmt["name"] == "MyOutput"
|
||||
assert fmt["strict"] is True
|
||||
assert fmt["schema"]["additionalProperties"] is False
|
||||
assert "title" not in fmt["schema"]
|
||||
|
||||
|
||||
def test_response_format_raw_json_schema_no_title() -> None:
|
||||
"""Test raw JSON schema without title defaults name to 'response'."""
|
||||
client = OpenAIChatClient(model="test-model", api_key="test-key")
|
||||
|
||||
response_format = {"type": "object", "properties": {"x": {"type": "string"}}}
|
||||
|
||||
_, text_config = client._prepare_response_and_text_format(response_format=response_format, text_config=None)
|
||||
|
||||
assert text_config is not None
|
||||
assert text_config["format"]["name"] == "response"
|
||||
|
||||
|
||||
def test_response_format_raw_json_schema_preserves_additional_properties() -> None:
|
||||
"""Test raw JSON schema preserves existing additionalProperties."""
|
||||
client = OpenAIChatClient(model="test-model", api_key="test-key")
|
||||
|
||||
response_format = {"type": "object", "properties": {"x": {"type": "string"}}, "additionalProperties": True}
|
||||
|
||||
_, text_config = client._prepare_response_and_text_format(response_format=response_format, text_config=None)
|
||||
|
||||
assert text_config is not None
|
||||
assert text_config["format"]["schema"]["additionalProperties"] is True
|
||||
|
||||
|
||||
def test_response_format_raw_json_schema_non_object_type() -> None:
|
||||
"""Test raw JSON schema with non-object type does not inject additionalProperties."""
|
||||
client = OpenAIChatClient(model="test-model", api_key="test-key")
|
||||
|
||||
response_format = {"type": "array", "items": {"type": "string"}}
|
||||
|
||||
_, text_config = client._prepare_response_and_text_format(response_format=response_format, text_config=None)
|
||||
|
||||
assert text_config is not None
|
||||
assert "additionalProperties" not in text_config["format"]["schema"]
|
||||
|
||||
|
||||
def test_response_format_raw_json_schema_with_anyof() -> None:
|
||||
"""Test raw JSON schema with anyOf keyword is detected."""
|
||||
client = OpenAIChatClient(model="test-model", api_key="test-key")
|
||||
|
||||
response_format = {"anyOf": [{"type": "string"}, {"type": "number"}]}
|
||||
|
||||
_, text_config = client._prepare_response_and_text_format(response_format=response_format, text_config=None)
|
||||
|
||||
assert text_config is not None
|
||||
assert text_config["format"]["type"] == "json_schema"
|
||||
|
||||
|
||||
def test_response_format_unsupported_type() -> None:
|
||||
"""Test unsupported response_format type raises error."""
|
||||
client = OpenAIChatClient(model="test-model", api_key="test-key")
|
||||
|
||||
Reference in New Issue
Block a user