mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
Python: [BREAKING] Simplify API: ChatAgent -> Agent, ChatMessage -> Message (#3747)
* [BREAKING] Rename ChatAgent -> Agent, ChatMessage -> Message, ChatClientProtocol -> SupportsChatGetResponse Simplify the public API by removing redundant 'Chat' prefix from core types: - ChatAgent -> Agent - RawChatAgent -> RawAgent - ChatMessage -> Message - ChatClientProtocol -> SupportsChatGetResponse Also renamed internal WorkflowMessage (was Message in _runner_context) to avoid collision. No backward compatibility aliases - this is a clean breaking change. * [BREAKING] Rename Agent chat_client parameter to client * Fix rebase issues: WorkflowMessage references and broken markdown links * Fix formatting and lint issues from code quality checks * Fix import ordering in workflow sample files * fixed rebase * Fix test failures: use WorkflowMessage and A2AMessage after ChatMessage→Message rename - Replace Message(data=..., source_id=...) with WorkflowMessage(...) in workflow tests - Fix isinstance check in A2A agent to use A2AMessage instead of Message - Fix import in test_workflow_observability.py (Message→WorkflowMessage) * Fix lint, fmt, and sample errors after ChatMessage→Message rename - Auto-fix 70+ ruff lint issues across samples (ChatMessage→Message refs) - Fix HostedVectorStoreContent→Content.from_hosted_vector_store in file search sample - Fix _normalize_messages→normalize_messages in custom agent sample - Fix context.terminate→raise MiddlewareTermination in middleware samples - Fix with_update_hook→with_transform_hook in override middleware sample - Add TOptions_co import back to custom_chat_client sample - Add noqa for FastAPI File() default in chatkit sample - Fix B023 loop variable capture in weather agent sample * fix: update Agent constructor calls from chat_client to client in declaration-only tool tests * fix: add register_cleanup to devui lazy-loading proxy and type stub * fixed tests and updated new pieces * fix agui typevar * fix merge errors * fix merge conflicts * fiux merge * Remove unused links --------- Co-authored-by: Evan Mattson <evan.mattson@microsoft.com>
This commit is contained in:
committed by
GitHub
Unverified
parent
a4c9e43afb
commit
0521f5bed8
@@ -7,7 +7,7 @@ from typing import Any
|
||||
from uuid import uuid4
|
||||
|
||||
import redis.asyncio as redis
|
||||
from agent_framework import ChatMessage
|
||||
from agent_framework import Message
|
||||
from agent_framework._serialization import SerializationMixin
|
||||
from redis.credentials import CredentialProvider
|
||||
|
||||
@@ -64,7 +64,7 @@ class RedisChatMessageStore:
|
||||
thread_id: str | None = None,
|
||||
key_prefix: str = "chat_messages",
|
||||
max_messages: int | None = None,
|
||||
messages: Sequence[ChatMessage] | None = None,
|
||||
messages: Sequence[Message] | None = None,
|
||||
) -> None:
|
||||
"""Initialize the Redis chat message store.
|
||||
|
||||
@@ -186,14 +186,14 @@ class RedisChatMessageStore:
|
||||
self._initial_messages_added = True
|
||||
self._initial_messages.clear()
|
||||
|
||||
async def _add_redis_messages(self, messages: Sequence[ChatMessage]) -> None:
|
||||
async def _add_redis_messages(self, messages: Sequence[Message]) -> None:
|
||||
"""Add multiple messages to Redis using atomic pipeline operation.
|
||||
|
||||
This internal method efficiently adds multiple messages to the Redis list
|
||||
using a single atomic transaction to ensure consistency.
|
||||
|
||||
Args:
|
||||
messages: Sequence of ChatMessage objects to add to Redis.
|
||||
messages: Sequence of Message objects to add to Redis.
|
||||
"""
|
||||
if not messages:
|
||||
return
|
||||
@@ -207,7 +207,7 @@ class RedisChatMessageStore:
|
||||
await pipe.rpush(self.redis_key, serialized_message) # type: ignore[misc]
|
||||
await pipe.execute()
|
||||
|
||||
async def add_messages(self, messages: Sequence[ChatMessage]) -> None:
|
||||
async def add_messages(self, messages: Sequence[Message]) -> None:
|
||||
"""Add messages to the Redis store (ChatMessageStoreProtocol protocol method).
|
||||
|
||||
This method implements the required ChatMessageStoreProtocol protocol for adding messages.
|
||||
@@ -215,7 +215,7 @@ class RedisChatMessageStore:
|
||||
trimming if message limits are configured.
|
||||
|
||||
Args:
|
||||
messages: Sequence of ChatMessage objects to add to the store.
|
||||
messages: Sequence of Message objects to add to the store.
|
||||
Can be empty (no-op) or contain multiple messages.
|
||||
|
||||
Thread Safety:
|
||||
@@ -225,7 +225,7 @@ class RedisChatMessageStore:
|
||||
Example:
|
||||
.. code-block:: python
|
||||
|
||||
messages = [ChatMessage(role="user", text="Hello"), ChatMessage(role="assistant", text="Hi there!")]
|
||||
messages = [Message(role="user", text="Hello"), Message(role="assistant", text="Hi there!")]
|
||||
await store.add_messages(messages)
|
||||
"""
|
||||
if not messages:
|
||||
@@ -244,14 +244,14 @@ class RedisChatMessageStore:
|
||||
# Keep only the most recent max_messages using LTRIM
|
||||
await self._redis_client.ltrim(self.redis_key, -self.max_messages, -1) # type: ignore[misc]
|
||||
|
||||
async def list_messages(self) -> list[ChatMessage]:
|
||||
async def list_messages(self) -> list[Message]:
|
||||
"""Get all messages from the store in chronological order (ChatMessageStoreProtocol protocol method).
|
||||
|
||||
This method implements the required ChatMessageStoreProtocol protocol for retrieving messages.
|
||||
Returns all messages stored in Redis, ordered from oldest (index 0) to newest (index -1).
|
||||
|
||||
Returns:
|
||||
List of ChatMessage objects in chronological order (oldest first).
|
||||
List of Message objects in chronological order (oldest first).
|
||||
Returns empty list if no messages exist or if Redis connection fails.
|
||||
|
||||
Example:
|
||||
@@ -269,7 +269,7 @@ class RedisChatMessageStore:
|
||||
|
||||
if redis_messages:
|
||||
for serialized_message in redis_messages:
|
||||
# Deserialize each JSON message back to ChatMessage
|
||||
# Deserialize each JSON message back to Message
|
||||
message = self._deserialize_message(serialized_message)
|
||||
messages.append(message)
|
||||
|
||||
@@ -390,11 +390,11 @@ class RedisChatMessageStore:
|
||||
"""
|
||||
await self._redis_client.delete(self.redis_key)
|
||||
|
||||
def _serialize_message(self, message: ChatMessage) -> str:
|
||||
"""Serialize a ChatMessage to JSON string.
|
||||
def _serialize_message(self, message: Message) -> str:
|
||||
"""Serialize a Message to JSON string.
|
||||
|
||||
Args:
|
||||
message: ChatMessage to serialize.
|
||||
message: Message to serialize.
|
||||
|
||||
Returns:
|
||||
JSON string representation of the message.
|
||||
@@ -402,17 +402,17 @@ class RedisChatMessageStore:
|
||||
# Serialize to compact JSON (no extra whitespace for Redis efficiency)
|
||||
return message.to_json(separators=(",", ":"))
|
||||
|
||||
def _deserialize_message(self, serialized_message: str) -> ChatMessage:
|
||||
"""Deserialize a JSON string to ChatMessage.
|
||||
def _deserialize_message(self, serialized_message: str) -> Message:
|
||||
"""Deserialize a JSON string to Message.
|
||||
|
||||
Args:
|
||||
serialized_message: JSON string representation of a message.
|
||||
|
||||
Returns:
|
||||
ChatMessage object.
|
||||
Message object.
|
||||
"""
|
||||
# Reconstruct ChatMessage using custom deserialization
|
||||
return ChatMessage.from_json(serialized_message)
|
||||
# Reconstruct Message using custom deserialization
|
||||
return Message.from_json(serialized_message)
|
||||
|
||||
# ============================================================================
|
||||
# List-like Convenience Methods (Redis-optimized async versions)
|
||||
@@ -446,14 +446,14 @@ class RedisChatMessageStore:
|
||||
await self._ensure_initial_messages_added()
|
||||
return await self._redis_client.llen(self.redis_key) # type: ignore[misc,no-any-return]
|
||||
|
||||
async def getitem(self, index: int) -> ChatMessage:
|
||||
async def getitem(self, index: int) -> Message:
|
||||
"""Get a message by index using Redis LINDEX.
|
||||
|
||||
Args:
|
||||
index: The index of the message to retrieve.
|
||||
|
||||
Returns:
|
||||
The ChatMessage at the specified index.
|
||||
The Message at the specified index.
|
||||
|
||||
Raises:
|
||||
IndexError: If the index is out of range.
|
||||
@@ -467,12 +467,12 @@ class RedisChatMessageStore:
|
||||
|
||||
return self._deserialize_message(serialized_message)
|
||||
|
||||
async def setitem(self, index: int, item: ChatMessage) -> None:
|
||||
async def setitem(self, index: int, item: Message) -> None:
|
||||
"""Set a message at the specified index using Redis LSET.
|
||||
|
||||
Args:
|
||||
index: The index at which to set the message.
|
||||
item: The ChatMessage to set at the specified index.
|
||||
item: The Message to set at the specified index.
|
||||
|
||||
Raises:
|
||||
IndexError: If the index is out of range.
|
||||
@@ -490,11 +490,11 @@ class RedisChatMessageStore:
|
||||
serialized_message = self._serialize_message(item)
|
||||
await self._redis_client.lset(self.redis_key, index, serialized_message) # type: ignore[misc]
|
||||
|
||||
async def append(self, item: ChatMessage) -> None:
|
||||
async def append(self, item: Message) -> None:
|
||||
"""Append a message to the end of the store.
|
||||
|
||||
Args:
|
||||
item: The ChatMessage to append.
|
||||
item: The Message to append.
|
||||
"""
|
||||
await self.add_messages([item])
|
||||
|
||||
@@ -507,14 +507,14 @@ class RedisChatMessageStore:
|
||||
await self._ensure_initial_messages_added()
|
||||
return await self._redis_client.llen(self.redis_key) # type: ignore[misc,no-any-return]
|
||||
|
||||
async def index(self, item: ChatMessage) -> int:
|
||||
async def index(self, item: Message) -> int:
|
||||
"""Return the index of the first occurrence of the specified message.
|
||||
|
||||
Uses Redis LINDEX to iterate through the list without loading all messages.
|
||||
Still O(N) but more memory efficient for large lists.
|
||||
|
||||
Args:
|
||||
item: The ChatMessage to find.
|
||||
item: The Message to find.
|
||||
|
||||
Returns:
|
||||
The index of the first occurrence of the message.
|
||||
@@ -533,16 +533,16 @@ class RedisChatMessageStore:
|
||||
if redis_message == target_serialized:
|
||||
return i
|
||||
|
||||
raise ValueError("ChatMessage not found in store")
|
||||
raise ValueError("Message not found in store")
|
||||
|
||||
async def remove(self, item: ChatMessage) -> None:
|
||||
async def remove(self, item: Message) -> None:
|
||||
"""Remove the first occurrence of the specified message from the store.
|
||||
|
||||
Uses Redis LREM command for efficient removal by value.
|
||||
O(N) but performed natively in Redis without data transfer.
|
||||
|
||||
Args:
|
||||
item: The ChatMessage to remove.
|
||||
item: The Message to remove.
|
||||
|
||||
Raises:
|
||||
ValueError: If the message is not found in the store.
|
||||
@@ -556,13 +556,13 @@ class RedisChatMessageStore:
|
||||
removed_count = await self._redis_client.lrem(self.redis_key, 1, target_serialized) # type: ignore[misc]
|
||||
|
||||
if removed_count == 0:
|
||||
raise ValueError("ChatMessage not found in store")
|
||||
raise ValueError("Message not found in store")
|
||||
|
||||
async def extend(self, items: Sequence[ChatMessage]) -> None:
|
||||
async def extend(self, items: Sequence[Message]) -> None:
|
||||
"""Extend the store by appending all messages from the iterable.
|
||||
|
||||
Args:
|
||||
items: Sequence of ChatMessage objects to append.
|
||||
items: Sequence of Message objects to append.
|
||||
"""
|
||||
await self.add_messages(items)
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ from operator import and_
|
||||
from typing import TYPE_CHECKING, Any, Literal, cast
|
||||
|
||||
import numpy as np
|
||||
from agent_framework import ChatMessage
|
||||
from agent_framework import Message
|
||||
from agent_framework._sessions import AgentSession, BaseContextProvider, SessionContext
|
||||
from agent_framework.exceptions import (
|
||||
AgentException,
|
||||
@@ -142,7 +142,7 @@ class _RedisContextProvider(BaseContextProvider):
|
||||
if line_separated_memories:
|
||||
context.extend_messages(
|
||||
self.source_id,
|
||||
[ChatMessage(role="user", text=f"{self.context_prompt}\n{line_separated_memories}")],
|
||||
[Message(role="user", text=f"{self.context_prompt}\n{line_separated_memories}")],
|
||||
)
|
||||
|
||||
@override
|
||||
@@ -157,7 +157,7 @@ class _RedisContextProvider(BaseContextProvider):
|
||||
"""Store request/response messages to Redis for future retrieval."""
|
||||
self._validate_filters()
|
||||
|
||||
messages_to_store: list[ChatMessage] = list(context.input_messages)
|
||||
messages_to_store: list[Message] = list(context.input_messages)
|
||||
if context.response and context.response.messages:
|
||||
messages_to_store.extend(context.response.messages)
|
||||
|
||||
|
||||
@@ -3,34 +3,31 @@
|
||||
"""New-pattern Redis history provider using BaseHistoryProvider.
|
||||
|
||||
This module provides ``_RedisHistoryProvider``, a side-by-side implementation of
|
||||
:class:`RedisChatMessageStore` built on the new :class:`BaseHistoryProvider` hooks pattern.
|
||||
:class:`RedisMessageStore` built on the new :class:`BaseHistoryProvider` hooks pattern.
|
||||
It will be renamed to ``RedisHistoryProvider`` in PR2 when the old class is removed.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Sequence
|
||||
from typing import TYPE_CHECKING, Any
|
||||
from typing import Any
|
||||
|
||||
import redis.asyncio as redis
|
||||
from agent_framework import ChatMessage
|
||||
from agent_framework import Message
|
||||
from agent_framework._sessions import BaseHistoryProvider
|
||||
from redis.credentials import CredentialProvider
|
||||
|
||||
if TYPE_CHECKING:
|
||||
pass
|
||||
|
||||
|
||||
class _RedisHistoryProvider(BaseHistoryProvider):
|
||||
"""Redis-backed history provider using the new BaseHistoryProvider hooks pattern.
|
||||
|
||||
Stores conversation history in Redis Lists, with each session isolated by a
|
||||
unique Redis key. This is the new-pattern equivalent of
|
||||
:class:`RedisChatMessageStore`.
|
||||
:class:`RedisMessageStore`.
|
||||
|
||||
Note:
|
||||
This class uses a temporary ``_`` prefix to coexist with the existing
|
||||
:class:`RedisChatMessageStore`. It will be renamed to ``RedisHistoryProvider``
|
||||
:class:`RedisMessageStore`. It will be renamed to ``RedisHistoryProvider``
|
||||
in PR2.
|
||||
"""
|
||||
|
||||
@@ -115,7 +112,7 @@ class _RedisHistoryProvider(BaseHistoryProvider):
|
||||
"""Get the Redis key for a given session's messages."""
|
||||
return f"{self.key_prefix}:{session_id or 'default'}"
|
||||
|
||||
async def get_messages(self, session_id: str | None, **kwargs: Any) -> list[ChatMessage]:
|
||||
async def get_messages(self, session_id: str | None, **kwargs: Any) -> list[Message]:
|
||||
"""Retrieve stored messages for this session from Redis.
|
||||
|
||||
Args:
|
||||
@@ -123,17 +120,17 @@ class _RedisHistoryProvider(BaseHistoryProvider):
|
||||
**kwargs: Additional arguments (unused).
|
||||
|
||||
Returns:
|
||||
List of stored ChatMessage objects in chronological order.
|
||||
List of stored Message objects in chronological order.
|
||||
"""
|
||||
key = self._redis_key(session_id)
|
||||
redis_messages = await self._redis_client.lrange(key, 0, -1) # type: ignore[misc]
|
||||
messages: list[ChatMessage] = []
|
||||
messages: list[Message] = []
|
||||
if redis_messages:
|
||||
for serialized in redis_messages:
|
||||
messages.append(ChatMessage.from_dict(self._deserialize_json(serialized)))
|
||||
messages.append(Message.from_dict(self._deserialize_json(serialized)))
|
||||
return messages
|
||||
|
||||
async def save_messages(self, session_id: str | None, messages: Sequence[ChatMessage], **kwargs: Any) -> None:
|
||||
async def save_messages(self, session_id: str | None, messages: Sequence[Message], **kwargs: Any) -> None:
|
||||
"""Persist messages for this session to Redis.
|
||||
|
||||
Args:
|
||||
@@ -158,8 +155,8 @@ class _RedisHistoryProvider(BaseHistoryProvider):
|
||||
await self._redis_client.ltrim(key, -self.max_messages, -1) # type: ignore[misc]
|
||||
|
||||
@staticmethod
|
||||
def _serialize_json(message: ChatMessage) -> str:
|
||||
"""Serialize a ChatMessage to a JSON string for Redis storage."""
|
||||
def _serialize_json(message: Message) -> str:
|
||||
"""Serialize a Message to a JSON string for Redis storage."""
|
||||
import json
|
||||
|
||||
return json.dumps(message.to_dict())
|
||||
|
||||
@@ -10,7 +10,7 @@ from operator import and_
|
||||
from typing import Any, Literal, cast
|
||||
|
||||
import numpy as np
|
||||
from agent_framework import ChatMessage, Context, ContextProvider
|
||||
from agent_framework import Context, ContextProvider, Message
|
||||
from agent_framework.exceptions import (
|
||||
AgentException,
|
||||
ServiceInitializationError,
|
||||
@@ -484,19 +484,17 @@ class RedisProvider(ContextProvider):
|
||||
@override
|
||||
async def invoked(
|
||||
self,
|
||||
request_messages: ChatMessage | Sequence[ChatMessage],
|
||||
response_messages: ChatMessage | Sequence[ChatMessage] | None = None,
|
||||
request_messages: Message | Sequence[Message],
|
||||
response_messages: Message | Sequence[Message] | None = None,
|
||||
invoke_exception: Exception | None = None,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
self._validate_filters()
|
||||
|
||||
request_messages_list = (
|
||||
[request_messages] if isinstance(request_messages, ChatMessage) else list(request_messages)
|
||||
)
|
||||
request_messages_list = [request_messages] if isinstance(request_messages, Message) else list(request_messages)
|
||||
response_messages_list = (
|
||||
[response_messages]
|
||||
if isinstance(response_messages, ChatMessage)
|
||||
if isinstance(response_messages, Message)
|
||||
else list(response_messages)
|
||||
if response_messages
|
||||
else []
|
||||
@@ -518,7 +516,7 @@ class RedisProvider(ContextProvider):
|
||||
await self._add(data=messages)
|
||||
|
||||
@override
|
||||
async def invoking(self, messages: ChatMessage | MutableSequence[ChatMessage], **kwargs: Any) -> Context:
|
||||
async def invoking(self, messages: Message | MutableSequence[Message], **kwargs: Any) -> Context:
|
||||
"""Called before invoking the model to provide scoped context.
|
||||
|
||||
Concatenates recent messages into a query, fetches matching memories from Redis.
|
||||
@@ -534,7 +532,7 @@ class RedisProvider(ContextProvider):
|
||||
Context: Context object containing instructions with memories.
|
||||
"""
|
||||
self._validate_filters()
|
||||
messages_list = [messages] if isinstance(messages, ChatMessage) else list(messages)
|
||||
messages_list = [messages] if isinstance(messages, Message) else list(messages)
|
||||
input_text = "\n".join(msg.text for msg in messages_list if msg and msg.text and msg.text.strip())
|
||||
|
||||
memories = await self._redis_search(text=input_text)
|
||||
@@ -543,7 +541,7 @@ class RedisProvider(ContextProvider):
|
||||
)
|
||||
|
||||
return Context(
|
||||
messages=[ChatMessage(role="user", text=f"{self.context_prompt}\n{line_separated_memories}")]
|
||||
messages=[Message(role="user", text=f"{self.context_prompt}\n{line_separated_memories}")]
|
||||
if line_separated_memories
|
||||
else None
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user