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.

app_team.py 6.8 kB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188
  1. import json
  2. import logging
  3. import os
  4. from typing import Any, Awaitable, Callable, Optional
  5. import aiofiles
  6. import yaml
  7. from autogen_agentchat.agents import AssistantAgent, UserProxyAgent
  8. from autogen_agentchat.base import TaskResult
  9. from autogen_agentchat.messages import TextMessage, UserInputRequestedEvent
  10. from autogen_agentchat.teams import RoundRobinGroupChat
  11. from autogen_core import CancellationToken
  12. from autogen_core.models import ChatCompletionClient
  13. from fastapi import FastAPI, HTTPException, WebSocket, WebSocketDisconnect
  14. from fastapi.middleware.cors import CORSMiddleware
  15. from fastapi.responses import FileResponse
  16. from fastapi.staticfiles import StaticFiles
  17. logger = logging.getLogger(__name__)
  18. app = FastAPI()
  19. # Add CORS middleware
  20. app.add_middleware(
  21. CORSMiddleware,
  22. allow_origins=["*"], # Allows all origins
  23. allow_credentials=True,
  24. allow_methods=["*"], # Allows all methods
  25. allow_headers=["*"], # Allows all headers
  26. )
  27. model_config_path = "model_config.yaml"
  28. state_path = "team_state.json"
  29. history_path = "team_history.json"
  30. # Serve static files
  31. app.mount("/static", StaticFiles(directory="."), name="static")
  32. @app.get("/")
  33. async def root():
  34. """Serve the chat interface HTML file."""
  35. return FileResponse("app_team.html")
  36. async def get_team(
  37. user_input_func: Callable[[str, Optional[CancellationToken]], Awaitable[str]],
  38. ) -> RoundRobinGroupChat:
  39. # Get model client from config.
  40. async with aiofiles.open(model_config_path, "r") as file:
  41. model_config = yaml.safe_load(await file.read())
  42. model_client = ChatCompletionClient.load_component(model_config)
  43. # Create the team.
  44. agent = AssistantAgent(
  45. name="assistant",
  46. model_client=model_client,
  47. system_message="You are a helpful assistant.",
  48. )
  49. yoda = AssistantAgent(
  50. name="yoda",
  51. model_client=model_client,
  52. system_message="Repeat the same message in the tone of Yoda.",
  53. )
  54. user_proxy = UserProxyAgent(
  55. name="user",
  56. input_func=user_input_func, # Use the user input function.
  57. )
  58. team = RoundRobinGroupChat(
  59. [agent, yoda, user_proxy],
  60. )
  61. # Load state from file.
  62. if not os.path.exists(state_path):
  63. return team
  64. async with aiofiles.open(state_path, "r") as file:
  65. state = json.loads(await file.read())
  66. await team.load_state(state)
  67. return team
  68. async def get_history() -> list[dict[str, Any]]:
  69. """Get chat history from file."""
  70. if not os.path.exists(history_path):
  71. return []
  72. async with aiofiles.open(history_path, "r") as file:
  73. return json.loads(await file.read())
  74. @app.get("/history")
  75. async def history() -> list[dict[str, Any]]:
  76. try:
  77. return await get_history()
  78. except Exception as e:
  79. raise HTTPException(status_code=500, detail=str(e)) from e
  80. @app.websocket("/ws/chat")
  81. async def chat(websocket: WebSocket):
  82. await websocket.accept()
  83. # User input function used by the team.
  84. async def _user_input(prompt: str, cancellation_token: CancellationToken | None) -> str:
  85. try:
  86. data = await websocket.receive_json()
  87. message = TextMessage.model_validate(data)
  88. return message.content
  89. except WebSocketDisconnect:
  90. # Client disconnected while waiting for input - this is the root cause of the issue
  91. logger.info("Client disconnected while waiting for user input")
  92. raise # Let WebSocketDisconnect propagate to be handled by outer try/except
  93. try:
  94. while True:
  95. # Get user message.
  96. data = await websocket.receive_json()
  97. request = TextMessage.model_validate(data)
  98. try:
  99. # Get the team and respond to the message.
  100. team = await get_team(_user_input)
  101. history = await get_history()
  102. stream = team.run_stream(task=request)
  103. async for message in stream:
  104. if isinstance(message, TaskResult):
  105. continue
  106. await websocket.send_json(message.model_dump())
  107. if not isinstance(message, UserInputRequestedEvent):
  108. # Don't save user input events to history.
  109. history.append(message.model_dump())
  110. # Save team state to file.
  111. async with aiofiles.open(state_path, "w") as file:
  112. state = await team.save_state()
  113. await file.write(json.dumps(state))
  114. # Save chat history to file.
  115. async with aiofiles.open(history_path, "w") as file:
  116. await file.write(json.dumps(history))
  117. except WebSocketDisconnect:
  118. # Client disconnected during message processing - exit gracefully
  119. logger.info("Client disconnected during message processing")
  120. break
  121. except Exception as e:
  122. # Send error message to client
  123. error_message = {
  124. "type": "error",
  125. "content": f"Error: {str(e)}",
  126. "source": "system"
  127. }
  128. try:
  129. await websocket.send_json(error_message)
  130. # Re-enable input after error
  131. await websocket.send_json({
  132. "type": "UserInputRequestedEvent",
  133. "content": "An error occurred. Please try again.",
  134. "source": "system"
  135. })
  136. except WebSocketDisconnect:
  137. # Client disconnected while sending error - exit gracefully
  138. logger.info("Client disconnected while sending error message")
  139. break
  140. except Exception as send_error:
  141. logger.error(f"Failed to send error message: {str(send_error)}")
  142. break
  143. except WebSocketDisconnect:
  144. logger.info("Client disconnected")
  145. except Exception as e:
  146. logger.error(f"Unexpected error: {str(e)}")
  147. try:
  148. await websocket.send_json({
  149. "type": "error",
  150. "content": f"Unexpected error: {str(e)}",
  151. "source": "system"
  152. })
  153. except WebSocketDisconnect:
  154. # Client already disconnected - no need to send
  155. logger.info("Client disconnected before error could be sent")
  156. except Exception:
  157. # Failed to send error message - connection likely broken
  158. logger.error("Failed to send error message to client")
  159. pass
  160. # Example usage
  161. if __name__ == "__main__":
  162. import uvicorn
  163. uvicorn.run(app, host="0.0.0.0", port=8002)