Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions python/packages/core/agent_framework/_workflows/_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
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 (
Expand Down Expand Up @@ -269,7 +269,7 @@ def _convert_workflow_event_to_agent_update(
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
Expand Down
6 changes: 3 additions & 3 deletions python/packages/core/agent_framework/openai/_chat_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -214,7 +214,7 @@ def _create_chat_response(self, response: ChatCompletion, chat_options: ChatOpti
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,
Expand Down Expand Up @@ -249,7 +249,7 @@ def _create_chat_response_update(
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,
Expand Down
Original file line number Diff line number Diff line change
@@ -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

Expand Down Expand Up @@ -815,7 +815,9 @@ def _create_response_content(
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,
Expand Down
47 changes: 47 additions & 0 deletions python/packages/core/tests/core/test_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import base64
from collections.abc import AsyncIterable
from datetime import datetime, timezone
from typing import Any

import pytest
Expand Down Expand Up @@ -935,6 +936,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


Expand Down
73 changes: 73 additions & 0 deletions python/packages/core/tests/openai/test_openai_chat_client_base.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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}"
)
46 changes: 46 additions & 0 deletions python/packages/core/tests/openai/test_openai_responses_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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")
Expand Down
Loading