From 64c03ac1f5458c3b5a9aacb1239b0e3fbe4ec07b Mon Sep 17 00:00:00 2001 From: reatang Date: Thu, 12 Jun 2025 18:02:30 +0800 Subject: [PATCH 01/14] add llm alibaba qwen --- src/core/llm.py | 13 +++++++++++++ src/core/settings.py | 7 +++++++ src/schema/models.py | 10 ++++++++++ 3 files changed, 30 insertions(+) diff --git a/src/core/llm.py b/src/core/llm.py index 1e050453..7b64225b 100644 --- a/src/core/llm.py +++ b/src/core/llm.py @@ -17,6 +17,7 @@ AWSModelName, AzureOpenAIModelName, DeepseekModelName, + AlibabaQWenModelName, FakeModelName, GoogleModelName, GroqModelName, @@ -35,6 +36,9 @@ ), AzureOpenAIModelName.AZURE_GPT_4O: settings.AZURE_OPENAI_DEPLOYMENT_MAP.get("gpt-4o", ""), DeepseekModelName.DEEPSEEK_CHAT: "deepseek-chat", + AlibabaQWenModelName.QWEN_MAX: "qwen-max", + AlibabaQWenModelName.QWEN_PLUS: "qwen-plus", + AlibabaQWenModelName.QWEN_TURBO: "qwen-turbo", AnthropicModelName.HAIKU_3: "claude-3-haiku-20240307", AnthropicModelName.HAIKU_35: "claude-3-5-haiku-latest", AnthropicModelName.SONNET_35: "claude-3-5-sonnet-latest", @@ -118,6 +122,15 @@ def get_model(model_name: AllModelEnum, /) -> ModelT: openai_api_base="https://api.deepseek.com", openai_api_key=settings.DEEPSEEK_API_KEY, ) + if model_name in AlibabaQWenModelName: + return ChatOpenAI( + model=api_model_name, + temperature=0.5, + streaming=True, + openai_api_base="https://dashscope.aliyuncs.com/compatible-mode/v1/", + openai_api_key=settings.ALIBABA_QWEN_API_KEY, + # extra_body={"top_k": 5}, + ) if model_name in AnthropicModelName: return ChatAnthropic(model=api_model_name, temperature=0.5, streaming=True) if model_name in GoogleModelName: diff --git a/src/core/settings.py b/src/core/settings.py index ea027ac6..382f0ff8 100644 --- a/src/core/settings.py +++ b/src/core/settings.py @@ -19,6 +19,7 @@ AWSModelName, AzureOpenAIModelName, DeepseekModelName, + AlibabaQWenModelName, FakeModelName, GoogleModelName, GroqModelName, @@ -57,6 +58,7 @@ class Settings(BaseSettings): OPENAI_API_KEY: SecretStr | None = None DEEPSEEK_API_KEY: SecretStr | None = None + ALIBABA_QWEN_API_KEY: SecretStr | None = None ANTHROPIC_API_KEY: SecretStr | None = None GOOGLE_API_KEY: SecretStr | None = None GOOGLE_APPLICATION_CREDENTIALS: SecretStr | None = None @@ -110,6 +112,7 @@ def model_post_init(self, __context: Any) -> None: Provider.OPENAI: self.OPENAI_API_KEY, Provider.OPENAI_COMPATIBLE: self.COMPATIBLE_BASE_URL and self.COMPATIBLE_MODEL, Provider.DEEPSEEK: self.DEEPSEEK_API_KEY, + Provider.ALIBABA_QWEN: self.ALIBABA_QWEN_API_KEY, Provider.ANTHROPIC: self.ANTHROPIC_API_KEY, Provider.GOOGLE: self.GOOGLE_API_KEY, Provider.VERTEXAI: self.GOOGLE_APPLICATION_CREDENTIALS, @@ -137,6 +140,10 @@ def model_post_init(self, __context: Any) -> None: if self.DEFAULT_MODEL is None: self.DEFAULT_MODEL = DeepseekModelName.DEEPSEEK_CHAT self.AVAILABLE_MODELS.update(set(DeepseekModelName)) + case Provider.ALIBABA_QWEN: + if self.DEFAULT_MODEL is None: + self.DEFAULT_MODEL = AlibabaQWenModelName.QWEN_PLUS + self.AVAILABLE_MODELS.update(set(AlibabaQWenModelName)) case Provider.ANTHROPIC: if self.DEFAULT_MODEL is None: self.DEFAULT_MODEL = AnthropicModelName.HAIKU_3 diff --git a/src/schema/models.py b/src/schema/models.py index f2bba11f..b543186b 100644 --- a/src/schema/models.py +++ b/src/schema/models.py @@ -7,6 +7,7 @@ class Provider(StrEnum): OPENAI_COMPATIBLE = auto() AZURE_OPENAI = auto() DEEPSEEK = auto() + ALIBABA_QWEN = auto() ANTHROPIC = auto() GOOGLE = auto() VERTEXAI = auto() @@ -36,6 +37,14 @@ class DeepseekModelName(StrEnum): DEEPSEEK_CHAT = "deepseek-chat" +class AlibabaQWenModelName(StrEnum): + """https://help.aliyun.com/zh/model-studio/user-guide/text-generation/""" + + QWEN_MAX = "qwen-max" + QWEN_PLUS = "qwen-plus" + QWEN_TURBO = "qwen-turbo" + + class AnthropicModelName(StrEnum): """https://docs.anthropic.com/en/docs/about-claude/models#model-names""" @@ -100,6 +109,7 @@ class FakeModelName(StrEnum): | OpenAICompatibleName | AzureOpenAIModelName | DeepseekModelName + | AlibabaQWenModelName | AnthropicModelName | GoogleModelName | VertexAIModelName From ab24b25e7c8555f7d9556680c5da39b26de85bfe Mon Sep 17 00:00:00 2001 From: reatang Date: Thu, 12 Jun 2025 20:54:47 +0800 Subject: [PATCH 02/14] add ag-ui protocol stream output --- pyproject.toml | 5 +- src/schema/schema.py | 5 ++ src/service/service.py | 150 +++++++++++++++++++++++++++++++++++++++-- src/service/utils.py | 91 +++++++++++++++++++++++++ uv.lock | 139 ++++++++++++++++++++++++-------------- 5 files changed, 334 insertions(+), 56 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 0afe2a1b..821faa33 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -41,7 +41,7 @@ dependencies = [ "pandas ~=2.2.3", "psycopg[binary,pool] ~=3.2.4", "pyarrow >=18.1.0", - "pydantic ~=2.10.1", + "pydantic ~=2.11.1", "pydantic-settings ~=2.6.1", "pyowm ~=3.3.0", "python-dotenv ~=1.0.1", @@ -49,6 +49,7 @@ dependencies = [ "streamlit ~=1.40.1", "tiktoken >=0.8.0", "uvicorn ~=0.32.1", + "ag-ui-protocol>=0.1.5", ] [dependency-groups] @@ -67,7 +68,7 @@ dev = [ # To install run: `uv sync --frozen --only-group client` client = [ "httpx~=0.27.2", - "pydantic ~=2.10.1", + "pydantic~=2.11.1", "python-dotenv ~=1.0.1", "streamlit~=1.40.1", ] diff --git a/src/schema/schema.py b/src/schema/schema.py index d6a12488..f35a330d 100644 --- a/src/schema/schema.py +++ b/src/schema/schema.py @@ -70,6 +70,11 @@ class StreamInput(UserInput): default=True, ) + stream_protocol: Literal["sse", "agui"] = Field( + description="The protocol to use for streaming the agent's response.", + default="sse", + ) + class ToolCall(TypedDict): """Represents a request to call a tool.""" diff --git a/src/service/service.py b/src/service/service.py index 733d5262..6a8f13dd 100644 --- a/src/service/service.py +++ b/src/service/service.py @@ -17,6 +17,16 @@ from langgraph.types import Command, Interrupt from langsmith import Client as LangsmithClient +# AG-UI Protocol imports +from ag_ui.core import ( + EventType, + RunStartedEvent, + RunFinishedEvent, + RunErrorEvent, +) +from ag_ui.core.events import TextMessageChunkEvent +from ag_ui.encoder import EventEncoder + from agents import DEFAULT_AGENT, get_agent, get_all_agent_info from core import settings from memory import initialize_database @@ -34,6 +44,7 @@ convert_message_content_to_string, langchain_to_chat_message, remove_tool_calls, + convert_message_to_agui_events, ) warnings.filterwarnings("ignore", category=LangChainBetaWarning) @@ -280,6 +291,129 @@ async def message_generator( yield "data: [DONE]\n\n" +async def message_generator_agui( + user_input: StreamInput, + agent_id: str = DEFAULT_AGENT, +) -> AsyncGenerator[str, None]: + """ + Generate a stream of messages from the agent using the AG-UI protocol. + + This closely mirrors message_generator but outputs AG-UI compatible events. + """ + agent: Pregel = get_agent(agent_id) + kwargs, run_id = await _handle_input(user_input, agent) + + # Create event encoder for AG-UI protocol + encoder = EventEncoder() + + try: + # Send run started event + yield encoder.encode( + RunStartedEvent( + type=EventType.RUN_STARTED, + thread_id=kwargs["config"]["configurable"]["thread_id"], + run_id=str(run_id), + ) + ) + + # Process streamed events from the graph - same structure as message_generator + async for stream_event in agent.astream( + **kwargs, stream_mode=["updates", "messages", "custom"] + ): + if not isinstance(stream_event, tuple): + continue + stream_mode, event = stream_event + new_messages = [] + + # Handle updates - same logic as original + if stream_mode == "updates": + for node, updates in event.items(): + if node == "__interrupt__": + interrupt: Interrupt + for interrupt in updates: + new_messages.append(AIMessage(content=interrupt.value)) + continue + updates = updates or {} + update_messages = updates.get("messages", []) + # special cases for using langgraph-supervisor library + if node == "supervisor": + ai_messages = [msg for msg in update_messages if isinstance(msg, AIMessage)] + if ai_messages: + update_messages = [ai_messages[-1]] + if node in ("research_expert", "math_expert"): + msg = ToolMessage( + content=update_messages[0].content, + name=node, + tool_call_id="", + ) + update_messages = [msg] + new_messages.extend(update_messages) + + # Handle custom events - same logic as original + if stream_mode == "custom": + new_messages = [event] + + # Process message parts - similar to original but simpler + processed_messages = [] + current_message: dict[str, Any] = {} + for message in new_messages: + if isinstance(message, tuple): + key, value = message + current_message[key] = value + else: + if current_message: + processed_messages.append(_create_ai_message(current_message)) + current_message = {} + processed_messages.append(message) + + if current_message: + processed_messages.append(_create_ai_message(current_message)) + + # Convert messages to AG-UI events - this is the main difference + for message in processed_messages: + # Skip re-sent input messages + if isinstance(message, HumanMessage) and message.content == user_input.message: + continue + + # Convert each message to appropriate AG-UI events + async for agui_event in convert_message_to_agui_events(message, encoder): + yield agui_event + + # Handle token streaming - similar to original + if stream_mode == "messages": + if not user_input.stream_tokens: + continue + msg, metadata = event + if "skip_stream" in metadata.get("tags", []): + continue + if not isinstance(msg, AIMessageChunk): + continue + content = remove_tool_calls(msg.content) + if content: + # Convert to AG-UI text content event + message_id = str(uuid4()) + yield encoder.encode( + TextMessageChunkEvent( + type=EventType.TEXT_MESSAGE_CHUNK, + message_id=message_id, + delta=convert_message_content_to_string(content), + ) + ) + + # Send run finished event + yield encoder.encode( + RunFinishedEvent( + type=EventType.RUN_FINISHED, + thread_id=kwargs["config"]["configurable"]["thread_id"], + run_id=str(run_id), + ) + ) + + except Exception as e: + logger.error(f"Error in AG-UI message generator: {e}") + yield encoder.encode(RunErrorEvent(type=EventType.RUN_ERROR, message=str(e))) + + def _create_ai_message(parts: dict) -> AIMessage: sig = inspect.signature(AIMessage) valid_keys = set(sig.parameters) @@ -317,10 +451,18 @@ async def stream(user_input: StreamInput, agent_id: str = DEFAULT_AGENT) -> Stre Set `stream_tokens=false` to return intermediate messages but not token-by-token. """ - return StreamingResponse( - message_generator(user_input, agent_id), - media_type="text/event-stream", - ) + if user_input.stream_protocol == "sse": + return StreamingResponse( + message_generator(user_input, agent_id), + media_type="text/event-stream", + ) + elif user_input.stream_protocol == "agui": + return StreamingResponse( + message_generator_agui(user_input, agent_id), + media_type="text/event-stream", + ) + else: + raise HTTPException(status_code=400, detail="Invalid stream protocol") @router.post("/feedback") diff --git a/src/service/utils.py b/src/service/utils.py index 004bfbcf..cd1ed70d 100644 --- a/src/service/utils.py +++ b/src/service/utils.py @@ -1,13 +1,30 @@ +from typing import AsyncGenerator, Any +from uuid import uuid4 + from langchain_core.messages import ( AIMessage, BaseMessage, HumanMessage, ToolMessage, + AnyMessage, ) from langchain_core.messages import ( ChatMessage as LangchainChatMessage, ) +from ag_ui.core.events import EventEncoder +from ag_ui.core.events import ( + EventType, + ToolCallStartEvent, + ToolCallArgsEvent, + ToolCallEndEvent, + TextMessageStartEvent, + TextMessageContentEvent, + TextMessageEndEvent, + CustomEvent, + RawEvent, +) + from schema import ChatMessage @@ -74,3 +91,77 @@ def remove_tool_calls(content: str | list[str | dict]) -> str | list[str | dict] for content_item in content if isinstance(content_item, str) or content_item["type"] != "tool_use" ] + + +async def convert_message_to_agui_events( + message: AnyMessage, encoder: EventEncoder +) -> AsyncGenerator[str, None]: + """Convert a single LangChain message to AG-UI events.""" + message_id = str(uuid4()) + + if isinstance(message, AIMessage): + # Handle tool calls first + if hasattr(message, "tool_calls") and message.tool_calls: + for tool_call in message.tool_calls: + tool_call_id = tool_call.get("id", str(uuid4())) + yield encoder.encode( + ToolCallStartEvent( + type=EventType.TOOL_CALL_START, + tool_call_id=tool_call_id, + tool_call_name=tool_call.get("name", "unknown"), + ) + ) + yield encoder.encode( + ToolCallArgsEvent( + type=EventType.TOOL_CALL_ARGS, + tool_call_id=tool_call_id, + delta=str(tool_call.get("args", {})), + ) + ) + yield encoder.encode( + ToolCallEndEvent(type=EventType.TOOL_CALL_END, tool_call_id=tool_call_id) + ) + + # Handle text content + content = remove_tool_calls(message.content) + if content: + yield encoder.encode( + TextMessageStartEvent( + type=EventType.TEXT_MESSAGE_START, message_id=message_id, role="assistant" + ) + ) + yield encoder.encode( + TextMessageContentEvent( + type=EventType.TEXT_MESSAGE_CONTENT, + message_id=message_id, + delta=convert_message_content_to_string(content), + ) + ) + yield encoder.encode( + TextMessageEndEvent(type=EventType.TEXT_MESSAGE_END, message_id=message_id) + ) + + elif isinstance(message, ToolMessage): + """ + AG-UI protocol does not support tool messages. + """ + pass + + elif message.role == "custom": + # Handle custom messages as raw events + yield encoder.encode( + CustomEvent( + type=EventType.CUSTOM, + name=message.content[0].get("type", "unknown"), + value=message.content[0].get("data", {}), + ) + ) + + else: + # Handle other message types as raw events + yield encoder.encode( + RawEvent( + type=EventType.RAW, + event=str(message.content) if hasattr(message, "content") else str(message), + ) + ) diff --git a/uv.lock b/uv.lock index 614c1f1c..5f944494 100644 --- a/uv.lock +++ b/uv.lock @@ -8,11 +8,24 @@ resolution-markers = [ "python_full_version < '3.12'", ] +[[package]] +name = "ag-ui-protocol" +version = "0.1.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/90/26/1d5530e3fa84da37a8b58300f7a4352f763be43b2c393b0fad4d119f8653/ag_ui_protocol-0.1.5.tar.gz", hash = "sha256:48757afe82a4ee88eb078f31ef9672e09df624573d82045054f5a5b5dc021832", size = 4175, upload-time = "2025-05-20T11:37:06.835Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/db/39/c488044d3195f82e35102c190f92b605a8af1ad63f26b9166e9be460e1c1/ag_ui_protocol-0.1.5-py3-none-any.whl", hash = "sha256:d51a0ad9635059b629b4cb57a9a2ec425b4cc8220e91d50a8f9d559571737ae9", size = 5819, upload-time = "2025-05-20T11:37:05.521Z" }, +] + [[package]] name = "agent-service-toolkit" version = "0.1.0" source = { virtual = "." } dependencies = [ + { name = "ag-ui-protocol" }, { name = "duckduckgo-search" }, { name = "fastapi" }, { name = "grpcio" }, @@ -67,6 +80,7 @@ dev = [ [package.metadata] requires-dist = [ + { name = "ag-ui-protocol", specifier = ">=0.1.5" }, { name = "duckduckgo-search", specifier = ">=7.3.0" }, { name = "fastapi", specifier = "~=0.115.5" }, { name = "grpcio", specifier = ">=1.68.0" }, @@ -92,7 +106,7 @@ requires-dist = [ { name = "pandas", specifier = "~=2.2.3" }, { name = "psycopg", extras = ["binary", "pool"], specifier = "~=3.2.4" }, { name = "pyarrow", specifier = ">=18.1.0" }, - { name = "pydantic", specifier = "~=2.10.1" }, + { name = "pydantic", specifier = "~=2.11.1" }, { name = "pydantic-settings", specifier = "~=2.6.1" }, { name = "pyowm", specifier = "~=3.3.0" }, { name = "python-dotenv", specifier = "~=1.0.1" }, @@ -105,7 +119,7 @@ requires-dist = [ [package.metadata.requires-dev] client = [ { name = "httpx", specifier = "~=0.27.2" }, - { name = "pydantic", specifier = "~=2.10.1" }, + { name = "pydantic", specifier = "~=2.11.1" }, { name = "python-dotenv", specifier = "~=1.0.1" }, { name = "streamlit", specifier = "~=1.40.1" }, ] @@ -2321,69 +2335,82 @@ wheels = [ [[package]] name = "pydantic" -version = "2.10.6" +version = "2.11.5" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "annotated-types" }, { name = "pydantic-core" }, { name = "typing-extensions" }, + { name = "typing-inspection" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b7/ae/d5220c5c52b158b1de7ca89fc5edb72f304a70a4c540c84c8844bf4008de/pydantic-2.10.6.tar.gz", hash = "sha256:ca5daa827cce33de7a42be142548b0096bf05a7e7b365aebfa5f8eeec7128236", size = 761681, upload-time = "2025-01-24T01:42:12.693Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f0/86/8ce9040065e8f924d642c58e4a344e33163a07f6b57f836d0d734e0ad3fb/pydantic-2.11.5.tar.gz", hash = "sha256:7f853db3d0ce78ce8bbb148c401c2cdd6431b3473c0cdff2755c7690952a7b7a", size = 787102, upload-time = "2025-05-22T21:18:08.761Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f4/3c/8cc1cc84deffa6e25d2d0c688ebb80635dfdbf1dbea3e30c541c8cf4d860/pydantic-2.10.6-py3-none-any.whl", hash = "sha256:427d664bf0b8a2b34ff5dd0f5a18df00591adcee7198fbd71981054cef37b584", size = 431696, upload-time = "2025-01-24T01:42:10.371Z" }, + { url = "https://files.pythonhosted.org/packages/b5/69/831ed22b38ff9b4b64b66569f0e5b7b97cf3638346eb95a2147fdb49ad5f/pydantic-2.11.5-py3-none-any.whl", hash = "sha256:f9c26ba06f9747749ca1e5c94d6a85cb84254577553c8785576fd38fa64dc0f7", size = 444229, upload-time = "2025-05-22T21:18:06.329Z" }, ] [[package]] name = "pydantic-core" -version = "2.27.2" +version = "2.33.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/fc/01/f3e5ac5e7c25833db5eb555f7b7ab24cd6f8c322d3a3ad2d67a952dc0abc/pydantic_core-2.27.2.tar.gz", hash = "sha256:eb026e5a4c1fee05726072337ff51d1efb6f59090b7da90d30ea58625b1ffb39", size = 413443, upload-time = "2024-12-18T11:31:54.917Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c2/89/f3450af9d09d44eea1f2c369f49e8f181d742f28220f88cc4dfaae91ea6e/pydantic_core-2.27.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:8e10c99ef58cfdf2a66fc15d66b16c4a04f62bca39db589ae8cba08bc55331bc", size = 1893421, upload-time = "2024-12-18T11:27:55.409Z" }, - { url = "https://files.pythonhosted.org/packages/9e/e3/71fe85af2021f3f386da42d291412e5baf6ce7716bd7101ea49c810eda90/pydantic_core-2.27.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:26f32e0adf166a84d0cb63be85c562ca8a6fa8de28e5f0d92250c6b7e9e2aff7", size = 1814998, upload-time = "2024-12-18T11:27:57.252Z" }, - { url = "https://files.pythonhosted.org/packages/a6/3c/724039e0d848fd69dbf5806894e26479577316c6f0f112bacaf67aa889ac/pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c19d1ea0673cd13cc2f872f6c9ab42acc4e4f492a7ca9d3795ce2b112dd7e15", size = 1826167, upload-time = "2024-12-18T11:27:59.146Z" }, - { url = "https://files.pythonhosted.org/packages/2b/5b/1b29e8c1fb5f3199a9a57c1452004ff39f494bbe9bdbe9a81e18172e40d3/pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5e68c4446fe0810e959cdff46ab0a41ce2f2c86d227d96dc3847af0ba7def306", size = 1865071, upload-time = "2024-12-18T11:28:02.625Z" }, - { url = "https://files.pythonhosted.org/packages/89/6c/3985203863d76bb7d7266e36970d7e3b6385148c18a68cc8915fd8c84d57/pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d9640b0059ff4f14d1f37321b94061c6db164fbe49b334b31643e0528d100d99", size = 2036244, upload-time = "2024-12-18T11:28:04.442Z" }, - { url = "https://files.pythonhosted.org/packages/0e/41/f15316858a246b5d723f7d7f599f79e37493b2e84bfc789e58d88c209f8a/pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:40d02e7d45c9f8af700f3452f329ead92da4c5f4317ca9b896de7ce7199ea459", size = 2737470, upload-time = "2024-12-18T11:28:07.679Z" }, - { url = "https://files.pythonhosted.org/packages/a8/7c/b860618c25678bbd6d1d99dbdfdf0510ccb50790099b963ff78a124b754f/pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1c1fd185014191700554795c99b347d64f2bb637966c4cfc16998a0ca700d048", size = 1992291, upload-time = "2024-12-18T11:28:10.297Z" }, - { url = "https://files.pythonhosted.org/packages/bf/73/42c3742a391eccbeab39f15213ecda3104ae8682ba3c0c28069fbcb8c10d/pydantic_core-2.27.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d81d2068e1c1228a565af076598f9e7451712700b673de8f502f0334f281387d", size = 1994613, upload-time = "2024-12-18T11:28:13.362Z" }, - { url = "https://files.pythonhosted.org/packages/94/7a/941e89096d1175d56f59340f3a8ebaf20762fef222c298ea96d36a6328c5/pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:1a4207639fb02ec2dbb76227d7c751a20b1a6b4bc52850568e52260cae64ca3b", size = 2002355, upload-time = "2024-12-18T11:28:16.587Z" }, - { url = "https://files.pythonhosted.org/packages/6e/95/2359937a73d49e336a5a19848713555605d4d8d6940c3ec6c6c0ca4dcf25/pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:3de3ce3c9ddc8bbd88f6e0e304dea0e66d843ec9de1b0042b0911c1663ffd474", size = 2126661, upload-time = "2024-12-18T11:28:18.407Z" }, - { url = "https://files.pythonhosted.org/packages/2b/4c/ca02b7bdb6012a1adef21a50625b14f43ed4d11f1fc237f9d7490aa5078c/pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:30c5f68ded0c36466acede341551106821043e9afaad516adfb6e8fa80a4e6a6", size = 2153261, upload-time = "2024-12-18T11:28:21.471Z" }, - { url = "https://files.pythonhosted.org/packages/72/9d/a241db83f973049a1092a079272ffe2e3e82e98561ef6214ab53fe53b1c7/pydantic_core-2.27.2-cp311-cp311-win32.whl", hash = "sha256:c70c26d2c99f78b125a3459f8afe1aed4d9687c24fd677c6a4436bc042e50d6c", size = 1812361, upload-time = "2024-12-18T11:28:23.53Z" }, - { url = "https://files.pythonhosted.org/packages/e8/ef/013f07248041b74abd48a385e2110aa3a9bbfef0fbd97d4e6d07d2f5b89a/pydantic_core-2.27.2-cp311-cp311-win_amd64.whl", hash = "sha256:08e125dbdc505fa69ca7d9c499639ab6407cfa909214d500897d02afb816e7cc", size = 1982484, upload-time = "2024-12-18T11:28:25.391Z" }, - { url = "https://files.pythonhosted.org/packages/10/1c/16b3a3e3398fd29dca77cea0a1d998d6bde3902fa2706985191e2313cc76/pydantic_core-2.27.2-cp311-cp311-win_arm64.whl", hash = "sha256:26f0d68d4b235a2bae0c3fc585c585b4ecc51382db0e3ba402a22cbc440915e4", size = 1867102, upload-time = "2024-12-18T11:28:28.593Z" }, - { url = "https://files.pythonhosted.org/packages/d6/74/51c8a5482ca447871c93e142d9d4a92ead74de6c8dc5e66733e22c9bba89/pydantic_core-2.27.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:9e0c8cfefa0ef83b4da9588448b6d8d2a2bf1a53c3f1ae5fca39eb3061e2f0b0", size = 1893127, upload-time = "2024-12-18T11:28:30.346Z" }, - { url = "https://files.pythonhosted.org/packages/d3/f3/c97e80721735868313c58b89d2de85fa80fe8dfeeed84dc51598b92a135e/pydantic_core-2.27.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:83097677b8e3bd7eaa6775720ec8e0405f1575015a463285a92bfdfe254529ef", size = 1811340, upload-time = "2024-12-18T11:28:32.521Z" }, - { url = "https://files.pythonhosted.org/packages/9e/91/840ec1375e686dbae1bd80a9e46c26a1e0083e1186abc610efa3d9a36180/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:172fce187655fece0c90d90a678424b013f8fbb0ca8b036ac266749c09438cb7", size = 1822900, upload-time = "2024-12-18T11:28:34.507Z" }, - { url = "https://files.pythonhosted.org/packages/f6/31/4240bc96025035500c18adc149aa6ffdf1a0062a4b525c932065ceb4d868/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:519f29f5213271eeeeb3093f662ba2fd512b91c5f188f3bb7b27bc5973816934", size = 1869177, upload-time = "2024-12-18T11:28:36.488Z" }, - { url = "https://files.pythonhosted.org/packages/fa/20/02fbaadb7808be578317015c462655c317a77a7c8f0ef274bc016a784c54/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:05e3a55d124407fffba0dd6b0c0cd056d10e983ceb4e5dbd10dda135c31071d6", size = 2038046, upload-time = "2024-12-18T11:28:39.409Z" }, - { url = "https://files.pythonhosted.org/packages/06/86/7f306b904e6c9eccf0668248b3f272090e49c275bc488a7b88b0823444a4/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9c3ed807c7b91de05e63930188f19e921d1fe90de6b4f5cd43ee7fcc3525cb8c", size = 2685386, upload-time = "2024-12-18T11:28:41.221Z" }, - { url = "https://files.pythonhosted.org/packages/8d/f0/49129b27c43396581a635d8710dae54a791b17dfc50c70164866bbf865e3/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6fb4aadc0b9a0c063206846d603b92030eb6f03069151a625667f982887153e2", size = 1997060, upload-time = "2024-12-18T11:28:44.709Z" }, - { url = "https://files.pythonhosted.org/packages/0d/0f/943b4af7cd416c477fd40b187036c4f89b416a33d3cc0ab7b82708a667aa/pydantic_core-2.27.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:28ccb213807e037460326424ceb8b5245acb88f32f3d2777427476e1b32c48c4", size = 2004870, upload-time = "2024-12-18T11:28:46.839Z" }, - { url = "https://files.pythonhosted.org/packages/35/40/aea70b5b1a63911c53a4c8117c0a828d6790483f858041f47bab0b779f44/pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:de3cd1899e2c279b140adde9357c4495ed9d47131b4a4eaff9052f23398076b3", size = 1999822, upload-time = "2024-12-18T11:28:48.896Z" }, - { url = "https://files.pythonhosted.org/packages/f2/b3/807b94fd337d58effc5498fd1a7a4d9d59af4133e83e32ae39a96fddec9d/pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:220f892729375e2d736b97d0e51466252ad84c51857d4d15f5e9692f9ef12be4", size = 2130364, upload-time = "2024-12-18T11:28:50.755Z" }, - { url = "https://files.pythonhosted.org/packages/fc/df/791c827cd4ee6efd59248dca9369fb35e80a9484462c33c6649a8d02b565/pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a0fcd29cd6b4e74fe8ddd2c90330fd8edf2e30cb52acda47f06dd615ae72da57", size = 2158303, upload-time = "2024-12-18T11:28:54.122Z" }, - { url = "https://files.pythonhosted.org/packages/9b/67/4e197c300976af185b7cef4c02203e175fb127e414125916bf1128b639a9/pydantic_core-2.27.2-cp312-cp312-win32.whl", hash = "sha256:1e2cb691ed9834cd6a8be61228471d0a503731abfb42f82458ff27be7b2186fc", size = 1834064, upload-time = "2024-12-18T11:28:56.074Z" }, - { url = "https://files.pythonhosted.org/packages/1f/ea/cd7209a889163b8dcca139fe32b9687dd05249161a3edda62860430457a5/pydantic_core-2.27.2-cp312-cp312-win_amd64.whl", hash = "sha256:cc3f1a99a4f4f9dd1de4fe0312c114e740b5ddead65bb4102884b384c15d8bc9", size = 1989046, upload-time = "2024-12-18T11:28:58.107Z" }, - { url = "https://files.pythonhosted.org/packages/bc/49/c54baab2f4658c26ac633d798dab66b4c3a9bbf47cff5284e9c182f4137a/pydantic_core-2.27.2-cp312-cp312-win_arm64.whl", hash = "sha256:3911ac9284cd8a1792d3cb26a2da18f3ca26c6908cc434a18f730dc0db7bfa3b", size = 1885092, upload-time = "2024-12-18T11:29:01.335Z" }, - { url = "https://files.pythonhosted.org/packages/41/b1/9bc383f48f8002f99104e3acff6cba1231b29ef76cfa45d1506a5cad1f84/pydantic_core-2.27.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:7d14bd329640e63852364c306f4d23eb744e0f8193148d4044dd3dacdaacbd8b", size = 1892709, upload-time = "2024-12-18T11:29:03.193Z" }, - { url = "https://files.pythonhosted.org/packages/10/6c/e62b8657b834f3eb2961b49ec8e301eb99946245e70bf42c8817350cbefc/pydantic_core-2.27.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:82f91663004eb8ed30ff478d77c4d1179b3563df6cdb15c0817cd1cdaf34d154", size = 1811273, upload-time = "2024-12-18T11:29:05.306Z" }, - { url = "https://files.pythonhosted.org/packages/ba/15/52cfe49c8c986e081b863b102d6b859d9defc63446b642ccbbb3742bf371/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:71b24c7d61131bb83df10cc7e687433609963a944ccf45190cfc21e0887b08c9", size = 1823027, upload-time = "2024-12-18T11:29:07.294Z" }, - { url = "https://files.pythonhosted.org/packages/b1/1c/b6f402cfc18ec0024120602bdbcebc7bdd5b856528c013bd4d13865ca473/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fa8e459d4954f608fa26116118bb67f56b93b209c39b008277ace29937453dc9", size = 1868888, upload-time = "2024-12-18T11:29:09.249Z" }, - { url = "https://files.pythonhosted.org/packages/bd/7b/8cb75b66ac37bc2975a3b7de99f3c6f355fcc4d89820b61dffa8f1e81677/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ce8918cbebc8da707ba805b7fd0b382816858728ae7fe19a942080c24e5b7cd1", size = 2037738, upload-time = "2024-12-18T11:29:11.23Z" }, - { url = "https://files.pythonhosted.org/packages/c8/f1/786d8fe78970a06f61df22cba58e365ce304bf9b9f46cc71c8c424e0c334/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eda3f5c2a021bbc5d976107bb302e0131351c2ba54343f8a496dc8783d3d3a6a", size = 2685138, upload-time = "2024-12-18T11:29:16.396Z" }, - { url = "https://files.pythonhosted.org/packages/a6/74/d12b2cd841d8724dc8ffb13fc5cef86566a53ed358103150209ecd5d1999/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bd8086fa684c4775c27f03f062cbb9eaa6e17f064307e86b21b9e0abc9c0f02e", size = 1997025, upload-time = "2024-12-18T11:29:20.25Z" }, - { url = "https://files.pythonhosted.org/packages/a0/6e/940bcd631bc4d9a06c9539b51f070b66e8f370ed0933f392db6ff350d873/pydantic_core-2.27.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8d9b3388db186ba0c099a6d20f0604a44eabdeef1777ddd94786cdae158729e4", size = 2004633, upload-time = "2024-12-18T11:29:23.877Z" }, - { url = "https://files.pythonhosted.org/packages/50/cc/a46b34f1708d82498c227d5d80ce615b2dd502ddcfd8376fc14a36655af1/pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:7a66efda2387de898c8f38c0cf7f14fca0b51a8ef0b24bfea5849f1b3c95af27", size = 1999404, upload-time = "2024-12-18T11:29:25.872Z" }, - { url = "https://files.pythonhosted.org/packages/ca/2d/c365cfa930ed23bc58c41463bae347d1005537dc8db79e998af8ba28d35e/pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:18a101c168e4e092ab40dbc2503bdc0f62010e95d292b27827871dc85450d7ee", size = 2130130, upload-time = "2024-12-18T11:29:29.252Z" }, - { url = "https://files.pythonhosted.org/packages/f4/d7/eb64d015c350b7cdb371145b54d96c919d4db516817f31cd1c650cae3b21/pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ba5dd002f88b78a4215ed2f8ddbdf85e8513382820ba15ad5ad8955ce0ca19a1", size = 2157946, upload-time = "2024-12-18T11:29:31.338Z" }, - { url = "https://files.pythonhosted.org/packages/a4/99/bddde3ddde76c03b65dfd5a66ab436c4e58ffc42927d4ff1198ffbf96f5f/pydantic_core-2.27.2-cp313-cp313-win32.whl", hash = "sha256:1ebaf1d0481914d004a573394f4be3a7616334be70261007e47c2a6fe7e50130", size = 1834387, upload-time = "2024-12-18T11:29:33.481Z" }, - { url = "https://files.pythonhosted.org/packages/71/47/82b5e846e01b26ac6f1893d3c5f9f3a2eb6ba79be26eef0b759b4fe72946/pydantic_core-2.27.2-cp313-cp313-win_amd64.whl", hash = "sha256:953101387ecf2f5652883208769a79e48db18c6df442568a0b5ccd8c2723abee", size = 1990453, upload-time = "2024-12-18T11:29:35.533Z" }, - { url = "https://files.pythonhosted.org/packages/51/b2/b2b50d5ecf21acf870190ae5d093602d95f66c9c31f9d5de6062eb329ad1/pydantic_core-2.27.2-cp313-cp313-win_arm64.whl", hash = "sha256:ac4dbfd1691affb8f48c2c13241a2e3b60ff23247cbcf981759c768b6633cf8b", size = 1885186, upload-time = "2024-12-18T11:29:37.649Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/ad/88/5f2260bdfae97aabf98f1778d43f69574390ad787afb646292a638c923d4/pydantic_core-2.33.2.tar.gz", hash = "sha256:7cb8bc3605c29176e1b105350d2e6474142d7c1bd1d9327c4a9bdb46bf827acc", size = 435195, upload-time = "2025-04-23T18:33:52.104Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3f/8d/71db63483d518cbbf290261a1fc2839d17ff89fce7089e08cad07ccfce67/pydantic_core-2.33.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:4c5b0a576fb381edd6d27f0a85915c6daf2f8138dc5c267a57c08a62900758c7", size = 2028584, upload-time = "2025-04-23T18:31:03.106Z" }, + { url = "https://files.pythonhosted.org/packages/24/2f/3cfa7244ae292dd850989f328722d2aef313f74ffc471184dc509e1e4e5a/pydantic_core-2.33.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e799c050df38a639db758c617ec771fd8fb7a5f8eaaa4b27b101f266b216a246", size = 1855071, upload-time = "2025-04-23T18:31:04.621Z" }, + { url = "https://files.pythonhosted.org/packages/b3/d3/4ae42d33f5e3f50dd467761304be2fa0a9417fbf09735bc2cce003480f2a/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dc46a01bf8d62f227d5ecee74178ffc448ff4e5197c756331f71efcc66dc980f", size = 1897823, upload-time = "2025-04-23T18:31:06.377Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f3/aa5976e8352b7695ff808599794b1fba2a9ae2ee954a3426855935799488/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a144d4f717285c6d9234a66778059f33a89096dfb9b39117663fd8413d582dcc", size = 1983792, upload-time = "2025-04-23T18:31:07.93Z" }, + { url = "https://files.pythonhosted.org/packages/d5/7a/cda9b5a23c552037717f2b2a5257e9b2bfe45e687386df9591eff7b46d28/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:73cf6373c21bc80b2e0dc88444f41ae60b2f070ed02095754eb5a01df12256de", size = 2136338, upload-time = "2025-04-23T18:31:09.283Z" }, + { url = "https://files.pythonhosted.org/packages/2b/9f/b8f9ec8dd1417eb9da784e91e1667d58a2a4a7b7b34cf4af765ef663a7e5/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3dc625f4aa79713512d1976fe9f0bc99f706a9dee21dfd1810b4bbbf228d0e8a", size = 2730998, upload-time = "2025-04-23T18:31:11.7Z" }, + { url = "https://files.pythonhosted.org/packages/47/bc/cd720e078576bdb8255d5032c5d63ee5c0bf4b7173dd955185a1d658c456/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:881b21b5549499972441da4758d662aeea93f1923f953e9cbaff14b8b9565aef", size = 2003200, upload-time = "2025-04-23T18:31:13.536Z" }, + { url = "https://files.pythonhosted.org/packages/ca/22/3602b895ee2cd29d11a2b349372446ae9727c32e78a94b3d588a40fdf187/pydantic_core-2.33.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:bdc25f3681f7b78572699569514036afe3c243bc3059d3942624e936ec93450e", size = 2113890, upload-time = "2025-04-23T18:31:15.011Z" }, + { url = "https://files.pythonhosted.org/packages/ff/e6/e3c5908c03cf00d629eb38393a98fccc38ee0ce8ecce32f69fc7d7b558a7/pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:fe5b32187cbc0c862ee201ad66c30cf218e5ed468ec8dc1cf49dec66e160cc4d", size = 2073359, upload-time = "2025-04-23T18:31:16.393Z" }, + { url = "https://files.pythonhosted.org/packages/12/e7/6a36a07c59ebefc8777d1ffdaf5ae71b06b21952582e4b07eba88a421c79/pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:bc7aee6f634a6f4a95676fcb5d6559a2c2a390330098dba5e5a5f28a2e4ada30", size = 2245883, upload-time = "2025-04-23T18:31:17.892Z" }, + { url = "https://files.pythonhosted.org/packages/16/3f/59b3187aaa6cc0c1e6616e8045b284de2b6a87b027cce2ffcea073adf1d2/pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:235f45e5dbcccf6bd99f9f472858849f73d11120d76ea8707115415f8e5ebebf", size = 2241074, upload-time = "2025-04-23T18:31:19.205Z" }, + { url = "https://files.pythonhosted.org/packages/e0/ed/55532bb88f674d5d8f67ab121a2a13c385df382de2a1677f30ad385f7438/pydantic_core-2.33.2-cp311-cp311-win32.whl", hash = "sha256:6368900c2d3ef09b69cb0b913f9f8263b03786e5b2a387706c5afb66800efd51", size = 1910538, upload-time = "2025-04-23T18:31:20.541Z" }, + { url = "https://files.pythonhosted.org/packages/fe/1b/25b7cccd4519c0b23c2dd636ad39d381abf113085ce4f7bec2b0dc755eb1/pydantic_core-2.33.2-cp311-cp311-win_amd64.whl", hash = "sha256:1e063337ef9e9820c77acc768546325ebe04ee38b08703244c1309cccc4f1bab", size = 1952909, upload-time = "2025-04-23T18:31:22.371Z" }, + { url = "https://files.pythonhosted.org/packages/49/a9/d809358e49126438055884c4366a1f6227f0f84f635a9014e2deb9b9de54/pydantic_core-2.33.2-cp311-cp311-win_arm64.whl", hash = "sha256:6b99022f1d19bc32a4c2a0d544fc9a76e3be90f0b3f4af413f87d38749300e65", size = 1897786, upload-time = "2025-04-23T18:31:24.161Z" }, + { url = "https://files.pythonhosted.org/packages/18/8a/2b41c97f554ec8c71f2a8a5f85cb56a8b0956addfe8b0efb5b3d77e8bdc3/pydantic_core-2.33.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a7ec89dc587667f22b6a0b6579c249fca9026ce7c333fc142ba42411fa243cdc", size = 2009000, upload-time = "2025-04-23T18:31:25.863Z" }, + { url = "https://files.pythonhosted.org/packages/a1/02/6224312aacb3c8ecbaa959897af57181fb6cf3a3d7917fd44d0f2917e6f2/pydantic_core-2.33.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3c6db6e52c6d70aa0d00d45cdb9b40f0433b96380071ea80b09277dba021ddf7", size = 1847996, upload-time = "2025-04-23T18:31:27.341Z" }, + { url = "https://files.pythonhosted.org/packages/d6/46/6dcdf084a523dbe0a0be59d054734b86a981726f221f4562aed313dbcb49/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e61206137cbc65e6d5256e1166f88331d3b6238e082d9f74613b9b765fb9025", size = 1880957, upload-time = "2025-04-23T18:31:28.956Z" }, + { url = "https://files.pythonhosted.org/packages/ec/6b/1ec2c03837ac00886ba8160ce041ce4e325b41d06a034adbef11339ae422/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eb8c529b2819c37140eb51b914153063d27ed88e3bdc31b71198a198e921e011", size = 1964199, upload-time = "2025-04-23T18:31:31.025Z" }, + { url = "https://files.pythonhosted.org/packages/2d/1d/6bf34d6adb9debd9136bd197ca72642203ce9aaaa85cfcbfcf20f9696e83/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c52b02ad8b4e2cf14ca7b3d918f3eb0ee91e63b3167c32591e57c4317e134f8f", size = 2120296, upload-time = "2025-04-23T18:31:32.514Z" }, + { url = "https://files.pythonhosted.org/packages/e0/94/2bd0aaf5a591e974b32a9f7123f16637776c304471a0ab33cf263cf5591a/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:96081f1605125ba0855dfda83f6f3df5ec90c61195421ba72223de35ccfb2f88", size = 2676109, upload-time = "2025-04-23T18:31:33.958Z" }, + { url = "https://files.pythonhosted.org/packages/f9/41/4b043778cf9c4285d59742281a769eac371b9e47e35f98ad321349cc5d61/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f57a69461af2a5fa6e6bbd7a5f60d3b7e6cebb687f55106933188e79ad155c1", size = 2002028, upload-time = "2025-04-23T18:31:39.095Z" }, + { url = "https://files.pythonhosted.org/packages/cb/d5/7bb781bf2748ce3d03af04d5c969fa1308880e1dca35a9bd94e1a96a922e/pydantic_core-2.33.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:572c7e6c8bb4774d2ac88929e3d1f12bc45714ae5ee6d9a788a9fb35e60bb04b", size = 2100044, upload-time = "2025-04-23T18:31:41.034Z" }, + { url = "https://files.pythonhosted.org/packages/fe/36/def5e53e1eb0ad896785702a5bbfd25eed546cdcf4087ad285021a90ed53/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:db4b41f9bd95fbe5acd76d89920336ba96f03e149097365afe1cb092fceb89a1", size = 2058881, upload-time = "2025-04-23T18:31:42.757Z" }, + { url = "https://files.pythonhosted.org/packages/01/6c/57f8d70b2ee57fc3dc8b9610315949837fa8c11d86927b9bb044f8705419/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:fa854f5cf7e33842a892e5c73f45327760bc7bc516339fda888c75ae60edaeb6", size = 2227034, upload-time = "2025-04-23T18:31:44.304Z" }, + { url = "https://files.pythonhosted.org/packages/27/b9/9c17f0396a82b3d5cbea4c24d742083422639e7bb1d5bf600e12cb176a13/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:5f483cfb75ff703095c59e365360cb73e00185e01aaea067cd19acffd2ab20ea", size = 2234187, upload-time = "2025-04-23T18:31:45.891Z" }, + { url = "https://files.pythonhosted.org/packages/b0/6a/adf5734ffd52bf86d865093ad70b2ce543415e0e356f6cacabbc0d9ad910/pydantic_core-2.33.2-cp312-cp312-win32.whl", hash = "sha256:9cb1da0f5a471435a7bc7e439b8a728e8b61e59784b2af70d7c169f8dd8ae290", size = 1892628, upload-time = "2025-04-23T18:31:47.819Z" }, + { url = "https://files.pythonhosted.org/packages/43/e4/5479fecb3606c1368d496a825d8411e126133c41224c1e7238be58b87d7e/pydantic_core-2.33.2-cp312-cp312-win_amd64.whl", hash = "sha256:f941635f2a3d96b2973e867144fde513665c87f13fe0e193c158ac51bfaaa7b2", size = 1955866, upload-time = "2025-04-23T18:31:49.635Z" }, + { url = "https://files.pythonhosted.org/packages/0d/24/8b11e8b3e2be9dd82df4b11408a67c61bb4dc4f8e11b5b0fc888b38118b5/pydantic_core-2.33.2-cp312-cp312-win_arm64.whl", hash = "sha256:cca3868ddfaccfbc4bfb1d608e2ccaaebe0ae628e1416aeb9c4d88c001bb45ab", size = 1888894, upload-time = "2025-04-23T18:31:51.609Z" }, + { url = "https://files.pythonhosted.org/packages/46/8c/99040727b41f56616573a28771b1bfa08a3d3fe74d3d513f01251f79f172/pydantic_core-2.33.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1082dd3e2d7109ad8b7da48e1d4710c8d06c253cbc4a27c1cff4fbcaa97a9e3f", size = 2015688, upload-time = "2025-04-23T18:31:53.175Z" }, + { url = "https://files.pythonhosted.org/packages/3a/cc/5999d1eb705a6cefc31f0b4a90e9f7fc400539b1a1030529700cc1b51838/pydantic_core-2.33.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f517ca031dfc037a9c07e748cefd8d96235088b83b4f4ba8939105d20fa1dcd6", size = 1844808, upload-time = "2025-04-23T18:31:54.79Z" }, + { url = "https://files.pythonhosted.org/packages/6f/5e/a0a7b8885c98889a18b6e376f344da1ef323d270b44edf8174d6bce4d622/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a9f2c9dd19656823cb8250b0724ee9c60a82f3cdf68a080979d13092a3b0fef", size = 1885580, upload-time = "2025-04-23T18:31:57.393Z" }, + { url = "https://files.pythonhosted.org/packages/3b/2a/953581f343c7d11a304581156618c3f592435523dd9d79865903272c256a/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2b0a451c263b01acebe51895bfb0e1cc842a5c666efe06cdf13846c7418caa9a", size = 1973859, upload-time = "2025-04-23T18:31:59.065Z" }, + { url = "https://files.pythonhosted.org/packages/e6/55/f1a813904771c03a3f97f676c62cca0c0a4138654107c1b61f19c644868b/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ea40a64d23faa25e62a70ad163571c0b342b8bf66d5fa612ac0dec4f069d916", size = 2120810, upload-time = "2025-04-23T18:32:00.78Z" }, + { url = "https://files.pythonhosted.org/packages/aa/c3/053389835a996e18853ba107a63caae0b9deb4a276c6b472931ea9ae6e48/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0fb2d542b4d66f9470e8065c5469ec676978d625a8b7a363f07d9a501a9cb36a", size = 2676498, upload-time = "2025-04-23T18:32:02.418Z" }, + { url = "https://files.pythonhosted.org/packages/eb/3c/f4abd740877a35abade05e437245b192f9d0ffb48bbbbd708df33d3cda37/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fdac5d6ffa1b5a83bca06ffe7583f5576555e6c8b3a91fbd25ea7780f825f7d", size = 2000611, upload-time = "2025-04-23T18:32:04.152Z" }, + { url = "https://files.pythonhosted.org/packages/59/a7/63ef2fed1837d1121a894d0ce88439fe3e3b3e48c7543b2a4479eb99c2bd/pydantic_core-2.33.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:04a1a413977ab517154eebb2d326da71638271477d6ad87a769102f7c2488c56", size = 2107924, upload-time = "2025-04-23T18:32:06.129Z" }, + { url = "https://files.pythonhosted.org/packages/04/8f/2551964ef045669801675f1cfc3b0d74147f4901c3ffa42be2ddb1f0efc4/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c8e7af2f4e0194c22b5b37205bfb293d166a7344a5b0d0eaccebc376546d77d5", size = 2063196, upload-time = "2025-04-23T18:32:08.178Z" }, + { url = "https://files.pythonhosted.org/packages/26/bd/d9602777e77fc6dbb0c7db9ad356e9a985825547dce5ad1d30ee04903918/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:5c92edd15cd58b3c2d34873597a1e20f13094f59cf88068adb18947df5455b4e", size = 2236389, upload-time = "2025-04-23T18:32:10.242Z" }, + { url = "https://files.pythonhosted.org/packages/42/db/0e950daa7e2230423ab342ae918a794964b053bec24ba8af013fc7c94846/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:65132b7b4a1c0beded5e057324b7e16e10910c106d43675d9bd87d4f38dde162", size = 2239223, upload-time = "2025-04-23T18:32:12.382Z" }, + { url = "https://files.pythonhosted.org/packages/58/4d/4f937099c545a8a17eb52cb67fe0447fd9a373b348ccfa9a87f141eeb00f/pydantic_core-2.33.2-cp313-cp313-win32.whl", hash = "sha256:52fb90784e0a242bb96ec53f42196a17278855b0f31ac7c3cc6f5c1ec4811849", size = 1900473, upload-time = "2025-04-23T18:32:14.034Z" }, + { url = "https://files.pythonhosted.org/packages/a0/75/4a0a9bac998d78d889def5e4ef2b065acba8cae8c93696906c3a91f310ca/pydantic_core-2.33.2-cp313-cp313-win_amd64.whl", hash = "sha256:c083a3bdd5a93dfe480f1125926afcdbf2917ae714bdb80b36d34318b2bec5d9", size = 1955269, upload-time = "2025-04-23T18:32:15.783Z" }, + { url = "https://files.pythonhosted.org/packages/f9/86/1beda0576969592f1497b4ce8e7bc8cbdf614c352426271b1b10d5f0aa64/pydantic_core-2.33.2-cp313-cp313-win_arm64.whl", hash = "sha256:e80b087132752f6b3d714f041ccf74403799d3b23a72722ea2e6ba2e892555b9", size = 1893921, upload-time = "2025-04-23T18:32:18.473Z" }, + { url = "https://files.pythonhosted.org/packages/a4/7d/e09391c2eebeab681df2b74bfe6c43422fffede8dc74187b2b0bf6fd7571/pydantic_core-2.33.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:61c18fba8e5e9db3ab908620af374db0ac1baa69f0f32df4f61ae23f15e586ac", size = 1806162, upload-time = "2025-04-23T18:32:20.188Z" }, + { url = "https://files.pythonhosted.org/packages/f1/3d/847b6b1fed9f8ed3bb95a9ad04fbd0b212e832d4f0f50ff4d9ee5a9f15cf/pydantic_core-2.33.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95237e53bb015f67b63c91af7518a62a8660376a6a0db19b89acc77a4d6199f5", size = 1981560, upload-time = "2025-04-23T18:32:22.354Z" }, + { url = "https://files.pythonhosted.org/packages/6f/9a/e73262f6c6656262b5fdd723ad90f518f579b7bc8622e43a942eec53c938/pydantic_core-2.33.2-cp313-cp313t-win_amd64.whl", hash = "sha256:c2fc0a768ef76c15ab9238afa6da7f69895bb5d1ee83aeea2e3509af4472d0b9", size = 1935777, upload-time = "2025-04-23T18:32:25.088Z" }, + { url = "https://files.pythonhosted.org/packages/7b/27/d4ae6487d73948d6f20dddcd94be4ea43e74349b56eba82e9bdee2d7494c/pydantic_core-2.33.2-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:dd14041875d09cc0f9308e37a6f8b65f5585cf2598a53aa0123df8b129d481f8", size = 2025200, upload-time = "2025-04-23T18:33:14.199Z" }, + { url = "https://files.pythonhosted.org/packages/f1/b8/b3cb95375f05d33801024079b9392a5ab45267a63400bf1866e7ce0f0de4/pydantic_core-2.33.2-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:d87c561733f66531dced0da6e864f44ebf89a8fba55f31407b00c2f7f9449593", size = 1859123, upload-time = "2025-04-23T18:33:16.555Z" }, + { url = "https://files.pythonhosted.org/packages/05/bc/0d0b5adeda59a261cd30a1235a445bf55c7e46ae44aea28f7bd6ed46e091/pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2f82865531efd18d6e07a04a17331af02cb7a651583c418df8266f17a63c6612", size = 1892852, upload-time = "2025-04-23T18:33:18.513Z" }, + { url = "https://files.pythonhosted.org/packages/3e/11/d37bdebbda2e449cb3f519f6ce950927b56d62f0b84fd9cb9e372a26a3d5/pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bfb5112df54209d820d7bf9317c7a6c9025ea52e49f46b6a2060104bba37de7", size = 2067484, upload-time = "2025-04-23T18:33:20.475Z" }, + { url = "https://files.pythonhosted.org/packages/8c/55/1f95f0a05ce72ecb02a8a8a1c3be0579bbc29b1d5ab68f1378b7bebc5057/pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:64632ff9d614e5eecfb495796ad51b0ed98c453e447a76bcbeeb69615079fc7e", size = 2108896, upload-time = "2025-04-23T18:33:22.501Z" }, + { url = "https://files.pythonhosted.org/packages/53/89/2b2de6c81fa131f423246a9109d7b2a375e83968ad0800d6e57d0574629b/pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:f889f7a40498cc077332c7ab6b4608d296d852182211787d4f3ee377aaae66e8", size = 2069475, upload-time = "2025-04-23T18:33:24.528Z" }, + { url = "https://files.pythonhosted.org/packages/b8/e9/1f7efbe20d0b2b10f6718944b5d8ece9152390904f29a78e68d4e7961159/pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:de4b83bb311557e439b9e186f733f6c645b9417c84e2eb8203f3f820a4b988bf", size = 2239013, upload-time = "2025-04-23T18:33:26.621Z" }, + { url = "https://files.pythonhosted.org/packages/3c/b2/5309c905a93811524a49b4e031e9851a6b00ff0fb668794472ea7746b448/pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:82f68293f055f51b51ea42fafc74b6aad03e70e191799430b90c13d643059ebb", size = 2238715, upload-time = "2025-04-23T18:33:28.656Z" }, + { url = "https://files.pythonhosted.org/packages/32/56/8a7ca5d2cd2cda1d245d34b1c9a942920a718082ae8e54e5f3e5a58b7add/pydantic_core-2.33.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:329467cecfb529c925cf2bbd4d60d2c509bc2fb52a20c1045bf09bb70971a9c1", size = 2066757, upload-time = "2025-04-23T18:33:30.645Z" }, ] [[package]] @@ -3093,6 +3120,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/65/f3/107a22063bf27bdccf2024833d3445f4eea42b2e598abfbd46f6a63b6cb0/typing_inspect-0.9.0-py3-none-any.whl", hash = "sha256:9ee6fc59062311ef8547596ab6b955e1b8aa46242d854bfc78f4f6b0eff35f9f", size = 8827, upload-time = "2023-05-24T20:25:45.287Z" }, ] +[[package]] +name = "typing-inspection" +version = "0.4.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f8/b1/0c11f5058406b3af7609f121aaa6b609744687f1d158b3c3a5bf4cc94238/typing_inspection-0.4.1.tar.gz", hash = "sha256:6ae134cc0203c33377d43188d4064e9b357dba58cff3185f22924610e70a9d28", size = 75726, upload-time = "2025-05-21T18:55:23.885Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/17/69/cd203477f944c353c31bade965f880aa1061fd6bf05ded0726ca845b6ff7/typing_inspection-0.4.1-py3-none-any.whl", hash = "sha256:389055682238f53b04f7badcb49b989835495a96700ced5dab2d8feae4b26f51", size = 14552, upload-time = "2025-05-21T18:55:22.152Z" }, +] + [[package]] name = "tzdata" version = "2025.2" From ef671d881345b492903326ed57e4876b9f8b5106 Mon Sep 17 00:00:00 2001 From: reatang Date: Thu, 12 Jun 2025 21:08:10 +0800 Subject: [PATCH 03/14] add qwen --- src/core/llm.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/core/llm.py b/src/core/llm.py index 61b6b468..4495b588 100644 --- a/src/core/llm.py +++ b/src/core/llm.py @@ -32,6 +32,7 @@ | {m: m.value for m in OpenAICompatibleName} | {m: m.value for m in AzureOpenAIModelName} | {m: m.value for m in DeepseekModelName} + | {m: m.value for m in AlibabaQWenModelName} | {m: m.value for m in AnthropicModelName} | {m: m.value for m in GoogleModelName} | {m: m.value for m in VertexAIModelName} From d3de24a1bc0feb8405b4899c6acaabf089bc20a7 Mon Sep 17 00:00:00 2001 From: reatang Date: Thu, 12 Jun 2025 21:13:14 +0800 Subject: [PATCH 04/14] fix lock file --- uv.lock | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/uv.lock b/uv.lock index 35202508..d8269e29 100644 --- a/uv.lock +++ b/uv.lock @@ -4021,18 +4021,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/17/69/cd203477f944c353c31bade965f880aa1061fd6bf05ded0726ca845b6ff7/typing_inspection-0.4.1-py3-none-any.whl", hash = "sha256:389055682238f53b04f7badcb49b989835495a96700ced5dab2d8feae4b26f51", size = 14552, upload-time = "2025-05-21T18:55:22.152Z" }, ] -[[package]] -name = "typing-inspection" -version = "0.4.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/f8/b1/0c11f5058406b3af7609f121aaa6b609744687f1d158b3c3a5bf4cc94238/typing_inspection-0.4.1.tar.gz", hash = "sha256:6ae134cc0203c33377d43188d4064e9b357dba58cff3185f22924610e70a9d28", size = 75726, upload-time = "2025-05-21T18:55:23.885Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/17/69/cd203477f944c353c31bade965f880aa1061fd6bf05ded0726ca845b6ff7/typing_inspection-0.4.1-py3-none-any.whl", hash = "sha256:389055682238f53b04f7badcb49b989835495a96700ced5dab2d8feae4b26f51", size = 14552, upload-time = "2025-05-21T18:55:22.152Z" }, -] - [[package]] name = "tzdata" version = "2025.2" From e2932a4641a40bb558b7274470f7d638f3286231 Mon Sep 17 00:00:00 2001 From: reatang Date: Thu, 12 Jun 2025 21:59:05 +0800 Subject: [PATCH 05/14] ruff format --- src/core/llm.py | 2 +- src/core/settings.py | 2 +- src/service/service.py | 21 ++++++++++----------- src/service/utils.py | 29 ++++++++++++++--------------- 4 files changed, 26 insertions(+), 28 deletions(-) diff --git a/src/core/llm.py b/src/core/llm.py index 4495b588..edba3962 100644 --- a/src/core/llm.py +++ b/src/core/llm.py @@ -12,12 +12,12 @@ from core.settings import settings from schema.models import ( + AlibabaQWenModelName, AllModelEnum, AnthropicModelName, AWSModelName, AzureOpenAIModelName, DeepseekModelName, - AlibabaQWenModelName, FakeModelName, GoogleModelName, GroqModelName, diff --git a/src/core/settings.py b/src/core/settings.py index e0af2f65..b09895c7 100644 --- a/src/core/settings.py +++ b/src/core/settings.py @@ -14,12 +14,12 @@ from pydantic_settings import BaseSettings, SettingsConfigDict from schema.models import ( + AlibabaQWenModelName, AllModelEnum, AnthropicModelName, AWSModelName, AzureOpenAIModelName, DeepseekModelName, - AlibabaQWenModelName, FakeModelName, GoogleModelName, GroqModelName, diff --git a/src/service/service.py b/src/service/service.py index 16526ed3..54f2c1e0 100644 --- a/src/service/service.py +++ b/src/service/service.py @@ -7,6 +7,15 @@ from typing import Annotated, Any from uuid import UUID, uuid4 +# AG-UI Protocol imports +from ag_ui.core import ( + EventType, + RunErrorEvent, + RunFinishedEvent, + RunStartedEvent, +) +from ag_ui.core.events import TextMessageChunkEvent +from ag_ui.encoder import EventEncoder from fastapi import APIRouter, Depends, FastAPI, HTTPException, status from fastapi.responses import StreamingResponse from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer @@ -19,16 +28,6 @@ from langgraph.types import Command, Interrupt from langsmith import Client as LangsmithClient -# AG-UI Protocol imports -from ag_ui.core import ( - EventType, - RunStartedEvent, - RunFinishedEvent, - RunErrorEvent, -) -from ag_ui.core.events import TextMessageChunkEvent -from ag_ui.encoder import EventEncoder - from agents import DEFAULT_AGENT, get_agent, get_all_agent_info from core import settings from memory import initialize_database, initialize_store @@ -44,9 +43,9 @@ ) from service.utils import ( convert_message_content_to_string, + convert_message_to_agui_events, langchain_to_chat_message, remove_tool_calls, - convert_message_to_agui_events, ) warnings.filterwarnings("ignore", category=LangChainBetaWarning) diff --git a/src/service/utils.py b/src/service/utils.py index cd1ed70d..35176701 100644 --- a/src/service/utils.py +++ b/src/service/utils.py @@ -1,30 +1,29 @@ -from typing import AsyncGenerator, Any +from collections.abc import AsyncGenerator from uuid import uuid4 +from ag_ui.core.events import ( + CustomEvent, + EventEncoder, + EventType, + RawEvent, + TextMessageContentEvent, + TextMessageEndEvent, + TextMessageStartEvent, + ToolCallArgsEvent, + ToolCallEndEvent, + ToolCallStartEvent, +) from langchain_core.messages import ( AIMessage, + AnyMessage, BaseMessage, HumanMessage, ToolMessage, - AnyMessage, ) from langchain_core.messages import ( ChatMessage as LangchainChatMessage, ) -from ag_ui.core.events import EventEncoder -from ag_ui.core.events import ( - EventType, - ToolCallStartEvent, - ToolCallArgsEvent, - ToolCallEndEvent, - TextMessageStartEvent, - TextMessageContentEvent, - TextMessageEndEvent, - CustomEvent, - RawEvent, -) - from schema import ChatMessage From 96cdb10fd509d4e6d49d01a89dff2cf4a9654b16 Mon Sep 17 00:00:00 2001 From: reatang Date: Fri, 13 Jun 2025 12:12:04 +0800 Subject: [PATCH 06/14] fix bug --- pyproject.toml | 4 ++++ src/service/utils.py | 10 +++++++--- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 9aa174e1..d856f19c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -100,3 +100,7 @@ exclude = "src/streamlit_app.py" [[tool.mypy.overrides]] module = ["numexpr.*"] follow_untyped_imports = true + +[[tool.mypy.overrides]] +module = ["ag_ui.*"] +ignore_missing_imports = true diff --git a/src/service/utils.py b/src/service/utils.py index 35176701..3a837e95 100644 --- a/src/service/utils.py +++ b/src/service/utils.py @@ -1,4 +1,5 @@ from collections.abc import AsyncGenerator +from typing import Any, cast from uuid import uuid4 from ag_ui.core.events import ( @@ -146,13 +147,16 @@ async def convert_message_to_agui_events( """ pass - elif message.role == "custom": + elif isinstance(message, LangchainChatMessage) and message.role == "custom": + # m is TaskData in the form of a dict + m = cast(dict[str, Any], message.content[0]) + # Handle custom messages as raw events yield encoder.encode( CustomEvent( type=EventType.CUSTOM, - name=message.content[0].get("type", "unknown"), - value=message.content[0].get("data", {}), + name=m.get("name", "custom"), + value=m, ) ) From 216c35e598c3e2fbd495da17434e63750ed2201c Mon Sep 17 00:00:00 2001 From: reatang Date: Fri, 13 Jun 2025 12:15:21 +0800 Subject: [PATCH 07/14] fix bug2 --- src/service/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/service/utils.py b/src/service/utils.py index 3a837e95..3fcf1821 100644 --- a/src/service/utils.py +++ b/src/service/utils.py @@ -2,9 +2,9 @@ from typing import Any, cast from uuid import uuid4 +from ag_ui.encoder.encoder import EventEncoder from ag_ui.core.events import ( CustomEvent, - EventEncoder, EventType, RawEvent, TextMessageContentEvent, From d04bcf39323b6a9c4838a5fd198951df04f32208 Mon Sep 17 00:00:00 2001 From: reatang Date: Fri, 13 Jun 2025 12:18:05 +0800 Subject: [PATCH 08/14] ruff format --- src/service/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/service/utils.py b/src/service/utils.py index 3fcf1821..dd1e0537 100644 --- a/src/service/utils.py +++ b/src/service/utils.py @@ -2,7 +2,6 @@ from typing import Any, cast from uuid import uuid4 -from ag_ui.encoder.encoder import EventEncoder from ag_ui.core.events import ( CustomEvent, EventType, @@ -14,6 +13,7 @@ ToolCallEndEvent, ToolCallStartEvent, ) +from ag_ui.encoder.encoder import EventEncoder from langchain_core.messages import ( AIMessage, AnyMessage, From fcc852a0e70bad46d530730c4b97d57a57375c7e Mon Sep 17 00:00:00 2001 From: reatang Date: Fri, 13 Jun 2025 15:12:01 +0800 Subject: [PATCH 09/14] add unit test and api test --- tests/service/test_service_agui.py | 476 +++++++++++++++++++++++++++++ 1 file changed, 476 insertions(+) create mode 100644 tests/service/test_service_agui.py diff --git a/tests/service/test_service_agui.py b/tests/service/test_service_agui.py new file mode 100644 index 00000000..d27f2eac --- /dev/null +++ b/tests/service/test_service_agui.py @@ -0,0 +1,476 @@ +import pytest +from uuid import uuid4 +from ag_ui.core import EventType +from ag_ui.encoder import EventEncoder +from langchain_core.messages import AIMessage, ToolMessage, HumanMessage, ChatMessage as LangchainChatMessage +from service.utils import convert_message_to_agui_events +import asyncio +import json +from unittest.mock import AsyncMock, patch +from langchain_core.messages import AIMessageChunk +from langgraph.pregel.types import StateSnapshot +from langgraph.types import Interrupt + +@pytest.fixture +def encoder(): + return EventEncoder() + +def run_async_gen(gen): + loop = asyncio.new_event_loop() + return loop.run_until_complete(_collect_async_gen(gen)) + +def _collect_async_gen(gen): + result = [] + async def collect(): + async for item in gen: + result.append(item) + return result + return collect() + +# ============ Unit Tests ============ + +def test_ai_message_with_tool_and_text(encoder): + msg = AIMessage( + content="hello", + tool_calls=[{"name": "Calculator", "args": {"x": 1, "y": 2}, "id": str(uuid4())}] + ) + events = run_async_gen(convert_message_to_agui_events(msg, encoder)) + assert any(EventType.TOOL_CALL_START.value in e for e in events) + assert any(EventType.TOOL_CALL_ARGS.value in e for e in events) + assert any(EventType.TOOL_CALL_END.value in e for e in events) + assert any(EventType.TEXT_MESSAGE_START.value in e for e in events) + assert any(EventType.TEXT_MESSAGE_CONTENT.value in e for e in events) + assert any(EventType.TEXT_MESSAGE_END.value in e for e in events) + +def test_ai_message_only_text(encoder): + msg = AIMessage(content="just text", tool_calls=[]) + events = run_async_gen(convert_message_to_agui_events(msg, encoder)) + assert any(EventType.TEXT_MESSAGE_START.value in e for e in events) + assert any(EventType.TEXT_MESSAGE_CONTENT.value in e for e in events) + assert any(EventType.TEXT_MESSAGE_END.value in e for e in events) + assert not any(EventType.TOOL_CALL_START.value in e for e in events) + +def test_ai_message_only_tool(encoder): + msg = AIMessage(content="", tool_calls=[{"name": "Search", "args": {"q": "foo"}, "id": str(uuid4())}]) + events = run_async_gen(convert_message_to_agui_events(msg, encoder)) + assert any(EventType.TOOL_CALL_START.value in e for e in events) + assert any(EventType.TOOL_CALL_ARGS.value in e for e in events) + assert any(EventType.TOOL_CALL_END.value in e for e in events) + assert not any(EventType.TEXT_MESSAGE_START.value in e for e in events) + +def test_tool_message_no_event(encoder): + msg = ToolMessage(content="result", name="Calculator", tool_call_id=str(uuid4())) + events = run_async_gen(convert_message_to_agui_events(msg, encoder)) + assert len(events) == 0 + +def test_custom_langchain_message(encoder): + custom_data = {"name": "my_custom", "foo": 123} + msg = LangchainChatMessage(role="custom", content=[custom_data]) + events = run_async_gen(convert_message_to_agui_events(msg, encoder)) + assert any(EventType.CUSTOM.value in e for e in events) + assert any("my_custom" in e for e in events) + +def test_human_message_to_raw_event(encoder): + msg = HumanMessage(content="user input") + events = run_async_gen(convert_message_to_agui_events(msg, encoder)) + assert any(EventType.RAW.value in e for e in events) + assert any("user input" in e for e in events) + +def test_invalid_message_to_raw_event(encoder): + class Dummy: pass + events = run_async_gen(convert_message_to_agui_events(Dummy(), encoder)) + assert any(EventType.RAW.value in e for e in events) + +# ============ API Tests ============ + +def test_stream_agui_basic(test_client, mock_agent) -> None: + """Test basic streaming interface with AG-UI protocol""" + QUESTION = "What is the weather in Tokyo?" + ANSWER = "The weather in Tokyo is sunny." + + # Configure mock agent to return event stream + events = [ + ( + "updates", + {"chat_model": {"messages": [AIMessage(content=ANSWER)]}}, + ) + ] + + async def mock_astream(**kwargs): + for event in events: + yield event + + mock_agent.astream = mock_astream + + # Make streaming request with AG-UI protocol + with test_client.stream( + "POST", "/stream", json={"message": QUESTION, "stream_protocol": "agui"} + ) as response: + assert response.status_code == 200 + + # Collect all SSE messages + messages = [] + for line in response.iter_lines(): + if line and not line.startswith("data: [DONE]"): + try: + event_data = line.lstrip("data: ") + messages.append(event_data) + except json.JSONDecodeError: + continue + + # Verify AG-UI protocol events are present + event_content = "".join(messages) + assert EventType.RUN_STARTED.value in event_content + assert EventType.RUN_FINISHED.value in event_content + + +def test_stream_agui_with_tokens(test_client, mock_agent) -> None: + """Test AG-UI protocol token streaming""" + QUESTION = "What is the weather in Tokyo?" + TOKENS = ["The", " weather", " is", " sunny"] + FINAL_ANSWER = "The weather is sunny" + + # Configure mock agent to return token event stream + events = [ + ( + "messages", + ( + AIMessageChunk(content=token), + {"tags": []}, + ), + ) + for token in TOKENS + ] + [ + ( + "updates", + {"chat_model": {"messages": [AIMessage(content=FINAL_ANSWER)]}}, + ) + ] + + async def mock_astream(**kwargs): + for event in events: + yield event + + mock_agent.astream = mock_astream + + with test_client.stream( + "POST", "/stream", json={ + "message": QUESTION, + "stream_protocol": "agui", + "stream_tokens": True + } + ) as response: + assert response.status_code == 200 + + # Collect all SSE messages + messages = [] + for line in response.iter_lines(): + if line and not line.startswith("data: [DONE]"): + try: + event_data = line.lstrip("data: ") + messages.append(event_data) + except json.JSONDecodeError: + continue + + # Verify token streaming events are present + event_content = "".join(messages) + assert EventType.TEXT_MESSAGE_CHUNK.value in event_content + assert EventType.RUN_STARTED.value in event_content + assert EventType.RUN_FINISHED.value in event_content + + +def test_stream_agui_no_tokens(test_client, mock_agent) -> None: + """Test AG-UI protocol without token streaming""" + QUESTION = "What is the weather in Tokyo?" + TOKENS = ["The", " weather", " is", " sunny"] + FINAL_ANSWER = "The weather is sunny" + + # Configure mock agent to return event stream + events = [ + ( + "messages", + ( + AIMessageChunk(content=token), + {"tags": []}, + ), + ) + for token in TOKENS + ] + [ + ( + "updates", + {"chat_model": {"messages": [AIMessage(content=FINAL_ANSWER)]}}, + ) + ] + + async def mock_astream(**kwargs): + for event in events: + yield event + + mock_agent.astream = mock_astream + + with test_client.stream( + "POST", "/stream", json={ + "message": QUESTION, + "stream_protocol": "agui", + "stream_tokens": False + } + ) as response: + assert response.status_code == 200 + + # Collect all SSE messages + messages = [] + for line in response.iter_lines(): + if line and not line.startswith("data: [DONE]"): + try: + event_data = line.lstrip("data: ") + messages.append(event_data) + except json.JSONDecodeError: + continue + + # Verify no token streaming events but other AG-UI events are present + event_content = "".join(messages) + assert EventType.TEXT_MESSAGE_CHUNK.value not in event_content + assert EventType.RUN_STARTED.value in event_content + assert EventType.RUN_FINISHED.value in event_content + + +def test_stream_agui_with_tools(test_client, mock_agent) -> None: + """Test AG-UI protocol tool calls""" + QUESTION = "Calculate 2 + 3" + + # Create AI message with tool calls + tool_message = AIMessage( + content="I'll calculate that for you.", + tool_calls=[{ + "name": "Calculator", + "args": {"expression": "2 + 3"}, + "id": str(uuid4()) + }] + ) + + events = [ + ( + "updates", + {"math_tool": {"messages": [tool_message]}}, + ) + ] + + async def mock_astream(**kwargs): + for event in events: + yield event + + mock_agent.astream = mock_astream + + with test_client.stream( + "POST", "/stream", json={"message": QUESTION, "stream_protocol": "agui"} + ) as response: + assert response.status_code == 200 + + # Collect all SSE messages + messages = [] + for line in response.iter_lines(): + if line and not line.startswith("data: [DONE]"): + try: + event_data = line.lstrip("data: ") + messages.append(event_data) + except json.JSONDecodeError: + continue + + # Verify tool call related AG-UI events are present + event_content = "".join(messages) + assert EventType.TOOL_CALL_START.value in event_content + assert EventType.TOOL_CALL_ARGS.value in event_content + assert EventType.TOOL_CALL_END.value in event_content + assert EventType.RUN_STARTED.value in event_content + assert EventType.RUN_FINISHED.value in event_content + + +def test_stream_agui_custom_agent(test_client, mock_agent) -> None: + """Test AG-UI protocol with custom agent""" + CUSTOM_AGENT = "custom_agent" + QUESTION = "What is the weather?" + ANSWER = "It's sunny." + + events = [ + ( + "updates", + {"chat_model": {"messages": [AIMessage(content=ANSWER)]}}, + ) + ] + + async def mock_astream(**kwargs): + for event in events: + yield event + + mock_agent.astream = mock_astream + + def agent_lookup(agent_id): + if agent_id == CUSTOM_AGENT: + return mock_agent + return AsyncMock() + + with patch("service.service.get_agent", side_effect=agent_lookup): + with test_client.stream( + "POST", f"/{CUSTOM_AGENT}/stream", + json={"message": QUESTION, "stream_protocol": "agui"} + ) as response: + assert response.status_code == 200 + + # Collect all SSE messages + messages = [] + for line in response.iter_lines(): + if line and not line.startswith("data: [DONE]"): + try: + event_data = line.lstrip("data: ") + messages.append(event_data) + except json.JSONDecodeError: + continue + + # Verify AG-UI events are generated correctly + event_content = "".join(messages) + assert EventType.RUN_STARTED.value in event_content + assert EventType.RUN_FINISHED.value in event_content + + +def test_stream_agui_interrupt(test_client, mock_agent) -> None: + """Test AG-UI protocol interrupt handling""" + QUESTION = "Confirm this action" + INTERRUPT = "Please confirm: Continue with operation?" + + events = [ + ( + "updates", + {"__interrupt__": [Interrupt(value=INTERRUPT)]}, + ) + ] + + async def mock_astream(**kwargs): + for event in events: + yield event + + mock_agent.astream = mock_astream + + with test_client.stream( + "POST", "/stream", json={"message": QUESTION, "stream_protocol": "agui"} + ) as response: + assert response.status_code == 200 + + # Collect all SSE messages + messages = [] + for line in response.iter_lines(): + if line and not line.startswith("data: [DONE]"): + try: + event_data = line.lstrip("data: ") + messages.append(event_data) + except json.JSONDecodeError: + continue + + # Verify interrupt message is handled as text message + event_content = "".join(messages) + assert EventType.RUN_STARTED.value in event_content + assert EventType.TEXT_MESSAGE_START.value in event_content + assert INTERRUPT in event_content + assert EventType.RUN_FINISHED.value in event_content + + +def test_stream_agui_custom_message(test_client, mock_agent) -> None: + """Test AG-UI protocol custom messages""" + QUESTION = "Custom request" + + # Create custom message + custom_data = {"action": "special_operation", "data": {"key": "value"}} + + events = [ + ( + "custom", + custom_data, + ) + ] + + async def mock_astream(**kwargs): + for event in events: + yield event + + mock_agent.astream = mock_astream + + with test_client.stream( + "POST", "/stream", json={"message": QUESTION, "stream_protocol": "agui"} + ) as response: + assert response.status_code == 200 + + # Collect all SSE messages + messages = [] + for line in response.iter_lines(): + if line and not line.startswith("data: [DONE]"): + try: + event_data = line.lstrip("data: ") + messages.append(event_data) + except json.JSONDecodeError: + continue + + # Verify custom events + event_content = "".join(messages) + assert EventType.RUN_STARTED.value in event_content + assert EventType.RAW.value in event_content + assert "special_operation" in event_content + assert EventType.RUN_FINISHED.value in event_content + + +def test_stream_invalid_protocol(test_client, mock_agent) -> None: + """Test invalid stream protocol""" + QUESTION = "What is the weather?" + + response = test_client.post( + "/stream", + json={"message": QUESTION, "stream_protocol": "invalid"} + ) + assert response.status_code == 422 # Validation error + + +def test_stream_agui_model_param(test_client, mock_agent) -> None: + """Test AG-UI protocol model parameter passing""" + QUESTION = "What is the weather?" + CUSTOM_MODEL = "claude-3.5-sonnet" + ANSWER = "The weather is sunny." + + events = [ + ( + "updates", + {"chat_model": {"messages": [AIMessage(content=ANSWER)]}}, + ) + ] + + async def mock_astream(**kwargs): + # Verify model parameter passing is correct + config = kwargs.get("config", {}) + configurable = config.get("configurable", {}) + assert configurable.get("model") == CUSTOM_MODEL + + for event in events: + yield event + + mock_agent.astream = mock_astream + + with test_client.stream( + "POST", "/stream", + json={ + "message": QUESTION, + "stream_protocol": "agui", + "model": CUSTOM_MODEL + } + ) as response: + assert response.status_code == 200 + + # Collect all SSE messages to verify response is normal + messages = [] + for line in response.iter_lines(): + if line and not line.startswith("data: [DONE]"): + try: + event_data = line.lstrip("data: ") + messages.append(event_data) + except json.JSONDecodeError: + continue + + event_content = "".join(messages) + assert EventType.RUN_STARTED.value in event_content + assert EventType.RUN_FINISHED.value in event_content \ No newline at end of file From f74f65ea189d24b7d049d000148eb37131fa62d5 Mon Sep 17 00:00:00 2001 From: reatang Date: Fri, 13 Jun 2025 15:16:43 +0800 Subject: [PATCH 10/14] ruff format --- tests/service/test_service_agui.py | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/tests/service/test_service_agui.py b/tests/service/test_service_agui.py index d27f2eac..025ce233 100644 --- a/tests/service/test_service_agui.py +++ b/tests/service/test_service_agui.py @@ -1,16 +1,18 @@ -import pytest -from uuid import uuid4 -from ag_ui.core import EventType -from ag_ui.encoder import EventEncoder -from langchain_core.messages import AIMessage, ToolMessage, HumanMessage, ChatMessage as LangchainChatMessage -from service.utils import convert_message_to_agui_events import asyncio import json from unittest.mock import AsyncMock, patch -from langchain_core.messages import AIMessageChunk -from langgraph.pregel.types import StateSnapshot +from uuid import uuid4 + +import pytest +from ag_ui.core import EventType +from ag_ui.encoder import EventEncoder +from langchain_core.messages import AIMessage, AIMessageChunk, HumanMessage, ToolMessage +from langchain_core.messages import ChatMessage as LangchainChatMessage from langgraph.types import Interrupt +from service.utils import convert_message_to_agui_events + + @pytest.fixture def encoder(): return EventEncoder() @@ -76,8 +78,10 @@ def test_human_message_to_raw_event(encoder): assert any(EventType.RAW.value in e for e in events) assert any("user input" in e for e in events) +class Dummy: + pass + def test_invalid_message_to_raw_event(encoder): - class Dummy: pass events = run_async_gen(convert_message_to_agui_events(Dummy(), encoder)) assert any(EventType.RAW.value in e for e in events) From afb0a3675a027ac058f9708fda084970aa94c5e3 Mon Sep 17 00:00:00 2001 From: reatang Date: Fri, 13 Jun 2025 15:19:57 +0800 Subject: [PATCH 11/14] ruff format --- tests/service/test_service_agui.py | 70 +++++++++++++++--------------- 1 file changed, 36 insertions(+), 34 deletions(-) diff --git a/tests/service/test_service_agui.py b/tests/service/test_service_agui.py index 025ce233..89f58f8f 100644 --- a/tests/service/test_service_agui.py +++ b/tests/service/test_service_agui.py @@ -17,24 +17,30 @@ def encoder(): return EventEncoder() + def run_async_gen(gen): loop = asyncio.new_event_loop() return loop.run_until_complete(_collect_async_gen(gen)) + def _collect_async_gen(gen): result = [] + async def collect(): async for item in gen: result.append(item) return result + return collect() + # ============ Unit Tests ============ + def test_ai_message_with_tool_and_text(encoder): msg = AIMessage( content="hello", - tool_calls=[{"name": "Calculator", "args": {"x": 1, "y": 2}, "id": str(uuid4())}] + tool_calls=[{"name": "Calculator", "args": {"x": 1, "y": 2}, "id": str(uuid4())}], ) events = run_async_gen(convert_message_to_agui_events(msg, encoder)) assert any(EventType.TOOL_CALL_START.value in e for e in events) @@ -44,6 +50,7 @@ def test_ai_message_with_tool_and_text(encoder): assert any(EventType.TEXT_MESSAGE_CONTENT.value in e for e in events) assert any(EventType.TEXT_MESSAGE_END.value in e for e in events) + def test_ai_message_only_text(encoder): msg = AIMessage(content="just text", tool_calls=[]) events = run_async_gen(convert_message_to_agui_events(msg, encoder)) @@ -52,19 +59,24 @@ def test_ai_message_only_text(encoder): assert any(EventType.TEXT_MESSAGE_END.value in e for e in events) assert not any(EventType.TOOL_CALL_START.value in e for e in events) + def test_ai_message_only_tool(encoder): - msg = AIMessage(content="", tool_calls=[{"name": "Search", "args": {"q": "foo"}, "id": str(uuid4())}]) + msg = AIMessage( + content="", tool_calls=[{"name": "Search", "args": {"q": "foo"}, "id": str(uuid4())}] + ) events = run_async_gen(convert_message_to_agui_events(msg, encoder)) assert any(EventType.TOOL_CALL_START.value in e for e in events) assert any(EventType.TOOL_CALL_ARGS.value in e for e in events) assert any(EventType.TOOL_CALL_END.value in e for e in events) assert not any(EventType.TEXT_MESSAGE_START.value in e for e in events) + def test_tool_message_no_event(encoder): msg = ToolMessage(content="result", name="Calculator", tool_call_id=str(uuid4())) events = run_async_gen(convert_message_to_agui_events(msg, encoder)) assert len(events) == 0 + def test_custom_langchain_message(encoder): custom_data = {"name": "my_custom", "foo": 123} msg = LangchainChatMessage(role="custom", content=[custom_data]) @@ -72,21 +84,26 @@ def test_custom_langchain_message(encoder): assert any(EventType.CUSTOM.value in e for e in events) assert any("my_custom" in e for e in events) + def test_human_message_to_raw_event(encoder): msg = HumanMessage(content="user input") events = run_async_gen(convert_message_to_agui_events(msg, encoder)) assert any(EventType.RAW.value in e for e in events) assert any("user input" in e for e in events) -class Dummy: + +class Dummy: pass + def test_invalid_message_to_raw_event(encoder): events = run_async_gen(convert_message_to_agui_events(Dummy(), encoder)) assert any(EventType.RAW.value in e for e in events) + # ============ API Tests ============ + def test_stream_agui_basic(test_client, mock_agent) -> None: """Test basic streaming interface with AG-UI protocol""" QUESTION = "What is the weather in Tokyo?" @@ -158,11 +175,9 @@ async def mock_astream(**kwargs): mock_agent.astream = mock_astream with test_client.stream( - "POST", "/stream", json={ - "message": QUESTION, - "stream_protocol": "agui", - "stream_tokens": True - } + "POST", + "/stream", + json={"message": QUESTION, "stream_protocol": "agui", "stream_tokens": True}, ) as response: assert response.status_code == 200 @@ -213,11 +228,9 @@ async def mock_astream(**kwargs): mock_agent.astream = mock_astream with test_client.stream( - "POST", "/stream", json={ - "message": QUESTION, - "stream_protocol": "agui", - "stream_tokens": False - } + "POST", + "/stream", + json={"message": QUESTION, "stream_protocol": "agui", "stream_tokens": False}, ) as response: assert response.status_code == 200 @@ -241,15 +254,11 @@ async def mock_astream(**kwargs): def test_stream_agui_with_tools(test_client, mock_agent) -> None: """Test AG-UI protocol tool calls""" QUESTION = "Calculate 2 + 3" - + # Create AI message with tool calls tool_message = AIMessage( content="I'll calculate that for you.", - tool_calls=[{ - "name": "Calculator", - "args": {"expression": "2 + 3"}, - "id": str(uuid4()) - }] + tool_calls=[{"name": "Calculator", "args": {"expression": "2 + 3"}, "id": str(uuid4())}], ) events = [ @@ -315,8 +324,7 @@ def agent_lookup(agent_id): with patch("service.service.get_agent", side_effect=agent_lookup): with test_client.stream( - "POST", f"/{CUSTOM_AGENT}/stream", - json={"message": QUESTION, "stream_protocol": "agui"} + "POST", f"/{CUSTOM_AGENT}/stream", json={"message": QUESTION, "stream_protocol": "agui"} ) as response: assert response.status_code == 200 @@ -380,7 +388,7 @@ async def mock_astream(**kwargs): def test_stream_agui_custom_message(test_client, mock_agent) -> None: """Test AG-UI protocol custom messages""" QUESTION = "Custom request" - + # Create custom message custom_data = {"action": "special_operation", "data": {"key": "value"}} @@ -424,10 +432,7 @@ def test_stream_invalid_protocol(test_client, mock_agent) -> None: """Test invalid stream protocol""" QUESTION = "What is the weather?" - response = test_client.post( - "/stream", - json={"message": QUESTION, "stream_protocol": "invalid"} - ) + response = test_client.post("/stream", json={"message": QUESTION, "stream_protocol": "invalid"}) assert response.status_code == 422 # Validation error @@ -449,19 +454,16 @@ async def mock_astream(**kwargs): config = kwargs.get("config", {}) configurable = config.get("configurable", {}) assert configurable.get("model") == CUSTOM_MODEL - + for event in events: yield event mock_agent.astream = mock_astream with test_client.stream( - "POST", "/stream", - json={ - "message": QUESTION, - "stream_protocol": "agui", - "model": CUSTOM_MODEL - } + "POST", + "/stream", + json={"message": QUESTION, "stream_protocol": "agui", "model": CUSTOM_MODEL}, ) as response: assert response.status_code == 200 @@ -477,4 +479,4 @@ async def mock_astream(**kwargs): event_content = "".join(messages) assert EventType.RUN_STARTED.value in event_content - assert EventType.RUN_FINISHED.value in event_content \ No newline at end of file + assert EventType.RUN_FINISHED.value in event_content From bc49652e120d720c91196ba3d0a8bce50a8d560d Mon Sep 17 00:00:00 2001 From: reatang Date: Fri, 13 Jun 2025 15:37:34 +0800 Subject: [PATCH 12/14] fix end of files --- .env.example | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.env.example b/.env.example index ed307a0c..ff0802cd 100644 --- a/.env.example +++ b/.env.example @@ -76,4 +76,4 @@ OPENWEATHERMAP_API_KEY= # LANGFUSE Configuration #LANGFUSE_TRACING=true #LANGFUSE_PUBLIC_KEY=pk-... -#LANGFUSE_SECRET_KEY=sk-lf-.... \ No newline at end of file +#LANGFUSE_SECRET_KEY=sk-lf-.... From 95fc8bfac9331a2e94408417f91ff4300d3b41d1 Mon Sep 17 00:00:00 2001 From: reatang Date: Thu, 10 Jul 2025 11:18:07 +0800 Subject: [PATCH 13/14] add ag-ui webpage --- src/core/llm.py | 1 + src/service/service.py | 71 +- tests/service/test_service_agui.py | 27 +- web-agui/index.html | 1641 ++++++++++++++++++++++++++++ 4 files changed, 1715 insertions(+), 25 deletions(-) create mode 100644 web-agui/index.html diff --git a/src/core/llm.py b/src/core/llm.py index edba3962..07864650 100644 --- a/src/core/llm.py +++ b/src/core/llm.py @@ -107,6 +107,7 @@ def get_model(model_name: AllModelEnum, /) -> ModelT: openai_api_key=settings.DEEPSEEK_API_KEY, ) if model_name in AlibabaQWenModelName: + # https://help.aliyun.com/zh/model-studio/text-generation#9d84551784a6l return ChatOpenAI( model=api_model_name, temperature=0.5, diff --git a/src/service/service.py b/src/service/service.py index 54f2c1e0..f8b3ba72 100644 --- a/src/service/service.py +++ b/src/service/service.py @@ -14,10 +14,10 @@ RunFinishedEvent, RunStartedEvent, ) -from ag_ui.core.events import TextMessageChunkEvent +from ag_ui.core.events import TextMessageChunkEvent, TextMessageEndEvent, TextMessageStartEvent from ag_ui.encoder import EventEncoder from fastapi import APIRouter, Depends, FastAPI, HTTPException, status -from fastapi.responses import StreamingResponse +from fastapi.responses import Response, StreamingResponse from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer from langchain_core._api import LangChainBetaWarning from langchain_core.messages import AIMessage, AIMessageChunk, AnyMessage, HumanMessage, ToolMessage @@ -330,6 +330,9 @@ async def message_generator_agui( # Create event encoder for AG-UI protocol encoder = EventEncoder() + # Track current streaming message ID to ensure consistency + current_streaming_message_id = None + try: # Send run started event yield encoder.encode( @@ -393,20 +396,8 @@ async def message_generator_agui( if current_message: processed_messages.append(_create_ai_message(current_message)) - # Convert messages to AG-UI events - this is the main difference - for message in processed_messages: - # Skip re-sent input messages - if isinstance(message, HumanMessage) and message.content == user_input.message: - continue - - # Convert each message to appropriate AG-UI events - async for agui_event in convert_message_to_agui_events(message, encoder): - yield agui_event - - # Handle token streaming - similar to original - if stream_mode == "messages": - if not user_input.stream_tokens: - continue + # Handle token streaming - for real-time token chunks + if stream_mode == "messages" and user_input.stream_tokens: msg, metadata = event if "skip_stream" in metadata.get("tags", []): continue @@ -414,16 +405,50 @@ async def message_generator_agui( continue content = remove_tool_calls(msg.content) if content: - # Convert to AG-UI text content event - message_id = str(uuid4()) + # Use consistent message_id for all tokens in the same message + if current_streaming_message_id is None: + current_streaming_message_id = str(uuid4()) + # Send message start event for token streaming + yield encoder.encode( + TextMessageStartEvent( + type=EventType.TEXT_MESSAGE_START, + message_id=current_streaming_message_id, + role="assistant", + ) + ) + yield encoder.encode( TextMessageChunkEvent( type=EventType.TEXT_MESSAGE_CHUNK, - message_id=message_id, + message_id=current_streaming_message_id, delta=convert_message_content_to_string(content), ) ) + # Convert complete messages to AG-UI events - handle updates and custom events + # Skip AI messages when token streaming is enabled to avoid duplication + elif stream_mode in ["updates", "custom"]: + for message in processed_messages: + # Skip re-sent input messages + if isinstance(message, HumanMessage) and message.content == user_input.message: + continue + + # Skip AI messages when token streaming is enabled to avoid duplication + if isinstance(message, AIMessage) and user_input.stream_tokens: + continue + + # Convert each message to appropriate AG-UI events + async for agui_event in convert_message_to_agui_events(message, encoder): + yield agui_event + + # Send message end event if we were streaming + if current_streaming_message_id is not None: + yield encoder.encode( + TextMessageEndEvent( + type=EventType.TEXT_MESSAGE_END, message_id=current_streaming_message_id + ) + ) + # Send run finished event yield encoder.encode( RunFinishedEvent( @@ -546,4 +571,12 @@ async def health_check(): return health_status +@app.get("/agui/") +async def agui_root(): + with open("web-agui/index.html", encoding="utf-8") as file: + response = Response(content=file.read(), media_type="text/html") + + return response + + app.include_router(router) diff --git a/tests/service/test_service_agui.py b/tests/service/test_service_agui.py index 89f58f8f..8fc9e706 100644 --- a/tests/service/test_service_agui.py +++ b/tests/service/test_service_agui.py @@ -125,7 +125,9 @@ async def mock_astream(**kwargs): # Make streaming request with AG-UI protocol with test_client.stream( - "POST", "/stream", json={"message": QUESTION, "stream_protocol": "agui"} + "POST", + "/stream", + json={"message": QUESTION, "stream_protocol": "agui", "stream_tokens": False}, ) as response: assert response.status_code == 200 @@ -275,7 +277,9 @@ async def mock_astream(**kwargs): mock_agent.astream = mock_astream with test_client.stream( - "POST", "/stream", json={"message": QUESTION, "stream_protocol": "agui"} + "POST", + "/stream", + json={"message": QUESTION, "stream_protocol": "agui", "stream_tokens": False}, ) as response: assert response.status_code == 200 @@ -324,7 +328,9 @@ def agent_lookup(agent_id): with patch("service.service.get_agent", side_effect=agent_lookup): with test_client.stream( - "POST", f"/{CUSTOM_AGENT}/stream", json={"message": QUESTION, "stream_protocol": "agui"} + "POST", + f"/{CUSTOM_AGENT}/stream", + json={"message": QUESTION, "stream_protocol": "agui", "stream_tokens": False}, ) as response: assert response.status_code == 200 @@ -363,7 +369,9 @@ async def mock_astream(**kwargs): mock_agent.astream = mock_astream with test_client.stream( - "POST", "/stream", json={"message": QUESTION, "stream_protocol": "agui"} + "POST", + "/stream", + json={"message": QUESTION, "stream_protocol": "agui", "stream_tokens": False}, ) as response: assert response.status_code == 200 @@ -406,7 +414,9 @@ async def mock_astream(**kwargs): mock_agent.astream = mock_astream with test_client.stream( - "POST", "/stream", json={"message": QUESTION, "stream_protocol": "agui"} + "POST", + "/stream", + json={"message": QUESTION, "stream_protocol": "agui", "stream_tokens": False}, ) as response: assert response.status_code == 200 @@ -463,7 +473,12 @@ async def mock_astream(**kwargs): with test_client.stream( "POST", "/stream", - json={"message": QUESTION, "stream_protocol": "agui", "model": CUSTOM_MODEL}, + json={ + "message": QUESTION, + "stream_protocol": "agui", + "model": CUSTOM_MODEL, + "stream_tokens": False, + }, ) as response: assert response.status_code == 200 diff --git a/web-agui/index.html b/web-agui/index.html new file mode 100644 index 00000000..08d1f752 --- /dev/null +++ b/web-agui/index.html @@ -0,0 +1,1641 @@ + + + + + + + AG-UI 智能助手 + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + \ No newline at end of file From 72811331d5f6d28c46e6dcb45b471a1197954a35 Mon Sep 17 00:00:00 2001 From: reatang Date: Tue, 15 Jul 2025 17:01:09 +0800 Subject: [PATCH 14/14] add ag-ui webpage --- docker/Dockerfile.service | 1 + 1 file changed, 1 insertion(+) diff --git a/docker/Dockerfile.service b/docker/Dockerfile.service index 26f4b354..fe67f066 100644 --- a/docker/Dockerfile.service +++ b/docker/Dockerfile.service @@ -16,5 +16,6 @@ COPY src/memory/ ./memory/ COPY src/schema/ ./schema/ COPY src/service/ ./service/ COPY src/run_service.py . +COPY web-agui/ ./web-agui/ CMD ["python", "run_service.py"]