mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
Python: Fix: AgentRunResponse.created_at returns local datetime labeled as UTC (#2590)
* Initial plan * Fix created_at to use UTC datetime and add unit tests Co-authored-by: moonbox3 <35585003+moonbox3@users.noreply.github.com> * Address code review feedback: move datetime imports to top of test files Co-authored-by: moonbox3 <35585003+moonbox3@users.noreply.github.com> * Add UTC timestamp test for OpenAIResponsesClient._create_response_content Co-authored-by: moonbox3 <35585003+moonbox3@users.noreply.github.com> * Fix lint issue --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: moonbox3 <35585003+moonbox3@users.noreply.github.com> Co-authored-by: Evan Mattson <evan.mattson@microsoft.com>
This commit is contained in:
committed by
GitHub
Unverified
parent
ae06c9e4bb
commit
292a2078fa
@@ -5,7 +5,7 @@ import logging
|
||||
import uuid
|
||||
from collections.abc import AsyncIterable
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime
|
||||
from datetime import datetime, timezone
|
||||
from typing import TYPE_CHECKING, Any, ClassVar, TypedDict, cast
|
||||
|
||||
from agent_framework import (
|
||||
@@ -269,7 +269,7 @@ class WorkflowAgent(BaseAgent):
|
||||
author_name=self.name,
|
||||
response_id=response_id,
|
||||
message_id=str(uuid.uuid4()),
|
||||
created_at=datetime.now().strftime("%Y-%m-%dT%H:%M:%S.%fZ"),
|
||||
created_at=datetime.now(tz=timezone.utc).strftime("%Y-%m-%dT%H:%M:%S.%fZ"),
|
||||
)
|
||||
case _:
|
||||
# Ignore workflow-internal events
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import json
|
||||
import sys
|
||||
from collections.abc import AsyncIterable, Awaitable, Callable, Mapping, MutableMapping, MutableSequence, Sequence
|
||||
from datetime import datetime
|
||||
from datetime import datetime, timezone
|
||||
from itertools import chain
|
||||
from typing import Any, TypeVar
|
||||
|
||||
@@ -214,7 +214,7 @@ class OpenAIBaseChatClient(OpenAIBase, BaseChatClient):
|
||||
messages.append(ChatMessage(role="assistant", contents=contents))
|
||||
return ChatResponse(
|
||||
response_id=response.id,
|
||||
created_at=datetime.fromtimestamp(response.created).strftime("%Y-%m-%dT%H:%M:%S.%fZ"),
|
||||
created_at=datetime.fromtimestamp(response.created, tz=timezone.utc).strftime("%Y-%m-%dT%H:%M:%S.%fZ"),
|
||||
usage_details=self._usage_details_from_openai(response.usage) if response.usage else None,
|
||||
messages=messages,
|
||||
model_id=response.model,
|
||||
@@ -249,7 +249,7 @@ class OpenAIBaseChatClient(OpenAIBase, BaseChatClient):
|
||||
if text_content := self._parse_text_from_choice(choice):
|
||||
contents.append(text_content)
|
||||
return ChatResponseUpdate(
|
||||
created_at=datetime.fromtimestamp(chunk.created).strftime("%Y-%m-%dT%H:%M:%S.%fZ"),
|
||||
created_at=datetime.fromtimestamp(chunk.created, tz=timezone.utc).strftime("%Y-%m-%dT%H:%M:%S.%fZ"),
|
||||
contents=contents,
|
||||
role=Role.ASSISTANT,
|
||||
model_id=chunk.model,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
from collections.abc import AsyncIterable, Awaitable, Callable, Mapping, MutableMapping, MutableSequence, Sequence
|
||||
from datetime import datetime
|
||||
from datetime import datetime, timezone
|
||||
from itertools import chain
|
||||
from typing import Any, TypeVar
|
||||
|
||||
@@ -815,7 +815,9 @@ class OpenAIBaseResponsesClient(OpenAIBase, BaseChatClient):
|
||||
response_message = ChatMessage(role="assistant", contents=contents)
|
||||
args: dict[str, Any] = {
|
||||
"response_id": response.id,
|
||||
"created_at": datetime.fromtimestamp(response.created_at).strftime("%Y-%m-%dT%H:%M:%S.%fZ"),
|
||||
"created_at": datetime.fromtimestamp(response.created_at, tz=timezone.utc).strftime(
|
||||
"%Y-%m-%dT%H:%M:%S.%fZ"
|
||||
),
|
||||
"messages": response_message,
|
||||
"model_id": response.model,
|
||||
"additional_properties": metadata,
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import base64
|
||||
from collections.abc import AsyncIterable
|
||||
from datetime import datetime, timezone
|
||||
from typing import Any
|
||||
|
||||
import pytest
|
||||
@@ -936,6 +937,52 @@ def test_agent_run_response_update_str_method(text_content: TextContent) -> None
|
||||
assert str(update) == "Test content"
|
||||
|
||||
|
||||
def test_agent_run_response_update_created_at() -> None:
|
||||
"""Test that AgentRunResponseUpdate properly handles created_at timestamps."""
|
||||
# Test with a properly formatted UTC timestamp
|
||||
utc_timestamp = "2024-12-01T00:31:30.000000Z"
|
||||
update = AgentRunResponseUpdate(
|
||||
contents=[TextContent(text="test")],
|
||||
role=Role.ASSISTANT,
|
||||
created_at=utc_timestamp,
|
||||
)
|
||||
assert update.created_at == utc_timestamp
|
||||
assert update.created_at.endswith("Z"), "Timestamp should end with 'Z' for UTC"
|
||||
|
||||
# Verify that we can generate a proper UTC timestamp
|
||||
now_utc = datetime.now(tz=timezone.utc)
|
||||
formatted_utc = now_utc.strftime("%Y-%m-%dT%H:%M:%S.%fZ")
|
||||
update_with_now = AgentRunResponseUpdate(
|
||||
contents=[TextContent(text="test")],
|
||||
role=Role.ASSISTANT,
|
||||
created_at=formatted_utc,
|
||||
)
|
||||
assert update_with_now.created_at == formatted_utc
|
||||
assert update_with_now.created_at.endswith("Z")
|
||||
|
||||
|
||||
def test_agent_run_response_created_at() -> None:
|
||||
"""Test that AgentRunResponse properly handles created_at timestamps."""
|
||||
# Test with a properly formatted UTC timestamp
|
||||
utc_timestamp = "2024-12-01T00:31:30.000000Z"
|
||||
response = AgentRunResponse(
|
||||
messages=[ChatMessage(role=Role.ASSISTANT, text="Hello")],
|
||||
created_at=utc_timestamp,
|
||||
)
|
||||
assert response.created_at == utc_timestamp
|
||||
assert response.created_at.endswith("Z"), "Timestamp should end with 'Z' for UTC"
|
||||
|
||||
# Verify that we can generate a proper UTC timestamp
|
||||
now_utc = datetime.now(tz=timezone.utc)
|
||||
formatted_utc = now_utc.strftime("%Y-%m-%dT%H:%M:%S.%fZ")
|
||||
response_with_now = AgentRunResponse(
|
||||
messages=[ChatMessage(role=Role.ASSISTANT, text="Hello")],
|
||||
created_at=formatted_utc,
|
||||
)
|
||||
assert response_with_now.created_at == formatted_utc
|
||||
assert response_with_now.created_at.endswith("Z")
|
||||
|
||||
|
||||
# region ErrorContent
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
from copy import deepcopy
|
||||
from datetime import datetime, timezone
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
import pytest
|
||||
@@ -370,3 +371,75 @@ async def test_get_streaming_no_stream(
|
||||
messages=chat_history,
|
||||
)
|
||||
]
|
||||
|
||||
|
||||
# region UTC Timestamp Tests
|
||||
|
||||
|
||||
def test_chat_response_created_at_uses_utc(openai_unit_test_env: dict[str, str]):
|
||||
"""Test that ChatResponse.created_at uses UTC timestamp, not local time.
|
||||
|
||||
This is a regression test for the issue where created_at was using local time
|
||||
but labeling it as UTC (with 'Z' suffix).
|
||||
"""
|
||||
from agent_framework import ChatOptions
|
||||
|
||||
# Use a specific Unix timestamp: 1733011890 = 2024-12-01T00:31:30Z (UTC)
|
||||
# This ensures we test that the timestamp is actually converted to UTC
|
||||
utc_timestamp = 1733011890
|
||||
|
||||
mock_response = ChatCompletion(
|
||||
id="test_id",
|
||||
choices=[
|
||||
Choice(index=0, message=ChatCompletionMessage(content="test", role="assistant"), finish_reason="stop")
|
||||
],
|
||||
created=utc_timestamp,
|
||||
model="test",
|
||||
object="chat.completion",
|
||||
)
|
||||
|
||||
client = OpenAIChatClient()
|
||||
response = client._create_chat_response(mock_response, ChatOptions())
|
||||
|
||||
# Verify that created_at is correctly formatted as UTC
|
||||
assert response.created_at is not None
|
||||
assert response.created_at.endswith("Z"), "Timestamp should end with 'Z' for UTC"
|
||||
|
||||
# Parse the timestamp and verify it matches UTC time
|
||||
expected_utc_time = datetime.fromtimestamp(utc_timestamp, tz=timezone.utc)
|
||||
expected_formatted = expected_utc_time.strftime("%Y-%m-%dT%H:%M:%S.%fZ")
|
||||
assert response.created_at == expected_formatted, (
|
||||
f"Expected UTC timestamp {expected_formatted}, got {response.created_at}"
|
||||
)
|
||||
|
||||
|
||||
def test_chat_response_update_created_at_uses_utc(openai_unit_test_env: dict[str, str]):
|
||||
"""Test that ChatResponseUpdate.created_at uses UTC timestamp, not local time.
|
||||
|
||||
This is a regression test for the issue where created_at was using local time
|
||||
but labeling it as UTC (with 'Z' suffix).
|
||||
"""
|
||||
# Use a specific Unix timestamp: 1733011890 = 2024-12-01T00:31:30Z (UTC)
|
||||
utc_timestamp = 1733011890
|
||||
|
||||
mock_chunk = ChatCompletionChunk(
|
||||
id="test_id",
|
||||
choices=[ChunkChoice(index=0, delta=ChunkChoiceDelta(content="test", role="assistant"), finish_reason="stop")],
|
||||
created=utc_timestamp,
|
||||
model="test",
|
||||
object="chat.completion.chunk",
|
||||
)
|
||||
|
||||
client = OpenAIChatClient()
|
||||
response_update = client._create_chat_response_update(mock_chunk)
|
||||
|
||||
# Verify that created_at is correctly formatted as UTC
|
||||
assert response_update.created_at is not None
|
||||
assert response_update.created_at.endswith("Z"), "Timestamp should end with 'Z' for UTC"
|
||||
|
||||
# Parse the timestamp and verify it matches UTC time
|
||||
expected_utc_time = datetime.fromtimestamp(utc_timestamp, tz=timezone.utc)
|
||||
expected_formatted = expected_utc_time.strftime("%Y-%m-%dT%H:%M:%S.%fZ")
|
||||
assert response_update.created_at == expected_formatted, (
|
||||
f"Expected UTC timestamp {expected_formatted}, got {response_update.created_at}"
|
||||
)
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import asyncio
|
||||
import base64
|
||||
import os
|
||||
from datetime import datetime, timezone
|
||||
from typing import Annotated
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
@@ -684,6 +685,51 @@ def test_create_response_content_with_mcp_approval_request() -> None:
|
||||
assert req.function_call.additional_properties["server_label"] == "My_MCP"
|
||||
|
||||
|
||||
def test_responses_client_created_at_uses_utc(openai_unit_test_env: dict[str, str]) -> None:
|
||||
"""Test that ChatResponse from responses client uses UTC timestamp.
|
||||
|
||||
This is a regression test for the issue where created_at was using local time
|
||||
but labeling it as UTC (with 'Z' suffix).
|
||||
"""
|
||||
client = OpenAIResponsesClient()
|
||||
|
||||
# Use a specific Unix timestamp: 1733011890 = 2024-12-01T00:31:30Z (UTC)
|
||||
utc_timestamp = 1733011890
|
||||
|
||||
mock_response = MagicMock()
|
||||
mock_response.output_parsed = None
|
||||
mock_response.metadata = {}
|
||||
mock_response.usage = None
|
||||
mock_response.id = "test-id"
|
||||
mock_response.model = "test-model"
|
||||
mock_response.created_at = utc_timestamp
|
||||
|
||||
mock_message_content = MagicMock()
|
||||
mock_message_content.type = "output_text"
|
||||
mock_message_content.text = "Test response"
|
||||
mock_message_content.annotations = None
|
||||
|
||||
mock_message_item = MagicMock()
|
||||
mock_message_item.type = "message"
|
||||
mock_message_item.content = [mock_message_content]
|
||||
|
||||
mock_response.output = [mock_message_item]
|
||||
|
||||
with patch.object(client, "_get_metadata_from_response", return_value={}):
|
||||
response = client._create_response_content(mock_response, chat_options=ChatOptions()) # type: ignore
|
||||
|
||||
# Verify that created_at is correctly formatted as UTC
|
||||
assert response.created_at is not None
|
||||
assert response.created_at.endswith("Z"), "Timestamp should end with 'Z' for UTC"
|
||||
|
||||
# Parse the timestamp and verify it matches UTC time
|
||||
expected_utc_time = datetime.fromtimestamp(utc_timestamp, tz=timezone.utc)
|
||||
expected_formatted = expected_utc_time.strftime("%Y-%m-%dT%H:%M:%S.%fZ")
|
||||
assert response.created_at == expected_formatted, (
|
||||
f"Expected UTC timestamp {expected_formatted}, got {response.created_at}"
|
||||
)
|
||||
|
||||
|
||||
def test_tools_to_response_tools_with_raw_image_generation() -> None:
|
||||
"""Test that raw image_generation tool dict is handled correctly with parameter mapping."""
|
||||
client = OpenAIResponsesClient(model_id="test-model", api_key="test-key")
|
||||
|
||||
Reference in New Issue
Block a user