diff --git a/README.md b/README.md index 1e9ee1b80..89e4dce7d 100644 --- a/README.md +++ b/README.md @@ -95,13 +95,13 @@ > > **About Framework-Agnostic**: Currently, AgentScope Runtime supports the **AgentScope** framework. We plan to extend compatibility to more agent development frameworks in the future. This table shows the current version’s adapter support for different frameworks. The level of support for each functionality varies across frameworks: > -> | Framework/Feature | Message/Event | Tool | Service | -> | ------------------------------------------------------------ | ------------- | ---- | ------- | -> | [AgentScope](https://runtime.agentscope.io/en/quickstart.html) | ✅ | ✅ | ✅ | -> | [LangGraph](https://runtime.agentscope.io/en/langgraph_guidelines.html) | ✅ | 🚧 | 🚧 | -> | [Microsoft Agent Framework](https://runtime.agentscope.io/en/ms_agent_framework_guidelines.html) | ✅ | ✅ | 🚧 | -> | [Agno](https://runtime.agentscope.io/en/agno_guidelines.html) | ✅ | ✅ | 🚧 | -> | AutoGen | 🚧 | ✅ | 🚧 | +> | Framework/Feature | Message/Event | Tool | +> | ------------------------------------------------------------ | ------------- | ---- | +> | [AgentScope](https://runtime.agentscope.io/en/quickstart.html) | ✅ | ✅ | +> | [LangGraph](https://runtime.agentscope.io/en/langgraph_guidelines.html) | ✅ | 🚧 | +> | [Microsoft Agent Framework](https://runtime.agentscope.io/en/ms_agent_framework_guidelines.html) | ✅ | ✅ | +> | [Agno](https://runtime.agentscope.io/en/agno_guidelines.html) | ✅ | ✅ | +> | AutoGen | 🚧 | ✅ | --- @@ -155,14 +155,11 @@ from agentscope.formatter import DashScopeChatFormatter from agentscope.tool import Toolkit, execute_python_code from agentscope.pipeline import stream_printing_messages from agentscope.memory import InMemoryMemory +from agentscope.session import RedisSession from agentscope_runtime.engine import AgentApp from agentscope_runtime.engine.schemas.agent_schemas import AgentRequest -from agentscope_runtime.engine.services.agent_state import ( - InMemoryStateService, -) - agent_app = AgentApp( app_name="Friday", app_description="A helpful assistant", @@ -171,14 +168,13 @@ agent_app = AgentApp( @agent_app.init async def init_func(self): - self.state_service = InMemoryStateService() - - await self.state_service.start() - + import fakeredis -@agent_app.shutdown -async def shutdown_func(self): - await self.state_service.stop() + fake_redis = fakeredis.aioredis.FakeRedis(decode_responses=True) + # NOTE: This FakeRedis instance is for development/testing only. + # In production, replace it with your own Redis client/connection + # (e.g., aioredis.Redis) + self.session = RedisSession(connection_pool=fake_redis.connection_pool) @agent_app.query(framework="agentscope") @@ -191,11 +187,6 @@ async def query_func( session_id = request.session_id user_id = request.user_id - state = await self.state_service.export_state( - session_id=session_id, - user_id=user_id, - ) - toolkit = Toolkit() toolkit.register_tool_function(execute_python_code) @@ -213,8 +204,11 @@ async def query_func( ) agent.set_console_output_enabled(enabled=False) - if state: - agent.load_state_dict(state) + await self.session.load_session_state( + session_id=session_id, + user_id=user_id, + agent=agent, + ) async for msg, last in stream_printing_messages( agents=[agent], @@ -222,12 +216,10 @@ async def query_func( ): yield msg, last - state = agent.state_dict() - - await self.state_service.save_state( - user_id=user_id, + await self.session.save_session_state( session_id=session_id, - state=state, + user_id=user_id, + agent=agent, ) diff --git a/README_zh.md b/README_zh.md index 8abe73801..09d8994f1 100644 --- a/README_zh.md +++ b/README_zh.md @@ -96,13 +96,13 @@ > > **关于框架无关**:当前,AgentScope Runtime 支持 **AgentScope** 框架。未来我们计划扩展支持更多智能体开发框架。该表格展示了目前版本针对不同框架的适配器(adapter)支持情况,不同框架在各功能上的支持程度有所差异: > -> | 框架 / 功能项 | 消息 / 事件 | 工具 | 服务 | -> | ------------------------------------------------------------ | ------------- | ---- | ------- | -> | [AgentScope](https://runtime.agentscope.io/zh/quickstart.html) | ✅ | ✅ | ✅ | -> | [LangGraph](https://runtime.agentscope.io/zh/langgraph_guidelines.html) | ✅ | 🚧 | 🚧 | -> | [Microsoft Agent Framework](https://runtime.agentscope.io/zh/ms_agent_framework_guidelines.html) | ✅ | ✅ | 🚧 | -> | [Agno](https://runtime.agentscope.io/zh/agno_guidelines.html) | ✅ | ✅ | 🚧 | -> | AutoGen | 🚧 | ✅ | 🚧 | +> | 框架 / 功能项 | 消息 / 事件 | 工具 | +> | ------------------------------------------------------------ | ------------- | ---- | +> | [AgentScope](https://runtime.agentscope.io/zh/quickstart.html) | ✅ | ✅ | +> | [LangGraph](https://runtime.agentscope.io/zh/langgraph_guidelines.html) | ✅ | 🚧 | +> | [Microsoft Agent Framework](https://runtime.agentscope.io/zh/ms_agent_framework_guidelines.html) | ✅ | ✅ | +> | [Agno](https://runtime.agentscope.io/zh/agno_guidelines.html) | ✅ | ✅ | +> | AutoGen | 🚧 | ✅ | --- @@ -157,14 +157,11 @@ from agentscope.formatter import DashScopeChatFormatter from agentscope.tool import Toolkit, execute_python_code from agentscope.pipeline import stream_printing_messages from agentscope.memory import InMemoryMemory +from agentscope.session import RedisSession from agentscope_runtime.engine import AgentApp from agentscope_runtime.engine.schemas.agent_schemas import AgentRequest -from agentscope_runtime.engine.services.agent_state import ( - InMemoryStateService, -) - agent_app = AgentApp( app_name="Friday", app_description="A helpful assistant", @@ -173,14 +170,13 @@ agent_app = AgentApp( @agent_app.init async def init_func(self): - self.state_service = InMemoryStateService() - - await self.state_service.start() - + import fakeredis -@agent_app.shutdown -async def shutdown_func(self): - await self.state_service.stop() + fake_redis = fakeredis.aioredis.FakeRedis(decode_responses=True) + # 注意:这个 FakeRedis 实例仅用于开发/测试。 + # 在生产环境中,请替换为你自己的 Redis 客户端/连接 + #(例如 aioredis.Redis)。 + self.session = RedisSession(connection_pool=fake_redis.connection_pool) @agent_app.query(framework="agentscope") @@ -193,11 +189,6 @@ async def query_func( session_id = request.session_id user_id = request.user_id - state = await self.state_service.export_state( - session_id=session_id, - user_id=user_id, - ) - toolkit = Toolkit() toolkit.register_tool_function(execute_python_code) @@ -215,8 +206,11 @@ async def query_func( ) agent.set_console_output_enabled(enabled=False) - if state: - agent.load_state_dict(state) + await self.session.load_session_state( + session_id=session_id, + user_id=user_id, + agent=agent, + ) async for msg, last in stream_printing_messages( agents=[agent], @@ -224,12 +218,10 @@ async def query_func( ): yield msg, last - state = agent.state_dict() - - await self.state_service.save_state( - user_id=user_id, + await self.session.save_session_state( session_id=session_id, - state=state, + user_id=user_id, + agent=agent, ) diff --git a/cookbook/_toc.yml b/cookbook/_toc.yml index 879358054..1bc8bac10 100644 --- a/cookbook/_toc.yml +++ b/cookbook/_toc.yml @@ -11,6 +11,7 @@ parts: sections: - file: en/sandbox/sandbox.md sections: + - file: en/sandbox/sandbox_service.md - file: en/sandbox/advanced.md - file: en/sandbox/training_sandbox.md - file: en/sandbox/troubleshooting.md @@ -21,10 +22,6 @@ parts: - file: en/tools/modelstudio_generations.md - file: en/tools/alipay.md - file: en/tools/realtime_clients.md - - file: en/service/service.md - sections: - - file: en/service/sandbox.md - - file: en/service/state.md - file: en/deployment.md sections: - file: en/agent_app.md @@ -73,6 +70,7 @@ parts: sections: - file: zh/sandbox/sandbox.md sections: + - file: zh/sandbox/sandbox_service.md - file: zh/sandbox/advanced.md - file: zh/sandbox/training_sandbox.md - file: zh/sandbox/troubleshooting.md @@ -83,10 +81,6 @@ parts: - file: zh/tools/modelstudio_generations.md - file: zh/tools/alipay.md - file: zh/tools/realtime_clients.md - - file: zh/service/service.md - sections: - - file: zh/service/sandbox.md - - file: zh/service/state.md - file: zh/deployment.md sections: - file: zh/agent_app.md diff --git a/cookbook/en/advanced_deployment.md b/cookbook/en/advanced_deployment.md index 7cf149645..6ecbeab56 100644 --- a/cookbook/en/advanced_deployment.md +++ b/cookbook/en/advanced_deployment.md @@ -110,12 +110,10 @@ from agentscope.model import DashScopeChatModel from agentscope.pipeline import stream_printing_messages from agentscope.tool import Toolkit, execute_python_code from agentscope.memory import InMemoryMemory +from agentscope.session import RedisSession from agentscope_runtime.engine.app import AgentApp from agentscope_runtime.engine.schemas.agent_schemas import AgentRequest -from agentscope_runtime.engine.services.agent_state import ( - InMemoryStateService, -) app = AgentApp( app_name="Friday", @@ -125,13 +123,13 @@ app = AgentApp( @app.init async def init_func(self): - self.state_service = InMemoryStateService() - await self.state_service.start() - + import fakeredis -@app.shutdown -async def shutdown_func(self): - await self.state_service.stop() + fake_redis = fakeredis.aioredis.FakeRedis(decode_responses=True) + # NOTE: This FakeRedis instance is for development/testing only. + # In production, replace it with your own Redis client/connection + # (e.g., aioredis.Redis) + self.session = RedisSession(connection_pool=fake_redis.connection_pool) @app.query(framework="agentscope") @@ -145,11 +143,6 @@ async def query_func( session_id = request.session_id user_id = request.user_id - state = await self.state_service.export_state( - session_id=session_id, - user_id=user_id, - ) - toolkit = Toolkit() toolkit.register_tool_function(execute_python_code) @@ -167,8 +160,11 @@ async def query_func( formatter=DashScopeChatFormatter(), ) - if state: - agent.load_state_dict(state) + await self.session.load_session_state( + session_id=session_id, + user_id=user_id, + agent=agent, + ) async for msg, last in stream_printing_messages( agents=[agent], @@ -176,12 +172,10 @@ async def query_func( ): yield msg, last - state = agent.state_dict() - - await self.state_service.save_state( - user_id=user_id, + await self.session.save_session_state( session_id=session_id, - state=state, + user_id=user_id, + agent=agent, ) diff --git a/cookbook/en/agent_app.md b/cookbook/en/agent_app.md index 527c153c6..511a377de 100644 --- a/cookbook/en/agent_app.md +++ b/cookbook/en/agent_app.md @@ -188,8 +188,8 @@ This approach has the following advantages: 2. **Shared member variables** — Functions defined with decorators receive `self`, allowing access to the attributes and services of the `AgentApp` instance (for example, state services or session services started in `@app.init`), enabling convenient sharing and reuse of resources across different lifecycle stages or request handlers. ```{code-cell} +from agentscope.session import RedisSession from agentscope_runtime.engine import AgentApp -from agentscope_runtime.engine.services.agent_state import InMemoryStateService app = AgentApp( app_name="Friday", @@ -199,15 +199,18 @@ app = AgentApp( @app.init async def init_func(self): """Initialize service resources""" - self.state_service = InMemoryStateService() + import fakeredis - await self.state_service.start() + fake_redis = fakeredis.aioredis.FakeRedis(decode_responses=True) + # NOTE: This FakeRedis instance is for development/testing only. + # In production, replace it with your own Redis client/connection + # (e.g., aioredis.Redis) + self.session = RedisSession(connection_pool=fake_redis.connection_pool) print("✅ Service initialized") @app.shutdown async def shutdown_func(self): """Release service resources""" - await self.state_service.stop() print("✅ Resources released") ``` @@ -313,17 +316,12 @@ async def query_func( request: AgentRequest = None, **kwargs, ): - """Custom query handler""" session_id = request.session_id user_id = request.user_id - # Load session state - state = await self.state_service.export_state( - session_id=session_id, - user_id=user_id, - ) + toolkit = Toolkit() + toolkit.register_tool_function(execute_python_code) - # Build agent agent = ReActAgent( name="Friday", model=DashScopeChatModel( @@ -332,26 +330,28 @@ async def query_func( stream=True, ), sys_prompt="You're a helpful assistant named Friday.", + toolkit=toolkit, memory=InMemoryMemory(), + formatter=DashScopeChatFormatter(), ) + agent.set_console_output_enabled(enabled=False) - # Restore state if present - if state: - agent.load_state_dict(state) + await self.session.load_session_state( + session_id=session_id, + user_id=user_id, + agent=agent, + ) - # Stream responses async for msg, last in stream_printing_messages( agents=[agent], coroutine_task=agent(msgs), ): yield msg, last - # Persist state - state = agent.state_dict() - await self.state_service.save_state( - user_id=user_id, + await self.session.save_session_state( session_id=session_id, - state=state, + user_id=user_id, + agent=agent, ) ``` @@ -371,88 +371,82 @@ async def query_func( ```{code-cell} import os -from agentscope_runtime.engine import AgentApp -from agentscope_runtime.engine.schemas.agent_schemas import AgentRequest + from agentscope.agent import ReActAgent from agentscope.model import DashScopeChatModel +from agentscope.formatter import DashScopeChatFormatter from agentscope.tool import Toolkit, execute_python_code from agentscope.pipeline import stream_printing_messages from agentscope.memory import InMemoryMemory -from agentscope_runtime.engine.services.agent_state import InMemoryStateService +from agentscope.session import RedisSession -app = AgentApp( +from agentscope_runtime.engine import AgentApp +from agentscope_runtime.engine.schemas.agent_schemas import AgentRequest + +agent_app = AgentApp( app_name="Friday", - app_description="A helpful assistant with state management", + app_description="A helpful assistant", ) -@app.init + +@agent_app.init async def init_func(self): - """Start state and session services""" - self.state_service = InMemoryStateService() - await self.state_service.start() + import fakeredis -@app.shutdown -async def shutdown_func(self): - """Tear down services""" - await self.state_service.stop() + fake_redis = fakeredis.aioredis.FakeRedis(decode_responses=True) + # NOTE: This FakeRedis instance is for development/testing only. + # In production, replace it with your own Redis client/connection + # (e.g., aioredis.Redis) + self.session = RedisSession(connection_pool=fake_redis.connection_pool) -@app.query(framework="agentscope") + +@agent_app.query(framework="agentscope") async def query_func( self, msgs, request: AgentRequest = None, **kwargs, ): - """Query handler with state persistence""" session_id = request.session_id user_id = request.user_id - # Load historical state - state = await self.state_service.export_state( - session_id=session_id, - user_id=user_id, - ) - - # Register tools toolkit = Toolkit() toolkit.register_tool_function(execute_python_code) - # Build agent agent = ReActAgent( name="Friday", model=DashScopeChatModel( "qwen-turbo", api_key=os.getenv("DASHSCOPE_API_KEY"), - enable_thinking=True, stream=True, ), sys_prompt="You're a helpful assistant named Friday.", toolkit=toolkit, memory=InMemoryMemory(), + formatter=DashScopeChatFormatter(), ) agent.set_console_output_enabled(enabled=False) - # Restore state if any - if state: - agent.load_state_dict(state) + await self.session.load_session_state( + session_id=session_id, + user_id=user_id, + agent=agent, + ) - # Stream output async for msg, last in stream_printing_messages( agents=[agent], coroutine_task=agent(msgs), ): yield msg, last - # Save state - state = agent.state_dict() - await self.state_service.save_state( - user_id=user_id, + await self.session.save_session_state( session_id=session_id, - state=state, + user_id=user_id, + agent=agent, ) -# Launch service -app.run(host="0.0.0.0", port=8090) + +agent_app.run(host="127.0.0.1", port=8090) ``` ### Comparison with the V0 version`agent` Parameter Approach diff --git a/cookbook/en/api/engine.md b/cookbook/en/api/engine.md index e9dc0786a..9c93412e5 100644 --- a/cookbook/en/api/engine.md +++ b/cookbook/en/api/engine.md @@ -220,21 +220,6 @@ The engine module contains the core components of AgentScope Runtime. :no-index: ``` -### Services · Agent State -```{eval-rst} -.. automodule:: agentscope_runtime.engine.services.agent_state.state_service - :members: - :undoc-members: - :show-inheritance: - :no-index: - -.. automodule:: agentscope_runtime.engine.services.agent_state.redis_state_service - :members: - :undoc-members: - :show-inheritance: - :no-index: -``` - ### Services · Sandbox ```{eval-rst} .. automodule:: agentscope_runtime.engine.services.sandbox.sandbox_service @@ -244,15 +229,6 @@ The engine module contains the core components of AgentScope Runtime. :no-index: ``` -### Services · Utils -```{eval-rst} -.. automodule:: agentscope_runtime.engine.services.utils.tablestore_service_utils - :members: - :undoc-members: - :show-inheritance: - :no-index: -``` - ### Schemas ```{eval-rst} .. automodule:: agentscope_runtime.engine.schemas.agent_schemas diff --git a/cookbook/en/cli.md b/cookbook/en/cli.md index 897aac229..33695ce4d 100644 --- a/cookbook/en/cli.md +++ b/cookbook/en/cli.md @@ -2,21 +2,6 @@ The unified command-line interface for managing your agent development, deployment, and runtime operations. -## Table of Contents - -- [Quick Start](#quick-start) -- [Complete Example](#complete-example) -- [Core Commands](#core-commands) - - [Development: `agentscope chat`](#1-development-agentscope-chat) - - [Web UI: `agentscope web`](#2-web-ui-agentscope-web) - - [Run Agent Service: `agentscope run`](#3-run-agent-service-agentscope-run) - - [Deployment: `agentscope deploy`](#4-deployment-agentscope-deploy) - - [Deployment Management](#5-deployment-management) - - [Sandbox Management: `agentscope sandbox`](#6-sandbox-management-as-runtime-sandbox) -- [API Reference](#api-reference) -- [Common Workflows](#common-workflows) -- [Troubleshooting](#troubleshooting) - ## Quick Start ### Installation @@ -61,12 +46,10 @@ from agentscope.model import DashScopeChatModel from agentscope.pipeline import stream_printing_messages from agentscope.tool import Toolkit, execute_python_code from agentscope.memory import InMemoryMemory +from agentscope.session import RedisSession from agentscope_runtime.engine.app import AgentApp from agentscope_runtime.engine.schemas.agent_schemas import AgentRequest -from agentscope_runtime.engine.services.agent_state import ( - InMemoryStateService, -) # Create AgentApp instance agent_app = AgentApp( @@ -78,14 +61,13 @@ agent_app = AgentApp( @agent_app.init async def init_func(self): """Initialize services.""" - self.state_service = InMemoryStateService() - await self.state_service.start() - + import fakeredis -@agent_app.shutdown -async def shutdown_func(self): - """Cleanup services.""" - await self.state_service.stop() + fake_redis = fakeredis.aioredis.FakeRedis(decode_responses=True) + # 注意:这个 FakeRedis 实例仅用于开发/测试。 + # 在生产环境中,请替换为你自己的 Redis 客户端/连接 + #(例如 aioredis.Redis)。 + self.session = RedisSession(connection_pool=fake_redis.connection_pool) @agent_app.query(framework="agentscope") @@ -99,12 +81,6 @@ async def query_func( session_id = request.session_id user_id = request.user_id - # Load state if exists - state = await self.state_service.export_state( - session_id=session_id, - user_id=user_id, - ) - # Create toolkit with Python execution toolkit = Toolkit() toolkit.register_tool_function(execute_python_code) @@ -125,23 +101,22 @@ async def query_func( ) agent.set_console_output_enabled(False) - # Load state if available - if state: - agent.load_state_dict(state) + await self.session.load_session_state( + session_id=session_id, + user_id=user_id, + agent=agent, + ) - # Process query and stream response async for msg, last in stream_printing_messages( agents=[agent], coroutine_task=agent(msgs), ): yield msg, last - # Save state - state = agent.state_dict() - await self.state_service.save_state( - user_id=user_id, + await self.session.save_session_state( session_id=session_id, - state=state, + user_id=user_id, + agent=agent, ) @@ -1143,8 +1118,8 @@ environment: ## Next Steps -- See [examples/](../../examples/) for complete agent implementations -- Check [API documentation](../api/) for programmatic usage +- See [examples/](https://github.com/agentscope-ai/agentscope-runtime/tree/main/examples) for complete agent implementations +- Check [API documentation](https://runtime.agentscope.io/en/api/index.html) for programmatic usage - Join community on Discord/DingTalk for support ## Feedback diff --git a/cookbook/en/deployment.md b/cookbook/en/deployment.md index 90d7631a0..8c625d057 100644 --- a/cookbook/en/deployment.md +++ b/cookbook/en/deployment.md @@ -28,10 +28,6 @@ Most deployments follow these stages: ## Section Guide -### Service - -The `Service` chapter explains the built-in session history, memory, sandbox, and state services, as well as the shared lifecycle interface. It helps you pick the right implementations (in-memory, Redis, Tablestore, and more) and shows how to manage them via `start()`, `stop()`, and `health()` so your deployment has a reliable backbone. See {doc}`service/service`. - ### Simple Deployment Runtime includes a lightweight deployment helper named `agent_app`, which chains multiple agents, tools, and context sources into an application. This section covers: diff --git a/cookbook/en/ms_agent_framework_guidelines.md b/cookbook/en/ms_agent_framework_guidelines.md index 64cf0ea6f..d5618d211 100644 --- a/cookbook/en/ms_agent_framework_guidelines.md +++ b/cookbook/en/ms_agent_framework_guidelines.md @@ -36,7 +36,6 @@ from agent_framework.openai import OpenAIChatClient from agentscope_runtime.engine import AgentApp from agentscope_runtime.engine.schemas.agent_schemas import AgentRequest -from agentscope_runtime.engine.services.agent_state import InMemoryStateService PORT = 8090 @@ -50,14 +49,13 @@ def run_app(): @agent_app.init async def init_func(self): - self.state_service = InMemoryStateService() - await self.state_service.start() + self.thread_storage = {} # Only for testing @agent_app.shutdown async def shutdown_func(self): - await self.state_service.stop() + pass - @agent_app.query(framework="agno") + @agent_app.query(framework="ms_agent_framework") async def query_func( self, msgs, @@ -69,10 +67,8 @@ def run_app(): user_id = request.user_id # Export historical context - thread = await self.state_service.export_state( - session_id=session_id, - user_id=user_id, - ) + id_key = f"{user_id}_{session_id}" + thread = self.thread_storage.get(id_key) # Create agent agent = OpenAIChatClient( @@ -99,11 +95,7 @@ def run_app(): # Save session state serialized_thread = await thread.serialize() - await self.state_service.save_state( - user_id=user_id, - session_id=session_id, - state=serialized_thread, - ) + self.thread_storage[id_key] = serialized_thread agent_app.run(host="127.0.0.1", port=PORT) diff --git a/cookbook/en/quickstart.md b/cookbook/en/quickstart.md index 24c8d5d97..be1b63a65 100644 --- a/cookbook/en/quickstart.md +++ b/cookbook/en/quickstart.md @@ -49,13 +49,11 @@ from agentscope.formatter import DashScopeChatFormatter from agentscope.tool import Toolkit, execute_python_code from agentscope.pipeline import stream_printing_messages from agentscope.memory import InMemoryMemory +from agentscope.session import RedisSession from agentscope_runtime.engine import AgentApp from agentscope_runtime.engine.schemas.agent_schemas import AgentRequest from agentscope_runtime.engine.deployers import LocalDeployManager -from agentscope_runtime.engine.services.agent_state import ( - InMemoryStateService, -) print("✅ Dependencies imported successfully") ``` @@ -80,12 +78,17 @@ Define what happens when the service starts (state/session services) and how res ```{code-cell} @agent_app.init async def init_func(self): - self.state_service = InMemoryStateService() - await self.state_service.start() + import fakeredis + + fake_redis = fakeredis.aioredis.FakeRedis(decode_responses=True) + # NOTE: This FakeRedis instance is for development/testing only. + # In production, replace it with your own Redis client/connection + # (e.g., aioredis.Redis) + self.session = RedisSession(connection_pool=fake_redis.connection_pool) @agent_app.shutdown async def shutdown_func(self): - await self.state_service.stop() + pass ``` ### Step 4: Define the AgentScope Query Logic @@ -94,13 +97,12 @@ async def shutdown_func(self): ⚠️ Important The Agent setup shown here (model, tools, conversation memory, formatter, etc.) is provided as an example configuration only. Please adapt and replace these components with your own implementations based on your requirements. -For details on available service types, adapter usage, and how to swap them out, see {doc}`service/service`. ``` When the agent endpoint is invoked, we: - **Load session context** to keep different sessions isolated. -- **Build an Agent**: includes the model, tools (such as executing Python code), conversation memory modules, and formatter — for details, please refer to {doc}`service/service`. +- **Build an Agent**: includes the model, tools (such as executing Python code), conversation memory modules, and formatter. - **Stream responses** via `stream_printing_messages`, yielding `(msg, last)` so clients receive output as it is generated. - **Persist state** so the next request can resume. @@ -115,11 +117,6 @@ async def query_func( session_id = request.session_id user_id = request.user_id - state = await self.state_service.export_state( - session_id=session_id, - user_id=user_id, - ) - toolkit = Toolkit() toolkit.register_tool_function(execute_python_code) @@ -137,8 +134,11 @@ async def query_func( ) agent.set_console_output_enabled(enabled=False) - if state: - agent.load_state_dict(state) + await self.session.load_session_state( + session_id=session_id, + user_id=user_id, + agent=agent, + ) async for msg, last in stream_printing_messages( agents=[agent], @@ -146,12 +146,10 @@ async def query_func( ): yield msg, last - state = agent.state_dict() - - await self.state_service.save_state( - user_id=user_id, + await self.session.save_session_state( session_id=session_id, - state=state, + user_id=user_id, + agent=agent, ) ``` diff --git a/cookbook/en/react_agent.md b/cookbook/en/react_agent.md index a6616139a..c59ecf145 100644 --- a/cookbook/en/react_agent.md +++ b/cookbook/en/react_agent.md @@ -61,10 +61,10 @@ from agentscope.formatter import DashScopeChatFormatter from agentscope.tool import Toolkit, execute_python_code from agentscope.pipeline import stream_printing_messages from agentscope.memory import InMemoryMemory +from agentscope.session import RedisSession from agentscope_runtime.engine import AgentApp from agentscope_runtime.engine.schemas.agent_schemas import AgentRequest -from agentscope_runtime.engine.services.agent_state import InMemoryStateService from agentscope_runtime.engine.services.sandbox import SandboxService from agentscope_runtime.sandbox import BrowserSandbox ``` @@ -114,7 +114,6 @@ Here, `sandbox_types=["browser"]` matches the test suite, so a single browser sa ⚠️ Important The Agent setup shown here (model, tools, conversation memory, formatter, etc.) is provided as an example configuration only. Please adapt and replace these components with your own implementations based on your requirements. -For details on available service types, adapter usage, and how to swap them out, see {doc}`service/service`. ``` The logic mirrors the `run_app()` test: initialize services, wire up session memory, and stream responses. @@ -130,16 +129,20 @@ agent_app = AgentApp( @agent_app.init async def init_func(self): - self.state_service = InMemoryStateService() - self.sandbox_service = SandboxService() + import fakeredis + + fake_redis = fakeredis.aioredis.FakeRedis(decode_responses=True) + # NOTE: This FakeRedis instance is for development/testing only. + # In production, replace it with your own Redis client/connection + # (e.g., aioredis.Redis) + self.session = RedisSession(connection_pool=fake_redis.connection_pool) - await self.state_service.start() + self.sandbox_service = SandboxService() await self.sandbox_service.start() @agent_app.shutdown async def shutdown_func(self): - await self.state_service.stop() await self.sandbox_service.stop() @@ -148,11 +151,6 @@ async def query_func(self, msgs, request: AgentRequest = None, **kwargs): session_id = request.session_id user_id = request.user_id - state = await self.state_service.export_state( - session_id=session_id, - user_id=user_id, - ) - sandboxes = self.sandbox_service.connect( session_id=session_id, user_id=user_id, @@ -186,8 +184,11 @@ async def query_func(self, msgs, request: AgentRequest = None, **kwargs): ) agent.set_console_output_enabled(enabled=False) - if state: - agent.load_state_dict(state) + await self.session.load_session_state( + session_id=session_id, + user_id=user_id, + agent=agent, + ) async for msg, last in stream_printing_messages( agents=[agent], @@ -195,10 +196,10 @@ async def query_func(self, msgs, request: AgentRequest = None, **kwargs): ): yield msg, last - await self.state_service.save_state( - user_id=user_id, + await self.session.save_session_state( session_id=session_id, - state=agent.state_dict(), + user_id=user_id, + agent=agent, ) ``` diff --git a/cookbook/en/service/sandbox.md b/cookbook/en/sandbox/sandbox_service.md similarity index 100% rename from cookbook/en/service/sandbox.md rename to cookbook/en/sandbox/sandbox_service.md diff --git a/cookbook/en/service/service.md b/cookbook/en/service/service.md deleted file mode 100644 index 37cc46728..000000000 --- a/cookbook/en/service/service.md +++ /dev/null @@ -1,181 +0,0 @@ ---- -jupytext: - formats: md:myst - text_representation: - extension: .md - format_name: myst - format_version: 0.13 - jupytext_version: 1.11.5 -kernelspec: - display_name: Python 3 - language: python - name: python3 ---- - -# Services and Adapters - -## Overview - -In **AgentScope Runtime**, **Services** (`Service`) provide core capabilities to the agent execution environment, including: - -- **Sandbox management** -- **Agent state management** - -All services implement a unified abstract interface called `ServiceWithLifecycleManager` (lifecycle management pattern), which provides standard methods: - -- `start()` — Start the service -- `stop()` — Stop the service -- `health()` — Check the health status of the service - -## Why Use Services via Adapters? - -- **Decoupling**: Agent frameworks don’t need to know the implementation details of underlying services. -- **Cross-framework reuse**: The same service can be integrated into different agent frameworks. -- **Unified lifecycle**: Runner/Engine starts and stops all services in a coordinated manner. -- **Better maintainability**: You can swap service implementations (e.g., switch to database storage) without changing agent business logic. - -## Available Services and How to Use Their Adapters - -### 1. Sandbox Service (`SandboxService`) - -**Sandbox Services** manage and provide sandboxed tool execution environments for different users and sessions. -Sandboxes are organized using a composite key of session ID and user ID, giving each user session an isolated execution environment. - -#### Usage in AgentScope - -In AgentScope, bind methods from the Sandbox Service to the `ToolKit` module via the `sandbox_tool_adapter`: - -```{code-cell} -from agentscope_runtime.engine.services.sandbox import SandboxService - -sandboxes = sandbox_service.connect( - session_id=session_id, - user_id=user_id, - sandbox_types=["browser"], -) - -toolkit = Toolkit() -for tool in [ - sandboxes[0].browser_navigate, - sandboxes[0].browser_take_screenshot, -]: - toolkit.register_tool_function(sandbox_tool_adapter(tool)) -``` - -For more service types and detailed usage, see {doc}`sandbox`. - -### 2. State Service (`StateService`) - -Allows saving and retrieving the agent's serializable state, preserving context across multiple turns—or even across sessions. - -#### Usage in AgentScope - -In AgentScope, you don’t need an adapter — directly call `StateService`’s `export_state` and `save_state`: - -```{code-cell} -from agentscope_runtime.engine.services.agent_state import InMemoryStateService - -state_service = InMemoryStateService() -state = await state_service.export_state(session_id, user_id) -agent.load_state_dict(state) - -await state_service.save_state(session_id, user_id, state=agent.state_dict()) -``` - -For more service types and detailed usage, see {doc}`state`. - -## Service Interface - -All services must implement the `ServiceWithLifecycleManager` abstract class, for example: - -```{code-cell} -from agentscope_runtime.engine.services.base import ServiceWithLifecycleManager - -class MockService(ServiceWithLifecycleManager): - def __init__(self, name: str): - self.name = name - self.started = False - self.stopped = False - - async def start(self): - self.started = True - - async def stop(self): - self.stopped = True - - async def health(self) -> bool: - return self.started and not self.stopped -``` - -Lifecycle Pattern Example: - -```{code-cell} -import asyncio -from agentscope_runtime.engine.services.memory import InMemoryMemoryService - -async def main(): - memory_service = InMemoryMemoryService() - - await memory_service.start() - print("Health:", await memory_service.health()) - - await memory_service.stop() -``` - -## ServiceFactory:Unified Service Creation Pattern - -In practice, the same type of service (such as `Sandbox`, `State`) may have multiple backend implementations — for example, in-memory, Redis, or database-based. - -To make service creation more flexible and configurable, **AgentScope Runtime** provides a general **service factory base class** `ServiceFactory` which supports: - -- **Unified registration** of multiple backend constructors (`register_backend`) -- **Environment variable configuration** of service parameters, with `BACKEND` determining which backend to use -- **`kwargs` overriding** of environment variable configurations (priority: `kwargs` > environment variables) -- **Automatic filtering of invalid parameters** (only parameters accepted by the constructor are passed) -- **Asynchronous instance creation**, suited for services requiring asynchronous initialization via `start()` -- **Image reuse benefit**: within the same runtime image, you can switch backend implementations simply by changing environment variables, without rebuilding the image — making deployment and testing easier - -### Example Creation Process - -```{code-cell} -# Example: State Service -from agentscope_runtime.engine.services.agent_state import StateServiceFactory - -# Use environment variable configuration -# export STATE_BACKEND=redis -# export STATE_REDIS_REDIS_URL="redis://localhost:6379/5" -service = await StateServiceFactory.create() - -# Use kwargs to override environment variables -service = await StateServiceFactory.create( - backend_type="redis", - redis_url="redis://otherhost:6379/1" -) - -# Register a custom backend -from my_backend import PostgresStateService -StateServiceFactory.register_backend("postgres", PostgresStateService) -service = await StateServiceFactory.create(backend_type="postgres") -``` - -### Common `ServiceFactory` and Default Backends - -| ServiceFactory Subclass | Managed Service Type | Environment Variable Prefix | Default Backend | Registered Default Backend Types | -| ----------------------- | -------------------- | --------------------------- | --------------- | -------------------------------- | -| `StateServiceFactory` | `StateService` | `STATE_` | `in_memory` | `in_memory`, `redis` | -| `SandboxServiceFactory` | `SandboxService` | `SANDBOX_` | `default` | `default` | - -### Usage Tips - -- **Choosing a backend**: Set the `BACKEND` environment variable to select the implementation - - Example: - - ```bash - export STATE_BACKEND=redis - export STATE_REDIS_REDIS_URL="redis://localhost:6379/5" - ``` - -- **Parameter priority**: `kwargs` > environment variables - -- **Custom backend**: Use `.register_backend("name", constructor)` to register a new implementation diff --git a/cookbook/en/service/state.md b/cookbook/en/service/state.md deleted file mode 100644 index b0efbcff0..000000000 --- a/cookbook/en/service/state.md +++ /dev/null @@ -1,113 +0,0 @@ ---- -jupytext: - formats: md:myst - text_representation: - extension: .md - format_name: myst - format_version: 0.13 - jupytext_version: 1.11.5 -kernelspec: - display_name: Python 3.10 - language: python - name: python3 ---- - -# Agent State Service - -## Overview - -The **Agent State Service** is a core component used to **store and manage the serializable state of agents**, enabling them to maintain context across multiple turns or even across sessions. -Unlike the **Session History Service**, which mainly stores text messages, the **State Service** focuses on saving the agent’s **structured, serializable internal data**, such as: - -- Variable values -- Execution progress -- Intermediate results from tool usage -- Environment configuration or preferences - -The State Service organizes data according to the following dimensions: - -- `user_id`: distinguishes users -- `session_id`: distinguishes sessions (default is `"default"`) -- `round_id`: distinguishes conversation turns (optional; if omitted, the latest turn ID is generated automatically) - -Role during agent execution: - -- **Save state** (`save_state`): Store the agent’s state at a given moment. -- **Restore state** (`export_state`): Restore previously saved state in the next conversation turn or session. - -```{note} -The state is a fully serializable Python `dict`, usually generated by the agent’s `state_dict()` method and loadable via `load_state_dict`, making it easy to wrap and transfer across platforms. -``` - -## Usage in AgentScope - -Unlike the **Session History Service**, the **Agent State Service** in AgentScope generally does **not require an adapter**. It can directly call `save_state` and `export_state` to persist and load state. - -```{code-cell} -from agentscope_runtime.engine.services.agent_state import InMemoryStateService - -# Initialize the service -state_service = InMemoryStateService() -await state_service.start() - -# Suppose the agent has a state_dict method -agent_state = agent.state_dict() - -# Save state (returns round_id) -round_id = await state_service.save_state( - user_id="User1", - session_id="TestSession", - state=agent_state -) -print(f"State saved in round {round_id}") - -# Export latest state (or specify round_id) -loaded_state = await state_service.export_state( - user_id="User1", - session_id="TestSession" -) -agent.load_state_dict(loaded_state) -``` - -## Available Backend Implementations - -Similar to the Session History Service, the Agent State Service also supports different storage backends: - -| Service Type | Import Path | Storage Location | Persistence | Production-ready | Features | Applicable Scenarios | -| ------------------------ | ------------------------------------------------------------ | -------------------------- | -------------------- | ---------------- | ------------------------------------------------------------ | ------------------------------------------------------- | -| **InMemoryStateService** | `from agentscope_runtime.engine.services.agent_state import InMemoryStateService` | In-process memory | ❌ No | ❌ No | Simple and fast, no external dependencies, data lost when process ends | Development, debugging, unit testing | -| **RedisStateService** | `from agentscope_runtime.engine.services.agent_state import RedisStateService` | Redis (in-memory database) | ✅ Optional (RDB/AOF) | ✅ Yes | Supports distributed shared state, cross-process access, Redis persistence optional | High-performance production, cross-process data sharing | - -## Switching Between Implementations - -Since the `StateService` interface is consistent across implementations, switching backend types is straightforward—just replace the instantiation. - -Example: InMemory → Redis - -```{code-cell} -from agentscope_runtime.engine.services.agent_state import RedisStateService - -state_service = RedisStateService(redis_url="redis://localhost:6379/0") -await state_service.start() - -# Save state -await state_service.save_state(user_id="User1", session_id="ProdSession", state=agent.state_dict()) - -# Export state -state = await state_service.export_state(user_id="User1", session_id="ProdSession") -agent.load_state_dict(state) -``` - -## Recommendations - -- **Development, debugging, or testing**: Use `InMemoryStateService` — no external dependencies, fast iteration. -- **Production with cross-process state sharing**: Use `RedisStateService` — can be combined with Redis persistence and HA clusters. -- **Long-term storage with strong consistency or auditing needs**: Consider implementing a database-backed custom `StateService`. - -## Summary - -- The **Agent State Service** saves internal serializable state, unlike the Session History Service which only stores messages. -- State is organized by `user_id`, `session_id`, and `round_id`. -- In AgentScope, it is usually called directly without adapters. -- Backend switching is easy thanks to a unified interface. -- Choose between InMemory, Redis, or custom implementations based on your scenario. diff --git a/cookbook/zh/advanced_deployment.md b/cookbook/zh/advanced_deployment.md index d5162402c..29976744e 100644 --- a/cookbook/zh/advanced_deployment.md +++ b/cookbook/zh/advanced_deployment.md @@ -106,12 +106,10 @@ from agentscope.model import DashScopeChatModel from agentscope.pipeline import stream_printing_messages from agentscope.tool import Toolkit, execute_python_code from agentscope.memory import InMemoryMemory +from agentscope.session import RedisSession from agentscope_runtime.engine.app import AgentApp from agentscope_runtime.engine.schemas.agent_schemas import AgentRequest -from agentscope_runtime.engine.services.agent_state import ( - InMemoryStateService, -) app = AgentApp( app_name="Friday", @@ -121,13 +119,13 @@ app = AgentApp( @app.init async def init_func(self): - self.state_service = InMemoryStateService() - await self.state_service.start() - + import fakeredis -@app.shutdown -async def shutdown_func(self): - await self.state_service.stop() + fake_redis = fakeredis.aioredis.FakeRedis(decode_responses=True) + # 注意:这个 FakeRedis 实例仅用于开发/测试。 + # 在生产环境中,请替换为你自己的 Redis 客户端/连接 + #(例如 aioredis.Redis)。 + self.session = RedisSession(connection_pool=fake_redis.connection_pool) @app.query(framework="agentscope") @@ -141,11 +139,6 @@ async def query_func( session_id = request.session_id user_id = request.user_id - state = await self.state_service.export_state( - session_id=session_id, - user_id=user_id, - ) - toolkit = Toolkit() toolkit.register_tool_function(execute_python_code) @@ -163,8 +156,11 @@ async def query_func( formatter=DashScopeChatFormatter(), ) - if state: - agent.load_state_dict(state) + await self.session.load_session_state( + session_id=session_id, + user_id=user_id, + agent=agent, + ) async for msg, last in stream_printing_messages( agents=[agent], @@ -172,12 +168,10 @@ async def query_func( ): yield msg, last - state = agent.state_dict() - - await self.state_service.save_state( - user_id=user_id, + await self.session.save_session_state( session_id=session_id, - state=state, + user_id=user_id, + agent=agent, ) diff --git a/cookbook/zh/agent_app.md b/cookbook/zh/agent_app.md index c03572434..f310c1217 100644 --- a/cookbook/zh/agent_app.md +++ b/cookbook/zh/agent_app.md @@ -189,8 +189,8 @@ app = AgentApp( 2. **可共享成员变量** —— 装饰器定义的函数会接收 `self`,可以访问 `AgentApp` 实例的属性和服务(例如 `@app.init` 中启动的状态服务、会话服务等),方便在不同生命周期或请求处理逻辑中共享和复用资源; ```{code-cell} +from agentscope.session import RedisSession from agentscope_runtime.engine import AgentApp -from agentscope_runtime.engine.services.agent_state import InMemoryStateService app = AgentApp( app_name="Friday", @@ -200,16 +200,19 @@ app = AgentApp( @app.init async def init_func(self): """初始化服务资源""" - self.state_service = InMemoryStateService() + import fakeredis - await self.state_service.start() + fake_redis = fakeredis.aioredis.FakeRedis(decode_responses=True) + # NOTE: This FakeRedis instance is for development/testing only. + # In production, replace it with your own Redis client/connection + # (e.g., aioredis.Redis) + self.session = RedisSession(connection_pool=fake_redis.connection_pool) print("✅ 服务初始化完成") @app.shutdown async def shutdown_func(self): """清理服务资源""" - await self.state_service.stop() - print("✅ 服务资源已清理") + print("✅ 资源已清理") ``` **装饰器说明** @@ -318,17 +321,12 @@ async def query_func( request: AgentRequest = None, **kwargs, ): - """自定义查询处理函数""" session_id = request.session_id user_id = request.user_id - # 加载会话状态 - state = await self.state_service.export_state( - session_id=session_id, - user_id=user_id, - ) + toolkit = Toolkit() + toolkit.register_tool_function(execute_python_code) - # 创建 Agent 实例 agent = ReActAgent( name="Friday", model=DashScopeChatModel( @@ -337,26 +335,28 @@ async def query_func( stream=True, ), sys_prompt="You're a helpful assistant named Friday.", + toolkit=toolkit, memory=InMemoryMemory(), + formatter=DashScopeChatFormatter(), ) + agent.set_console_output_enabled(enabled=False) - # 恢复状态(如果存在) - if state: - agent.load_state_dict(state) + await self.session.load_session_state( + session_id=session_id, + user_id=user_id, + agent=agent, + ) - # 流式处理消息 async for msg, last in stream_printing_messages( agents=[agent], coroutine_task=agent(msgs), ): yield msg, last - # 保存状态 - state = agent.state_dict() - await self.state_service.save_state( - user_id=user_id, + await self.session.save_session_state( session_id=session_id, - state=state, + user_id=user_id, + agent=agent, ) ``` @@ -377,88 +377,82 @@ async def query_func( ```{code-cell} import os -from agentscope_runtime.engine import AgentApp -from agentscope_runtime.engine.schemas.agent_schemas import AgentRequest + from agentscope.agent import ReActAgent from agentscope.model import DashScopeChatModel +from agentscope.formatter import DashScopeChatFormatter from agentscope.tool import Toolkit, execute_python_code from agentscope.pipeline import stream_printing_messages from agentscope.memory import InMemoryMemory -from agentscope_runtime.engine.services.agent_state import InMemoryStateService +from agentscope.session import RedisSession -app = AgentApp( +from agentscope_runtime.engine import AgentApp +from agentscope_runtime.engine.schemas.agent_schemas import AgentRequest + +agent_app = AgentApp( app_name="Friday", - app_description="A helpful assistant with state management", + app_description="A helpful assistant", ) -@app.init + +@agent_app.init async def init_func(self): - """初始化状态和会话服务""" - self.state_service = InMemoryStateService() - await self.state_service.start() + import fakeredis -@app.shutdown -async def shutdown_func(self): - """清理服务""" - await self.state_service.stop() + fake_redis = fakeredis.aioredis.FakeRedis(decode_responses=True) + # 注意:这个 FakeRedis 实例仅用于开发/测试。 + # 在生产环境中,请替换为你自己的 Redis 客户端/连接 + #(例如 aioredis.Redis)。 + self.session = RedisSession(connection_pool=fake_redis.connection_pool) -@app.query(framework="agentscope") + +@agent_app.query(framework="agentscope") async def query_func( self, msgs, request: AgentRequest = None, **kwargs, ): - """带状态管理的查询处理""" session_id = request.session_id user_id = request.user_id - # 加载历史状态 - state = await self.state_service.export_state( - session_id=session_id, - user_id=user_id, - ) - - # 创建工具包 toolkit = Toolkit() toolkit.register_tool_function(execute_python_code) - # 创建 Agent agent = ReActAgent( name="Friday", model=DashScopeChatModel( "qwen-turbo", api_key=os.getenv("DASHSCOPE_API_KEY"), - enable_thinking=True, stream=True, ), sys_prompt="You're a helpful assistant named Friday.", toolkit=toolkit, memory=InMemoryMemory(), + formatter=DashScopeChatFormatter(), ) agent.set_console_output_enabled(enabled=False) - # 恢复状态 - if state: - agent.load_state_dict(state) + await self.session.load_session_state( + session_id=session_id, + user_id=user_id, + agent=agent, + ) - # 流式处理 async for msg, last in stream_printing_messages( agents=[agent], coroutine_task=agent(msgs), ): yield msg, last - # 保存状态 - state = agent.state_dict() - await self.state_service.save_state( - user_id=user_id, + await self.session.save_session_state( session_id=session_id, - state=state, + user_id=user_id, + agent=agent, ) -# 运行服务 -app.run(host="0.0.0.0", port=8090) + +agent_app.run(host="127.0.0.1", port=8090) ``` ### 与 V0 版本 Agent 参数方式的区别 diff --git a/cookbook/zh/api/engine.md b/cookbook/zh/api/engine.md index f6deadd2d..023975646 100644 --- a/cookbook/zh/api/engine.md +++ b/cookbook/zh/api/engine.md @@ -220,21 +220,6 @@ Engine 模块涵盖 AgentScope Runtime 的核心执行、部署与服务能力 :no-index: ``` -### Services · Agent State -```{eval-rst} -.. automodule:: agentscope_runtime.engine.services.agent_state.state_service - :members: - :undoc-members: - :show-inheritance: - :no-index: - -.. automodule:: agentscope_runtime.engine.services.agent_state.redis_state_service - :members: - :undoc-members: - :show-inheritance: - :no-index: -``` - ### Services · Sandbox ```{eval-rst} .. automodule:: agentscope_runtime.engine.services.sandbox.sandbox_service @@ -244,15 +229,6 @@ Engine 模块涵盖 AgentScope Runtime 的核心执行、部署与服务能力 :no-index: ``` -### Services · Utils -```{eval-rst} -.. automodule:: agentscope_runtime.engine.services.utils.tablestore_service_utils - :members: - :undoc-members: - :show-inheritance: - :no-index: -``` - ### Schemas ```{eval-rst} .. automodule:: agentscope_runtime.engine.schemas.agent_schemas diff --git a/cookbook/zh/cli.md b/cookbook/zh/cli.md index 91f67153c..4a289f864 100644 --- a/cookbook/zh/cli.md +++ b/cookbook/zh/cli.md @@ -2,21 +2,6 @@ 用于管理智能体开发、部署和运行时操作的统一命令行接口。 -## 目录 - -- [快速开始](#快速开始) -- [完整示例](#完整示例) -- [核心命令](#核心命令) - - [开发:`agentscope chat`](#1-开发agentscope-chat) - - [Web UI:`agentscope web`](#2-web-uiagentscope-web) - - [运行智能体服务:`agentscope run`](#3-运行智能体服务agentscope-run) - - [部署:`agentscope deploy`](#4-部署agentscope-deploy) - - [部署管理](#5-部署管理) - - [沙箱管理:`agentscope sandbox`](#6-沙箱管理as-runtime-sandbox) -- [API 参考](#api-参考) -- [常用工作流](#常用工作流) -- [故障排除](#故障排除) - ## 快速开始 ### 安装 @@ -61,12 +46,10 @@ from agentscope.model import DashScopeChatModel from agentscope.pipeline import stream_printing_messages from agentscope.tool import Toolkit, execute_python_code from agentscope.memory import InMemoryMemory +from agentscope.session import RedisSession from agentscope_runtime.engine.app import AgentApp from agentscope_runtime.engine.schemas.agent_schemas import AgentRequest -from agentscope_runtime.engine.services.agent_state import ( - InMemoryStateService, -) # Create AgentApp instance agent_app = AgentApp( @@ -77,15 +60,14 @@ agent_app = AgentApp( @agent_app.init async def init_func(self): - """Initialize services.""" - self.state_service = InMemoryStateService() - await self.state_service.start() - + """初始化服务。""" + import fakeredis -@agent_app.shutdown -async def shutdown_func(self): - """Cleanup services.""" - await self.state_service.stop() + fake_redis = fakeredis.aioredis.FakeRedis(decode_responses=True) + # 注意:这个 FakeRedis 实例仅用于开发/测试。 + # 在生产环境中,请替换为你自己的 Redis 客户端/连接 + #(例如 aioredis.Redis)。 + self.session = RedisSession(connection_pool=fake_redis.connection_pool) @agent_app.query(framework="agentscope") @@ -99,12 +81,6 @@ async def query_func( session_id = request.session_id user_id = request.user_id - # Load state if exists - state = await self.state_service.export_state( - session_id=session_id, - user_id=user_id, - ) - # Create toolkit with Python execution toolkit = Toolkit() toolkit.register_tool_function(execute_python_code) @@ -125,23 +101,22 @@ async def query_func( ) agent.set_console_output_enabled(False) - # Load state if available - if state: - agent.load_state_dict(state) + await self.session.load_session_state( + session_id=session_id, + user_id=user_id, + agent=agent, + ) - # Process query and stream response async for msg, last in stream_printing_messages( agents=[agent], coroutine_task=agent(msgs), ): yield msg, last - # Save state - state = agent.state_dict() - await self.state_service.save_state( - user_id=user_id, + await self.session.save_session_state( session_id=session_id, - state=state, + user_id=user_id, + agent=agent, ) @@ -1141,8 +1116,8 @@ environment: ## 下一步 -- 查看 [examples/](../../examples/) 获取完整的智能体实现示例 -- 查看 [API 文档](../api/) 了解编程用法 +- 查看 [examples/](https://github.com/agentscope-ai/agentscope-runtime/tree/main/examples) 获取完整的智能体实现示例 +- 查看 [API 文档](https://runtime.agentscope.io/zh/api/index.html) 了解编程用法 - 加入 Discord/DingTalk 社区获取支持 ## 反馈 diff --git a/cookbook/zh/deployment.md b/cookbook/zh/deployment.md index 8a32deb79..bdd111460 100644 --- a/cookbook/zh/deployment.md +++ b/cookbook/zh/deployment.md @@ -28,11 +28,6 @@ ## 子章节导读 -### 服务 - -`Service` 章节介绍了 Runtime 内建的会话历史、记忆、沙箱、状态等基础服务,以及统一的生命周期接口。阅读该章节可以了解如何选择合适的实现(内存、Redis、Tablestore 等),以及如何通过 `start()`、`stop()`、`health()` 管理服务,确保部署环境具备稳定的支撑能力。 -详细文档见 {doc}`service/service`。 - ### 简单部署 Runtime包含一个简单的部署工具`agent_app` 。它是将多个智能体、工具与上下文串联的应用形态。子章节会说明: diff --git a/cookbook/zh/ms_agent_framework_guidelines.md b/cookbook/zh/ms_agent_framework_guidelines.md index 5b6a357f9..3f7d0890e 100644 --- a/cookbook/zh/ms_agent_framework_guidelines.md +++ b/cookbook/zh/ms_agent_framework_guidelines.md @@ -36,7 +36,6 @@ from agent_framework.openai import OpenAIChatClient from agentscope_runtime.engine import AgentApp from agentscope_runtime.engine.schemas.agent_schemas import AgentRequest -from agentscope_runtime.engine.services.agent_state import InMemoryStateService PORT = 8090 @@ -50,14 +49,13 @@ def run_app(): @agent_app.init async def init_func(self): - self.state_service = InMemoryStateService() - await self.state_service.start() + self.thread_storage = {} # 仅测试用 @agent_app.shutdown async def shutdown_func(self): - await self.state_service.stop() + pass - @agent_app.query(framework="agno") + @agent_app.query(framework="ms_agent_framework") async def query_func( self, msgs, @@ -69,10 +67,8 @@ def run_app(): user_id = request.user_id # 导出历史上下文 - thread = await self.state_service.export_state( - session_id=session_id, - user_id=user_id, - ) + id_key = f"{user_id}_{session_id}" + thread = self.thread_storage.get(id_key) # 创建 agent agent = OpenAIChatClient( @@ -99,11 +95,7 @@ def run_app(): # 保存会话状态 serialized_thread = await thread.serialize() - await self.state_service.save_state( - user_id=user_id, - session_id=session_id, - state=serialized_thread, - ) + self.thread_storage[id_key] = serialized_thread agent_app.run(host="127.0.0.1", port=PORT) diff --git a/cookbook/zh/quickstart.md b/cookbook/zh/quickstart.md index f381db317..bb6221d1a 100644 --- a/cookbook/zh/quickstart.md +++ b/cookbook/zh/quickstart.md @@ -49,12 +49,11 @@ from agentscope.formatter import DashScopeChatFormatter from agentscope.tool import Toolkit, execute_python_code from agentscope.pipeline import stream_printing_messages from agentscope.memory import InMemoryMemory +from agentscope.session import RedisSession from agentscope_runtime.engine import AgentApp from agentscope_runtime.engine.schemas.agent_schemas import AgentRequest -from agentscope_runtime.engine.services.agent_state import ( - InMemoryStateService, -) +from agentscope_runtime.engine.deployers import LocalDeployManager print("✅ 依赖导入成功") ``` @@ -79,12 +78,17 @@ print("✅ Agent App创建成功") ```{code-cell} @agent_app.init async def init_func(self): - self.state_service = InMemoryStateService() - await self.state_service.start() + import fakeredis + + fake_redis = fakeredis.aioredis.FakeRedis(decode_responses=True) + # 注意:这个 FakeRedis 实例仅用于开发/测试。 + # 在生产环境中,请替换为你自己的 Redis 客户端/连接 + #(例如 aioredis.Redis)。 + self.session = RedisSession(connection_pool=fake_redis.connection_pool) @agent_app.shutdown async def shutdown_func(self): - await self.state_service.stop() + pass ``` ### 步骤4:定义 AgentScope Agent 的查询逻辑 @@ -92,9 +96,7 @@ async def shutdown_func(self): ```{important} ⚠️ **提示** -此处的 Agent 构建(模型、工具、会话记忆、格式化器等)只是一个示例配置, -您需要根据实际需求替换为自己的模块实现。 -关于可用的服务类型、适配器用法以及如何替换,请参考 {doc}`service/service`。 +此处的 Agent 构建(模型、工具、会话记忆等)只是一个示例配置,您需要根据实际需求替换为自己的模块实现。 ``` 这一部分定义了Agent API 被调用时的业务逻辑: @@ -115,11 +117,6 @@ async def query_func( session_id = request.session_id user_id = request.user_id - state = await self.state_service.export_state( - session_id=session_id, - user_id=user_id, - ) - toolkit = Toolkit() toolkit.register_tool_function(execute_python_code) @@ -137,8 +134,11 @@ async def query_func( ) agent.set_console_output_enabled(enabled=False) - if state: - agent.load_state_dict(state) + await self.session.load_session_state( + session_id=session_id, + user_id=user_id, + agent=agent, + ) async for msg, last in stream_printing_messages( agents=[agent], @@ -146,12 +146,10 @@ async def query_func( ): yield msg, last - state = agent.state_dict() - - await self.state_service.save_state( - user_id=user_id, + await self.session.save_session_state( session_id=session_id, - state=state, + user_id=user_id, + agent=agent, ) ``` diff --git a/cookbook/zh/react_agent.md b/cookbook/zh/react_agent.md index f0e870978..93242718f 100644 --- a/cookbook/zh/react_agent.md +++ b/cookbook/zh/react_agent.md @@ -61,10 +61,10 @@ from agentscope.formatter import DashScopeChatFormatter from agentscope.tool import Toolkit, execute_python_code from agentscope.pipeline import stream_printing_messages from agentscope.memory import InMemoryMemory +from agentscope.session import RedisSession from agentscope_runtime.engine import AgentApp from agentscope_runtime.engine.schemas.agent_schemas import AgentRequest -from agentscope_runtime.engine.services.agent_state import InMemoryStateService from agentscope_runtime.engine.services.sandbox import SandboxService from agentscope_runtime.sandbox import BrowserSandbox ``` @@ -112,9 +112,7 @@ asyncio.run(bootstrap_browser_sandbox()) ```{important} ⚠️ **提示** -此处的 Agent 构建(模型、工具、会话记忆、格式化器等)只是一个示例配置, -您需要根据实际需求替换为自己的模块实现。 -关于可用的服务类型、适配器用法以及如何替换,请参考 {doc}`service/service`。 +此处的 Agent 构建(模型、工具、会话记忆、格式化器等)只是一个示例配置,您需要根据实际需求替换为自己的模块实现。 ``` 下面的逻辑与测试用例 `run_app()` 完全一致,包含状态服务初始化、会话记忆以及流式响应: @@ -130,16 +128,20 @@ agent_app = AgentApp( @agent_app.init async def init_func(self): - self.state_service = InMemoryStateService() - self.sandbox_service = SandboxService() + import fakeredis + + fake_redis = fakeredis.aioredis.FakeRedis(decode_responses=True) + # NOTE: This FakeRedis instance is for development/testing only. + # In production, replace it with your own Redis client/connection + # (e.g., aioredis.Redis) + self.session = RedisSession(connection_pool=fake_redis.connection_pool) - await self.state_service.start() + self.sandbox_service = SandboxService() await self.sandbox_service.start() @agent_app.shutdown async def shutdown_func(self): - await self.state_service.stop() await self.sandbox_service.stop() @@ -148,11 +150,6 @@ async def query_func(self, msgs, request: AgentRequest = None, **kwargs): session_id = request.session_id user_id = request.user_id - state = await self.state_service.export_state( - session_id=session_id, - user_id=user_id, - ) - sandboxes = self.sandbox_service.connect( session_id=session_id, user_id=user_id, @@ -186,8 +183,11 @@ async def query_func(self, msgs, request: AgentRequest = None, **kwargs): ) agent.set_console_output_enabled(enabled=False) - if state: - agent.load_state_dict(state) + await self.session.load_session_state( + session_id=session_id, + user_id=user_id, + agent=agent, + ) async for msg, last in stream_printing_messages( agents=[agent], @@ -195,10 +195,10 @@ async def query_func(self, msgs, request: AgentRequest = None, **kwargs): ): yield msg, last - await self.state_service.save_state( - user_id=user_id, + await self.session.save_session_state( session_id=session_id, - state=agent.state_dict(), + user_id=user_id, + agent=agent, ) ``` diff --git a/cookbook/zh/service/sandbox.md b/cookbook/zh/sandbox/sandbox_service.md similarity index 100% rename from cookbook/zh/service/sandbox.md rename to cookbook/zh/sandbox/sandbox_service.md diff --git a/cookbook/zh/service/service.md b/cookbook/zh/service/service.md deleted file mode 100644 index 5b1b638ee..000000000 --- a/cookbook/zh/service/service.md +++ /dev/null @@ -1,179 +0,0 @@ ---- -jupytext: - formats: md:myst - text_representation: - extension: .md - format_name: myst - format_version: 0.13 - jupytext_version: 1.11.5 -kernelspec: - display_name: Python 3 - language: python - name: python3 ---- - -# 服务与适配器 - -## 概述 - -AgentScope Runtime 中的服务(`Service`)为智能体运行环境提供核心能力,包括: - -- **沙箱管理** -- **智能体状态管理** - -所有服务都实现了统一的抽象接口 `ServiceWithLifecycleManager`(生命周期管理模式),提供标准方法: - -- `start()`:启动服务 -- `stop()`:停止服务 -- `health()`:检查服务健康状态 - -## 为什么要通过适配器使用服务? - -- **解耦**:智能体框架不用直接感知底层服务实现 -- **跨框架复用**:相同的服务可以接入不同的智能体框架 -- **统一生命周期**:Runner/Engine 统一启动和关闭所有服务 -- **增强可维护性**:更换服务实现(如切换为数据库存储)时,无需修改智能体业务代码 - -## 可用服务及适配器用法 - -### 1. 沙箱服务(SandboxService) - -**沙箱服务** 管理并为不同用户和会话提供沙箱化工具执行环境的访问。沙箱通过会话ID和用户ID的复合键组织,为每个用户会话提供隔离的执行环境。 - -#### AgentScope用法 - -在 AgentScope 框架中,通过Runtime的`sandbox_tool_adapter`适配器来绑定沙箱服务提供的沙箱的方法到`ToolKit`模块: - -```{code-cell} -from agentscope_runtime.engine.services.sandbox import SandboxService - -sandboxes = sandbox_service.connect( - session_id=session_id, - user_id=user_id, - sandbox_types=["browser"], -) - -toolkit = Toolkit() -for tool in [ - sandboxes[0].browser_navigate, - sandboxes[0].browser_take_screenshot, -]: - toolkit.register_tool_function(sandbox_tool_adapter(tool)) -``` - -更多可用服务类型与详细的用法请参见{doc}`sandbox`。 - -### 2. StateService - -存取智能体的可序列化状态,让智能体在多轮会话甚至跨会话间保持上下文。 - -#### AgentScope用法 - -在 AgentScope 框架中,无需通过适配器,直接调用`StateService`的`export_state`和`save_state`来保: - -```{code-cell} -from agentscope_runtime.engine.services.agent_state import InMemoryStateService - -state_service = InMemoryStateService() -state = await state_service.export_state(session_id, user_id) -agent.load_state_dict(state) - -await state_service.save_state(session_id, user_id, state=agent.state_dict()) -``` - -更多可用服务类型与详细的用法请参见{doc}`state`。 - -## 服务的接口 - -所有服务必须实现 `ServiceWithLifecycleManager` 抽象类,例如: - -```{code-cell} -from agentscope_runtime.engine.services.base import ServiceWithLifecycleManager - -class MockService(ServiceWithLifecycleManager): - def __init__(self, name: str): - self.name = name - self.started = False - self.stopped = False - - async def start(self): - self.started = True - - async def stop(self): - self.stopped = True - - async def health(self) -> bool: - return self.started and not self.stopped -``` - -生命周期模式示例: - -```{code-cell} -import asyncio -from agentscope_runtime.engine.services.memory import InMemoryMemoryService - -async def main(): - memory_service = InMemoryMemoryService() - - await memory_service.start() - print("Health:", await memory_service.health()) - - await memory_service.stop() -``` - -## ServiceFactory:统一的服务创建模式 - -在实际使用中,同一种服务(如 Sandbox、State)可能会有多种实现后端,例如内存版、Redis、数据库版等。 -为了让服务的创建更灵活、可配置,AgentScope Runtime 提供了一个通用的 **服务工厂基类** `ServiceFactory`: - -- **统一注册**多种后端构造方法(`register_backend`) -- **支持环境变量配置服务参数**,通过 `BACKEND` 决定使用哪个后端 -- **支持 `kwargs` 覆盖环境变量配置**(优先级:kwargs > 环境变量) -- **自动过滤无效参数**(仅传递构造函数能接收的参数) -- **异步创建实例**,适合需要 `start()` 异步初始化的服务 -- **镜像复用优势**:同一运行镜像下,可通过改变环境变量快速切换不同后端实现,无需重新构建镜像,从而便于部署和测试 - -### 创建流程示例 - -```{code-cell} -# 以状态服务为例 -from agentscope_runtime.engine.services.agent_state import StateServiceFactory - -# 使用环境变量配置 -# export STATE_BACKEND=redis -# export STATE_REDIS_REDIS_URL="redis://localhost:6379/5" -service = await StateServiceFactory.create() - -# 使用 kwargs 覆盖环境变量 -service = await StateServiceFactory.create( - backend_type="redis", - redis_url="redis://otherhost:6379/1" -) - -# 注册自定义后端 -from my_backend import PostgresStateService -StateServiceFactory.register_backend("postgres", PostgresStateService) -service = await StateServiceFactory.create(backend_type="postgres") -``` - -### 常用`ServiceFactory`与默认后端 - -| ServiceFactory 子类 | 管理的 Service 类型 | 环境变量前缀 | 默认后端 | 已注册的默认后端类型 | -| ----------------------- | ------------------- | ------------ | ----------- | -------------------- | -| `StateServiceFactory` | `StateService` | `STATE_` | `in_memory` | `in_memory`、`redis` | -| `SandboxServiceFactory` | `SandboxService` | `SANDBOX_` | `default` | `default` | - -### 使用提示 - -- 选择后端:通过设置`BACKEND`环境变量选择实现 - - 例如: - - ```bash - export STATE_BACKEND=redis - export STATE_REDIS_REDIS_URL="redis://localhost:6379/5" - ``` - -- **参数优先级**:`kwargs` > 环境变量 - -- **自定义后端**:使用 `.register_backend("name", constructor)` 注册新实现 diff --git a/cookbook/zh/service/state.md b/cookbook/zh/service/state.md deleted file mode 100644 index 47d7035f5..000000000 --- a/cookbook/zh/service/state.md +++ /dev/null @@ -1,114 +0,0 @@ ---- -jupytext: - formats: md:myst - text_representation: - extension: .md - format_name: myst - format_version: 0.13 - jupytext_version: 1.11.5 -kernelspec: - display_name: Python 3.10 - language: python - name: python3 ---- - -# 智能体状态服务 - -## 概述 - -**智能体状态服务**是用于**存储和管理智能体可序列化状态**的核心组件,它可以让智能体在多轮甚至跨会话的交互中保持上下文信息。 -与**会话历史服务**主要保存文本消息不同,**状态服务**更关注保存智能体内部的**结构化可序列化数据**,例如: - -- 变量值 -- 执行进度 -- 工具使用的中间结果 -- 环境配置或偏好等 - -状态服务按照以下维度组织数据: - -- `user_id`:区分用户 -- `session_id`:区分会话(默认 `"default"`) -- `round_id`:区分会话轮数(可选,如省略则自动生成最新轮数) - -在智能体运行过程中的作用: - -- **保存状态**(`save_state`):把某个时刻的智能体状态保存下来 -- **恢复状态**(`export_state`):在下一轮对话或下次会话中恢复之前的状态 - -```{note} -状态是完全可序列化的 Python dict,通常由智能体的 state_dict() 生成并可直接加载回去 (load_state_dict),便于封装和跨平台传输。 -``` - -## 在 AgentScope 中的使用方法 - -与**会话历史服务**不同,**智能体状态服务**在 AgentScope 中通常**无需适配器**。它可以直接调用 `save_state` 和 `export_state` 方法来持久化和加载状态。 - -```{code-cell} -from agentscope_runtime.engine.services.agent_state import InMemoryStateService - -# 初始化服务 -state_service = InMemoryStateService() -await state_service.start() - -# 假设 agent 有 state_dict 方法 -agent_state = agent.state_dict() - -# 保存状态(返回 round_id) -round_id = await state_service.save_state( - user_id="User1", - session_id="TestSession", - state=agent_state -) -print(f"State saved in round {round_id}") - -# 导出最新状态(或指定 round_id) -loaded_state = await state_service.export_state( - user_id="User1", - session_id="TestSession" -) -agent.load_state_dict(loaded_state) - -``` - -## 可选的后端实现类型 - -与会话历史类似,智能体状态服务也有不同的存储后端实现: - -| 服务类型 | 导入路径 | 存储位置 | 持久化 | 生产可用性 | 特点 | 适用场景 | -| ------------------------ | ------------------------------------------------------------ | ---------------- | ---------- | ---------- | ------------------------------------------------------- | ------------------------------ | -| **InMemoryStateService** | `from agentscope_runtime.engine.services.agent_state import InMemoryStateService` | 进程内存 | ❌ 无 | ❌ | 简单快速,无需外部依赖,进程结束数据丢失 | 开发调试、单元测试 | -| **RedisStateService** | `from agentscope_runtime.engine.services.agent_state import RedisStateService` | Redis 内存数据库 | ✅ 可持久化 | ✅ | 支持分布式共享状态,跨进程,Redis 可选持久化(RDB/AOF) | 高性能生产部署、跨进程数据共享 | - -## 切换不同实现 - -由于业务代码对 `StateService` 接口一致,切换后端非常简单,只需替换实例化的类型。 - -InMemory → Redis: - -```{code-cell} -from agentscope_runtime.engine.services.agent_state import RedisStateService - -state_service = RedisStateService(redis_url="redis://localhost:6379/0") -await state_service.start() - -# 保存状态 -await state_service.save_state(user_id="User1", session_id="ProdSession", state=agent.state_dict()) - -# 导出状态 -state = await state_service.export_state(user_id="User1", session_id="ProdSession") -agent.load_state_dict(state) -``` - -## 选型建议 - -- **开发阶段、调试或测试**:`InMemoryStateService`,无外部依赖,快速迭代。 -- **生产环境、需要跨进程共享状态**:`RedisStateService`,可配合 Redis 持久化和高可用集群。 -- **长周期和强一致性、审计**:可考虑自行实现数据库版 `StateService`。 - -## 小结 - -- **智能体状态服务**负责保存可序列化的内部状态,区别于会话历史只存消息。 -- 支持 `user_id`、`session_id` 和 `round_id` 三维组织数据。 -- 在 AgentScope 中通常直接调用,无需适配器。 -- 切换存储后端简单,接口定义统一。 -- 按需选择 InMemory、Redis 或自行扩展实现。 diff --git a/examples/deployments/agentrun_deploy/app_deploy_to_agentrun.py b/examples/deployments/agentrun_deploy/app_deploy_to_agentrun.py index 67a56aa4d..017846ef7 100644 --- a/examples/deployments/agentrun_deploy/app_deploy_to_agentrun.py +++ b/examples/deployments/agentrun_deploy/app_deploy_to_agentrun.py @@ -10,6 +10,7 @@ from agentscope.pipeline import stream_printing_messages from agentscope.tool import Toolkit, execute_python_code from agentscope.memory import InMemoryMemory +from agentscope.session import RedisSession from agentscope_runtime.engine.app import AgentApp from agentscope_runtime.engine.deployers.agentrun_deployer import ( @@ -18,9 +19,6 @@ AgentRunConfig, ) from agentscope_runtime.engine.schemas.agent_schemas import AgentRequest -from agentscope_runtime.engine.services.agent_state import ( - InMemoryStateService, -) agent_app = AgentApp( app_name="Friday", @@ -30,14 +28,13 @@ @agent_app.init async def init_func(self): - self.state_service = InMemoryStateService() - - await self.state_service.start() + import fakeredis - -@agent_app.shutdown -async def shutdown_func(self): - await self.state_service.stop() + fake_redis = fakeredis.aioredis.FakeRedis(decode_responses=True) + # NOTE: This FakeRedis instance is for development/testing only. + # In production, replace it with your own Redis client/connection + # (e.g., aioredis.Redis) + self.session = RedisSession(connection_pool=fake_redis.connection_pool) @agent_app.query(framework="agentscope") @@ -51,11 +48,6 @@ async def query_func( session_id = request.session_id user_id = request.user_id - state = await self.state_service.export_state( - session_id=session_id, - user_id=user_id, - ) - toolkit = Toolkit() toolkit.register_tool_function(execute_python_code) @@ -73,8 +65,11 @@ async def query_func( formatter=DashScopeChatFormatter(), ) - if state: - agent.load_state_dict(state) + await self.session.load_session_state( + session_id=session_id, + user_id=user_id, + agent=agent, + ) async for msg, last in stream_printing_messages( agents=[agent], @@ -82,12 +77,10 @@ async def query_func( ): yield msg, last - state = agent.state_dict() - - await self.state_service.save_state( - user_id=user_id, + await self.session.save_session_state( session_id=session_id, - state=state, + user_id=user_id, + agent=agent, ) diff --git a/examples/deployments/daemon_local_deploy/app_deploy.py b/examples/deployments/daemon_local_deploy/app_deploy.py index d14e48e74..69502ed15 100644 --- a/examples/deployments/daemon_local_deploy/app_deploy.py +++ b/examples/deployments/daemon_local_deploy/app_deploy.py @@ -9,6 +9,7 @@ from agentscope.tool import Toolkit, execute_python_code from agentscope.pipeline import stream_printing_messages from agentscope.memory import InMemoryMemory +from agentscope.session import RedisSession from agentscope_runtime.engine.app import AgentApp @@ -16,9 +17,6 @@ LocalDeployManager, ) from agentscope_runtime.engine.schemas.agent_schemas import AgentRequest -from agentscope_runtime.engine.services.agent_state import ( - InMemoryStateService, -) agent_app = AgentApp( app_name="Friday", @@ -28,14 +26,13 @@ @agent_app.init async def init_func(self): - self.state_service = InMemoryStateService() - - await self.state_service.start() + import fakeredis - -@agent_app.shutdown -async def shutdown_func(self): - await self.state_service.stop() + fake_redis = fakeredis.aioredis.FakeRedis(decode_responses=True) + # NOTE: This FakeRedis instance is for development/testing only. + # In production, replace it with your own Redis client/connection + # (e.g., aioredis.Redis) + self.session = RedisSession(connection_pool=fake_redis.connection_pool) @agent_app.query(framework="agentscope") @@ -49,11 +46,6 @@ async def query_func( session_id = request.session_id user_id = request.user_id - state = await self.state_service.export_state( - session_id=session_id, - user_id=user_id, - ) - toolkit = Toolkit() toolkit.register_tool_function(execute_python_code) @@ -71,8 +63,11 @@ async def query_func( formatter=DashScopeChatFormatter(), ) - if state: - agent.load_state_dict(state) + await self.session.load_session_state( + session_id=session_id, + user_id=user_id, + agent=agent, + ) async for msg, last in stream_printing_messages( agents=[agent], @@ -80,12 +75,10 @@ async def query_func( ): yield msg, last - state = agent.state_dict() - - await self.state_service.save_state( - user_id=user_id, + await self.session.save_session_state( session_id=session_id, - state=state, + user_id=user_id, + agent=agent, ) diff --git a/examples/deployments/detached_local_deploy/app_agent.py b/examples/deployments/detached_local_deploy/app_agent.py index 0d06202ea..b26c099a1 100644 --- a/examples/deployments/detached_local_deploy/app_agent.py +++ b/examples/deployments/detached_local_deploy/app_agent.py @@ -7,12 +7,10 @@ from agentscope.pipeline import stream_printing_messages from agentscope.tool import Toolkit, execute_python_code from agentscope.memory import InMemoryMemory +from agentscope.session import RedisSession from agentscope_runtime.engine.app import AgentApp from agentscope_runtime.engine.schemas.agent_schemas import AgentRequest -from agentscope_runtime.engine.services.agent_state import ( - InMemoryStateService, -) agent_app = AgentApp( app_name="Friday", @@ -22,9 +20,13 @@ @agent_app.init async def init_func(self): - self.state_service = InMemoryStateService() + import fakeredis - await self.state_service.start() + fake_redis = fakeredis.aioredis.FakeRedis(decode_responses=True) + # NOTE: This FakeRedis instance is for development/testing only. + # In production, replace it with your own Redis client/connection + # (e.g., aioredis.Redis) + self.session = RedisSession(connection_pool=fake_redis.connection_pool) @agent_app.shutdown @@ -43,11 +45,6 @@ async def query_func( session_id = request.session_id user_id = request.user_id - state = await self.state_service.export_state( - session_id=session_id, - user_id=user_id, - ) - toolkit = Toolkit() toolkit.register_tool_function(execute_python_code) @@ -66,8 +63,11 @@ async def query_func( ) agent.set_console_output_enabled(False) - if state: - agent.load_state_dict(state) + await self.session.load_session_state( + session_id=session_id, + user_id=user_id, + agent=agent, + ) async for msg, last in stream_printing_messages( agents=[agent], @@ -75,12 +75,10 @@ async def query_func( ): yield msg, last - state = agent.state_dict() - - await self.state_service.save_state( - user_id=user_id, + await self.session.save_session_state( session_id=session_id, - state=state, + user_id=user_id, + agent=agent, ) diff --git a/examples/deployments/fc_deploy/app_deploy_to_fc.py b/examples/deployments/fc_deploy/app_deploy_to_fc.py index 2fc0fee71..af28b4df1 100644 --- a/examples/deployments/fc_deploy/app_deploy_to_fc.py +++ b/examples/deployments/fc_deploy/app_deploy_to_fc.py @@ -1,7 +1,6 @@ # -*- coding: utf-8 -*- import asyncio -import logging import os import time @@ -11,6 +10,7 @@ from agentscope.pipeline import stream_printing_messages from agentscope.tool import Toolkit, execute_python_code from agentscope.memory import InMemoryMemory +from agentscope.session import RedisSession from agentscope_runtime.engine.app import AgentApp from agentscope_runtime.engine.deployers.fc_deployer import ( @@ -19,14 +19,6 @@ FCConfig, ) from agentscope_runtime.engine.schemas.agent_schemas import AgentRequest -from agentscope_runtime.engine.services.agent_state import ( - InMemoryStateService, -) - -logging.basicConfig( - level=logging.DEBUG, # 或 logging.INFO - format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", -) agent_app = AgentApp( app_name="Friday", @@ -36,14 +28,13 @@ @agent_app.init async def init_func(self): - self.state_service = InMemoryStateService() - - await self.state_service.start() + import fakeredis - -@agent_app.shutdown -async def shutdown_func(self): - await self.state_service.stop() + fake_redis = fakeredis.aioredis.FakeRedis(decode_responses=True) + # NOTE: This FakeRedis instance is for development/testing only. + # In production, replace it with your own Redis client/connection + # (e.g., aioredis.Redis) + self.session = RedisSession(connection_pool=fake_redis.connection_pool) @agent_app.query(framework="agentscope") @@ -57,11 +48,6 @@ async def query_func( session_id = request.session_id user_id = request.user_id - state = await self.state_service.export_state( - session_id=session_id, - user_id=user_id, - ) - toolkit = Toolkit() toolkit.register_tool_function(execute_python_code) @@ -79,8 +65,11 @@ async def query_func( formatter=DashScopeChatFormatter(), ) - if state: - agent.load_state_dict(state) + await self.session.load_session_state( + session_id=session_id, + user_id=user_id, + agent=agent, + ) async for msg, last in stream_printing_messages( agents=[agent], @@ -88,12 +77,10 @@ async def query_func( ): yield msg, last - state = agent.state_dict() - - await self.state_service.save_state( - user_id=user_id, + await self.session.save_session_state( session_id=session_id, - state=state, + user_id=user_id, + agent=agent, ) diff --git a/examples/deployments/k8s_deploy/app_deploy_to_k8s.py b/examples/deployments/k8s_deploy/app_deploy_to_k8s.py index cebb541c3..4a80dc4aa 100644 --- a/examples/deployments/k8s_deploy/app_deploy_to_k8s.py +++ b/examples/deployments/k8s_deploy/app_deploy_to_k8s.py @@ -9,6 +9,7 @@ from agentscope.tool import Toolkit, execute_python_code from agentscope.pipeline import stream_printing_messages from agentscope.memory import InMemoryMemory +from agentscope.session import RedisSession from agentscope_runtime.engine.app import AgentApp @@ -18,9 +19,6 @@ K8sConfig, ) from agentscope_runtime.engine.schemas.agent_schemas import AgentRequest -from agentscope_runtime.engine.services.agent_state import ( - InMemoryStateService, -) agent_app = AgentApp( app_name="Friday", @@ -30,14 +28,13 @@ @agent_app.init async def init_func(self): - self.state_service = InMemoryStateService() - - await self.state_service.start() + import fakeredis - -@agent_app.shutdown -async def shutdown_func(self): - await self.state_service.stop() + fake_redis = fakeredis.aioredis.FakeRedis(decode_responses=True) + # NOTE: This FakeRedis instance is for development/testing only. + # In production, replace it with your own Redis client/connection + # (e.g., aioredis.Redis) + self.session = RedisSession(connection_pool=fake_redis.connection_pool) @agent_app.query(framework="agentscope") @@ -51,11 +48,6 @@ async def query_func( session_id = request.session_id user_id = request.user_id - state = await self.state_service.export_state( - session_id=session_id, - user_id=user_id, - ) - toolkit = Toolkit() toolkit.register_tool_function(execute_python_code) @@ -73,8 +65,11 @@ async def query_func( formatter=DashScopeChatFormatter(), ) - if state: - agent.load_state_dict(state) + await self.session.load_session_state( + session_id=session_id, + user_id=user_id, + agent=agent, + ) async for msg, last in stream_printing_messages( agents=[agent], @@ -82,12 +77,10 @@ async def query_func( ): yield msg, last - state = agent.state_dict() - - await self.state_service.save_state( - user_id=user_id, + await self.session.save_session_state( session_id=session_id, - state=state, + user_id=user_id, + agent=agent, ) diff --git a/examples/deployments/knative_deploy/app_deploy_to_knative.py b/examples/deployments/knative_deploy/app_deploy_to_knative.py index 4c0c1b8ce..47c9f85ac 100644 --- a/examples/deployments/knative_deploy/app_deploy_to_knative.py +++ b/examples/deployments/knative_deploy/app_deploy_to_knative.py @@ -9,6 +9,7 @@ from agentscope.tool import Toolkit, execute_python_code from agentscope.pipeline import stream_printing_messages from agentscope.memory import InMemoryMemory +from agentscope.session import RedisSession from agentscope_runtime.engine.app import AgentApp from agentscope_runtime.engine.deployers.knative_deployer import ( @@ -17,9 +18,6 @@ K8sConfig, ) from agentscope_runtime.engine.schemas.agent_schemas import AgentRequest -from agentscope_runtime.engine.services.agent_state import ( - InMemoryStateService, -) agent_app = AgentApp( app_name="Friday", @@ -29,14 +27,13 @@ @agent_app.init async def init_func(self): - self.state_service = InMemoryStateService() - - await self.state_service.start() + import fakeredis - -@agent_app.shutdown -async def shutdown_func(self): - await self.state_service.stop() + fake_redis = fakeredis.aioredis.FakeRedis(decode_responses=True) + # NOTE: This FakeRedis instance is for development/testing only. + # In production, replace it with your own Redis client/connection + # (e.g., aioredis.Redis) + self.session = RedisSession(connection_pool=fake_redis.connection_pool) @agent_app.query(framework="agentscope") @@ -50,11 +47,6 @@ async def query_func( session_id = request.session_id user_id = request.user_id - state = await self.state_service.export_state( - session_id=session_id, - user_id=user_id, - ) - toolkit = Toolkit() toolkit.register_tool_function(execute_python_code) @@ -72,8 +64,11 @@ async def query_func( formatter=DashScopeChatFormatter(), ) - if state: - agent.load_state_dict(state) + await self.session.load_session_state( + session_id=session_id, + user_id=user_id, + agent=agent, + ) async for msg, last in stream_printing_messages( agents=[agent], @@ -81,12 +76,10 @@ async def query_func( ): yield msg, last - state = agent.state_dict() - - await self.state_service.save_state( - user_id=user_id, + await self.session.save_session_state( session_id=session_id, - state=state, + user_id=user_id, + agent=agent, ) diff --git a/examples/deployments/modelstudio_deploy/app_deploy_to_modelstudio.py b/examples/deployments/modelstudio_deploy/app_deploy_to_modelstudio.py index f47827ba0..5d591ba86 100644 --- a/examples/deployments/modelstudio_deploy/app_deploy_to_modelstudio.py +++ b/examples/deployments/modelstudio_deploy/app_deploy_to_modelstudio.py @@ -10,6 +10,7 @@ from agentscope.pipeline import stream_printing_messages from agentscope.tool import Toolkit, execute_python_code from agentscope.memory import InMemoryMemory +from agentscope.session import RedisSession from agentscope_runtime.engine.app import AgentApp from agentscope_runtime.engine.deployers.modelstudio_deployer import ( @@ -18,9 +19,6 @@ ModelstudioConfig, ) from agentscope_runtime.engine.schemas.agent_schemas import AgentRequest -from agentscope_runtime.engine.services.agent_state import ( - InMemoryStateService, -) agent_app = AgentApp( app_name="Friday", @@ -30,15 +28,13 @@ @agent_app.init async def init_func(self): - self.state_service = InMemoryStateService() - - await self.state_service.start() + import fakeredis - -@agent_app.shutdown -async def shutdown_func(self): - await self.state_service.stop() - await self.session_service.stop() + fake_redis = fakeredis.aioredis.FakeRedis(decode_responses=True) + # NOTE: This FakeRedis instance is for development/testing only. + # In production, replace it with your own Redis client/connection + # (e.g., aioredis.Redis) + self.session = RedisSession(connection_pool=fake_redis.connection_pool) @agent_app.query(framework="agentscope") @@ -52,11 +48,6 @@ async def query_func( session_id = request.session_id user_id = request.user_id - state = await self.state_service.export_state( - session_id=session_id, - user_id=user_id, - ) - toolkit = Toolkit() toolkit.register_tool_function(execute_python_code) @@ -74,8 +65,11 @@ async def query_func( formatter=DashScopeChatFormatter(), ) - if state: - agent.load_state_dict(state) + await self.session.load_session_state( + session_id=session_id, + user_id=user_id, + agent=agent, + ) async for msg, last in stream_printing_messages( agents=[agent], @@ -83,12 +77,10 @@ async def query_func( ): yield msg, last - state = agent.state_dict() - - await self.state_service.save_state( - user_id=user_id, + await self.session.save_session_state( session_id=session_id, - state=state, + user_id=user_id, + agent=agent, ) diff --git a/examples/deployments/pai_deploy/my_agent/agent.py b/examples/deployments/pai_deploy/my_agent/agent.py index 2b246aab4..fbba4a48c 100644 --- a/examples/deployments/pai_deploy/my_agent/agent.py +++ b/examples/deployments/pai_deploy/my_agent/agent.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- import os -from typing import AsyncIterator, List, Optional +from typing import AsyncIterator, List from agentscope.agent import ReActAgent from agentscope.formatter import DashScopeChatFormatter @@ -8,13 +8,14 @@ from agentscope.model import OpenAIChatModel from agentscope.pipeline import stream_printing_messages from agentscope.tool import ToolResponse, Toolkit, execute_python_code +from agentscope.session import RedisSession + from agentscope_runtime.engine import AgentApp from agentscope_runtime.engine.runner import Runner from agentscope_runtime.engine.schemas.agent_schemas import ( AgentRequest, ) -from agentscope_runtime.engine.services.agent_state import InMemoryStateService agent_app = AgentApp( app_name="SimpleAgent", @@ -42,30 +43,38 @@ async def get_weather(location: str) -> ToolResponse: @agent_app.init async def init_func(runner: Runner): - runner.state_service = InMemoryStateService() - await runner.state_service.start() - + import fakeredis -@agent_app.shutdown -async def shutdown_func(runner: Runner): - await runner.state_service.stop() + fake_redis = fakeredis.aioredis.FakeRedis(decode_responses=True) + # NOTE: This FakeRedis instance is for development/testing only. + # In production, replace it with your own Redis client/connection + # (e.g., aioredis.Redis) + runner.session = RedisSession(connection_pool=fake_redis.connection_pool) -def create_stateful_agent( - state: Optional[dict] = None, -) -> ReActAgent: +@agent_app.query(framework="agentscope") +async def query_func( + runner: Runner, + msgs: List[Msg], + request: AgentRequest = None, + **kwargs, # pylint: disable=unused-argument +) -> AsyncIterator[tuple[Msg, bool]]: """ - Create a stateful agent with the given session service, session id, user - id, and state. + Main entry point for agent execution. Args: - state (Optional[dict]): State to load into the agent + runner: Runner instance + msgs: List of messages to process + request: AgentRequest instance + **kwargs: Additional keyword arguments Returns: - tuple[dict, Toolkit]: Tuple containing the state and toolkit - + Iterator[tuple[Msg, bool]]: Iterator of messages and last flag """ + session_id = request.session_id + user_id = request.user_id + toolkit = Toolkit() toolkit.register_tool_function(execute_python_code) toolkit.register_tool_function(get_weather) @@ -87,41 +96,10 @@ def create_stateful_agent( ) agent.set_console_output_enabled(enabled=False) - if state: - agent.load_state_dict(state) - - return agent - - -@agent_app.query(framework="agentscope") -async def query_func( - runner: Runner, - msgs: List[Msg], - request: AgentRequest = None, - **kwargs, # pylint: disable=unused-argument -) -> AsyncIterator[tuple[Msg, bool]]: - """ - Main entry point for agent execution. - - Args: - runner: Runner instance - msgs: List of messages to process - request: AgentRequest instance - **kwargs: Additional keyword arguments - - Returns: - Iterator[tuple[Msg, bool]]: Iterator of messages and last flag - """ - - session_id = request.session_id - user_id = request.user_id - - state = await runner.state_service.export_state( + await runner.session.load_session_state( session_id=session_id, user_id=user_id, - ) - agent = create_stateful_agent( - state=state, + agent=agent, ) async for msg, last in stream_printing_messages( @@ -130,10 +108,8 @@ async def query_func( ): yield msg, last - state = agent.state_dict() - - await runner.state_service.save_state( - user_id=user_id, + await runner.session.save_session_state( session_id=session_id, - state=state, + user_id=user_id, + agent=agent, ) diff --git a/examples/integrations/ag-ui/agent.py b/examples/integrations/ag-ui/agent.py index 7b98296bd..64a631707 100644 --- a/examples/integrations/ag-ui/agent.py +++ b/examples/integrations/ag-ui/agent.py @@ -8,6 +8,7 @@ from agentscope.model import OpenAIChatModel from agentscope.pipeline import stream_printing_messages from agentscope.tool import ToolResponse, Toolkit, execute_python_code +from agentscope.session import RedisSession from agentscope_runtime.engine import AgentApp from agentscope_runtime.engine.deployers.adapter.agui import AGUIAdaptorConfig @@ -16,7 +17,6 @@ AgentRequest, Message, ) -from agentscope_runtime.engine.services.agent_state import InMemoryStateService agent_app = AgentApp( agui_config=AGUIAdaptorConfig( @@ -45,13 +45,13 @@ async def get_weather(location: str) -> ToolResponse: @agent_app.init async def init_func(runner: Runner): - runner.state_service = InMemoryStateService() - await runner.state_service.start() + import fakeredis - -@agent_app.shutdown -async def shutdown_func(runner: Runner): - await runner.state_service.stop() + fake_redis = fakeredis.aioredis.FakeRedis(decode_responses=True) + # NOTE: This FakeRedis instance is for development/testing only. + # In production, replace it with your own Redis client/connection + # (e.g., aioredis.Redis) + runner.session = RedisSession(connection_pool=fake_redis.connection_pool) async def get_unseen_messages( @@ -85,21 +85,29 @@ async def get_unseen_messages( ] -def create_stateful_agent( - state: Optional[dict] = None, -) -> ReActAgent: +@agent_app.query(framework="agentscope") +async def query_func( + runner: Runner, + msgs: List[Msg], + request: AgentRequest = None, + **kwargs, # pylint: disable=unused-argument +) -> AsyncIterator[tuple[Msg, bool]]: """ - Create a stateful agent with the given session service, session id, user - id, and state. + Main entry point for agent execution. Args: - state (Optional[dict]): State to load into the agent + runner: Runner instance + msgs: List of messages to process + request: AgentRequest instance + **kwargs: Additional keyword arguments Returns: - tuple[dict, Toolkit]: Tuple containing the state and toolkit - + Iterator[tuple[Msg, bool]]: Iterator of messages and last flag """ + session_id = request.session_id + user_id = request.user_id + toolkit = Toolkit() toolkit.register_tool_function(execute_python_code) toolkit.register_tool_function(get_weather) @@ -121,44 +129,10 @@ def create_stateful_agent( ) agent.set_console_output_enabled(enabled=False) - if state: - agent.load_state_dict(state) - - return agent - - -@agent_app.query(framework="agentscope") -async def query_func( - runner: Runner, - msgs: List[Msg], - request: AgentRequest = None, - **kwargs, # pylint: disable=unused-argument -) -> AsyncIterator[tuple[Msg, bool]]: - """ - Main entry point for agent execution. - - Args: - runner: Runner instance - msgs: List of messages to process - request: AgentRequest instance - **kwargs: Additional keyword arguments - - Returns: - Iterator[tuple[Msg, bool]]: Iterator of messages and last flag - """ - - session_id = request.session_id - user_id = request.user_id - - # If state is provided in the request via AG-UI, use it directly. - state = getattr(request, "state", None) - if not state: - state = await runner.state_service.export_state( - session_id=session_id, - user_id=user_id, - ) - agent = create_stateful_agent( - state=state, + await runner.session.load_session_state( + session_id=session_id, + user_id=user_id, + agent=agent, ) unseen_messages = await get_unseen_messages( @@ -174,10 +148,8 @@ async def query_func( ): yield msg, last - state = agent.state_dict() - - await runner.state_service.save_state( - user_id=user_id, + await runner.session.save_session_state( session_id=session_id, - state=state, + user_id=user_id, + agent=agent, ) diff --git a/pyproject.toml b/pyproject.toml index 6b839577c..c772afa76 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "agentscope-runtime" -version = "1.1.0a1" +version = "1.1.0b2" description = "A production-ready runtime framework for agent applications, providing secure sandboxed execution environments and scalable deployment solutions with multi-framework support." readme = "README.md" requires-python = ">=3.10" @@ -11,7 +11,7 @@ dependencies = [ "openai", "pydantic>=2.11.7", "requests>=2.32.4", - "agentscope>=1.0.12", + "agentscope>=1.0.14", "docker>=7.1.0", "redis>=6.0.0", "oss2>=2.19.1", diff --git a/src/agentscope_runtime/adapters/agentscope/message.py b/src/agentscope_runtime/adapters/agentscope/message.py index 276c54dda..bc700607e 100644 --- a/src/agentscope_runtime/adapters/agentscope/message.py +++ b/src/agentscope_runtime/adapters/agentscope/message.py @@ -4,7 +4,7 @@ import json from collections import OrderedDict -from typing import Union, List +from typing import Union, List, Callable, Optional, Dict from urllib.parse import urlparse from mcp.types import CallToolResult @@ -37,12 +37,18 @@ def matches_typed_dict_structure(obj, typed_dict_cls): def message_to_agentscope_msg( messages: Union[Message, List[Message]], + type_converters: Optional[Dict[str, Callable]] = None, ) -> Union[Msg, List[Msg]]: """ Convert AgentScope runtime Message(s) to AgentScope Msg(s). Args: messages: A single AgentScope runtime Message or list of Messages. + type_converters: Optional mapping from ``message.type`` to a callable + ``converter(message)``. When provided and the current + ``message.type`` exists in the mapping, the corresponding converter + will be used and the built-in conversion logic will be skipped for + that message. Returns: A single Msg object or a list of Msg objects. @@ -59,6 +65,10 @@ def _try_loads(v, default, keep_original=False): return default def _convert_one(message: Message) -> Msg: + # Used for custom conversion + if type_converters and message.type in type_converters: + return type_converters[message.type](message) + # Normalize role if message.role == "tool": role_label = "system" # AgentScope not support tool as role diff --git a/src/agentscope_runtime/adapters/agentscope/stream.py b/src/agentscope_runtime/adapters/agentscope/stream.py index acb4b6db0..38fe227aa 100644 --- a/src/agentscope_runtime/adapters/agentscope/stream.py +++ b/src/agentscope_runtime/adapters/agentscope/stream.py @@ -1,9 +1,10 @@ # -*- coding: utf-8 -*- # pylint: disable=too-many-nested-blocks,too-many-branches,too-many-statements import copy +import inspect import json -from typing import AsyncIterator, Tuple, List, Union +from typing import AsyncIterator, Tuple, List, Union, Optional, Callable, Dict from urllib.parse import urlparse from agentscope import setup_logger @@ -30,6 +31,8 @@ async def adapt_agentscope_message_stream( source_stream: AsyncIterator[Tuple[Msg, bool]], + type_converters: Optional[Dict[str, Callable]] = None, + **kwargs, # pylint:disable=unused-argument ) -> AsyncIterator[Union[Message, Content]]: # Initialize variables to avoid uncaught errors msg_id = None @@ -137,6 +140,44 @@ async def adapt_agentscope_message_stream( index = text_delta_content.index yield text_delta_content elif isinstance(element, dict): + # Used for custom conversion + if ( + type_converters + and element.get("type") in type_converters + ): + blk_type = element.get("type") + if not isinstance(blk_type, str): + continue + fn = type_converters[blk_type] + # Send message, element, last, tool_start, metadata + # and usage + out = fn( + element, + message, + last, + tool_start, + metadata, + usage, + ) + # Case 1: async generator / async iterator + if hasattr(out, "__aiter__"): + async for ev in out: + yield ev + continue + + # Case 2: sync generator / iterator + if inspect.isgenerator(out): + for ev in out: + yield ev + continue + + # Only generator styles are supported + raise TypeError( + f"type_converters['{blk_type}'] must return a " + f"generator/iterator or an async generator/async " + f"iterator, got: {type(out)}", + ) + if element.get("type") == "text": # Text text = element.get( "text", diff --git a/src/agentscope_runtime/adapters/agno/message.py b/src/agentscope_runtime/adapters/agno/message.py index 11fdbb822..6381c44ce 100644 --- a/src/agentscope_runtime/adapters/agno/message.py +++ b/src/agentscope_runtime/adapters/agno/message.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -from typing import Union, List +from typing import Union, List, Callable, Optional, Dict from agentscope.formatter import OpenAIChatFormatter @@ -9,18 +9,27 @@ async def message_to_agno_message( messages: Union[Message, List[Message]], + type_converters: Optional[Dict[str, Callable]] = None, ) -> Union[dict, List[dict]]: """ Convert AgentScope runtime Message(s) to Agno Message(s). Args: messages: A single AgentScope runtime Message or list of Messages. + type_converters: Optional mapping from ``message.type`` to a callable + ``converter(message)``. When provided and the current + ``message.type`` exists in the mapping, the corresponding converter + will be used and the built-in conversion logic will be skipped for + that message. Returns: A single AgnoMessage object or a list of AgnoMessage objects. """ - as_msgs = message_to_agentscope_msg(messages) + as_msgs = message_to_agentscope_msg( + messages, + type_converters=type_converters, + ) raw_list = isinstance(as_msgs, list) as_msgs = as_msgs if raw_list else [as_msgs] diff --git a/src/agentscope_runtime/adapters/agno/stream.py b/src/agentscope_runtime/adapters/agno/stream.py index 1fd894515..721cce1db 100644 --- a/src/agentscope_runtime/adapters/agno/stream.py +++ b/src/agentscope_runtime/adapters/agno/stream.py @@ -31,6 +31,7 @@ async def adapt_agno_message_stream( source_stream: AsyncIterator[BaseAgentRunEvent], + **kwargs, # pylint:disable=unused-argument ) -> AsyncIterator[Union[Message, Content]]: rb = ResponseBuilder() mb = None diff --git a/src/agentscope_runtime/adapters/langgraph/__init__.py b/src/agentscope_runtime/adapters/langgraph/__init__.py index 8fa32ae22..296b99f9e 100644 --- a/src/agentscope_runtime/adapters/langgraph/__init__.py +++ b/src/agentscope_runtime/adapters/langgraph/__init__.py @@ -2,11 +2,9 @@ """LangGraph adapter for AgentScope runtime.""" # todo Message(reasoning) Adapter -# todo Memory Adapter # todo Sandbox Tools Adapter -from .message import langgraph_msg_to_message, message_to_langgraph_msg +from .message import message_to_langgraph_msg __all__ = [ - "langgraph_msg_to_message", "message_to_langgraph_msg", ] diff --git a/src/agentscope_runtime/adapters/langgraph/message.py b/src/agentscope_runtime/adapters/langgraph/message.py index 1108c5a41..c09bfc44f 100644 --- a/src/agentscope_runtime/adapters/langgraph/message.py +++ b/src/agentscope_runtime/adapters/langgraph/message.py @@ -4,7 +4,7 @@ import json from collections import OrderedDict -from typing import Union, List +from typing import Union, List, Callable, Optional, Dict from langchain_core.messages import ( AIMessage, @@ -16,129 +16,34 @@ from ...engine.schemas.agent_schemas import ( Message, - FunctionCall, MessageType, ) -from ...engine.helpers.agent_api_builder import ResponseBuilder - - -def langgraph_msg_to_message( - messages: Union[BaseMessage, List[BaseMessage]], -) -> List[Message]: - """ - Convert LangGraph BaseMessage(s) into one or more runtime Message objects - - Args: - messages: LangGraph message(s) from streaming. - - Returns: - List[Message]: One or more constructed runtime Message objects. - """ - if isinstance(messages, BaseMessage): - msgs = [messages] - elif isinstance(messages, list): - msgs = messages - else: - raise TypeError( - f"Expected BaseMessage or list[BaseMessage], got {type(messages)}", - ) - - results: List[Message] = [] - - for msg in msgs: - # Map LangGraph roles to runtime roles - if isinstance(msg, HumanMessage): - role = "user" - elif isinstance(msg, AIMessage): - role = "assistant" - elif isinstance(msg, SystemMessage): - role = "system" - elif isinstance(msg, ToolMessage): - role = "tool" - else: - role = "assistant" # default fallback - - # Handle tool calls in AIMessage - if isinstance(msg, AIMessage) and msg.tool_calls: - # Convert each tool call to a PLUGIN_CALL message - for tool_call in msg.tool_calls: - rb = ResponseBuilder() - mb = rb.create_message_builder( - role=role, - message_type=MessageType.PLUGIN_CALL, - ) - # Add metadata - mb.message.metadata = { - "original_id": getattr(msg, "id", None), - "name": getattr(msg, "name", None), - "metadata": getattr(msg, "additional_kwargs", {}), - } - cb = mb.create_content_builder(content_type="data") - - call_data = FunctionCall( - call_id=tool_call.get("id", ""), - name=tool_call.get("name", ""), - arguments=json.dumps(tool_call.get("args", {})), - ).model_dump() - cb.set_data(call_data) - cb.complete() - mb.complete() - results.append(mb.get_message_data()) - - # If there's content in addition to tool calls, - # create a separate message - if msg.content: - rb = ResponseBuilder() - mb = rb.create_message_builder( - role=role, - message_type=MessageType.MESSAGE, - ) - mb.message.metadata = { - "original_id": getattr(msg, "id", None), - "name": getattr(msg, "name", None), - "metadata": getattr(msg, "additional_kwargs", {}), - } - cb = mb.create_content_builder(content_type="text") - cb.set_text(str(msg.content)) - cb.complete() - mb.complete() - results.append(mb.get_message_data()) - else: - # Regular message conversion - rb = ResponseBuilder() - mb = rb.create_message_builder( - role=role, - message_type=MessageType.MESSAGE, - ) - # Add metadata - mb.message.metadata = { - "original_id": getattr(msg, "id", None), - "name": getattr(msg, "name", None), - "metadata": getattr(msg, "additional_kwargs", {}), - } - cb = mb.create_content_builder(content_type="text") - cb.set_text(str(msg.content) if msg.content else "") - cb.complete() - mb.complete() - results.append(mb.get_message_data()) - - return results def message_to_langgraph_msg( messages: Union[Message, List[Message]], + type_converters: Optional[Dict[str, Callable]] = None, ) -> Union[BaseMessage, List[BaseMessage]]: """ Convert AgentScope runtime Message(s) to LangGraph BaseMessage(s). Args: messages: A single AgentScope runtime Message or list of Messages. + type_converters: Optional mapping from ``message.type`` to a callable + ``converter(message)``. When provided and the current + ``message.type`` exists in the mapping, the corresponding converter + will be used and the built-in conversion logic will be skipped for + that message. Returns: A single BaseMessage object or a list of BaseMessage objects. """ def _convert_one(message: Message) -> BaseMessage: + # Used for custom conversion + if type_converters and message.type in type_converters: + return type_converters[message.type](message) + # Map runtime roles to LangGraph roles role_map = { "user": HumanMessage, diff --git a/src/agentscope_runtime/adapters/langgraph/stream.py b/src/agentscope_runtime/adapters/langgraph/stream.py index 96257240e..df5f3ef0c 100644 --- a/src/agentscope_runtime/adapters/langgraph/stream.py +++ b/src/agentscope_runtime/adapters/langgraph/stream.py @@ -27,6 +27,7 @@ async def adapt_langgraph_message_stream( source_stream: AsyncIterator[Tuple[BaseMessage, bool]], + **kwargs, # pylint:disable=unused-argument ) -> AsyncIterator[Message]: """ Optimized version of the stream adapter for LangGraph messages. diff --git a/src/agentscope_runtime/adapters/ms_agent_framework/message.py b/src/agentscope_runtime/adapters/ms_agent_framework/message.py index 71862459b..7e9fb566f 100644 --- a/src/agentscope_runtime/adapters/ms_agent_framework/message.py +++ b/src/agentscope_runtime/adapters/ms_agent_framework/message.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- # pylint: disable=too-many-branches,too-many-statements import json -from typing import Union, List +from typing import Union, List, Callable, Optional, Dict from collections import OrderedDict from agent_framework import ( @@ -22,6 +22,7 @@ def message_to_ms_agent_framework_message( messages: Union[Message, List[Message]], + type_converters: Optional[Dict[str, Callable]] = None, ) -> Union[ChatMessage, List[ChatMessage]]: """ Convert AgentScope runtime Message(s) to Microsoft agent framework @@ -33,6 +34,11 @@ def message_to_ms_agent_framework_message( Args: messages: A single AgentScope runtime Message or list of Messages. + type_converters: Optional mapping from ``message.type`` to a callable + ``converter(message)``. When provided and the current + ``message.type`` exists in the mapping, the corresponding converter + will be used and the built-in conversion logic will be skipped for + that message. Returns: A single Microsoft agent framework Message object or a list of @@ -50,6 +56,10 @@ def _try_loads(v, default, keep_original=False): return default def _convert_one(message: Message) -> ChatMessage: + # Used for custom conversion + if type_converters and message.type in type_converters: + return type_converters[message.type](message) + result = { "author_name": getattr(message, "name", message.role), "role": message.role or "assistant", diff --git a/src/agentscope_runtime/adapters/ms_agent_framework/stream.py b/src/agentscope_runtime/adapters/ms_agent_framework/stream.py index f6f6b694b..dd5abadce 100644 --- a/src/agentscope_runtime/adapters/ms_agent_framework/stream.py +++ b/src/agentscope_runtime/adapters/ms_agent_framework/stream.py @@ -35,6 +35,7 @@ async def adapt_ms_agent_framework_message_stream( source_stream: AsyncIterator[AgentRunResponseUpdate], + **kwargs, # pylint:disable=unused-argument ) -> AsyncIterator[Union[Message, Content]]: # Initialize variables to avoid uncaught errors msg_id = None diff --git a/src/agentscope_runtime/adapters/text/stream.py b/src/agentscope_runtime/adapters/text/stream.py index 6124b5902..16fe07713 100644 --- a/src/agentscope_runtime/adapters/text/stream.py +++ b/src/agentscope_runtime/adapters/text/stream.py @@ -11,6 +11,7 @@ async def adapt_text_stream( source_stream: AsyncIterator[str], + **kwargs, # pylint:disable=unused-argument ) -> AsyncIterator[Message]: rb = ResponseBuilder() mb = rb.create_message_builder( diff --git a/src/agentscope_runtime/common/container_clients/agentrun_client.py b/src/agentscope_runtime/common/container_clients/agentrun_client.py index 42a72b93b..abaaa3e31 100644 --- a/src/agentscope_runtime/common/container_clients/agentrun_client.py +++ b/src/agentscope_runtime/common/container_clients/agentrun_client.py @@ -1095,6 +1095,3 @@ def _replace_agent_runtime_images(self, image: str) -> str: image_name = image return replacement_map.get(image_name.strip(), image) - - def _is_browser_image(self, image: str): - return image.startswith("agentscope/runtime-sandbox-browser") diff --git a/src/agentscope_runtime/common/container_clients/fc_client.py b/src/agentscope_runtime/common/container_clients/fc_client.py index cc1538d69..d73a54460 100644 --- a/src/agentscope_runtime/common/container_clients/fc_client.py +++ b/src/agentscope_runtime/common/container_clients/fc_client.py @@ -844,14 +844,3 @@ def _replace_fc_images(self, image: str) -> str: image_name = image return replacement_map.get(image_name.strip(), image) - - def _is_browser_image(self, image: str) -> bool: - """Check if the image is a browser image. - - Args: - image (str): The image name to check. - - Returns: - bool: True if the image is a browser image, False otherwise. - """ - return image.startswith("agentscope/runtime-sandbox-browser") diff --git a/src/agentscope_runtime/engine/runner.py b/src/agentscope_runtime/engine/runner.py index d55897467..d2ad6e1a1 100644 --- a/src/agentscope_runtime/engine/runner.py +++ b/src/agentscope_runtime/engine/runner.py @@ -14,6 +14,7 @@ Union, Dict, AsyncIterator, + Callable, ) from .deployers import ( @@ -49,6 +50,9 @@ def __init__(self) -> None: """ self.framework_type = None + self.in_type_converters: Optional[Dict[str, Callable]] = None + self.out_type_converters: Optional[Dict[str, Callable]] = None + self._deploy_managers = {} self._health = False self._exit_stack = AsyncExitStack() @@ -251,7 +255,12 @@ async def stream_query( # pylint:disable=unused-argument stream_adapter = adapt_agentscope_message_stream kwargs.update( - {"msgs": message_to_agentscope_msg(request.input)}, + { + "msgs": message_to_agentscope_msg( + request.input, + type_converters=self.in_type_converters, + ), + }, ) elif self.framework_type == "langgraph": from ..adapters.langgraph.stream import ( @@ -261,7 +270,12 @@ async def stream_query( # pylint:disable=unused-argument stream_adapter = adapt_langgraph_message_stream kwargs.update( - {"msgs": message_to_langgraph_msg(request.input)}, + { + "msgs": message_to_langgraph_msg( + request.input, + type_converters=self.in_type_converters, + ), + }, ) elif self.framework_type == "agno": from ..adapters.agno.stream import ( @@ -271,7 +285,12 @@ async def stream_query( # pylint:disable=unused-argument stream_adapter = adapt_agno_message_stream kwargs.update( - {"msgs": await message_to_agno_message(request.input)}, + { + "msgs": await message_to_agno_message( + request.input, + type_converters=self.in_type_converters, + ), + }, ) elif self.framework_type == "ms_agent_framework": from ..adapters.ms_agent_framework.stream import ( @@ -283,7 +302,12 @@ async def stream_query( # pylint:disable=unused-argument stream_adapter = adapt_ms_agent_framework_message_stream kwargs.update( - {"msgs": message_to_ms_agent_framework_message(request.input)}, + { + "msgs": message_to_ms_agent_framework_message( + request.input, + type_converters=self.in_type_converters, + ), + }, ) # TODO: support other frameworks else: @@ -303,6 +327,7 @@ def identity_stream_adapter( **query_kwargs, **kwargs, ), + type_converters=self.out_type_converters, ): if ( event.status == RunStatus.Completed diff --git a/src/agentscope_runtime/engine/schemas/agent_schemas.py b/src/agentscope_runtime/engine/schemas/agent_schemas.py index 48c5c5c97..1f9061944 100644 --- a/src/agentscope_runtime/engine/schemas/agent_schemas.py +++ b/src/agentscope_runtime/engine/schemas/agent_schemas.py @@ -469,6 +469,7 @@ class ToolCallOutput(BaseModel): AudioContent, FileContent, RefusalContent, + VideoContent, ], Field(discriminator="type"), ] diff --git a/src/agentscope_runtime/engine/services/agent_state/__init__.py b/src/agentscope_runtime/engine/services/agent_state/__init__.py deleted file mode 100644 index 8357d20d4..000000000 --- a/src/agentscope_runtime/engine/services/agent_state/__init__.py +++ /dev/null @@ -1,25 +0,0 @@ -# -*- coding: utf-8 -*- -from typing import TYPE_CHECKING -from ....common.utils.lazy_loader import install_lazy_loader -from ....common.utils.deprecation import deprecated_module - -deprecated_module( - module_name=__name__, - removed_in="v1.1", - alternative="agentscope.session", -) - -if TYPE_CHECKING: - from .state_service import StateService, InMemoryStateService - from .redis_state_service import RedisStateService - from .state_service_factory import StateServiceFactory - -install_lazy_loader( - globals(), - { - "StateService": ".state_service", - "InMemoryStateService": ".state_service", - "RedisStateService": ".redis_state_service", - "StateServiceFactory": ".state_service_factory", - }, -) diff --git a/src/agentscope_runtime/engine/services/agent_state/redis_state_service.py b/src/agentscope_runtime/engine/services/agent_state/redis_state_service.py deleted file mode 100644 index ecb664aa5..000000000 --- a/src/agentscope_runtime/engine/services/agent_state/redis_state_service.py +++ /dev/null @@ -1,166 +0,0 @@ -# -*- coding: utf-8 -*- -import json -from typing import Optional, Dict, Any - -import redis.asyncio as aioredis - -from .state_service import StateService - - -class RedisStateService(StateService): - """ - Redis-based implementation of StateService. - - Stores agent states in Redis using a hash per (user_id, session_id), - with round_id as the hash field and serialized state as the value. - """ - - _DEFAULT_SESSION_ID = "default" - - def __init__( - self, - redis_url: str = "redis://localhost:6379/0", - redis_client: Optional[aioredis.Redis] = None, - socket_timeout: Optional[float] = 5.0, - socket_connect_timeout: Optional[float] = 5.0, - max_connections: Optional[int] = None, - retry_on_timeout: bool = True, - ttl_seconds: Optional[int] = 3600, # 1 hour in seconds - health_check_interval: Optional[float] = 30.0, - socket_keepalive: bool = True, - ): - """ - Initialize RedisStateService. - - Args: - redis_url: Redis connection URL - redis_client: Optional pre-configured Redis client - socket_timeout: Socket timeout in seconds (default: 5.0) - socket_connect_timeout: Socket connect timeout in seconds - (default: 5.0) - max_connections: Maximum number of connections in the pool - (default: None) - retry_on_timeout: Whether to retry on timeout (default: True) - ttl_seconds: Time-to-live in seconds for state data. If None, - data never expires (default: 3600, i.e., 1 hour) - health_check_interval: Interval in seconds for health checks on - idle connections (default: 30.0). - Connections idle longer than this will be checked before reuse. - Set to 0 to disable. - socket_keepalive: Enable TCP keepalive to prevent - silent disconnections (default: True) - """ - self._redis_url = redis_url - self._redis = redis_client - self._socket_timeout = socket_timeout - self._socket_connect_timeout = socket_connect_timeout - self._max_connections = max_connections - self._retry_on_timeout = retry_on_timeout - self._ttl_seconds = ttl_seconds - self._health_check_interval = health_check_interval - self._socket_keepalive = socket_keepalive - - async def start(self) -> None: - """Starts the Redis connection with proper timeout and connection - pool settings.""" - if self._redis is None: - self._redis = aioredis.from_url( - self._redis_url, - decode_responses=True, - socket_timeout=self._socket_timeout, - socket_connect_timeout=self._socket_connect_timeout, - max_connections=self._max_connections, - retry_on_timeout=self._retry_on_timeout, - health_check_interval=self._health_check_interval, - socket_keepalive=self._socket_keepalive, - ) - - async def stop(self) -> None: - """Closes the Redis connection.""" - if self._redis: - await self._redis.aclose() - self._redis = None - - async def health(self) -> bool: - """Checks the health of the service.""" - if not self._redis: - return False - try: - pong = await self._redis.ping() - return pong is True or pong == "PONG" - except Exception: - return False - - def _session_key(self, user_id: str, session_id: str) -> str: - """Generate the Redis key for a user's session.""" - return f"user_state:{user_id}:{session_id}" - - async def save_state( - self, - user_id: str, - state: Dict[str, Any], - session_id: Optional[str] = None, - round_id: Optional[int] = None, - ) -> int: - if not self._redis: - raise RuntimeError("Redis connection is not available") - - sid = session_id or self._DEFAULT_SESSION_ID - key = self._session_key(user_id, sid) - - existing_fields = await self._redis.hkeys(key) - existing_rounds = sorted( - int(f) for f in existing_fields if f.isdigit() - ) - - if round_id is None: - if existing_rounds: - round_id = max(existing_rounds) + 1 - else: - round_id = 1 - - await self._redis.hset(key, round_id, json.dumps(state)) - - # Set TTL for the state key if configured - if self._ttl_seconds is not None: - await self._redis.expire(key, self._ttl_seconds) - - return round_id - - async def export_state( - self, - user_id: str, - session_id: Optional[str] = None, - round_id: Optional[int] = None, - ) -> Optional[Dict[str, Any]]: - if not self._redis: - raise RuntimeError("Redis connection is not available") - - sid = session_id or self._DEFAULT_SESSION_ID - key = self._session_key(user_id, sid) - - existing_fields = await self._redis.hkeys(key) - if not existing_fields: - return None - - if round_id is None: - numeric_fields = [int(f) for f in existing_fields if f.isdigit()] - if not numeric_fields: - return None - latest_round_id = max(numeric_fields) - state_json = await self._redis.hget(key, latest_round_id) - else: - state_json = await self._redis.hget(key, round_id) - - if state_json is None: - return None - - # Refresh TTL when accessing the state - if self._ttl_seconds is not None: - await self._redis.expire(key, self._ttl_seconds) - - try: - return json.loads(state_json) - except json.JSONDecodeError: - # Return None for corrupted state data instead of raising exception - return None diff --git a/src/agentscope_runtime/engine/services/agent_state/state_service.py b/src/agentscope_runtime/engine/services/agent_state/state_service.py deleted file mode 100644 index e924b9cb8..000000000 --- a/src/agentscope_runtime/engine/services/agent_state/state_service.py +++ /dev/null @@ -1,179 +0,0 @@ -# -*- coding: utf-8 -*- -import copy - -from abc import abstractmethod -from typing import Dict, Any, Optional - -from ..base import ServiceWithLifecycleManager - - -class StateService(ServiceWithLifecycleManager): - """ - Abstract base class for agent state management services. - - Stores and manages agent states organized by user_id, session_id, - and round_id. Supports saving, retrieving, listing, and deleting states. - """ - - async def start(self) -> None: - pass - - async def stop(self) -> None: - pass - - @abstractmethod - async def save_state( - self, - user_id: str, - state: Dict[str, Any], - session_id: Optional[str] = None, - round_id: Optional[int] = None, - ) -> int: - """ - Save serialized state data for a specific user/session. - - If round_id is provided, store the state in that round. - If round_id is None, append as a new round with automatically - assigned round_id. - - Args: - user_id: The unique ID of the user. - state: A dictionary representing serialized agent state. - session_id: Optional session/conversation ID. Defaults to - "default". - round_id: Optional conversation round number. - - Returns: - The round_id in which the state was saved. - """ - - @abstractmethod - async def export_state( - self, - user_id: str, - session_id: Optional[str] = None, - round_id: Optional[int] = None, - ) -> Optional[Dict[str, Any]]: - """ - Retrieve serialized state data for a user/session. - - If round_id is provided, return that round's state. - If round_id is None, return the latest round's state. - - Args: - user_id: The unique ID of the user. - session_id: Optional session/conversation ID. - round_id: Optional round number. - - Returns: - A dictionary representing the agent state, or None if not found. - """ - - -class InMemoryStateService(StateService): - """ - In-memory implementation of StateService using dictionaries - for sparse round storage. - - - Multiple users, sessions, and non-contiguous round IDs are supported. - - If round_id is None when saving, a new round is appended automatically. - - If round_id is None when exporting, the latest round is returned. - """ - - _DEFAULT_SESSION_ID = "default" - - def __init__(self) -> None: - # Structure: - # { user_id: { session_id: { round_id: state_dict } } } - self._store: Optional[ - Dict[str, Dict[str, Dict[int, Dict[str, Any]]]] - ] = None - self._health = False - - async def start(self) -> None: - """Initialize the in-memory store.""" - if self._store is None: - self._store = {} - self._health = True - - async def stop(self) -> None: - """Clear all in-memory state data.""" - if self._store is not None: - self._store.clear() - self._store = None - self._health = False - - async def health(self) -> bool: - """Service health check.""" - return self._health - - async def save_state( - self, - user_id: str, - state: Dict[str, Any], - session_id: Optional[str] = None, - round_id: Optional[int] = None, - ) -> int: - """ - Save serialized state in sparse dict storage. - - If round_id is None, a new round_id will be assigned - as (max existing round_id + 1) or 1 if none exist. - Otherwise, the given round_id will be overwritten. - - Returns: - The round_id where the state was saved. - """ - if self._store is None: - raise RuntimeError("Service not started") - - sid = session_id or self._DEFAULT_SESSION_ID - - self._store.setdefault(user_id, {}) - self._store[user_id].setdefault(sid, {}) - - rounds_dict = self._store[user_id][sid] - - # Auto-generate round_id if not provided - if round_id is None: - if rounds_dict: - round_id = max(rounds_dict.keys()) + 1 - else: - round_id = 1 - - # Store a deep copy so caller modifications don't affect saved state - rounds_dict[round_id] = copy.deepcopy(state) - - return round_id - - async def export_state( - self, - user_id: str, - session_id: Optional[str] = None, - round_id: Optional[int] = None, - ) -> Optional[Dict[str, Any]]: - """ - Retrieve state data for given user/session/round. - - If round_id is None: return the latest round. - If round_id is provided: return that round's state. - - Returns: - Dictionary representing the agent state, or None if not found. - """ - if self._store is None: - raise RuntimeError("Service not started") - - sid = session_id or self._DEFAULT_SESSION_ID - sessions = self._store.get(user_id, {}) - rounds_dict = sessions.get(sid, {}) - - if not rounds_dict: - return None - - if round_id is None: - # Get the latest round_id - latest_round_id = max(rounds_dict.keys()) - return rounds_dict[latest_round_id] - - return rounds_dict.get(round_id) diff --git a/src/agentscope_runtime/engine/services/agent_state/state_service_factory.py b/src/agentscope_runtime/engine/services/agent_state/state_service_factory.py deleted file mode 100644 index 4f9b755e7..000000000 --- a/src/agentscope_runtime/engine/services/agent_state/state_service_factory.py +++ /dev/null @@ -1,52 +0,0 @@ -# -*- coding: utf-8 -*- - -from typing import Callable, Dict - -from ..service_factory import ServiceFactory -from .state_service import StateService, InMemoryStateService -from .redis_state_service import RedisStateService - - -class StateServiceFactory(ServiceFactory[StateService]): - """ - Factory for StateService, supports both environment variables and kwargs - parameters. - - Usage examples: - 1. Start with environment variables only: - export STATE_BACKEND=redis - export STATE_REDIS_REDIS_URL="redis://localhost:6379/5" - service = await StateServiceFactory.create() - - 2. Override environment variables with arguments: - export STATE_BACKEND=redis - export STATE_REDIS_REDIS_URL="redis://localhost:6379/5" - service = await StateServiceFactory.create( - redis_url="redis://otherhost:6379/1" - ) - - 3. User-defined backend: - from my_backend import PostgresStateService - StateServiceFactory.register_backend( - "postgres", - PostgresStateService, - ) - export STATE_BACKEND=postgres - export STATE_POSTGRES_DSN="postgresql://user:pass@localhost/db" - service = await StateServiceFactory.create() - """ - - _registry: Dict[str, Callable[..., StateService]] = {} - _env_prefix = "STATE_" - _default_backend = "in_memory" - - -StateServiceFactory.register_backend( - "in_memory", - InMemoryStateService, -) - -StateServiceFactory.register_backend( - "redis", - RedisStateService, -) diff --git a/src/agentscope_runtime/sandbox/box/gui/gui_sandbox.py b/src/agentscope_runtime/sandbox/box/gui/gui_sandbox.py index cc3dd7334..110781950 100644 --- a/src/agentscope_runtime/sandbox/box/gui/gui_sandbox.py +++ b/src/agentscope_runtime/sandbox/box/gui/gui_sandbox.py @@ -87,7 +87,7 @@ def __init__( # pylint: disable=useless-parent-delegation sandbox_type, workspace_dir, ) - if get_platform() == "linux/arm64": + if "arm" in get_platform(): logger.warning( "\nCompatibility Notice: This GUI Sandbox may have issues on " "arm64 CPU architectures, due to the computer-use-mcp does " diff --git a/src/agentscope_runtime/sandbox/manager/sandbox_manager.py b/src/agentscope_runtime/sandbox/manager/sandbox_manager.py index 29e196b78..aec22a2b0 100644 --- a/src/agentscope_runtime/sandbox/manager/sandbox_manager.py +++ b/src/agentscope_runtime/sandbox/manager/sandbox_manager.py @@ -842,7 +842,7 @@ def create( volumes=volume_bindings, environment={ "SECRET_TOKEN": runtime_token, - "NGINX_TIMEOUT": TIMEOUT, + "NGINX_TIMEOUT": str(TIMEOUT) if TIMEOUT else "60", **environment, }, runtime_config=config.runtime_config, diff --git a/src/agentscope_runtime/version.py b/src/agentscope_runtime/version.py index a5d36c1e4..2f6b226ff 100644 --- a/src/agentscope_runtime/version.py +++ b/src/agentscope_runtime/version.py @@ -1,2 +1,2 @@ # -*- coding: utf-8 -*- -__version__ = "v1.1.0a1" +__version__ = "v1.1.0b2" diff --git a/tests/deploy/assets/agent_for_test.py b/tests/deploy/assets/agent_for_test.py index 48fb7c3b0..230f56a11 100644 --- a/tests/deploy/assets/agent_for_test.py +++ b/tests/deploy/assets/agent_for_test.py @@ -9,16 +9,16 @@ from agentscope.pipeline import stream_printing_messages from agentscope.tool import Toolkit from agentscope.memory import InMemoryMemory +from agentscope.session import RedisSession +from agentscope.tool import ToolResponse +from agentscope.message import TextBlock from agentscope_runtime.engine.app import AgentApp from agentscope_runtime.engine.schemas.agent_schemas import AgentRequest -from agentscope_runtime.engine.services.agent_state import ( - InMemoryStateService, -) from others.other_project import version -def weather_search(query: str) -> str: +def weather_search(query: str) -> ToolResponse: """Search for weather information based on location query. Args: @@ -32,7 +32,7 @@ def weather_search(query: str) -> str: else: result = "It's 90 degrees and sunny." - return result + return ToolResponse(content=[TextBlock(type="text", text=result)]) # Create AgentApp @@ -44,13 +44,13 @@ def weather_search(query: str) -> str: @agent_app.init async def init_func(self): - self.state_service = InMemoryStateService() - await self.state_service.start() - + import fakeredis -@agent_app.shutdown -async def shutdown_func(self): - await self.state_service.stop() + fake_redis = fakeredis.aioredis.FakeRedis(decode_responses=True) + # NOTE: This FakeRedis instance is for development/testing only. + # In production, replace it with your own Redis client/connection + # (e.g., aioredis.Redis) + self.session = RedisSession(connection_pool=fake_redis.connection_pool) @agent_app.query(framework="agentscope") @@ -63,11 +63,6 @@ async def query_func( session_id = request.session_id user_id = request.user_id - state = await self.state_service.export_state( - session_id=session_id, - user_id=user_id, - ) - toolkit = Toolkit() toolkit.register_tool_function(weather_search) @@ -84,8 +79,11 @@ async def query_func( formatter=DashScopeChatFormatter(), ) - if state: - agent.load_state_dict(state) + await self.session.load_session_state( + session_id=session_id, + user_id=user_id, + agent=agent, + ) async for msg, last in stream_printing_messages( agents=[agent], @@ -93,12 +91,10 @@ async def query_func( ): yield msg, last - state = agent.state_dict() - - await self.state_service.save_state( - user_id=user_id, + await self.session.save_session_state( session_id=session_id, - state=state, + user_id=user_id, + agent=agent, ) diff --git a/tests/deploy/test_local_deployer_a2a.py b/tests/deploy/test_local_deployer_a2a.py index e0ddc6193..c42dffb32 100644 --- a/tests/deploy/test_local_deployer_a2a.py +++ b/tests/deploy/test_local_deployer_a2a.py @@ -10,6 +10,7 @@ from agentscope.model import DashScopeChatModel from agentscope.pipeline import stream_printing_messages from agentscope.memory import InMemoryMemory +from agentscope.session import RedisSession from agentscope_runtime.engine.app import AgentApp from agentscope_runtime.engine.deployers.adapter.a2a import ( @@ -19,9 +20,6 @@ LocalDeployManager, ) from agentscope_runtime.engine.schemas.agent_schemas import AgentRequest -from agentscope_runtime.engine.services.agent_state import ( - InMemoryStateService, -) def local_deploy(): @@ -46,12 +44,13 @@ async def _local_deploy(): # Initialize services @agent_app.init async def init_func(self): - self.state_service = InMemoryStateService() - await self.state_service.start() + import fakeredis - @agent_app.shutdown - async def shutdown_func(self): - await self.state_service.stop() + fake_redis = fakeredis.aioredis.FakeRedis(decode_responses=True) + # NOTE: This FakeRedis instance is for development/testing only. + # In production, replace it with your own Redis client/connection + # (e.g., aioredis.Redis) + self.session = RedisSession(connection_pool=fake_redis.connection_pool) # Define query handler @agent_app.query(framework="agentscope") @@ -64,11 +63,6 @@ async def query_func( session_id = request.session_id user_id = request.user_id - state = await self.state_service.export_state( - session_id=session_id, - user_id=user_id, - ) - agent = ReActAgent( name="Friday", model=DashScopeChatModel( @@ -81,8 +75,11 @@ async def query_func( formatter=DashScopeChatFormatter(), ) - if state: - agent.load_state_dict(state) + await self.session.load_session_state( + session_id=session_id, + user_id=user_id, + agent=agent, + ) async for msg, last in stream_printing_messages( agents=[agent], @@ -90,12 +87,10 @@ async def query_func( ): yield msg, last - state = agent.state_dict() - - await self.state_service.save_state( - user_id=user_id, + await self.session.save_session_state( session_id=session_id, - state=state, + user_id=user_id, + agent=agent, ) # Create A2A protocol adapter diff --git a/tests/deploy/test_local_deployer_context.py b/tests/deploy/test_local_deployer_context.py index be4b2e5da..0c6d138df 100644 --- a/tests/deploy/test_local_deployer_context.py +++ b/tests/deploy/test_local_deployer_context.py @@ -11,15 +11,13 @@ from agentscope.model import DashScopeChatModel from agentscope.pipeline import stream_printing_messages from agentscope.memory import InMemoryMemory +from agentscope.session import RedisSession from agentscope_runtime.engine.app import AgentApp from agentscope_runtime.engine.deployers.local_deployer import ( LocalDeployManager, ) from agentscope_runtime.engine.schemas.agent_schemas import AgentRequest -from agentscope_runtime.engine.services.agent_state import ( - InMemoryStateService, -) def parse_sse_line(line): @@ -94,12 +92,13 @@ async def test_local_deployer_context(): # Initialize services @agent_app.init async def init_func(self): - self.state_service = InMemoryStateService() - await self.state_service.start() + import fakeredis - @agent_app.shutdown - async def shutdown_func(self): - await self.state_service.stop() + fake_redis = fakeredis.aioredis.FakeRedis(decode_responses=True) + # NOTE: This FakeRedis instance is for development/testing only. + # In production, replace it with your own Redis client/connection + # (e.g., aioredis.Redis) + self.session = RedisSession(connection_pool=fake_redis.connection_pool) # Define query handler @agent_app.query(framework="agentscope") @@ -112,11 +111,6 @@ async def query_func( session_id = request.session_id user_id = request.user_id - state = await self.state_service.export_state( - session_id=session_id, - user_id=user_id, - ) - agent = ReActAgent( name="Friday", model=DashScopeChatModel( @@ -129,8 +123,11 @@ async def query_func( formatter=DashScopeChatFormatter(), ) - if state: - agent.load_state_dict(state) + await self.session.load_session_state( + session_id=session_id, + user_id=user_id, + agent=agent, + ) async for msg, last in stream_printing_messages( agents=[agent], @@ -138,12 +135,10 @@ async def query_func( ): yield msg, last - state = agent.state_dict() - - await self.state_service.save_state( - user_id=user_id, + await self.session.save_session_state( session_id=session_id, - state=state, + user_id=user_id, + agent=agent, ) deploy_manager = LocalDeployManager(host=server_host, port=server_port) diff --git a/tests/deploy/test_local_deployer_responses_api.py b/tests/deploy/test_local_deployer_responses_api.py index 4407956e9..3d52452d7 100644 --- a/tests/deploy/test_local_deployer_responses_api.py +++ b/tests/deploy/test_local_deployer_responses_api.py @@ -9,6 +9,7 @@ from agentscope.model import DashScopeChatModel from agentscope.pipeline import stream_printing_messages from agentscope.memory import InMemoryMemory +from agentscope.session import RedisSession from agentscope_runtime.engine.app import AgentApp from agentscope_runtime.engine.deployers.adapter.responses.response_api_protocol_adapter import ( # noqa: E501 @@ -18,9 +19,6 @@ LocalDeployManager, ) from agentscope_runtime.engine.schemas.agent_schemas import AgentRequest -from agentscope_runtime.engine.services.agent_state import ( - InMemoryStateService, -) def local_deploy(): @@ -44,12 +42,13 @@ async def _local_deploy(): # Initialize services @agent_app.init async def init_func(self): - self.state_service = InMemoryStateService() - await self.state_service.start() + import fakeredis - @agent_app.shutdown - async def shutdown_func(self): - await self.state_service.stop() + fake_redis = fakeredis.aioredis.FakeRedis(decode_responses=True) + # NOTE: This FakeRedis instance is for development/testing only. + # In production, replace it with your own Redis client/connection + # (e.g., aioredis.Redis) + self.session = RedisSession(connection_pool=fake_redis.connection_pool) # Define query handler @agent_app.query(framework="agentscope") @@ -62,11 +61,6 @@ async def query_func( session_id = request.session_id user_id = request.user_id - state = await self.state_service.export_state( - session_id=session_id, - user_id=user_id, - ) - agent = ReActAgent( name="Friday", model=DashScopeChatModel( @@ -79,8 +73,11 @@ async def query_func( formatter=DashScopeChatFormatter(), ) - if state: - agent.load_state_dict(state) + await self.session.load_session_state( + session_id=session_id, + user_id=user_id, + agent=agent, + ) async for msg, last in stream_printing_messages( agents=[agent], @@ -88,12 +85,10 @@ async def query_func( ): yield msg, last - state = agent.state_dict() - - await self.state_service.save_state( - user_id=user_id, + await self.session.save_session_state( session_id=session_id, - state=state, + user_id=user_id, + agent=agent, ) # Create responses adapter diff --git a/tests/integrated/test_agent_app.py b/tests/integrated/test_agent_app.py index bcd4af312..5db00e125 100644 --- a/tests/integrated/test_agent_app.py +++ b/tests/integrated/test_agent_app.py @@ -14,12 +14,10 @@ from agentscope.tool import Toolkit, execute_python_code from agentscope.pipeline import stream_printing_messages from agentscope.memory import InMemoryMemory +from agentscope.session import RedisSession from agentscope_runtime.engine import AgentApp from agentscope_runtime.engine.schemas.agent_schemas import AgentRequest -from agentscope_runtime.engine.services.agent_state import ( - InMemoryStateService, -) PORT = 8090 @@ -33,13 +31,13 @@ def run_app(): @agent_app.init async def init_func(self): - self.state_service = InMemoryStateService() + import fakeredis - await self.state_service.start() - - @agent_app.shutdown - async def shutdown_func(self): - await self.state_service.stop() + fake_redis = fakeredis.aioredis.FakeRedis(decode_responses=True) + # NOTE: This FakeRedis instance is for development/testing only. + # In production, replace it with your own Redis client/connection + # (e.g., aioredis.Redis) + self.session = RedisSession(connection_pool=fake_redis.connection_pool) @agent_app.query(framework="agentscope") async def query_func( @@ -51,11 +49,6 @@ async def query_func( session_id = request.session_id user_id = request.user_id - state = await self.state_service.export_state( - session_id=session_id, - user_id=user_id, - ) - toolkit = Toolkit() toolkit.register_tool_function(execute_python_code) @@ -74,8 +67,11 @@ async def query_func( ) agent.set_console_output_enabled(enabled=False) - if state: - agent.load_state_dict(state) + await self.session.load_session_state( + session_id=session_id, + user_id=user_id, + agent=agent, + ) async for msg, last in stream_printing_messages( agents=[agent], @@ -83,12 +79,10 @@ async def query_func( ): yield msg, last - state = agent.state_dict() - - await self.state_service.save_state( - user_id=user_id, + await self.session.save_session_state( session_id=session_id, - state=state, + user_id=user_id, + agent=agent, ) agent_app.run(host="127.0.0.1", port=PORT) diff --git a/tests/integrated/test_agui_integration.py b/tests/integrated/test_agui_integration.py index 3dbe53350..698f22c35 100644 --- a/tests/integrated/test_agui_integration.py +++ b/tests/integrated/test_agui_integration.py @@ -26,6 +26,7 @@ from agentscope.tool import ToolResponse, Toolkit from agentscope.pipeline import stream_printing_messages from agentscope.memory import InMemoryMemory +from agentscope.session import RedisSession from langchain_core.messages import BaseMessage from langchain.agents import AgentState, create_agent @@ -38,9 +39,6 @@ from agentscope_runtime.engine import AgentApp from agentscope_runtime.engine.runner import Runner from agentscope_runtime.engine.schemas.agent_schemas import AgentRequest -from agentscope_runtime.engine.services.agent_state import ( - InMemoryStateService, -) AGENTSCOPE_APP_PORT = 8091 LANGGRAPH_APP_PORT = 8092 @@ -73,8 +71,15 @@ async def get_weather(location: str) -> ToolResponse: @agent_app.init async def init_func(runner: Runner): - runner.state_service = InMemoryStateService() - await runner.state_service.start() + import fakeredis + + fake_redis = fakeredis.aioredis.FakeRedis(decode_responses=True) + # NOTE: This FakeRedis instance is for development/testing only. + # In production, replace it with your own Redis client/connection + # (e.g., aioredis.Redis) + runner.session = RedisSession( + connection_pool=fake_redis.connection_pool, + ) @agent_app.query(framework="agentscope") async def query_func( @@ -86,11 +91,6 @@ async def query_func( session_id = request.session_id user_id = request.user_id - state = await runner.state_service.export_state( - session_id=session_id, - user_id=user_id, - ) - toolkit = Toolkit() toolkit.register_tool_function(get_weather) @@ -109,8 +109,11 @@ async def query_func( ) agent.set_console_output_enabled(enabled=False) - if state: - agent.load_state_dict(state) + await runner.session.load_session_state( + session_id=session_id, + user_id=user_id, + agent=agent, + ) async for msg, last in stream_printing_messages( agents=[agent], @@ -118,12 +121,10 @@ async def query_func( ): yield msg, last - state = agent.state_dict() - - await runner.state_service.save_state( - user_id=user_id, + await runner.session.save_session_state( session_id=session_id, - state=state, + user_id=user_id, + agent=agent, ) agent_app.run(host="127.0.0.1", port=AGENTSCOPE_APP_PORT) diff --git a/tests/integrated/test_runner_stream_agentscope.py b/tests/integrated/test_runner_stream_agentscope.py index 12a2012c2..c8de66bfe 100644 --- a/tests/integrated/test_runner_stream_agentscope.py +++ b/tests/integrated/test_runner_stream_agentscope.py @@ -1,4 +1,5 @@ # -*- coding: utf-8 -*- +# pylint: disable=unused-argument import os import pytest @@ -9,15 +10,14 @@ from agentscope.tool import Toolkit from agentscope.pipeline import stream_printing_messages from agentscope.memory import InMemoryMemory +from agentscope.session import RedisSession + from agentscope_runtime.engine.schemas.agent_schemas import ( AgentRequest, MessageType, RunStatus, ) from agentscope_runtime.engine.runner import Runner -from agentscope_runtime.engine.services.agent_state import ( - InMemoryStateService, -) from agentscope_runtime.adapters.agentscope.tool import sandbox_tool_adapter from agentscope_runtime.engine.services.sandbox import SandboxService @@ -39,11 +39,6 @@ async def query_handler( session_id = request.session_id user_id = request.user_id - state = await self.state_service.export_state( - session_id=session_id, - user_id=user_id, - ) - # Get sandbox sandboxes = self.sandbox_service.connect( session_id=session_id, @@ -79,34 +74,42 @@ async def query_handler( ) agent.set_console_output_enabled(enabled=False) - if state: - agent.load_state_dict(state) + await self.session.load_session_state( + session_id=session_id, + user_id=user_id, + agent=agent, + ) + async for msg, last in stream_printing_messages( agents=[agent], coroutine_task=agent(msgs), ): yield msg, last - state = agent.state_dict() - await self.state_service.save_state( - user_id=user_id, + + await self.session.save_session_state( session_id=session_id, - state=state, + user_id=user_id, + agent=agent, ) async def init_handler(self, *args, **kwargs): """ Init handler. """ - self.state_service = InMemoryStateService() - self.sandbox_service = SandboxService() - await self.state_service.start() + import fakeredis + + fake_redis = fakeredis.aioredis.FakeRedis(decode_responses=True) + # NOTE: This FakeRedis instance is for development/testing only. + # In production, replace it with your own Redis client/connection + # (e.g., aioredis.Redis) + self.session = RedisSession(connection_pool=fake_redis.connection_pool) + self.sandbox_service = SandboxService(drain_on_stop=True) await self.sandbox_service.start() async def shutdown_handler(self, *args, **kwargs): """ Shutdown handler. """ - await self.state_service.stop() await self.sandbox_service.stop() diff --git a/tests/integrated/test_runner_stream_agentscope_thinking.py b/tests/integrated/test_runner_stream_agentscope_thinking.py index 06d7ffd82..e46b670ce 100644 --- a/tests/integrated/test_runner_stream_agentscope_thinking.py +++ b/tests/integrated/test_runner_stream_agentscope_thinking.py @@ -1,4 +1,5 @@ # -*- coding: utf-8 -*- +# pylint: disable=unused-argument import os import pytest @@ -9,15 +10,14 @@ from agentscope.tool import Toolkit from agentscope.pipeline import stream_printing_messages from agentscope.memory import InMemoryMemory +from agentscope.session import RedisSession + from agentscope_runtime.engine.schemas.agent_schemas import ( AgentRequest, MessageType, RunStatus, ) from agentscope_runtime.engine.runner import Runner -from agentscope_runtime.engine.services.agent_state import ( - InMemoryStateService, -) from agentscope_runtime.adapters.agentscope.tool import sandbox_tool_adapter from agentscope_runtime.engine.services.sandbox import SandboxService @@ -39,11 +39,6 @@ async def query_handler( session_id = request.session_id user_id = request.user_id - state = await self.state_service.export_state( - session_id=session_id, - user_id=user_id, - ) - # Get sandbox sandboxes = self.sandbox_service.connect( session_id=session_id, @@ -80,34 +75,43 @@ async def query_handler( ) agent.set_console_output_enabled(enabled=False) - if state: - agent.load_state_dict(state) + await self.session.load_session_state( + session_id=session_id, + user_id=user_id, + agent=agent, + ) + async for msg, last in stream_printing_messages( agents=[agent], coroutine_task=agent(msgs), ): yield msg, last - state = agent.state_dict() - await self.state_service.save_state( - user_id=user_id, + + await self.session.save_session_state( session_id=session_id, - state=state, + user_id=user_id, + agent=agent, ) async def init_handler(self, *args, **kwargs): """ Init handler. """ - self.state_service = InMemoryStateService() - self.sandbox_service = SandboxService() - await self.state_service.start() + import fakeredis + + fake_redis = fakeredis.aioredis.FakeRedis(decode_responses=True) + # NOTE: This FakeRedis instance is for development/testing only. + # In production, replace it with your own Redis client/connection + # (e.g., aioredis.Redis) + self.session = RedisSession(connection_pool=fake_redis.connection_pool) + + self.sandbox_service = SandboxService(drain_on_stop=True) await self.sandbox_service.start() async def shutdown_handler(self, *args, **kwargs): """ Shutdown handler. """ - await self.state_service.stop() await self.sandbox_service.stop() diff --git a/tests/integrated/test_runner_stream_ms.py b/tests/integrated/test_runner_stream_ms.py index 1656a96fa..bcadb1186 100644 --- a/tests/integrated/test_runner_stream_ms.py +++ b/tests/integrated/test_runner_stream_ms.py @@ -12,14 +12,12 @@ ) from agentscope_runtime.engine.runner import Runner from agentscope_runtime.engine.services.sandbox import SandboxService -from agentscope_runtime.engine.services.agent_state import InMemoryStateService class MyRunner(Runner): def __init__(self) -> None: super().__init__() self.framework_type = "ms_agent_framework" - self.state_service = InMemoryStateService() async def query_handler( self, @@ -32,11 +30,9 @@ async def query_handler( """ session_id = request.session_id user_id = request.user_id + id_key = f"{user_id}_{session_id}" - thread = await self.state_service.export_state( - session_id=session_id, - user_id=user_id, - ) + thread = self.thread_storage.get(id_key) # Get sandbox sandboxes = self.sandbox_service.connect( @@ -77,26 +73,21 @@ async def query_handler( yield event serialized_thread = await thread.serialize() - await self.state_service.save_state( - user_id=user_id, - session_id=session_id, - state=serialized_thread, - ) + self.thread_storage[id_key] = serialized_thread async def init_handler(self, *args, **kwargs): """ Init handler. """ + self.thread_storage = {} # Only for testing self.sandbox_service = SandboxService() await self.sandbox_service.start() - await self.state_service.start() async def shutdown_handler(self, *args, **kwargs): """ Shutdown handler. """ await self.sandbox_service.stop() - await self.state_service.stop() @pytest.mark.asyncio(loop_scope="session") diff --git a/tests/test_data/agentscope_agent/agent.py b/tests/test_data/agentscope_agent/agent.py index 8d3b66e73..982c4711c 100644 --- a/tests/test_data/agentscope_agent/agent.py +++ b/tests/test_data/agentscope_agent/agent.py @@ -8,6 +8,7 @@ from agentscope.model import OpenAIChatModel from agentscope.pipeline import stream_printing_messages from agentscope.tool import ToolResponse, Toolkit, execute_python_code +from agentscope.session import RedisSession from agentscope_runtime.engine import AgentApp from agentscope_runtime.engine.runner import Runner @@ -15,7 +16,6 @@ AgentRequest, Message, ) -from agentscope_runtime.engine.services.agent_state import InMemoryStateService agent_app = AgentApp( app_name="Friday", @@ -43,13 +43,13 @@ async def get_weather(location: str) -> ToolResponse: @agent_app.init async def init_func(runner: Runner): - runner.state_service = InMemoryStateService() - await runner.state_service.start() + import fakeredis - -@agent_app.shutdown -async def shutdown_func(runner: Runner): - await runner.state_service.stop() + fake_redis = fakeredis.aioredis.FakeRedis(decode_responses=True) + # NOTE: This FakeRedis instance is for development/testing only. + # In production, replace it with your own Redis client/connection + # (e.g., aioredis.Redis) + runner.session = RedisSession(connection_pool=fake_redis.connection_pool) async def get_unseen_messages( @@ -83,21 +83,29 @@ async def get_unseen_messages( ] -def create_stateful_agent( - state: Optional[dict] = None, -) -> ReActAgent: +@agent_app.query(framework="agentscope") +async def query_func( + runner: Runner, + msgs: List[Msg], + request: AgentRequest = None, + **kwargs, # pylint: disable=unused-argument +) -> AsyncIterator[tuple[Msg, bool]]: """ - Create a stateful agent with the given session service, session id, user - id, and state. + Main entry point for agent execution. Args: - state (Optional[dict]): State to load into the agent + runner: Runner instance + msgs: List of messages to process + request: AgentRequest instance + **kwargs: Additional keyword arguments Returns: - tuple[dict, Toolkit]: Tuple containing the state and toolkit - + Iterator[tuple[Msg, bool]]: Iterator of messages and last flag """ + session_id = request.session_id + user_id = request.user_id + toolkit = Toolkit() toolkit.register_tool_function(execute_python_code) toolkit.register_tool_function(get_weather) @@ -119,44 +127,10 @@ def create_stateful_agent( ) agent.set_console_output_enabled(enabled=False) - if state: - agent.load_state_dict(state) - - return agent - - -@agent_app.query(framework="agentscope") -async def query_func( - runner: Runner, - msgs: List[Msg], - request: AgentRequest = None, - **kwargs, # pylint: disable=unused-argument -) -> AsyncIterator[tuple[Msg, bool]]: - """ - Main entry point for agent execution. - - Args: - runner: Runner instance - msgs: List of messages to process - request: AgentRequest instance - **kwargs: Additional keyword arguments - - Returns: - Iterator[tuple[Msg, bool]]: Iterator of messages and last flag - """ - - session_id = request.session_id - user_id = request.user_id - - # If state is provided in the request via AG-UI, use it directly. - state = getattr(request, "state", None) - if not state: - state = await runner.state_service.export_state( - session_id=session_id, - user_id=user_id, - ) - agent = create_stateful_agent( - state=state, + await runner.session.load_session_state( + session_id=session_id, + user_id=user_id, + agent=agent, ) unseen_messages = await get_unseen_messages( @@ -172,10 +146,8 @@ async def query_func( ): yield msg, last - state = agent.state_dict() - - await runner.state_service.save_state( - user_id=user_id, + await runner.session.save_session_state( session_id=session_id, - state=state, + user_id=user_id, + agent=agent, ) diff --git a/tests/unit/test_redis_state_service.py b/tests/unit/test_redis_state_service.py deleted file mode 100644 index b69b2b5e4..000000000 --- a/tests/unit/test_redis_state_service.py +++ /dev/null @@ -1,341 +0,0 @@ -# -*- coding: utf-8 -*- -# pylint: disable=redefined-outer-name, protected-access -import asyncio -import pytest -import pytest_asyncio -import fakeredis.aioredis -from agentscope_runtime.engine.services.agent_state import ( - RedisStateService, -) - - -@pytest_asyncio.fixture -async def state_service() -> RedisStateService: - """Provides an instance of RedisStateService for testing.""" - fake_redis = fakeredis.aioredis.FakeRedis(decode_responses=True) - # Set ttl_seconds=None to maintain the original test behavior (no TTL) - service = RedisStateService(redis_client=fake_redis, ttl_seconds=None) - await service.start() - # check redis - healthy = await service.health() - if not healthy: - raise RuntimeError( - "Redis is unavailable(default:localhost:6379)", - ) - try: - yield service - finally: - await service.stop() - - -@pytest.fixture -def user_id() -> str: - """Provides a sample user ID.""" - return "test_user" - - -@pytest.mark.asyncio -async def test_service_lifecycle(): - """Test service lifecycle (start, health, stop).""" - fake_redis = fakeredis.aioredis.FakeRedis(decode_responses=True) - service = RedisStateService(redis_client=fake_redis, ttl_seconds=None) - await service.start() - assert await service.health() is True - await service.stop() - assert await service.health() is False - - -@pytest.mark.asyncio -async def test_save_and_export_state( - state_service: RedisStateService, - user_id: str, -) -> None: - """Tests saving and exporting state.""" - state = {"key": "value", "number": 42} - round_id = await state_service.save_state(user_id, state) - assert round_id == 1 - - exported = await state_service.export_state(user_id) - assert exported is not None - assert exported == state - - -@pytest.mark.asyncio -async def test_save_state_with_session_id( - state_service: RedisStateService, - user_id: str, -) -> None: - """Tests saving state with a specific session ID.""" - session_id = "session1" - state = {"session": "specific"} - round_id = await state_service.save_state( - user_id, - state, - session_id=session_id, - ) - assert round_id == 1 - - exported = await state_service.export_state( - user_id, - session_id=session_id, - ) - assert exported is not None - assert exported == state - - -@pytest.mark.asyncio -async def test_save_state_with_round_id( - state_service: RedisStateService, - user_id: str, -) -> None: - """Tests saving state with a specific round ID.""" - state1 = {"round": 1} - state2 = {"round": 2} - - round_id1 = await state_service.save_state(user_id, state1, round_id=10) - assert round_id1 == 10 - - round_id2 = await state_service.save_state(user_id, state2, round_id=20) - assert round_id2 == 20 - - exported1 = await state_service.export_state(user_id, round_id=10) - assert exported1 == state1 - - exported2 = await state_service.export_state(user_id, round_id=20) - assert exported2 == state2 - - -@pytest.mark.asyncio -async def test_export_state_returns_latest( - state_service: RedisStateService, - user_id: str, -) -> None: - """Tests that export_state returns the latest round when round_id is - None.""" - state1 = {"round": 1} - state2 = {"round": 2} - state3 = {"round": 3} - - await state_service.save_state(user_id, state1) - await state_service.save_state(user_id, state2) - await state_service.save_state(user_id, state3) - - exported = await state_service.export_state(user_id) - assert exported is not None - assert exported == state3 - - -@pytest.mark.asyncio -async def test_export_state_nonexistent( - state_service: RedisStateService, - user_id: str, -) -> None: - """Tests exporting state that doesn't exist.""" - exported = await state_service.export_state(user_id) - assert exported is None - - exported_with_round = await state_service.export_state( - user_id, - round_id=999, - ) - assert exported_with_round is None - - -@pytest.mark.asyncio -async def test_multiple_rounds_auto_increment( - state_service: RedisStateService, - user_id: str, -) -> None: - """Tests that round IDs auto-increment when not specified.""" - state1 = {"round": 1} - state2 = {"round": 2} - state3 = {"round": 3} - - round_id1 = await state_service.save_state(user_id, state1) - round_id2 = await state_service.save_state(user_id, state2) - round_id3 = await state_service.save_state(user_id, state3) - - assert round_id1 == 1 - assert round_id2 == 2 - assert round_id3 == 3 - - exported1 = await state_service.export_state(user_id, round_id=1) - exported2 = await state_service.export_state(user_id, round_id=2) - exported3 = await state_service.export_state(user_id, round_id=3) - - assert exported1 == state1 - assert exported2 == state2 - assert exported3 == state3 - - -@pytest.mark.asyncio -async def test_ttl_expiration(): - """Test that state data expires after TTL.""" - fake_redis = fakeredis.aioredis.FakeRedis(decode_responses=True) - # Use a short TTL for quick test completion - service = RedisStateService( - redis_client=fake_redis, - ttl_seconds=1, # 1 second TTL - ) - await service.start() - - try: - user_id = "ttl_test_user" - state = {"key": "value"} - - # Save state - await service.save_state(user_id, state) - key = service._session_key(user_id, "default") - - # Immediately verify state exists - exported = await service.export_state(user_id) - assert exported is not None - assert exported == state - - # Verify key exists and has TTL - ttl = await fake_redis.ttl(key) - assert ttl > 0, "Key should have a TTL set" - assert ttl <= 1, "TTL should be 1 second or less" - - # Wait for TTL to expire (wait 1.5 seconds to ensure expiration) - await asyncio.sleep(1.5) - - # Verify data has been deleted (key does not exist or has expired) - key_exists = await fake_redis.exists(key) - assert key_exists == 0, "Key should be expired and deleted" - - # Verify export_state returns None after expiry - exported_after_expiry = await service.export_state(user_id) - assert ( - exported_after_expiry is None - ), "State should return None after expiry" - - finally: - await service.stop() - - -@pytest.mark.asyncio -async def test_ttl_refresh_on_write(): - """Test that TTL is refreshed when new state is written.""" - fake_redis = fakeredis.aioredis.FakeRedis(decode_responses=True) - service = RedisStateService( - redis_client=fake_redis, - ttl_seconds=2, # 2 seconds TTL - ) - await service.start() - - try: - user_id = "ttl_refresh_user" - key = service._session_key(user_id, "default") - - # First write - state1 = {"round": 1} - await service.save_state(user_id, state1) - - # Check TTL - ttl1 = await fake_redis.ttl(key) - assert 0 < ttl1 <= 2 - - # Wait 1 second (TTL should decrease) - await asyncio.sleep(1.1) - - # Second write (should refresh TTL) - state2 = {"round": 2} - await service.save_state(user_id, state2) - - # Verify TTL is refreshed (should be close to 2 seconds) - ttl2 = await fake_redis.ttl(key) - assert 0 < ttl2 <= 2 - # TTL should be refreshed, so it should be longer than the - # remaining time after waiting - # Since we waited 1.1 seconds, if TTL was not refreshed, - # remaining time should be < 1 second - # If refreshed, remaining time should be close to 2 seconds - assert ttl2 > 1.5, "TTL should be refreshed to close to 2 seconds" - - # Verify both states exist - exported1 = await service.export_state(user_id, round_id=1) - exported2 = await service.export_state(user_id, round_id=2) - assert exported1 == state1, "First state should exist" - assert exported2 == state2, "Second state should exist" - - finally: - await service.stop() - - -@pytest.mark.asyncio -async def test_ttl_refresh_on_read(): - """Test that TTL is refreshed when state is accessed.""" - fake_redis = fakeredis.aioredis.FakeRedis(decode_responses=True) - service = RedisStateService( - redis_client=fake_redis, - ttl_seconds=2, # 2 seconds TTL - ) - await service.start() - - try: - user_id = "ttl_refresh_read_user" - key = service._session_key(user_id, "default") - - # Save state - state = {"key": "value"} - await service.save_state(user_id, state) - - # Check initial TTL - ttl1 = await fake_redis.ttl(key) - assert 0 < ttl1 <= 2 - - # Wait 1 second (TTL should decrease) - await asyncio.sleep(1.1) - - # Export state (should refresh TTL) - exported = await service.export_state(user_id) - - # Verify TTL is refreshed - ttl2 = await fake_redis.ttl(key) - assert 0 < ttl2 <= 2 - assert ttl2 > 1.5, "TTL should be refreshed to close to 2 seconds" - - assert exported is not None - assert exported == state - - finally: - await service.stop() - - -@pytest.mark.asyncio -async def test_ttl_disabled(): - """Test that when ttl_seconds is None, data never expires.""" - fake_redis = fakeredis.aioredis.FakeRedis(decode_responses=True) - service = RedisStateService( - redis_client=fake_redis, - ttl_seconds=None, # Disable TTL - ) - await service.start() - - try: - user_id = "no_ttl_user" - key = service._session_key(user_id, "default") - - # Save state - state = {"key": "value"} - await service.save_state(user_id, state) - - # Verify key exists and has no TTL - # (TTL returns -1 when no expiration is set) - ttl = await fake_redis.ttl(key) - assert ttl == -1, "Key should not have TTL when ttl_seconds is None" - - # Wait for a while - await asyncio.sleep(0.5) - - # Verify data still exists - key_exists = await fake_redis.exists(key) - assert key_exists == 1, "Key should still exist without TTL" - - exported = await service.export_state(user_id) - assert exported is not None, "State should still be available" - assert exported == state - - finally: - await service.stop() diff --git a/tests/unit/test_state_service_factory.py b/tests/unit/test_state_service_factory.py deleted file mode 100644 index 6c5e5b061..000000000 --- a/tests/unit/test_state_service_factory.py +++ /dev/null @@ -1,175 +0,0 @@ -# -*- coding: utf-8 -*- -# pylint: disable=redefined-outer-name, protected-access -import os -from unittest.mock import AsyncMock, patch - -import pytest - -from agentscope_runtime.engine.services.agent_state import ( - StateServiceFactory, - InMemoryStateService, - RedisStateService, -) - - -class TestStateServiceFactory: - """Unit tests for StateServiceFactory""" - - @pytest.mark.asyncio - async def test_create_default_memory_backend(self): - """Test creating the default memory backend""" - with patch.dict(os.environ, {}, clear=True): - service = await StateServiceFactory.create( - backend_type="in_memory", - ) - assert isinstance(service, InMemoryStateService) - await service.start() - assert await service.health() is True - await service.stop() - - @pytest.mark.asyncio - async def test_create_from_env_backend(self): - """Test creating backend from environment variable""" - with patch.dict( - os.environ, - {"STATE_BACKEND": "in_memory"}, - clear=False, - ): - service = await StateServiceFactory.create() - assert isinstance(service, InMemoryStateService) - - @pytest.mark.asyncio - async def test_create_redis_backend_with_env(self): - """Test creating Redis backend from environment variables""" - with patch.dict( - os.environ, - { - "STATE_BACKEND": "redis", - "STATE_REDIS_REDIS_URL": "redis://localhost:6379/5", - }, - clear=False, - ): - service = await StateServiceFactory.create() - assert isinstance(service, RedisStateService) - assert service._redis_url == "redis://localhost:6379/5" - - @pytest.mark.asyncio - async def test_create_redis_backend_with_kwargs_override(self): - """Test that kwargs override environment variables""" - with patch.dict( - os.environ, - { - "STATE_BACKEND": "redis", - "STATE_REDIS_REDIS_URL": "redis://localhost:6379/5", - }, - clear=False, - ): - service = await StateServiceFactory.create( - redis_url="redis://otherhost:6379/1", - ) - assert isinstance(service, RedisStateService) - assert service._redis_url == "redis://otherhost:6379/1" - - @pytest.mark.asyncio - async def test_create_redis_backend_with_client(self): - """Test using a provided redis_client""" - mock_client = AsyncMock() - service = await StateServiceFactory.create( - backend_type="redis", - redis_client=mock_client, - ) - assert isinstance(service, RedisStateService) - assert service._redis == mock_client - - @pytest.mark.asyncio - async def test_unsupported_backend(self): - """Test unsupported backend type""" - with pytest.raises(ValueError, match="Unsupported backend type"): - await StateServiceFactory.create(backend_type="unknown") - - @pytest.mark.asyncio - async def test_register_custom_backend(self): - """Test registering a custom backend""" - - class CustomStateService(InMemoryStateService): - def __init__(self, custom_param=None): - super().__init__() - self.custom_param = custom_param - - # Register the custom backend - StateServiceFactory.register_backend( - "custom", - CustomStateService, - ) - - try: - # Test creation from environment vars - with patch.dict( - os.environ, - { - "STATE_BACKEND": "custom", - "STATE_CUSTOM_CUSTOM_PARAM": "test_value", - }, - clear=False, - ): - service = await StateServiceFactory.create() - assert isinstance(service, CustomStateService) - assert service.custom_param == "test_value" - - # Test kwargs override - service = await StateServiceFactory.create( - backend_type="custom", - custom_param="override_value", - ) - assert isinstance(service, CustomStateService) - assert service.custom_param == "override_value" - finally: - # Clean up registered backend - if "custom" in StateServiceFactory._registry: - del StateServiceFactory._registry["custom"] - - @pytest.mark.asyncio - async def test_backend_type_case_insensitive(self): - """Test backend type is case insensitive""" - service1 = await StateServiceFactory.create(backend_type="IN_MEMORY") - service2 = await StateServiceFactory.create(backend_type="In_Memory") - service3 = await StateServiceFactory.create(backend_type="in_memory") - - assert isinstance(service1, InMemoryStateService) - assert isinstance(service2, InMemoryStateService) - assert isinstance(service3, InMemoryStateService) - - def test_load_env_kwargs(self): - """Test loading kwargs from environment variables""" - with patch.dict( - os.environ, - { - "STATE_REDIS_REDIS_URL": "redis://localhost:6379/0", - "STATE_REDIS_PASSWORD": "secret", - }, - clear=False, - ): - kwargs = StateServiceFactory._load_env_kwargs("redis") - assert kwargs["redis_url"] == "redis://localhost:6379/0" - assert kwargs["password"] == "secret" - - def test_load_env_kwargs_empty(self): - """ - Test that an empty dictionary is returned when no environment - variables exist - """ - with patch.dict(os.environ, {}, clear=True): - kwargs = StateServiceFactory._load_env_kwargs("redis") - assert not kwargs - - @pytest.mark.asyncio - async def test_create_with_extra_kwargs_filtered(self): - """Test that extra kwargs unrelated to the backend are ignored""" - # Passing redis_url to in_memory should not cause errors - service = await StateServiceFactory.create( - backend_type="in_memory", - redis_url="redis://localhost:6379/0", - # extra param for another backend - some_unused_param="hello", - ) - assert isinstance(service, InMemoryStateService)