mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
Python: Fix Http Schema (#2112)
* Rename to threadid * Respond in plain text * Make snake-case * Add http prefix * rename to wait-for-response * Add query param check * address comments
This commit is contained in:
committed by
GitHub
Unverified
parent
ebab25b196
commit
ff28066c9c
@@ -45,9 +45,9 @@ class TestSampleSingleAgent:
|
||||
"""Test sending a simple message with JSON payload."""
|
||||
response = SampleTestHelper.post_json(
|
||||
f"{self.base_url}/run",
|
||||
{"message": "Tell me a short joke about cloud computing.", "sessionId": "test-simple-json"},
|
||||
{"message": "Tell me a short joke about cloud computing.", "thread_id": "test-simple-json"},
|
||||
)
|
||||
# Agent can return 200 (immediate) or 202 (async with wait_for_completion=false)
|
||||
# Agent can return 200 (immediate) or 202 (async with wait_for_response=false)
|
||||
assert response.status_code in [200, 202]
|
||||
data = response.json()
|
||||
|
||||
@@ -58,37 +58,35 @@ class TestSampleSingleAgent:
|
||||
assert data["message_count"] >= 1
|
||||
else:
|
||||
# Async response - check we got correlation info
|
||||
assert "correlationId" in data or "sessionId" in data
|
||||
assert "correlation_id" in data or "thread_id" in data
|
||||
|
||||
def test_simple_message_plain_text(self) -> None:
|
||||
"""Test sending a message with plain text payload."""
|
||||
response = SampleTestHelper.post_text(f"{self.base_url}/run", "Tell me a short joke about networking.")
|
||||
assert response.status_code in [200, 202]
|
||||
data = response.json()
|
||||
|
||||
if response.status_code == 200:
|
||||
assert data["status"] == "success"
|
||||
assert "response" in data
|
||||
# Agent responded with plain text when the request body was text/plain.
|
||||
assert response.text.strip()
|
||||
assert response.headers.get("x-ms-thread-id") is not None
|
||||
|
||||
def test_session_key_in_query(self) -> None:
|
||||
"""Test using sessionKey in query parameter."""
|
||||
def test_thread_id_in_query(self) -> None:
|
||||
"""Test using thread_id in query parameter."""
|
||||
response = SampleTestHelper.post_text(
|
||||
f"{self.base_url}/run?sessionKey=test-query-session", "Tell me a short joke about weather in Texas."
|
||||
f"{self.base_url}/run?thread_id=test-query-thread", "Tell me a short joke about weather in Texas."
|
||||
)
|
||||
assert response.status_code in [200, 202]
|
||||
data = response.json()
|
||||
|
||||
if response.status_code == 200:
|
||||
assert data["status"] == "success"
|
||||
assert response.text.strip()
|
||||
assert response.headers.get("x-ms-thread-id") == "test-query-thread"
|
||||
|
||||
def test_conversation_continuity(self) -> None:
|
||||
"""Test conversation context is maintained across requests."""
|
||||
session_id = "test-continuity"
|
||||
thread_id = "test-continuity"
|
||||
|
||||
# First message
|
||||
response1 = SampleTestHelper.post_json(
|
||||
f"{self.base_url}/run",
|
||||
{"message": "Tell me a short joke about weather in Seattle.", "sessionId": session_id},
|
||||
{"message": "Tell me a short joke about weather in Seattle.", "thread_id": thread_id},
|
||||
)
|
||||
assert response1.status_code in [200, 202]
|
||||
|
||||
@@ -98,7 +96,7 @@ class TestSampleSingleAgent:
|
||||
|
||||
# Second message in same session
|
||||
response2 = SampleTestHelper.post_json(
|
||||
f"{self.base_url}/run", {"message": "What about San Francisco?", "sessionId": session_id}
|
||||
f"{self.base_url}/run", {"message": "What about San Francisco?", "thread_id": thread_id}
|
||||
)
|
||||
assert response2.status_code == 200
|
||||
data2 = response2.json()
|
||||
@@ -107,7 +105,7 @@ class TestSampleSingleAgent:
|
||||
# In async mode, we can't easily test message count
|
||||
# Just verify we can make multiple calls
|
||||
response2 = SampleTestHelper.post_json(
|
||||
f"{self.base_url}/run", {"message": "What about Texas?", "sessionId": session_id}
|
||||
f"{self.base_url}/run", {"message": "What about Texas?", "thread_id": thread_id}
|
||||
)
|
||||
assert response2.status_code == 202
|
||||
|
||||
|
||||
@@ -38,21 +38,26 @@ class TestSampleMultiAgent:
|
||||
def test_weather_agent(self) -> None:
|
||||
"""Test WeatherAgent endpoint."""
|
||||
response = SampleTestHelper.post_json(
|
||||
f"{self.weather_base_url}/run", {"message": "What is the weather in Seattle?"}
|
||||
f"{self.weather_base_url}/run",
|
||||
{"message": "What is the weather in Seattle?"},
|
||||
)
|
||||
assert response.status_code == 202
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["status"] == "accepted"
|
||||
assert data["status"] == "success"
|
||||
assert "response" in data
|
||||
|
||||
def test_math_agent(self) -> None:
|
||||
"""Test MathAgent endpoint."""
|
||||
response = SampleTestHelper.post_json(
|
||||
f"{self.math_base_url}/run", {"message": "Calculate a 20% tip on a $50 bill"}
|
||||
f"{self.math_base_url}/run",
|
||||
{"message": "Calculate a 20% tip on a $50 bill", "wait_for_response": False},
|
||||
)
|
||||
assert response.status_code == 202
|
||||
data = response.json()
|
||||
|
||||
assert data["status"] == "accepted"
|
||||
assert "response" in data
|
||||
assert "correlation_id" in data
|
||||
assert "thread_id" in data
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -14,6 +14,8 @@ Usage:
|
||||
uv run pytest packages/azurefunctions/tests/integration_tests/test_03_callbacks.py -v
|
||||
"""
|
||||
|
||||
from typing import Any
|
||||
|
||||
import pytest
|
||||
import requests
|
||||
|
||||
@@ -39,39 +41,60 @@ class TestSampleCallbacks:
|
||||
"""Provide the callback agent base URL for each test."""
|
||||
self.base_url = f"{base_url}/api/agents/CallbackAgent"
|
||||
|
||||
@staticmethod
|
||||
def _wait_for_callback_events(base_url: str, thread_id: str) -> list[dict[str, Any]]:
|
||||
events: list[dict[str, Any]] = []
|
||||
response = SampleTestHelper.get(f"{base_url}/callbacks/{thread_id}")
|
||||
if response.status_code == 200:
|
||||
events = response.json()
|
||||
return events
|
||||
|
||||
def test_agent_with_callbacks(self) -> None:
|
||||
"""Test agent execution with callback tracking."""
|
||||
conversation_id = "test-callback"
|
||||
thread_id = "test-callback"
|
||||
|
||||
response = SampleTestHelper.post_json(
|
||||
f"{self.base_url}/run", {"message": "Tell me about Python", "conversationId": conversation_id}
|
||||
f"{self.base_url}/run",
|
||||
{"message": "Tell me about Python", "thread_id": thread_id},
|
||||
)
|
||||
assert response.status_code == 202
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["status"] == "accepted"
|
||||
|
||||
assert data["status"] == "success"
|
||||
|
||||
events = self._wait_for_callback_events(self.base_url, thread_id)
|
||||
|
||||
assert events
|
||||
assert any(event.get("event_type") == "final" for event in events)
|
||||
|
||||
def test_get_callbacks(self) -> None:
|
||||
"""Test retrieving callback events."""
|
||||
conversation_id = "test-callback-retrieve"
|
||||
thread_id = "test-callback-retrieve"
|
||||
|
||||
# Send a message first
|
||||
SampleTestHelper.post_json(f"{self.base_url}/run", {"message": "Hello", "conversationId": conversation_id})
|
||||
SampleTestHelper.post_json(
|
||||
f"{self.base_url}/run",
|
||||
{"message": "Hello", "thread_id": thread_id, "wait_for_response": False},
|
||||
)
|
||||
|
||||
# Get callbacks
|
||||
response = SampleTestHelper.get(f"{self.base_url}/callbacks/{conversation_id}")
|
||||
response = SampleTestHelper.get(f"{self.base_url}/callbacks/{thread_id}")
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert isinstance(data, list)
|
||||
|
||||
def test_delete_callbacks(self) -> None:
|
||||
"""Test clearing callback events."""
|
||||
conversation_id = "test-callback-delete"
|
||||
thread_id = "test-callback-delete"
|
||||
|
||||
# Send a message first
|
||||
SampleTestHelper.post_json(f"{self.base_url}/run", {"message": "Test", "conversationId": conversation_id})
|
||||
SampleTestHelper.post_json(
|
||||
f"{self.base_url}/run",
|
||||
{"message": "Test", "thread_id": thread_id, "wait_for_response": False},
|
||||
)
|
||||
|
||||
# Delete callbacks
|
||||
response = requests.delete(f"{self.base_url}/callbacks/{conversation_id}", timeout=TIMEOUT)
|
||||
response = requests.delete(f"{self.base_url}/callbacks/{thread_id}", timeout=TIMEOUT)
|
||||
assert response.status_code == 204
|
||||
|
||||
|
||||
|
||||
@@ -12,8 +12,8 @@ import pytest
|
||||
from agent_framework import AgentRunResponse, ChatMessage
|
||||
|
||||
from agent_framework_azurefunctions import AgentFunctionApp
|
||||
from agent_framework_azurefunctions._app import WAIT_FOR_RESPONSE_FIELD, WAIT_FOR_RESPONSE_HEADER
|
||||
from agent_framework_azurefunctions._entities import AgentEntity, AgentState, create_agent_entity
|
||||
from agent_framework_azurefunctions._errors import IncomingRequestError
|
||||
|
||||
TFunc = TypeVar("TFunc", bound=Callable[..., Any])
|
||||
|
||||
@@ -150,6 +150,38 @@ class TestAgentFunctionAppSetup:
|
||||
# Verify agent is registered
|
||||
assert "TestAgent" in app.agents
|
||||
|
||||
def test_http_function_name_uses_prefix_format(self) -> None:
|
||||
"""Ensure function names follow the prefix-agent naming convention."""
|
||||
mock_agent = Mock()
|
||||
mock_agent.name = "Agent 42"
|
||||
|
||||
captured_names: list[str] = []
|
||||
|
||||
def capture_function_name(
|
||||
self: AgentFunctionApp, name: str, *args: Any, **kwargs: Any
|
||||
) -> Callable[[TFunc], TFunc]:
|
||||
def decorator(func: TFunc) -> TFunc:
|
||||
captured_names.append(name)
|
||||
return func
|
||||
|
||||
return decorator
|
||||
|
||||
def passthrough_decorator(*args: Any, **kwargs: Any) -> Callable[[TFunc], TFunc]:
|
||||
def decorator(func: TFunc) -> TFunc:
|
||||
return func
|
||||
|
||||
return decorator
|
||||
|
||||
with (
|
||||
patch.object(AgentFunctionApp, "function_name", new=capture_function_name),
|
||||
patch.object(AgentFunctionApp, "route", new=passthrough_decorator),
|
||||
patch.object(AgentFunctionApp, "durable_client_input", new=passthrough_decorator),
|
||||
patch.object(AgentFunctionApp, "entity_trigger", new=passthrough_decorator),
|
||||
):
|
||||
AgentFunctionApp(agents=[mock_agent])
|
||||
|
||||
assert captured_names == ["http-Agent_42"]
|
||||
|
||||
def test_setup_skips_http_trigger_when_disabled(self) -> None:
|
||||
"""Test that HTTP trigger is not created when disabled."""
|
||||
mock_agent = Mock()
|
||||
@@ -236,8 +268,8 @@ class TestAgentFunctionAppSetup:
|
||||
assert "Agent2" in app2.agents
|
||||
|
||||
|
||||
class TestWaitForCompletionAndCorrelationId:
|
||||
"""Tests for wait_for_completion flag and correlation ID handling."""
|
||||
class TestWaitForResponseAndCorrelationId:
|
||||
"""Tests for wait_for_response flag and correlation ID handling."""
|
||||
|
||||
def _create_app(self) -> AgentFunctionApp:
|
||||
mock_agent = Mock()
|
||||
@@ -255,21 +287,35 @@ class TestWaitForCompletionAndCorrelationId:
|
||||
request.params = params or {}
|
||||
return request
|
||||
|
||||
def test_wait_for_completion_header_true(self) -> None:
|
||||
"""Test that the wait-for-completion header is honored."""
|
||||
def test_wait_for_response_header_true(self) -> None:
|
||||
"""Test that the wait-for-response header is honored."""
|
||||
app = self._create_app()
|
||||
request = self._make_request(headers={"X-Wait-For-Completion": "true"})
|
||||
request = self._make_request(headers={WAIT_FOR_RESPONSE_HEADER: "true"})
|
||||
|
||||
assert app._should_wait_for_completion(request, {}) is True
|
||||
assert app._should_wait_for_response(request, {}) is True
|
||||
|
||||
def test_wait_for_completion_body_variants(self) -> None:
|
||||
"""Test that multiple payload spellings are accepted."""
|
||||
def test_wait_for_response_body_snake_case(self) -> None:
|
||||
"""Test that payload controls wait_for_response."""
|
||||
app = self._create_app()
|
||||
request = self._make_request()
|
||||
|
||||
assert app._should_wait_for_completion(request, {"wait_for_completion": "true"}) is True
|
||||
assert app._should_wait_for_completion(request, {"waitForCompletion": "1"}) is True
|
||||
assert app._should_wait_for_completion(request, {"WaitForCompletion": "no"}) is False
|
||||
assert app._should_wait_for_response(request, {WAIT_FOR_RESPONSE_FIELD: "true"}) is True
|
||||
assert app._should_wait_for_response(request, {WAIT_FOR_RESPONSE_FIELD: "false"}) is False
|
||||
assert app._should_wait_for_response(request, {WAIT_FOR_RESPONSE_FIELD: "0"}) is False
|
||||
|
||||
def test_wait_for_response_query_parameter(self) -> None:
|
||||
"""Test that query parameter controls wait_for_response."""
|
||||
app = self._create_app()
|
||||
request = self._make_request(params={WAIT_FOR_RESPONSE_FIELD: "true"})
|
||||
|
||||
assert app._should_wait_for_response(request, {}) is True
|
||||
|
||||
def test_wait_for_response_query_precedence(self) -> None:
|
||||
"""Test that query parameter overrides body value."""
|
||||
app = self._create_app()
|
||||
request = self._make_request(params={WAIT_FOR_RESPONSE_FIELD: "false"})
|
||||
|
||||
assert app._should_wait_for_response(request, {WAIT_FOR_RESPONSE_FIELD: "true"}) is False
|
||||
|
||||
|
||||
class TestAgentEntityOperations:
|
||||
@@ -287,13 +333,13 @@ class TestAgentEntityOperations:
|
||||
|
||||
result = await entity.run_agent(
|
||||
mock_context,
|
||||
{"message": "Test message", "conversation_id": "test-conv-123", "correlation_id": "corr-app-entity-1"},
|
||||
{"message": "Test message", "thread_id": "test-conv-123", "correlation_id": "corr-app-entity-1"},
|
||||
)
|
||||
|
||||
assert result["status"] == "success"
|
||||
assert result["response"] == "Test response"
|
||||
assert result["message"] == "Test message"
|
||||
assert result["conversation_id"] == "test-conv-123"
|
||||
assert result["thread_id"] == "test-conv-123"
|
||||
assert entity.state.message_count == 1
|
||||
|
||||
async def test_entity_stores_conversation_history(self) -> None:
|
||||
@@ -308,7 +354,7 @@ class TestAgentEntityOperations:
|
||||
|
||||
# Send first message
|
||||
await entity.run_agent(
|
||||
mock_context, {"message": "Message 1", "conversation_id": "conv-1", "correlation_id": "corr-app-entity-2"}
|
||||
mock_context, {"message": "Message 1", "thread_id": "conv-1", "correlation_id": "corr-app-entity-2"}
|
||||
)
|
||||
|
||||
history = entity.state.conversation_history
|
||||
@@ -337,12 +383,12 @@ class TestAgentEntityOperations:
|
||||
assert entity.state.message_count == 0
|
||||
|
||||
await entity.run_agent(
|
||||
mock_context, {"message": "Message 1", "conversation_id": "conv-1", "correlation_id": "corr-app-entity-3a"}
|
||||
mock_context, {"message": "Message 1", "thread_id": "conv-1", "correlation_id": "corr-app-entity-3a"}
|
||||
)
|
||||
assert entity.state.message_count == 1
|
||||
|
||||
await entity.run_agent(
|
||||
mock_context, {"message": "Message 2", "conversation_id": "conv-1", "correlation_id": "corr-app-entity-3b"}
|
||||
mock_context, {"message": "Message 2", "thread_id": "conv-1", "correlation_id": "corr-app-entity-3b"}
|
||||
)
|
||||
assert entity.state.message_count == 2
|
||||
|
||||
@@ -391,7 +437,7 @@ class TestAgentEntityFactory:
|
||||
mock_context.operation_name = "run_agent"
|
||||
mock_context.get_input.return_value = {
|
||||
"message": "Test message",
|
||||
"conversation_id": "conv-123",
|
||||
"thread_id": "conv-123",
|
||||
"correlation_id": "corr-app-factory-1",
|
||||
}
|
||||
mock_context.get_state.return_value = None
|
||||
@@ -478,7 +524,7 @@ class TestErrorHandling:
|
||||
mock_context = Mock()
|
||||
|
||||
result = await entity.run_agent(
|
||||
mock_context, {"message": "Test message", "conversation_id": "conv-1", "correlation_id": "corr-app-error-1"}
|
||||
mock_context, {"message": "Test message", "thread_id": "conv-1", "correlation_id": "corr-app-error-1"}
|
||||
)
|
||||
|
||||
assert result["status"] == "error"
|
||||
@@ -521,48 +567,67 @@ class TestIncomingRequestParsing:
|
||||
app = self._create_app()
|
||||
|
||||
request = Mock()
|
||||
request.headers = {}
|
||||
request.params = {}
|
||||
request.get_json.side_effect = ValueError("Invalid JSON")
|
||||
request.get_body.return_value = b"Plain text message"
|
||||
|
||||
req_body, message = app._parse_incoming_request(request)
|
||||
req_body, message, response_format = app._parse_incoming_request(request)
|
||||
|
||||
assert req_body == {}
|
||||
assert message == "Plain text message"
|
||||
|
||||
def test_parse_plain_text_requires_content(self) -> None:
|
||||
"""Test that plain-text requests require message content."""
|
||||
assert response_format == "text"
|
||||
|
||||
def test_parse_plain_text_trims_whitespace(self) -> None:
|
||||
"""Plain-text parser returns an empty string when the body contains only whitespace."""
|
||||
app = self._create_app()
|
||||
|
||||
request = Mock()
|
||||
request.headers = {}
|
||||
request.params = {}
|
||||
request.get_json.side_effect = ValueError("Invalid JSON")
|
||||
request.get_body.return_value = b" "
|
||||
|
||||
with pytest.raises(IncomingRequestError) as exc_info:
|
||||
app._parse_incoming_request(request)
|
||||
req_body, message, response_format = app._parse_incoming_request(request)
|
||||
|
||||
assert "Message is required" in str(exc_info.value)
|
||||
assert req_body == {}
|
||||
assert message == ""
|
||||
assert response_format == "text"
|
||||
|
||||
def test_extract_session_key_from_query_params(self) -> None:
|
||||
"""Test session key extraction from query parameters."""
|
||||
def test_accept_header_prefers_json(self) -> None:
|
||||
"""Test that the Accept header can force JSON responses for plain-text bodies."""
|
||||
app = self._create_app()
|
||||
|
||||
request = Mock()
|
||||
request.params = {"sessionId": "query-session"}
|
||||
request.headers = {"accept": "application/json"}
|
||||
request.params = {}
|
||||
request.get_json.side_effect = ValueError("Invalid JSON")
|
||||
request.get_body.return_value = b"Plain text message"
|
||||
|
||||
_, message, response_format = app._parse_incoming_request(request)
|
||||
|
||||
assert message == "Plain text message"
|
||||
assert response_format == "json"
|
||||
|
||||
def test_extract_thread_id_from_query_params(self) -> None:
|
||||
"""Test thread identifier extraction from query parameters."""
|
||||
app = self._create_app()
|
||||
|
||||
request = Mock()
|
||||
request.params = {"thread_id": "query-thread"}
|
||||
req_body = {}
|
||||
|
||||
session_key = app._resolve_session_key(request, req_body)
|
||||
thread_id = app._resolve_thread_id(request, req_body)
|
||||
|
||||
assert session_key == "query-session"
|
||||
assert thread_id == "query-thread"
|
||||
|
||||
|
||||
class TestHttpRunRoute:
|
||||
"""Tests for the HTTP run route behavior."""
|
||||
|
||||
async def test_http_run_accepts_plain_text(self) -> None:
|
||||
"""Test that the HTTP handler accepts plain-text requests."""
|
||||
mock_agent = Mock()
|
||||
mock_agent.name = "HttpAgent"
|
||||
|
||||
@staticmethod
|
||||
def _get_run_handler(agent: Mock) -> Callable[[func.HttpRequest, Any], Awaitable[func.HttpResponse]]:
|
||||
captured_handlers: dict[str | None, Callable[..., Awaitable[func.HttpResponse]]] = {}
|
||||
|
||||
def capture_decorator(*args: Any, **kwargs: Any) -> Callable[[TFunc], TFunc]:
|
||||
@@ -585,13 +650,20 @@ class TestHttpRunRoute:
|
||||
patch.object(AgentFunctionApp, "durable_client_input", new=capture_decorator),
|
||||
patch.object(AgentFunctionApp, "entity_trigger", new=capture_decorator),
|
||||
):
|
||||
AgentFunctionApp(agents=[mock_agent], enable_health_check=False)
|
||||
AgentFunctionApp(agents=[agent], enable_health_check=False)
|
||||
|
||||
run_route = f"agents/{mock_agent.name}/run"
|
||||
handler = captured_handlers[run_route]
|
||||
run_route = f"agents/{agent.name}/run"
|
||||
return captured_handlers[run_route]
|
||||
|
||||
async def test_http_run_accepts_plain_text(self) -> None:
|
||||
"""Test that the HTTP handler accepts plain-text requests."""
|
||||
mock_agent = Mock()
|
||||
mock_agent.name = "HttpAgent"
|
||||
|
||||
handler = self._get_run_handler(mock_agent)
|
||||
|
||||
request = Mock()
|
||||
request.headers = {}
|
||||
request.headers = {WAIT_FOR_RESPONSE_HEADER: "false"}
|
||||
request.params = {}
|
||||
request.route_params = {}
|
||||
request.get_json.side_effect = ValueError("Invalid JSON")
|
||||
@@ -602,12 +674,64 @@ class TestHttpRunRoute:
|
||||
response = await handler(request, client)
|
||||
|
||||
assert response.status_code == 202
|
||||
assert response.mimetype == "text/plain"
|
||||
assert response.headers.get("x-ms-thread-id") is not None
|
||||
assert response.get_body().decode("utf-8") == "Agent request accepted"
|
||||
|
||||
signal_args = client.signal_entity.call_args[0]
|
||||
run_request = signal_args[2]
|
||||
|
||||
assert run_request["message"] == "Plain text via HTTP"
|
||||
assert run_request["role"] == "user"
|
||||
assert "thread_id" in run_request
|
||||
|
||||
async def test_http_run_accept_header_returns_json(self) -> None:
|
||||
"""Test that Accept header requesting JSON results in JSON response."""
|
||||
mock_agent = Mock()
|
||||
mock_agent.name = "HttpAgentJson"
|
||||
|
||||
handler = self._get_run_handler(mock_agent)
|
||||
|
||||
request = Mock()
|
||||
request.headers = {WAIT_FOR_RESPONSE_HEADER: "false", "Accept": "application/json"}
|
||||
request.params = {}
|
||||
request.route_params = {}
|
||||
request.get_json.side_effect = ValueError("Invalid JSON")
|
||||
request.get_body.return_value = b"Plain text via HTTP"
|
||||
|
||||
client = AsyncMock()
|
||||
|
||||
response = await handler(request, client)
|
||||
|
||||
assert response.status_code == 202
|
||||
assert response.mimetype == "application/json"
|
||||
assert response.headers.get("x-ms-thread-id") is None
|
||||
body = response.get_body().decode("utf-8")
|
||||
assert '"status": "accepted"' in body
|
||||
|
||||
async def test_http_run_rejects_empty_message(self) -> None:
|
||||
"""Test that the HTTP handler rejects empty messages with a 400 response."""
|
||||
mock_agent = Mock()
|
||||
mock_agent.name = "HttpAgentEmpty"
|
||||
|
||||
handler = self._get_run_handler(mock_agent)
|
||||
|
||||
request = Mock()
|
||||
request.headers = {WAIT_FOR_RESPONSE_HEADER: "false"}
|
||||
request.params = {}
|
||||
request.route_params = {}
|
||||
request.get_json.side_effect = ValueError("Invalid JSON")
|
||||
request.get_body.return_value = b" "
|
||||
|
||||
client = AsyncMock()
|
||||
|
||||
response = await handler(request, client)
|
||||
|
||||
assert response.status_code == 400
|
||||
assert response.mimetype == "text/plain"
|
||||
assert response.headers.get("x-ms-thread-id") is not None
|
||||
assert response.get_body().decode("utf-8") == "Message is required"
|
||||
client.signal_entity.assert_not_called()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -12,11 +12,11 @@ from typing import Any, TypeVar
|
||||
from unittest.mock import AsyncMock, Mock, patch
|
||||
|
||||
import pytest
|
||||
from agent_framework import AgentRunResponse, AgentRunResponseUpdate, ChatMessage
|
||||
from agent_framework import AgentRunResponse, AgentRunResponseUpdate, ChatMessage, Role
|
||||
from pydantic import BaseModel
|
||||
|
||||
from agent_framework_azurefunctions._entities import AgentEntity, create_agent_entity
|
||||
from agent_framework_azurefunctions._models import ChatRole, RunRequest
|
||||
from agent_framework_azurefunctions._models import RunRequest
|
||||
from agent_framework_azurefunctions._state import AgentState
|
||||
|
||||
TFunc = TypeVar("TFunc", bound=Callable[..., Any])
|
||||
@@ -112,7 +112,7 @@ class TestAgentEntityRunAgent:
|
||||
mock_context = Mock()
|
||||
|
||||
result = await entity.run_agent(
|
||||
mock_context, {"message": "Test message", "conversation_id": "conv-123", "correlation_id": "corr-entity-1"}
|
||||
mock_context, {"message": "Test message", "thread_id": "conv-123", "correlation_id": "corr-entity-1"}
|
||||
)
|
||||
|
||||
# Verify agent.run was called
|
||||
@@ -130,7 +130,7 @@ class TestAgentEntityRunAgent:
|
||||
assert result["status"] == "success"
|
||||
assert result["response"] == "Test response"
|
||||
assert result["message"] == "Test message"
|
||||
assert result["conversation_id"] == "conv-123"
|
||||
assert result["thread_id"] == "conv-123"
|
||||
|
||||
async def test_run_agent_streaming_callbacks_invoked(self) -> None:
|
||||
"""Ensure streaming updates trigger callbacks and run() is not used."""
|
||||
@@ -157,7 +157,7 @@ class TestAgentEntityRunAgent:
|
||||
mock_context,
|
||||
{
|
||||
"message": "Tell me something",
|
||||
"conversation_id": "session-1",
|
||||
"thread_id": "session-1",
|
||||
"correlation_id": "corr-stream-1",
|
||||
},
|
||||
)
|
||||
@@ -175,7 +175,7 @@ class TestAgentEntityRunAgent:
|
||||
context = recorded_call.args[1]
|
||||
assert context.agent_name == "StreamingAgent"
|
||||
assert context.correlation_id == "corr-stream-1"
|
||||
assert context.conversation_id == "session-1"
|
||||
assert context.thread_id == "session-1"
|
||||
assert context.request_message == "Tell me something"
|
||||
|
||||
final_call = callback.response_mock.await_args
|
||||
@@ -183,7 +183,7 @@ class TestAgentEntityRunAgent:
|
||||
final_response, final_context = final_call.args
|
||||
assert final_context.agent_name == "StreamingAgent"
|
||||
assert final_context.correlation_id == "corr-stream-1"
|
||||
assert final_context.conversation_id == "session-1"
|
||||
assert final_context.thread_id == "session-1"
|
||||
assert final_context.request_message == "Tell me something"
|
||||
assert getattr(final_response, "text", "").strip()
|
||||
|
||||
@@ -204,7 +204,7 @@ class TestAgentEntityRunAgent:
|
||||
mock_context,
|
||||
{
|
||||
"message": "Hi",
|
||||
"conversation_id": "session-2",
|
||||
"thread_id": "session-2",
|
||||
"correlation_id": "corr-final-1",
|
||||
},
|
||||
)
|
||||
@@ -220,7 +220,7 @@ class TestAgentEntityRunAgent:
|
||||
final_context = final_call.args[1]
|
||||
assert final_context.agent_name == "NonStreamingAgent"
|
||||
assert final_context.correlation_id == "corr-final-1"
|
||||
assert final_context.conversation_id == "session-2"
|
||||
assert final_context.thread_id == "session-2"
|
||||
assert final_context.request_message == "Hi"
|
||||
|
||||
async def test_run_agent_updates_conversation_history(self) -> None:
|
||||
@@ -233,7 +233,7 @@ class TestAgentEntityRunAgent:
|
||||
mock_context = Mock()
|
||||
|
||||
await entity.run_agent(
|
||||
mock_context, {"message": "User message", "conversation_id": "conv-1", "correlation_id": "corr-entity-2"}
|
||||
mock_context, {"message": "User message", "thread_id": "conv-1", "correlation_id": "corr-entity-2"}
|
||||
)
|
||||
|
||||
# Should have 2 entries: user message + assistant response
|
||||
@@ -260,17 +260,17 @@ class TestAgentEntityRunAgent:
|
||||
assert entity.state.message_count == 0
|
||||
|
||||
await entity.run_agent(
|
||||
mock_context, {"message": "Message 1", "conversation_id": "conv-1", "correlation_id": "corr-entity-3a"}
|
||||
mock_context, {"message": "Message 1", "thread_id": "conv-1", "correlation_id": "corr-entity-3a"}
|
||||
)
|
||||
assert entity.state.message_count == 1
|
||||
|
||||
await entity.run_agent(
|
||||
mock_context, {"message": "Message 2", "conversation_id": "conv-1", "correlation_id": "corr-entity-3b"}
|
||||
mock_context, {"message": "Message 2", "thread_id": "conv-1", "correlation_id": "corr-entity-3b"}
|
||||
)
|
||||
assert entity.state.message_count == 2
|
||||
|
||||
await entity.run_agent(
|
||||
mock_context, {"message": "Message 3", "conversation_id": "conv-1", "correlation_id": "corr-entity-3c"}
|
||||
mock_context, {"message": "Message 3", "thread_id": "conv-1", "correlation_id": "corr-entity-3c"}
|
||||
)
|
||||
assert entity.state.message_count == 3
|
||||
|
||||
@@ -283,27 +283,27 @@ class TestAgentEntityRunAgent:
|
||||
mock_context = Mock()
|
||||
|
||||
await entity.run_agent(
|
||||
mock_context, {"message": "Message 1", "conversation_id": "conv-1", "correlation_id": "corr-entity-4a"}
|
||||
mock_context, {"message": "Message 1", "thread_id": "conv-1", "correlation_id": "corr-entity-4a"}
|
||||
)
|
||||
assert entity.state.last_response == "Response 1"
|
||||
|
||||
mock_agent.run = AsyncMock(return_value=_agent_response("Response 2"))
|
||||
await entity.run_agent(
|
||||
mock_context, {"message": "Message 2", "conversation_id": "conv-1", "correlation_id": "corr-entity-4b"}
|
||||
mock_context, {"message": "Message 2", "thread_id": "conv-1", "correlation_id": "corr-entity-4b"}
|
||||
)
|
||||
assert entity.state.last_response == "Response 2"
|
||||
|
||||
async def test_run_agent_with_none_conversation_id(self) -> None:
|
||||
"""Test run_agent with a None conversation identifier."""
|
||||
async def test_run_agent_with_none_thread_id(self) -> None:
|
||||
"""Test run_agent with a None thread identifier."""
|
||||
mock_agent = Mock()
|
||||
mock_agent.run = AsyncMock(return_value=_agent_response("Response"))
|
||||
|
||||
entity = AgentEntity(mock_agent)
|
||||
mock_context = Mock()
|
||||
|
||||
with pytest.raises(ValueError, match="conversation_id"):
|
||||
with pytest.raises(ValueError, match="thread_id"):
|
||||
await entity.run_agent(
|
||||
mock_context, {"message": "Message", "conversation_id": None, "correlation_id": "corr-entity-5"}
|
||||
mock_context, {"message": "Message", "thread_id": None, "correlation_id": "corr-entity-5"}
|
||||
)
|
||||
|
||||
async def test_run_agent_handles_response_without_text_attribute(self) -> None:
|
||||
@@ -322,7 +322,7 @@ class TestAgentEntityRunAgent:
|
||||
mock_context = Mock()
|
||||
|
||||
result = await entity.run_agent(
|
||||
mock_context, {"message": "Message", "conversation_id": "conv-1", "correlation_id": "corr-entity-6"}
|
||||
mock_context, {"message": "Message", "thread_id": "conv-1", "correlation_id": "corr-entity-6"}
|
||||
)
|
||||
|
||||
# Should handle gracefully
|
||||
@@ -338,7 +338,7 @@ class TestAgentEntityRunAgent:
|
||||
mock_context = Mock()
|
||||
|
||||
result = await entity.run_agent(
|
||||
mock_context, {"message": "Message", "conversation_id": "conv-1", "correlation_id": "corr-entity-7"}
|
||||
mock_context, {"message": "Message", "thread_id": "conv-1", "correlation_id": "corr-entity-7"}
|
||||
)
|
||||
|
||||
assert result["status"] == "success"
|
||||
@@ -354,13 +354,13 @@ class TestAgentEntityRunAgent:
|
||||
|
||||
# Send multiple messages
|
||||
await entity.run_agent(
|
||||
mock_context, {"message": "Message 1", "conversation_id": "conv-1", "correlation_id": "corr-entity-8a"}
|
||||
mock_context, {"message": "Message 1", "thread_id": "conv-1", "correlation_id": "corr-entity-8a"}
|
||||
)
|
||||
await entity.run_agent(
|
||||
mock_context, {"message": "Message 2", "conversation_id": "conv-1", "correlation_id": "corr-entity-8b"}
|
||||
mock_context, {"message": "Message 2", "thread_id": "conv-1", "correlation_id": "corr-entity-8b"}
|
||||
)
|
||||
await entity.run_agent(
|
||||
mock_context, {"message": "Message 3", "conversation_id": "conv-1", "correlation_id": "corr-entity-8c"}
|
||||
mock_context, {"message": "Message 3", "thread_id": "conv-1", "correlation_id": "corr-entity-8c"}
|
||||
)
|
||||
|
||||
history = entity.state.conversation_history
|
||||
@@ -421,10 +421,10 @@ class TestAgentEntityReset:
|
||||
|
||||
# Have a conversation
|
||||
await entity.run_agent(
|
||||
mock_context, {"message": "Message 1", "conversation_id": "conv-1", "correlation_id": "corr-entity-10a"}
|
||||
mock_context, {"message": "Message 1", "thread_id": "conv-1", "correlation_id": "corr-entity-10a"}
|
||||
)
|
||||
await entity.run_agent(
|
||||
mock_context, {"message": "Message 2", "conversation_id": "conv-1", "correlation_id": "corr-entity-10b"}
|
||||
mock_context, {"message": "Message 2", "thread_id": "conv-1", "correlation_id": "corr-entity-10b"}
|
||||
)
|
||||
|
||||
# Verify state before reset
|
||||
@@ -463,7 +463,7 @@ class TestCreateAgentEntity:
|
||||
mock_context.operation_name = "run_agent"
|
||||
mock_context.get_input.return_value = {
|
||||
"message": "Test message",
|
||||
"conversation_id": "conv-123",
|
||||
"thread_id": "conv-123",
|
||||
"correlation_id": "corr-entity-factory",
|
||||
}
|
||||
mock_context.get_state.return_value = None
|
||||
@@ -591,7 +591,7 @@ class TestErrorHandling:
|
||||
mock_context = Mock()
|
||||
|
||||
result = await entity.run_agent(
|
||||
mock_context, {"message": "Message", "conversation_id": "conv-1", "correlation_id": "corr-entity-error-1"}
|
||||
mock_context, {"message": "Message", "thread_id": "conv-1", "correlation_id": "corr-entity-error-1"}
|
||||
)
|
||||
|
||||
assert result["status"] == "error"
|
||||
@@ -608,7 +608,7 @@ class TestErrorHandling:
|
||||
mock_context = Mock()
|
||||
|
||||
result = await entity.run_agent(
|
||||
mock_context, {"message": "Message", "conversation_id": "conv-1", "correlation_id": "corr-entity-error-2"}
|
||||
mock_context, {"message": "Message", "thread_id": "conv-1", "correlation_id": "corr-entity-error-2"}
|
||||
)
|
||||
|
||||
assert result["status"] == "error"
|
||||
@@ -624,7 +624,7 @@ class TestErrorHandling:
|
||||
mock_context = Mock()
|
||||
|
||||
result = await entity.run_agent(
|
||||
mock_context, {"message": "Message", "conversation_id": "conv-1", "correlation_id": "corr-entity-error-3"}
|
||||
mock_context, {"message": "Message", "thread_id": "conv-1", "correlation_id": "corr-entity-error-3"}
|
||||
)
|
||||
|
||||
assert result["status"] == "error"
|
||||
@@ -659,12 +659,12 @@ class TestErrorHandling:
|
||||
|
||||
result = await entity.run_agent(
|
||||
mock_context,
|
||||
{"message": "Test message", "conversation_id": "conv-123", "correlation_id": "corr-entity-error-4"},
|
||||
{"message": "Test message", "thread_id": "conv-123", "correlation_id": "corr-entity-error-4"},
|
||||
)
|
||||
|
||||
# Even on error, message info should be preserved
|
||||
assert result["message"] == "Test message"
|
||||
assert result["conversation_id"] == "conv-123"
|
||||
assert result["thread_id"] == "conv-123"
|
||||
assert result["status"] == "error"
|
||||
|
||||
|
||||
@@ -680,7 +680,7 @@ class TestConversationHistory:
|
||||
mock_context = Mock()
|
||||
|
||||
await entity.run_agent(
|
||||
mock_context, {"message": "Message", "conversation_id": "conv-1", "correlation_id": "corr-entity-history-1"}
|
||||
mock_context, {"message": "Message", "thread_id": "conv-1", "correlation_id": "corr-entity-history-1"}
|
||||
)
|
||||
|
||||
# Check both user and assistant messages have timestamps
|
||||
@@ -701,19 +701,19 @@ class TestConversationHistory:
|
||||
mock_agent.run = AsyncMock(return_value=_agent_response("Response 1"))
|
||||
await entity.run_agent(
|
||||
mock_context,
|
||||
{"message": "Message 1", "conversation_id": "conv-1", "correlation_id": "corr-entity-history-2a"},
|
||||
{"message": "Message 1", "thread_id": "conv-1", "correlation_id": "corr-entity-history-2a"},
|
||||
)
|
||||
|
||||
mock_agent.run = AsyncMock(return_value=_agent_response("Response 2"))
|
||||
await entity.run_agent(
|
||||
mock_context,
|
||||
{"message": "Message 2", "conversation_id": "conv-1", "correlation_id": "corr-entity-history-2b"},
|
||||
{"message": "Message 2", "thread_id": "conv-1", "correlation_id": "corr-entity-history-2b"},
|
||||
)
|
||||
|
||||
mock_agent.run = AsyncMock(return_value=_agent_response("Response 3"))
|
||||
await entity.run_agent(
|
||||
mock_context,
|
||||
{"message": "Message 3", "conversation_id": "conv-1", "correlation_id": "corr-entity-history-2c"},
|
||||
{"message": "Message 3", "thread_id": "conv-1", "correlation_id": "corr-entity-history-2c"},
|
||||
)
|
||||
|
||||
# Verify order
|
||||
@@ -735,11 +735,11 @@ class TestConversationHistory:
|
||||
|
||||
await entity.run_agent(
|
||||
mock_context,
|
||||
{"message": "Message 1", "conversation_id": "conv-1", "correlation_id": "corr-entity-history-3a"},
|
||||
{"message": "Message 1", "thread_id": "conv-1", "correlation_id": "corr-entity-history-3a"},
|
||||
)
|
||||
await entity.run_agent(
|
||||
mock_context,
|
||||
{"message": "Message 2", "conversation_id": "conv-1", "correlation_id": "corr-entity-history-3b"},
|
||||
{"message": "Message 2", "thread_id": "conv-1", "correlation_id": "corr-entity-history-3b"},
|
||||
)
|
||||
|
||||
# Check role alternation
|
||||
@@ -763,8 +763,8 @@ class TestRunRequestSupport:
|
||||
|
||||
request = RunRequest(
|
||||
message="Test message",
|
||||
conversation_id="conv-123",
|
||||
role=ChatRole.USER,
|
||||
thread_id="conv-123",
|
||||
role=Role.USER,
|
||||
enable_tool_calls=True,
|
||||
correlation_id="corr-runreq-1",
|
||||
)
|
||||
@@ -774,7 +774,7 @@ class TestRunRequestSupport:
|
||||
assert result["status"] == "success"
|
||||
assert result["response"] == "Response"
|
||||
assert result["message"] == "Test message"
|
||||
assert result["conversation_id"] == "conv-123"
|
||||
assert result["thread_id"] == "conv-123"
|
||||
|
||||
async def test_run_agent_with_dict_request(self) -> None:
|
||||
"""Test run_agent with a dictionary request."""
|
||||
@@ -786,7 +786,7 @@ class TestRunRequestSupport:
|
||||
|
||||
request_dict = {
|
||||
"message": "Test message",
|
||||
"conversation_id": "conv-456",
|
||||
"thread_id": "conv-456",
|
||||
"role": "system",
|
||||
"enable_tool_calls": False,
|
||||
"correlation_id": "corr-runreq-2",
|
||||
@@ -796,7 +796,7 @@ class TestRunRequestSupport:
|
||||
|
||||
assert result["status"] == "success"
|
||||
assert result["message"] == "Test message"
|
||||
assert result["conversation_id"] == "conv-456"
|
||||
assert result["thread_id"] == "conv-456"
|
||||
|
||||
async def test_run_agent_with_string_raises_without_correlation(self) -> None:
|
||||
"""Test that run_agent rejects legacy string input without correlation ID."""
|
||||
@@ -820,8 +820,8 @@ class TestRunRequestSupport:
|
||||
# Send as system role
|
||||
request = RunRequest(
|
||||
message="System message",
|
||||
conversation_id="conv-runreq-3",
|
||||
role=ChatRole.SYSTEM,
|
||||
thread_id="conv-runreq-3",
|
||||
role=Role.SYSTEM,
|
||||
correlation_id="corr-runreq-3",
|
||||
)
|
||||
|
||||
@@ -843,7 +843,7 @@ class TestRunRequestSupport:
|
||||
|
||||
request = RunRequest(
|
||||
message="What is the answer?",
|
||||
conversation_id="conv-runreq-4",
|
||||
thread_id="conv-runreq-4",
|
||||
response_format=EntityStructuredResponse,
|
||||
correlation_id="corr-runreq-4",
|
||||
)
|
||||
@@ -864,7 +864,7 @@ class TestRunRequestSupport:
|
||||
mock_context = Mock()
|
||||
|
||||
request = RunRequest(
|
||||
message="Test", conversation_id="conv-runreq-5", enable_tool_calls=False, correlation_id="corr-runreq-5"
|
||||
message="Test", thread_id="conv-runreq-5", enable_tool_calls=False, correlation_id="corr-runreq-5"
|
||||
)
|
||||
|
||||
result = await entity.run_agent(mock_context, request)
|
||||
@@ -884,7 +884,7 @@ class TestRunRequestSupport:
|
||||
mock_context.operation_name = "run_agent"
|
||||
mock_context.get_input.return_value = {
|
||||
"message": "Test message",
|
||||
"conversation_id": "conv-789",
|
||||
"thread_id": "conv-789",
|
||||
"role": "user",
|
||||
"enable_tool_calls": True,
|
||||
"correlation_id": "corr-runreq-6",
|
||||
|
||||
@@ -1,34 +1,19 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
"""Unit tests for data models (AgentSessionId, RunRequest, AgentResponse, ChatRole)."""
|
||||
"""Unit tests for data models (AgentSessionId, RunRequest, AgentResponse)."""
|
||||
|
||||
import azure.durable_functions as df
|
||||
import pytest
|
||||
from agent_framework import Role
|
||||
from pydantic import BaseModel
|
||||
|
||||
from agent_framework_azurefunctions._models import AgentResponse, AgentSessionId, ChatRole, RunRequest
|
||||
from agent_framework_azurefunctions._models import AgentResponse, AgentSessionId, RunRequest
|
||||
|
||||
|
||||
class ModuleStructuredResponse(BaseModel):
|
||||
value: int
|
||||
|
||||
|
||||
class TestChatRole:
|
||||
"""Test suite for ChatRole enum."""
|
||||
|
||||
def test_chat_role_values(self) -> None:
|
||||
"""Test that ChatRole has correct values."""
|
||||
assert ChatRole.USER == "user"
|
||||
assert ChatRole.SYSTEM == "system"
|
||||
assert ChatRole.ASSISTANT == "assistant"
|
||||
|
||||
def test_chat_role_is_string(self) -> None:
|
||||
"""Test that ChatRole values are strings."""
|
||||
assert isinstance(ChatRole.USER.value, str)
|
||||
assert isinstance(ChatRole.SYSTEM.value, str)
|
||||
assert isinstance(ChatRole.ASSISTANT.value, str)
|
||||
|
||||
|
||||
class TestAgentSessionId:
|
||||
"""Test suite for AgentSessionId."""
|
||||
|
||||
@@ -164,49 +149,55 @@ class TestRunRequest:
|
||||
|
||||
def test_init_with_defaults(self) -> None:
|
||||
"""Test RunRequest initialization with defaults."""
|
||||
request = RunRequest(message="Hello", conversation_id="conv-default")
|
||||
request = RunRequest(message="Hello", thread_id="thread-default")
|
||||
|
||||
assert request.message == "Hello"
|
||||
assert request.role == ChatRole.USER
|
||||
assert request.role == Role.USER
|
||||
assert request.response_format is None
|
||||
assert request.enable_tool_calls is True
|
||||
assert request.conversation_id == "conv-default"
|
||||
assert request.thread_id == "thread-default"
|
||||
|
||||
def test_init_with_all_fields(self) -> None:
|
||||
"""Test RunRequest initialization with all fields."""
|
||||
schema = ModuleStructuredResponse
|
||||
request = RunRequest(
|
||||
message="Hello",
|
||||
conversation_id="conv-123",
|
||||
role=ChatRole.SYSTEM,
|
||||
thread_id="thread-123",
|
||||
role=Role.SYSTEM,
|
||||
response_format=schema,
|
||||
enable_tool_calls=False,
|
||||
)
|
||||
|
||||
assert request.message == "Hello"
|
||||
assert request.role == ChatRole.SYSTEM
|
||||
assert request.role == Role.SYSTEM
|
||||
assert request.response_format is schema
|
||||
assert request.enable_tool_calls is False
|
||||
assert request.conversation_id == "conv-123"
|
||||
assert request.thread_id == "thread-123"
|
||||
|
||||
def test_init_coerces_string_role(self) -> None:
|
||||
"""Ensure string role values are coerced into Role instances."""
|
||||
request = RunRequest(message="Hello", thread_id="thread-str-role", role="system") # type: ignore[arg-type]
|
||||
|
||||
assert request.role == Role.SYSTEM
|
||||
|
||||
def test_to_dict_with_defaults(self) -> None:
|
||||
"""Test to_dict with default values."""
|
||||
request = RunRequest(message="Test message", conversation_id="conv-to-dict")
|
||||
request = RunRequest(message="Test message", thread_id="thread-to-dict")
|
||||
data = request.to_dict()
|
||||
|
||||
assert data["message"] == "Test message"
|
||||
assert data["enable_tool_calls"] is True
|
||||
assert data["role"] == "user"
|
||||
assert "response_format" not in data or data["response_format"] is None
|
||||
assert data["conversation_id"] == "conv-to-dict"
|
||||
assert data["thread_id"] == "thread-to-dict"
|
||||
|
||||
def test_to_dict_with_all_fields(self) -> None:
|
||||
"""Test to_dict with all fields."""
|
||||
schema = ModuleStructuredResponse
|
||||
request = RunRequest(
|
||||
message="Hello",
|
||||
conversation_id="conv-456",
|
||||
role=ChatRole.ASSISTANT,
|
||||
thread_id="thread-456",
|
||||
role=Role.ASSISTANT,
|
||||
response_format=schema,
|
||||
enable_tool_calls=False,
|
||||
)
|
||||
@@ -218,17 +209,17 @@ class TestRunRequest:
|
||||
assert data["response_format"]["module"] == schema.__module__
|
||||
assert data["response_format"]["qualname"] == schema.__qualname__
|
||||
assert data["enable_tool_calls"] is False
|
||||
assert data["conversation_id"] == "conv-456"
|
||||
assert data["thread_id"] == "thread-456"
|
||||
|
||||
def test_from_dict_with_defaults(self) -> None:
|
||||
"""Test from_dict with minimal data."""
|
||||
data = {"message": "Hello", "conversation_id": "conv-from-dict"}
|
||||
data = {"message": "Hello", "thread_id": "thread-from-dict"}
|
||||
request = RunRequest.from_dict(data)
|
||||
|
||||
assert request.message == "Hello"
|
||||
assert request.role == ChatRole.USER
|
||||
assert request.role == Role.USER
|
||||
assert request.enable_tool_calls is True
|
||||
assert request.conversation_id == "conv-from-dict"
|
||||
assert request.thread_id == "thread-from-dict"
|
||||
|
||||
def test_from_dict_with_all_fields(self) -> None:
|
||||
"""Test from_dict with all fields."""
|
||||
@@ -241,38 +232,39 @@ class TestRunRequest:
|
||||
"qualname": ModuleStructuredResponse.__qualname__,
|
||||
},
|
||||
"enable_tool_calls": False,
|
||||
"conversation_id": "conv-789",
|
||||
"thread_id": "thread-789",
|
||||
}
|
||||
request = RunRequest.from_dict(data)
|
||||
|
||||
assert request.message == "Test"
|
||||
assert request.role == ChatRole.SYSTEM
|
||||
assert request.role == Role.SYSTEM
|
||||
assert request.response_format is ModuleStructuredResponse
|
||||
assert request.enable_tool_calls is False
|
||||
assert request.conversation_id == "conv-789"
|
||||
assert request.thread_id == "thread-789"
|
||||
|
||||
def test_from_dict_invalid_role_defaults_to_user(self) -> None:
|
||||
"""Test from_dict with invalid role defaults to USER."""
|
||||
data = {"message": "Test", "role": "invalid_role", "conversation_id": "conv-invalid-role"}
|
||||
def test_from_dict_with_unknown_role_preserves_value(self) -> None:
|
||||
"""Test from_dict keeps custom roles intact."""
|
||||
data = {"message": "Test", "role": "reviewer", "thread_id": "thread-with-custom-role"}
|
||||
request = RunRequest.from_dict(data)
|
||||
|
||||
assert request.role == ChatRole.USER
|
||||
assert request.role.value == "reviewer"
|
||||
assert request.role != Role.USER
|
||||
|
||||
def test_from_dict_empty_message(self) -> None:
|
||||
"""Test from_dict with empty message."""
|
||||
data = {"conversation_id": "conv-empty"}
|
||||
data = {"thread_id": "thread-empty"}
|
||||
request = RunRequest.from_dict(data)
|
||||
|
||||
assert request.message == ""
|
||||
assert request.role == ChatRole.USER
|
||||
assert request.conversation_id == "conv-empty"
|
||||
assert request.role == Role.USER
|
||||
assert request.thread_id == "thread-empty"
|
||||
|
||||
def test_round_trip_dict_conversion(self) -> None:
|
||||
"""Test round-trip to_dict and from_dict."""
|
||||
original = RunRequest(
|
||||
message="Test message",
|
||||
conversation_id="conv-123",
|
||||
role=ChatRole.SYSTEM,
|
||||
thread_id="thread-123",
|
||||
role=Role.SYSTEM,
|
||||
response_format=ModuleStructuredResponse,
|
||||
enable_tool_calls=False,
|
||||
)
|
||||
@@ -284,13 +276,13 @@ class TestRunRequest:
|
||||
assert restored.role == original.role
|
||||
assert restored.response_format is ModuleStructuredResponse
|
||||
assert restored.enable_tool_calls == original.enable_tool_calls
|
||||
assert restored.conversation_id == original.conversation_id
|
||||
assert restored.thread_id == original.thread_id
|
||||
|
||||
def test_round_trip_with_pydantic_response_format(self) -> None:
|
||||
"""Ensure Pydantic response formats serialize and deserialize properly."""
|
||||
original = RunRequest(
|
||||
message="Structured",
|
||||
conversation_id="conv-pydantic",
|
||||
thread_id="thread-pydantic",
|
||||
response_format=ModuleStructuredResponse,
|
||||
)
|
||||
|
||||
@@ -305,14 +297,14 @@ class TestRunRequest:
|
||||
|
||||
def test_init_with_correlation_id(self) -> None:
|
||||
"""Test RunRequest initialization with correlation_id."""
|
||||
request = RunRequest(message="Test message", conversation_id="conv-corr-init", correlation_id="corr-123")
|
||||
request = RunRequest(message="Test message", thread_id="thread-corr-init", correlation_id="corr-123")
|
||||
|
||||
assert request.message == "Test message"
|
||||
assert request.correlation_id == "corr-123"
|
||||
|
||||
def test_to_dict_with_correlation_id(self) -> None:
|
||||
"""Test to_dict includes correlation_id."""
|
||||
request = RunRequest(message="Test", conversation_id="conv-corr-to-dict", correlation_id="corr-456")
|
||||
request = RunRequest(message="Test", thread_id="thread-corr-to-dict", correlation_id="corr-456")
|
||||
data = request.to_dict()
|
||||
|
||||
assert data["message"] == "Test"
|
||||
@@ -320,19 +312,19 @@ class TestRunRequest:
|
||||
|
||||
def test_from_dict_with_correlation_id(self) -> None:
|
||||
"""Test from_dict with correlation_id."""
|
||||
data = {"message": "Test", "correlation_id": "corr-789", "conversation_id": "conv-corr-from-dict"}
|
||||
data = {"message": "Test", "correlation_id": "corr-789", "thread_id": "thread-corr-from-dict"}
|
||||
request = RunRequest.from_dict(data)
|
||||
|
||||
assert request.message == "Test"
|
||||
assert request.correlation_id == "corr-789"
|
||||
assert request.conversation_id == "conv-corr-from-dict"
|
||||
assert request.thread_id == "thread-corr-from-dict"
|
||||
|
||||
def test_round_trip_with_correlation_id(self) -> None:
|
||||
"""Test round-trip to_dict and from_dict with correlation_id."""
|
||||
original = RunRequest(
|
||||
message="Test message",
|
||||
conversation_id="conv-123",
|
||||
role=ChatRole.SYSTEM,
|
||||
thread_id="thread-123",
|
||||
role=Role.SYSTEM,
|
||||
correlation_id="corr-123",
|
||||
)
|
||||
|
||||
@@ -342,7 +334,7 @@ class TestRunRequest:
|
||||
assert restored.message == original.message
|
||||
assert restored.role == original.role
|
||||
assert restored.correlation_id == original.correlation_id
|
||||
assert restored.conversation_id == original.conversation_id
|
||||
assert restored.thread_id == original.thread_id
|
||||
|
||||
|
||||
class TestAgentResponse:
|
||||
@@ -351,12 +343,12 @@ class TestAgentResponse:
|
||||
def test_init_with_required_fields(self) -> None:
|
||||
"""Test AgentResponse initialization with required fields."""
|
||||
response = AgentResponse(
|
||||
response="Test response", message="Test message", conversation_id="conv-123", status="success"
|
||||
response="Test response", message="Test message", thread_id="thread-123", status="success"
|
||||
)
|
||||
|
||||
assert response.response == "Test response"
|
||||
assert response.message == "Test message"
|
||||
assert response.conversation_id == "conv-123"
|
||||
assert response.thread_id == "thread-123"
|
||||
assert response.status == "success"
|
||||
assert response.message_count == 0
|
||||
assert response.error is None
|
||||
@@ -369,7 +361,7 @@ class TestAgentResponse:
|
||||
response = AgentResponse(
|
||||
response=None,
|
||||
message="What is the answer?",
|
||||
conversation_id="conv-456",
|
||||
thread_id="thread-456",
|
||||
status="success",
|
||||
message_count=5,
|
||||
error=None,
|
||||
@@ -384,13 +376,13 @@ class TestAgentResponse:
|
||||
def test_to_dict_with_text_response(self) -> None:
|
||||
"""Test to_dict with text response."""
|
||||
response = AgentResponse(
|
||||
response="Text response", message="Message", conversation_id="conv-1", status="success", message_count=3
|
||||
response="Text response", message="Message", thread_id="thread-1", status="success", message_count=3
|
||||
)
|
||||
data = response.to_dict()
|
||||
|
||||
assert data["response"] == "Text response"
|
||||
assert data["message"] == "Message"
|
||||
assert data["conversation_id"] == "conv-1"
|
||||
assert data["thread_id"] == "thread-1"
|
||||
assert data["status"] == "success"
|
||||
assert data["message_count"] == 3
|
||||
assert "structured_response" not in data
|
||||
@@ -403,7 +395,7 @@ class TestAgentResponse:
|
||||
response = AgentResponse(
|
||||
response=None,
|
||||
message="Question",
|
||||
conversation_id="conv-2",
|
||||
thread_id="thread-2",
|
||||
status="success",
|
||||
structured_response=structured,
|
||||
)
|
||||
@@ -417,7 +409,7 @@ class TestAgentResponse:
|
||||
response = AgentResponse(
|
||||
response=None,
|
||||
message="Failed message",
|
||||
conversation_id="conv-3",
|
||||
thread_id="thread-3",
|
||||
status="error",
|
||||
error="Something went wrong",
|
||||
error_type="ValueError",
|
||||
@@ -434,7 +426,7 @@ class TestAgentResponse:
|
||||
response = AgentResponse(
|
||||
response="Text response",
|
||||
message="Message",
|
||||
conversation_id="conv-4",
|
||||
thread_id="thread-4",
|
||||
status="success",
|
||||
structured_response=structured,
|
||||
)
|
||||
@@ -452,26 +444,26 @@ class TestModelIntegration:
|
||||
def test_run_request_with_session_id(self) -> None:
|
||||
"""Test using RunRequest with AgentSessionId."""
|
||||
session_id = AgentSessionId.with_random_key("AgentEntity")
|
||||
request = RunRequest(message="Test message", conversation_id=str(session_id))
|
||||
request = RunRequest(message="Test message", thread_id=str(session_id))
|
||||
|
||||
assert request.conversation_id is not None
|
||||
assert request.conversation_id == str(session_id)
|
||||
assert request.conversation_id.startswith("@AgentEntity@")
|
||||
assert request.thread_id is not None
|
||||
assert request.thread_id == str(session_id)
|
||||
assert request.thread_id.startswith("@AgentEntity@")
|
||||
|
||||
def test_response_from_run_request(self) -> None:
|
||||
"""Test creating AgentResponse from RunRequest."""
|
||||
request = RunRequest(message="What is 2+2?", conversation_id="conv-123", role=ChatRole.USER)
|
||||
request = RunRequest(message="What is 2+2?", thread_id="thread-123", role=Role.USER)
|
||||
|
||||
response = AgentResponse(
|
||||
response="4",
|
||||
message=request.message,
|
||||
conversation_id=request.conversation_id,
|
||||
thread_id=request.thread_id,
|
||||
status="success",
|
||||
message_count=1,
|
||||
)
|
||||
|
||||
assert response.message == request.message
|
||||
assert response.conversation_id == request.conversation_id
|
||||
assert response.thread_id == request.thread_id
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -129,8 +129,8 @@ class TestDurableAIAgent:
|
||||
assert request["enable_tool_calls"] is True
|
||||
assert "correlation_id" in request
|
||||
assert request["correlation_id"] == "correlation-guid"
|
||||
assert "conversation_id" in request
|
||||
assert request["conversation_id"] == "thread-guid"
|
||||
assert "thread_id" in request
|
||||
assert request["thread_id"] == "thread-guid"
|
||||
|
||||
def test_run_without_thread(self) -> None:
|
||||
"""Test that run() works without explicit thread (creates unique session key)."""
|
||||
|
||||
Reference in New Issue
Block a user