Download src/agentcade/game_server.py from left0ver/WatcherRobot-com.left0ver.agentcade: direct link, hf CLI and curl.
- Browser
- Download file 7.43 kB
-
https://huggingface.co/spaces/left0ver/WatcherRobot-com.left0ver.agentcade/resolve/main/src/agentcade/game_server.py
- Command line
-
hf download hf://spaces/left0ver/WatcherRobot-com.left0ver.agentcade/src/agentcade/game_server.py
-
curl -L -o game_server.py https://huggingface.co/spaces/left0ver/WatcherRobot-com.left0ver.agentcade/resolve/main/src/agentcade/game_server.py
7.43 kB
| import asyncio | |
| import logging | |
| import socket | |
| from contextlib import asynccontextmanager, nullcontext | |
| import uvicorn | |
| from a2a.server.agent_execution import AgentExecutor, RequestContext | |
| from a2a.server.events import EventQueue | |
| from a2a.server.request_handlers import DefaultRequestHandler | |
| from a2a.server.routes import create_agent_card_routes, create_jsonrpc_routes | |
| from a2a.server.tasks import TaskUpdater | |
| from a2a.types import ( | |
| AgentCapabilities, | |
| AgentCard, | |
| AgentInterface, | |
| AgentSkill, | |
| Task, | |
| TaskState, | |
| TaskStatus, | |
| ) | |
| from sse_starlette.sse import EventSourceResponse | |
| from starlette.applications import Starlette | |
| from starlette.routing import Route | |
| import observability | |
| from agentcade.protocol import PROTOCOL_VERSION, GameInput, GameReply, from_parts, to_part | |
| from agentcade.task_store import GameTaskStore | |
| logger = logging.getLogger(__name__) | |
| class GameExecutor(AgentExecutor): | |
| def __init__(self, factory, settings, device): | |
| self.factory, self.settings, self.device = factory, settings, device | |
| self.agent = None | |
| self.context_id = None | |
| self.lock = asyncio.Lock() | |
| self.turn: asyncio.Task | None = None | |
| async def execute(self, context: RequestContext, event_queue: EventQueue) -> None: | |
| message = context.message | |
| if message is None: | |
| raise ValueError("游戏请求缺少消息") | |
| request = from_parts(GameInput, message.parts) | |
| updater = TaskUpdater(event_queue, context.task_id, context.context_id) | |
| await event_queue.enqueue_event(Task( | |
| id=context.task_id, context_id=context.context_id, | |
| status=TaskStatus(state=TaskState.TASK_STATE_WORKING), history=[message], | |
| )) | |
| async def emit(reply): | |
| await updater.update_status( | |
| TaskState.TASK_STATE_WORKING, updater.new_agent_message([to_part(reply)]), | |
| ) | |
| if request.kind != "message": | |
| await self.cancel_turn() | |
| # 同一局的请求按到达顺序逐条处理。 | |
| async with self.lock: | |
| if request.kind == "start": | |
| # 上一局没有正常结束时(例如请求中途失败),新的一局直接替换它。 | |
| await self.release() | |
| self.agent = self.factory(self.settings, self.device) | |
| self.context_id = context.context_id | |
| if self.agent is None or self.context_id != context.context_id: | |
| # 这一局已经不存在,回复本局结束,主 Agent 据此清除自己的会话。 | |
| text = ("好,这局已经结束了。" if request.kind == "end" | |
| else "这一局已经结束了,想玩的话再说一次开始吧。") | |
| await updater.complete(updater.new_agent_message([ | |
| to_part(GameReply(text=text, session="ended")), | |
| ])) | |
| return | |
| agent = self.agent | |
| work = self.turn = asyncio.create_task( | |
| self.run(agent, request, emit, message.message_id), | |
| ) | |
| try: | |
| reply = await work | |
| except asyncio.CancelledError: | |
| # end 请求取消了这一轮;框架取消整个请求时继续向外传播。 | |
| if asyncio.current_task().cancelling(): | |
| raise | |
| await updater.cancel() | |
| return | |
| finally: | |
| self.turn = None | |
| if reply.session == "ended": | |
| await self.release() | |
| await updater.complete(updater.new_agent_message([to_part(reply)])) | |
| async def run(self, agent, request, emit, request_id): | |
| with observability.game_request(self.factory.skill.descriptor.id, request_id): | |
| return await agent.handle(request, emit) | |
| async def cancel(self, context: RequestContext, event_queue: EventQueue) -> None: | |
| raise ValueError("游戏使用 end 消息结束会话") | |
| async def cancel_turn(self): | |
| work = self.turn | |
| if work is not None and not work.done(): | |
| work.cancel() | |
| await asyncio.wait({work}) | |
| async def release(self): | |
| agent, self.agent, self.context_id = self.agent, None, None | |
| if agent is not None: | |
| try: | |
| await agent.close() | |
| except Exception: | |
| logger.exception("关闭游戏 Agent 失败") | |
| async def close(self): | |
| await self.cancel_turn() | |
| await self.release() | |
| class EmbeddedServer(uvicorn.Server): | |
| def capture_signals(self): | |
| return nullcontext() | |
| class GameEventSourceResponse(EventSourceResponse): | |
| def __init__(self, response, shutdown): | |
| super().__init__(response.body_iterator, headers=response.headers) | |
| self.shutdown = shutdown | |
| async def _listen_for_exit_signal(self): | |
| # 每个内嵌服务独立管理 SSE 退出,关闭一个游戏不会影响其他游戏。 | |
| await self.shutdown.wait() | |
| async def serve_game(factory, settings, device, *, task_history_limit=64): | |
| descriptor = factory.skill.descriptor | |
| listener = socket.socket() | |
| listener.bind(("127.0.0.1", 0)) | |
| url = f"http://127.0.0.1:{listener.getsockname()[1]}" | |
| card = AgentCard( | |
| name=descriptor.name, description=descriptor.description, version="1.0.0", | |
| capabilities=AgentCapabilities(streaming=True), | |
| default_input_modes=["application/json"], default_output_modes=["application/json"], | |
| skills=[AgentSkill( | |
| id=descriptor.id, name=descriptor.name, description=descriptor.description, | |
| tags=[PROTOCOL_VERSION, *[ | |
| f"capability:{name}" for name in descriptor.required_capabilities | |
| ]], examples=descriptor.aliases, | |
| )], | |
| supported_interfaces=[AgentInterface( | |
| protocol_binding="JSONRPC", protocol_version="1.0", url=url + "/a2a", | |
| )], | |
| ) | |
| executor = GameExecutor(factory, settings, device) | |
| handler = DefaultRequestHandler( | |
| agent_executor=executor, task_store=GameTaskStore(max_completed=task_history_limit), | |
| agent_card=card, | |
| ) | |
| shutdown = asyncio.Event() | |
| rpc_route, = create_jsonrpc_routes(handler, rpc_url="/a2a") | |
| async def rpc(request): | |
| response = await rpc_route.endpoint(request) | |
| if isinstance(response, EventSourceResponse): | |
| return GameEventSourceResponse(response, shutdown) | |
| return response | |
| app = Starlette(routes=create_agent_card_routes(card) + [ | |
| Route("/a2a", rpc, methods=["POST"]), | |
| ]) | |
| server = EmbeddedServer(uvicorn.Config( | |
| app, log_level="error", lifespan="off", timeout_graceful_shutdown=10, | |
| )) | |
| serving = asyncio.create_task(server.serve(sockets=[listener])) | |
| try: | |
| async with asyncio.timeout(10): | |
| while not server.started: | |
| if serving.done(): | |
| await serving | |
| raise RuntimeError("A2A 服务没有启动") | |
| await asyncio.sleep(0.01) | |
| yield url | |
| finally: | |
| server.should_exit = True | |
| try: | |
| await executor.close() | |
| finally: | |
| shutdown.set() | |
| try: | |
| await serving | |
| finally: | |
| try: | |
| await handler.aclose() | |
| finally: | |
| listener.close() | |