You can not select more than 25 topics Topics must start with a chinese character,a letter or number, can include dashes ('-') and can be up to 35 characters long.

connection.py 18 kB

Add Token Streaming in AGS , Support Env variables (#5659) <!-- Thank you for your contribution! Please review https://microsoft.github.io/autogen/docs/Contribute before opening a pull request. --> This PR has 3 main improvements. - Token streaming - Adds support for environment variables in the app settings - Updates AGS to persist Gallery entry in db. ## Adds Token Streaming in AGS. Agentchat now supports streaming of tokens via `ModelClientStreamingChunkEvent `. This PR is to track progress on supporting that in the AutoGen Studio UI. If `model_client_stream` is enabled in an assitant agent, then token will be streamed in UI. ```python streaming_assistant = AssistantAgent( name="assistant", model_client=model_client, system_message="You are a helpful assistant.", model_client_stream=True, # Enable streaming tokens. ) ``` https://github.com/user-attachments/assets/74d43d78-6359-40c3-a78e-c84dcb5e02a1 ## Env Variables Also adds support for env variables in AGS Settings You can set env variables that are loaded just before a team is run. Handy to set variable to be used by tools etc. <img width="1291" alt="image" src="https://github.com/user-attachments/assets/437b9d90-ccee-42f7-be5d-94ab191afd67" /> > Note: the set variables are available to the server process. <!-- Please add a reviewer to the assignee section when you create a PR. If you don't have the access to it, we will shortly find a reviewer and assign them to your PR. --> ## Why are these changes needed? <!-- Please give a short summary of the change and the problem this solves. --> ## Related issue number <!-- For example: "Closes #1234" --> Closes #5627 Closes #5662 Closes #5619 ## Checks - [ ] I've included any doc changes needed for <https://microsoft.github.io/autogen/>. See <https://github.com/microsoft/autogen/blob/main/CONTRIBUTING.md> to build and test documentation locally. - [ ] I've added tests (if relevant) corresponding to the changes introduced in this PR. - [ ] I've made sure all auto checks have passed.
1 year ago
Add Token Streaming in AGS , Support Env variables (#5659) <!-- Thank you for your contribution! Please review https://microsoft.github.io/autogen/docs/Contribute before opening a pull request. --> This PR has 3 main improvements. - Token streaming - Adds support for environment variables in the app settings - Updates AGS to persist Gallery entry in db. ## Adds Token Streaming in AGS. Agentchat now supports streaming of tokens via `ModelClientStreamingChunkEvent `. This PR is to track progress on supporting that in the AutoGen Studio UI. If `model_client_stream` is enabled in an assitant agent, then token will be streamed in UI. ```python streaming_assistant = AssistantAgent( name="assistant", model_client=model_client, system_message="You are a helpful assistant.", model_client_stream=True, # Enable streaming tokens. ) ``` https://github.com/user-attachments/assets/74d43d78-6359-40c3-a78e-c84dcb5e02a1 ## Env Variables Also adds support for env variables in AGS Settings You can set env variables that are loaded just before a team is run. Handy to set variable to be used by tools etc. <img width="1291" alt="image" src="https://github.com/user-attachments/assets/437b9d90-ccee-42f7-be5d-94ab191afd67" /> > Note: the set variables are available to the server process. <!-- Please add a reviewer to the assignee section when you create a PR. If you don't have the access to it, we will shortly find a reviewer and assign them to your PR. --> ## Why are these changes needed? <!-- Please give a short summary of the change and the problem this solves. --> ## Related issue number <!-- For example: "Closes #1234" --> Closes #5627 Closes #5662 Closes #5619 ## Checks - [ ] I've included any doc changes needed for <https://microsoft.github.io/autogen/>. See <https://github.com/microsoft/autogen/blob/main/CONTRIBUTING.md> to build and test documentation locally. - [ ] I've added tests (if relevant) corresponding to the changes introduced in this PR. - [ ] I've made sure all auto checks have passed.
1 year ago
Add Token Streaming in AGS , Support Env variables (#5659) <!-- Thank you for your contribution! Please review https://microsoft.github.io/autogen/docs/Contribute before opening a pull request. --> This PR has 3 main improvements. - Token streaming - Adds support for environment variables in the app settings - Updates AGS to persist Gallery entry in db. ## Adds Token Streaming in AGS. Agentchat now supports streaming of tokens via `ModelClientStreamingChunkEvent `. This PR is to track progress on supporting that in the AutoGen Studio UI. If `model_client_stream` is enabled in an assitant agent, then token will be streamed in UI. ```python streaming_assistant = AssistantAgent( name="assistant", model_client=model_client, system_message="You are a helpful assistant.", model_client_stream=True, # Enable streaming tokens. ) ``` https://github.com/user-attachments/assets/74d43d78-6359-40c3-a78e-c84dcb5e02a1 ## Env Variables Also adds support for env variables in AGS Settings You can set env variables that are loaded just before a team is run. Handy to set variable to be used by tools etc. <img width="1291" alt="image" src="https://github.com/user-attachments/assets/437b9d90-ccee-42f7-be5d-94ab191afd67" /> > Note: the set variables are available to the server process. <!-- Please add a reviewer to the assignee section when you create a PR. If you don't have the access to it, we will shortly find a reviewer and assign them to your PR. --> ## Why are these changes needed? <!-- Please give a short summary of the change and the problem this solves. --> ## Related issue number <!-- For example: "Closes #1234" --> Closes #5627 Closes #5662 Closes #5619 ## Checks - [ ] I've included any doc changes needed for <https://microsoft.github.io/autogen/>. See <https://github.com/microsoft/autogen/blob/main/CONTRIBUTING.md> to build and test documentation locally. - [ ] I've added tests (if relevant) corresponding to the changes introduced in this PR. - [ ] I've made sure all auto checks have passed.
1 year ago
Add Token Streaming in AGS , Support Env variables (#5659) <!-- Thank you for your contribution! Please review https://microsoft.github.io/autogen/docs/Contribute before opening a pull request. --> This PR has 3 main improvements. - Token streaming - Adds support for environment variables in the app settings - Updates AGS to persist Gallery entry in db. ## Adds Token Streaming in AGS. Agentchat now supports streaming of tokens via `ModelClientStreamingChunkEvent `. This PR is to track progress on supporting that in the AutoGen Studio UI. If `model_client_stream` is enabled in an assitant agent, then token will be streamed in UI. ```python streaming_assistant = AssistantAgent( name="assistant", model_client=model_client, system_message="You are a helpful assistant.", model_client_stream=True, # Enable streaming tokens. ) ``` https://github.com/user-attachments/assets/74d43d78-6359-40c3-a78e-c84dcb5e02a1 ## Env Variables Also adds support for env variables in AGS Settings You can set env variables that are loaded just before a team is run. Handy to set variable to be used by tools etc. <img width="1291" alt="image" src="https://github.com/user-attachments/assets/437b9d90-ccee-42f7-be5d-94ab191afd67" /> > Note: the set variables are available to the server process. <!-- Please add a reviewer to the assignee section when you create a PR. If you don't have the access to it, we will shortly find a reviewer and assign them to your PR. --> ## Why are these changes needed? <!-- Please give a short summary of the change and the problem this solves. --> ## Related issue number <!-- For example: "Closes #1234" --> Closes #5627 Closes #5662 Closes #5619 ## Checks - [ ] I've included any doc changes needed for <https://microsoft.github.io/autogen/>. See <https://github.com/microsoft/autogen/blob/main/CONTRIBUTING.md> to build and test documentation locally. - [ ] I've added tests (if relevant) corresponding to the changes introduced in this PR. - [ ] I've made sure all auto checks have passed.
1 year ago
Enable LLM Call Observability in AGS (#5457) <!-- Thank you for your contribution! Please review https://microsoft.github.io/autogen/docs/Contribute before opening a pull request. --> <!-- Please add a reviewer to the assignee section when you create a PR. If you don't have the access to it, we will shortly find a reviewer and assign them to your PR. --> It is often helpful to inspect the raw request and response to/from an LLM as agents act. This PR, does the following: - Updates TeamManager to yield LLMCallEvents from core library - Run in an async background task to listen for LLMCallEvent JIT style when a team is run - Add events to an async queue and - yield those events in addition to whatever actual agentchat team.run_stream yields. - Update the AGS UI to show those LLMCallEvents in the messages section as a team runs - Add settings panel to show/hide llm call events in messages. - Minor updates to default team <img width="1539" alt="image" src="https://github.com/user-attachments/assets/bfbb19fe-3560-4faa-b600-7dd244e0e974" /> <img width="1554" alt="image" src="https://github.com/user-attachments/assets/775624f5-ba83-46e8-81ff-512bfeed6bab" /> <img width="1538" alt="image" src="https://github.com/user-attachments/assets/3becbf85-b75a-4506-adb7-e5575ebbaec4" /> ## Why are these changes needed? <!-- Please give a short summary of the change and the problem this solves. --> ## Related issue number <!-- For example: "Closes #1234" --> Closes #5440 ## Checks - [ ] I've included any doc changes needed for https://microsoft.github.io/autogen/. See https://microsoft.github.io/autogen/docs/Contribute#documentation to build and test documentation locally. - [ ] I've added tests (if relevant) corresponding to the changes introduced in this PR. - [ ] I've made sure all auto checks have passed.
1 year ago
Add Token Streaming in AGS , Support Env variables (#5659) <!-- Thank you for your contribution! Please review https://microsoft.github.io/autogen/docs/Contribute before opening a pull request. --> This PR has 3 main improvements. - Token streaming - Adds support for environment variables in the app settings - Updates AGS to persist Gallery entry in db. ## Adds Token Streaming in AGS. Agentchat now supports streaming of tokens via `ModelClientStreamingChunkEvent `. This PR is to track progress on supporting that in the AutoGen Studio UI. If `model_client_stream` is enabled in an assitant agent, then token will be streamed in UI. ```python streaming_assistant = AssistantAgent( name="assistant", model_client=model_client, system_message="You are a helpful assistant.", model_client_stream=True, # Enable streaming tokens. ) ``` https://github.com/user-attachments/assets/74d43d78-6359-40c3-a78e-c84dcb5e02a1 ## Env Variables Also adds support for env variables in AGS Settings You can set env variables that are loaded just before a team is run. Handy to set variable to be used by tools etc. <img width="1291" alt="image" src="https://github.com/user-attachments/assets/437b9d90-ccee-42f7-be5d-94ab191afd67" /> > Note: the set variables are available to the server process. <!-- Please add a reviewer to the assignee section when you create a PR. If you don't have the access to it, we will shortly find a reviewer and assign them to your PR. --> ## Why are these changes needed? <!-- Please give a short summary of the change and the problem this solves. --> ## Related issue number <!-- For example: "Closes #1234" --> Closes #5627 Closes #5662 Closes #5619 ## Checks - [ ] I've included any doc changes needed for <https://microsoft.github.io/autogen/>. See <https://github.com/microsoft/autogen/blob/main/CONTRIBUTING.md> to build and test documentation locally. - [ ] I've added tests (if relevant) corresponding to the changes introduced in this PR. - [ ] I've made sure all auto checks have passed.
1 year ago
Enable LLM Call Observability in AGS (#5457) <!-- Thank you for your contribution! Please review https://microsoft.github.io/autogen/docs/Contribute before opening a pull request. --> <!-- Please add a reviewer to the assignee section when you create a PR. If you don't have the access to it, we will shortly find a reviewer and assign them to your PR. --> It is often helpful to inspect the raw request and response to/from an LLM as agents act. This PR, does the following: - Updates TeamManager to yield LLMCallEvents from core library - Run in an async background task to listen for LLMCallEvent JIT style when a team is run - Add events to an async queue and - yield those events in addition to whatever actual agentchat team.run_stream yields. - Update the AGS UI to show those LLMCallEvents in the messages section as a team runs - Add settings panel to show/hide llm call events in messages. - Minor updates to default team <img width="1539" alt="image" src="https://github.com/user-attachments/assets/bfbb19fe-3560-4faa-b600-7dd244e0e974" /> <img width="1554" alt="image" src="https://github.com/user-attachments/assets/775624f5-ba83-46e8-81ff-512bfeed6bab" /> <img width="1538" alt="image" src="https://github.com/user-attachments/assets/3becbf85-b75a-4506-adb7-e5575ebbaec4" /> ## Why are these changes needed? <!-- Please give a short summary of the change and the problem this solves. --> ## Related issue number <!-- For example: "Closes #1234" --> Closes #5440 ## Checks - [ ] I've included any doc changes needed for https://microsoft.github.io/autogen/. See https://microsoft.github.io/autogen/docs/Contribute#documentation to build and test documentation locally. - [ ] I've added tests (if relevant) corresponding to the changes introduced in this PR. - [ ] I've made sure all auto checks have passed.
1 year ago
Add Token Streaming in AGS , Support Env variables (#5659) <!-- Thank you for your contribution! Please review https://microsoft.github.io/autogen/docs/Contribute before opening a pull request. --> This PR has 3 main improvements. - Token streaming - Adds support for environment variables in the app settings - Updates AGS to persist Gallery entry in db. ## Adds Token Streaming in AGS. Agentchat now supports streaming of tokens via `ModelClientStreamingChunkEvent `. This PR is to track progress on supporting that in the AutoGen Studio UI. If `model_client_stream` is enabled in an assitant agent, then token will be streamed in UI. ```python streaming_assistant = AssistantAgent( name="assistant", model_client=model_client, system_message="You are a helpful assistant.", model_client_stream=True, # Enable streaming tokens. ) ``` https://github.com/user-attachments/assets/74d43d78-6359-40c3-a78e-c84dcb5e02a1 ## Env Variables Also adds support for env variables in AGS Settings You can set env variables that are loaded just before a team is run. Handy to set variable to be used by tools etc. <img width="1291" alt="image" src="https://github.com/user-attachments/assets/437b9d90-ccee-42f7-be5d-94ab191afd67" /> > Note: the set variables are available to the server process. <!-- Please add a reviewer to the assignee section when you create a PR. If you don't have the access to it, we will shortly find a reviewer and assign them to your PR. --> ## Why are these changes needed? <!-- Please give a short summary of the change and the problem this solves. --> ## Related issue number <!-- For example: "Closes #1234" --> Closes #5627 Closes #5662 Closes #5619 ## Checks - [ ] I've included any doc changes needed for <https://microsoft.github.io/autogen/>. See <https://github.com/microsoft/autogen/blob/main/CONTRIBUTING.md> to build and test documentation locally. - [ ] I've added tests (if relevant) corresponding to the changes introduced in this PR. - [ ] I've made sure all auto checks have passed.
1 year ago
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460
  1. import asyncio
  2. import logging
  3. import traceback
  4. from datetime import datetime, timezone
  5. from typing import Any, Callable, Dict, Optional, Union
  6. from autogen_agentchat.base._task import TaskResult
  7. from autogen_agentchat.messages import (
  8. AgentEvent,
  9. ChatMessage,
  10. HandoffMessage,
  11. ModelClientStreamingChunkEvent,
  12. MultiModalMessage,
  13. StopMessage,
  14. TextMessage,
  15. ToolCallExecutionEvent,
  16. ToolCallRequestEvent,
  17. )
  18. from autogen_core import CancellationToken
  19. from autogen_core import Image as AGImage
  20. from fastapi import WebSocket, WebSocketDisconnect
  21. from ...database import DatabaseManager
  22. from ...datamodel import (
  23. LLMCallEventMessage,
  24. Message,
  25. MessageConfig,
  26. Run,
  27. RunStatus,
  28. Settings,
  29. SettingsConfig,
  30. TeamResult,
  31. )
  32. from ...teammanager import TeamManager
  33. logger = logging.getLogger(__name__)
  34. class WebSocketManager:
  35. """Manages WebSocket connections and message streaming for team task execution"""
  36. def __init__(self, db_manager: DatabaseManager):
  37. self.db_manager = db_manager
  38. self._connections: Dict[int, WebSocket] = {}
  39. self._cancellation_tokens: Dict[int, CancellationToken] = {}
  40. # Track explicitly closed connections
  41. self._closed_connections: set[int] = set()
  42. self._input_responses: Dict[int, asyncio.Queue] = {}
  43. self._cancel_message = TeamResult(
  44. task_result=TaskResult(
  45. messages=[TextMessage(source="user", content="Run cancelled by user")], stop_reason="cancelled by user"
  46. ),
  47. usage="",
  48. duration=0,
  49. ).model_dump()
  50. def _get_stop_message(self, reason: str) -> dict:
  51. return TeamResult(
  52. task_result=TaskResult(messages=[TextMessage(source="user", content=reason)], stop_reason=reason),
  53. usage="",
  54. duration=0,
  55. ).model_dump()
  56. async def connect(self, websocket: WebSocket, run_id: int) -> bool:
  57. try:
  58. await websocket.accept()
  59. self._connections[run_id] = websocket
  60. self._closed_connections.discard(run_id)
  61. # Initialize input queue for this connection
  62. self._input_responses[run_id] = asyncio.Queue()
  63. await self._send_message(
  64. run_id, {"type": "system", "status": "connected", "timestamp": datetime.now(timezone.utc).isoformat()}
  65. )
  66. return True
  67. except Exception as e:
  68. logger.error(f"Connection error for run {run_id}: {e}")
  69. return False
  70. async def start_stream(self, run_id: int, task: str, team_config: dict) -> None:
  71. """Start streaming task execution with proper run management"""
  72. if run_id not in self._connections or run_id in self._closed_connections:
  73. raise ValueError(f"No active connection for run {run_id}")
  74. team_manager = TeamManager()
  75. cancellation_token = CancellationToken()
  76. self._cancellation_tokens[run_id] = cancellation_token
  77. final_result = None
  78. try:
  79. # Update run with task and status
  80. run = await self._get_run(run_id)
  81. # get user Settings
  82. user_settings = await self._get_settings(run.user_id)
  83. env_vars = SettingsConfig(**user_settings.config).environment if user_settings else None
  84. if run:
  85. run.task = MessageConfig(content=task, source="user").model_dump()
  86. run.status = RunStatus.ACTIVE
  87. self.db_manager.upsert(run)
  88. input_func = self.create_input_func(run_id)
  89. async for message in team_manager.run_stream(
  90. task=task,
  91. team_config=team_config,
  92. input_func=input_func,
  93. cancellation_token=cancellation_token,
  94. env_vars=env_vars,
  95. ):
  96. if cancellation_token.is_cancelled() or run_id in self._closed_connections:
  97. logger.info(f"Stream cancelled or connection closed for run {run_id}")
  98. break
  99. formatted_message = self._format_message(message)
  100. if formatted_message:
  101. await self._send_message(run_id, formatted_message)
  102. # Save messages by concrete type
  103. if isinstance(
  104. message,
  105. (
  106. TextMessage,
  107. MultiModalMessage,
  108. StopMessage,
  109. HandoffMessage,
  110. ToolCallRequestEvent,
  111. ToolCallExecutionEvent,
  112. LLMCallEventMessage,
  113. ),
  114. ):
  115. await self._save_message(run_id, message)
  116. # Capture final result if it's a TeamResult
  117. elif isinstance(message, TeamResult):
  118. final_result = message.model_dump()
  119. if not cancellation_token.is_cancelled() and run_id not in self._closed_connections:
  120. if final_result:
  121. await self._update_run(run_id, RunStatus.COMPLETE, team_result=final_result)
  122. else:
  123. logger.warning(f"No final result captured for completed run {run_id}")
  124. await self._update_run_status(run_id, RunStatus.COMPLETE)
  125. else:
  126. await self._send_message(
  127. run_id,
  128. {
  129. "type": "completion",
  130. "status": "cancelled",
  131. "data": self._cancel_message,
  132. "timestamp": datetime.now(timezone.utc).isoformat(),
  133. },
  134. )
  135. # Update run with cancellation result
  136. await self._update_run(run_id, RunStatus.STOPPED, team_result=self._cancel_message)
  137. except Exception as e:
  138. logger.error(f"Stream error for run {run_id}: {e}")
  139. traceback.print_exc()
  140. await self._handle_stream_error(run_id, e)
  141. finally:
  142. self._cancellation_tokens.pop(run_id, None)
  143. async def _save_message(self, run_id: int, message: Union[AgentEvent | ChatMessage, ChatMessage]) -> None:
  144. """Save a message to the database"""
  145. run = await self._get_run(run_id)
  146. if run:
  147. db_message = Message(
  148. session_id=run.session_id,
  149. run_id=run_id,
  150. config=message.model_dump(),
  151. user_id=None, # You might want to pass this from somewhere
  152. )
  153. self.db_manager.upsert(db_message)
  154. async def _update_run(
  155. self, run_id: int, status: RunStatus, team_result: Optional[dict] = None, error: Optional[str] = None
  156. ) -> None:
  157. """Update run status and result"""
  158. run = await self._get_run(run_id)
  159. if run:
  160. run.status = status
  161. if team_result:
  162. run.team_result = team_result
  163. if error:
  164. run.error_message = error
  165. self.db_manager.upsert(run)
  166. def create_input_func(self, run_id: int) -> Callable:
  167. """Creates an input function for a specific run"""
  168. async def input_handler(prompt: str = "", cancellation_token: Optional[CancellationToken] = None) -> str:
  169. try:
  170. # Send input request to client
  171. await self._send_message(
  172. run_id,
  173. {
  174. "type": "input_request",
  175. "prompt": prompt,
  176. "data": {"source": "system", "content": prompt},
  177. "timestamp": datetime.now(timezone.utc).isoformat(),
  178. },
  179. )
  180. # Wait for response
  181. if run_id in self._input_responses:
  182. response = await self._input_responses[run_id].get()
  183. return response
  184. else:
  185. raise ValueError(f"No input queue for run {run_id}")
  186. except Exception as e:
  187. logger.error(f"Error handling input for run {run_id}: {e}")
  188. raise
  189. return input_handler
  190. async def handle_input_response(self, run_id: int, response: str) -> None:
  191. """Handle input response from client"""
  192. if run_id in self._input_responses:
  193. await self._input_responses[run_id].put(response)
  194. else:
  195. logger.warning(f"Received input response for inactive run {run_id}")
  196. async def stop_run(self, run_id: int, reason: str) -> None:
  197. if run_id in self._cancellation_tokens:
  198. logger.info(f"Stopping run {run_id}")
  199. stop_message = self._get_stop_message(reason)
  200. try:
  201. # Update run record first
  202. await self._update_run(run_id, status=RunStatus.STOPPED, team_result=stop_message)
  203. # Then handle websocket communication if connection is active
  204. if run_id in self._connections and run_id not in self._closed_connections:
  205. await self._send_message(
  206. run_id,
  207. {
  208. "type": "completion",
  209. "status": "cancelled",
  210. "data": stop_message,
  211. "timestamp": datetime.now(timezone.utc).isoformat(),
  212. },
  213. )
  214. # Finally cancel the token
  215. self._cancellation_tokens[run_id].cancel()
  216. except Exception as e:
  217. logger.error(f"Error stopping run {run_id}: {e}")
  218. # We might want to force disconnect here if db update failed
  219. # await self.disconnect(run_id) # Optional
  220. async def disconnect(self, run_id: int) -> None:
  221. """Clean up connection and associated resources"""
  222. logger.info(f"Disconnecting run {run_id}")
  223. # Mark as closed before cleanup to prevent any new messages
  224. self._closed_connections.add(run_id)
  225. # Cancel any running tasks
  226. await self.stop_run(run_id, "Connection closed")
  227. # Clean up resources
  228. self._connections.pop(run_id, None)
  229. self._cancellation_tokens.pop(run_id, None)
  230. self._input_responses.pop(run_id, None)
  231. async def _send_message(self, run_id: int, message: dict) -> None:
  232. """Send a message through the WebSocket with connection state checking
  233. Args:
  234. run_id: id of the run
  235. message: Message dictionary to send
  236. """
  237. if run_id in self._closed_connections:
  238. logger.warning(f"Attempted to send message to closed connection for run {run_id}")
  239. return
  240. try:
  241. if run_id in self._connections:
  242. websocket = self._connections[run_id]
  243. await websocket.send_json(message)
  244. except WebSocketDisconnect:
  245. logger.warning(f"WebSocket disconnected while sending message for run {run_id}")
  246. await self.disconnect(run_id)
  247. except Exception as e:
  248. logger.error(f"Error sending message for run {run_id}: {e}, {message}")
  249. # Don't try to send error message here to avoid potential recursive loop
  250. await self._update_run_status(run_id, RunStatus.ERROR, str(e))
  251. await self.disconnect(run_id)
  252. async def _handle_stream_error(self, run_id: int, error: Exception) -> None:
  253. """Handle stream errors with proper run updates"""
  254. if run_id not in self._closed_connections:
  255. error_result = TeamResult(
  256. task_result=TaskResult(
  257. messages=[TextMessage(source="system", content=str(error))],
  258. stop_reason="An error occurred while processing this run",
  259. ),
  260. usage="",
  261. duration=0,
  262. ).model_dump()
  263. await self._send_message(
  264. run_id,
  265. {
  266. "type": "completion",
  267. "status": "error",
  268. "data": error_result,
  269. "timestamp": datetime.now(timezone.utc).isoformat(),
  270. },
  271. )
  272. await self._update_run(run_id, RunStatus.ERROR, team_result=error_result, error=str(error))
  273. def _format_message(self, message: Any) -> Optional[dict]:
  274. """Format message for WebSocket transmission
  275. Args:
  276. message: Message to format
  277. Returns:
  278. Optional[dict]: Formatted message or None if formatting fails
  279. """
  280. try:
  281. if isinstance(message, MultiModalMessage):
  282. message_dump = message.model_dump()
  283. message_dump["content"] = [
  284. message_dump["content"][0],
  285. {
  286. "url": f"data:image/png;base64,{message_dump['content'][1]['data']}",
  287. "alt": "WebSurfer Screenshot",
  288. },
  289. ]
  290. return {"type": "message", "data": message_dump}
  291. elif isinstance(message, TeamResult):
  292. return {
  293. "type": "result",
  294. "data": message.model_dump(),
  295. "status": "complete",
  296. }
  297. elif isinstance(message, ModelClientStreamingChunkEvent):
  298. return {"type": "message_chunk", "data": message.model_dump()}
  299. elif isinstance(
  300. message,
  301. (
  302. TextMessage,
  303. StopMessage,
  304. HandoffMessage,
  305. ToolCallRequestEvent,
  306. ToolCallExecutionEvent,
  307. LLMCallEventMessage,
  308. ),
  309. ):
  310. return {"type": "message", "data": message.model_dump()}
  311. return None
  312. except Exception as e:
  313. logger.error(f"Message formatting error: {e}")
  314. return None
  315. async def _get_run(self, run_id: int) -> Optional[Run]:
  316. """Get run from database
  317. Args:
  318. run_id: id of the run to retrieve
  319. Returns:
  320. Optional[Run]: Run object if found, None otherwise
  321. """
  322. response = self.db_manager.get(Run, filters={"id": run_id}, return_json=False)
  323. return response.data[0] if response.status and response.data else None
  324. async def _get_settings(self, user_id: str) -> Optional[Settings]:
  325. """Get user settings from database
  326. Args:
  327. user_id: User ID to retrieve settings for
  328. Returns:
  329. Optional[dict]: User settings if found, None otherwise
  330. """
  331. response = self.db_manager.get(filters={"user_id": user_id}, model_class=Settings, return_json=False)
  332. return response.data[0] if response.status and response.data else None
  333. async def _update_run_status(self, run_id: int, status: RunStatus, error: Optional[str] = None) -> None:
  334. """Update run status in database
  335. Args:
  336. run_id: id of the run to update
  337. status: New status to set
  338. error: Optional error message
  339. """
  340. run = await self._get_run(run_id)
  341. if run:
  342. run.status = status
  343. run.error_message = error
  344. self.db_manager.upsert(run)
  345. async def cleanup(self) -> None:
  346. """Clean up all active connections and resources when server is shutting down"""
  347. logger.info(f"Cleaning up {len(self.active_connections)} active connections")
  348. try:
  349. # First cancel all running tasks
  350. for run_id in self.active_runs.copy():
  351. if run_id in self._cancellation_tokens:
  352. self._cancellation_tokens[run_id].cancel()
  353. run = await self._get_run(run_id)
  354. if run and run.status == RunStatus.ACTIVE:
  355. interrupted_result = TeamResult(
  356. task_result=TaskResult(
  357. messages=[TextMessage(source="system", content="Run interrupted by server shutdown")],
  358. stop_reason="server_shutdown",
  359. ),
  360. usage="",
  361. duration=0,
  362. ).model_dump()
  363. run.status = RunStatus.STOPPED
  364. run.team_result = interrupted_result
  365. self.db_manager.upsert(run)
  366. # Then disconnect all websockets with timeout
  367. # 10 second timeout for entire cleanup
  368. async with asyncio.timeout(10):
  369. for run_id in self.active_connections.copy():
  370. try:
  371. # Give each disconnect operation 2 seconds
  372. async with asyncio.timeout(2):
  373. await self.disconnect(run_id)
  374. except asyncio.TimeoutError:
  375. logger.warning(f"Timeout disconnecting run {run_id}")
  376. except Exception as e:
  377. logger.error(f"Error disconnecting run {run_id}: {e}")
  378. except asyncio.TimeoutError:
  379. logger.warning("WebSocketManager cleanup timed out")
  380. except Exception as e:
  381. logger.error(f"Error during WebSocketManager cleanup: {e}")
  382. finally:
  383. # Always clear internal state, even if cleanup had errors
  384. self._connections.clear()
  385. self._cancellation_tokens.clear()
  386. self._closed_connections.clear()
  387. self._input_responses.clear()
  388. @property
  389. def active_connections(self) -> set[int]:
  390. """Get set of active run IDs"""
  391. return set(self._connections.keys()) - self._closed_connections
  392. @property
  393. def active_runs(self) -> set[int]:
  394. """Get set of runs with active cancellation tokens"""
  395. return set(self._cancellation_tokens.keys())