import asyncio import json import logging import os import uuid from queue import Empty from queue import Queue from typing import Any import uvicorn from fastapi import FastAPI from fastapi import Request from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import StreamingResponse from pydantic import BaseModel from pydantic import Field import Agent_E.ae.core.playwright_manager as browserManager from Agent_E.ae.config import SOURCE_LOG_FOLDER_PATH from Agent_E.ae.core.agents_llm_config import AgentsLLMConfig from Agent_E.ae.core.autogen_wrapper import AutogenWrapper from Agent_E.ae.utils.formatting_helper import is_terminating_message from Agent_E.ae.utils.ui_messagetype import MessageType browser_manager = browserManager.PlaywrightManager(headless=False) APP_VERSION = "1.0.0" APP_NAME = "Agent-E Web API" API_PREFIX = "/api" IS_DEBUG = False HOST = os.getenv("HOST", "0.0.0.0") PORT = int(os.getenv("PORT", 8080)) WORKERS = 1 container_id = os.getenv("CONTAINER_ID", "") # Configure logging logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s") logger = logging.getLogger("uvicorn") class CommandQueryModel(BaseModel): command: str = Field(..., description="The command related to web navigation to execute.") # Required field with description llm_config: dict[str,Any] | None = Field(None, description="The LLM configuration string to use for the agents.") planner_max_chat_round: int = Field(50, description="The maximum number of chat rounds for the planner.") browser_nav_max_chat_round: int = Field(10, description="The maximum number of chat rounds for the browser navigation agent.") clientid: str | None = Field(None, description="Client identifier, optional") request_originator: str | None = Field(None, description="Optional id of the request originator") def get_app() -> FastAPI: """Starts the Application""" fast_app = FastAPI(title=APP_NAME, version=APP_VERSION, debug=IS_DEBUG) fast_app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_credentials=True, allow_methods=["*"], allow_headers=["*"]) return fast_app app = get_app() @app.on_event("startup") # type: ignore async def startup_event(): """ Startup event handler to initialize browser manager asynchronously. """ global container_id if container_id.strip() == "": container_id = str(uuid.uuid4()) os.environ["CONTAINER_ID"] = container_id await browser_manager.async_initialize() @app.post("/execute_task", description="Execute a given command related to web navigation and return the result.") async def execute_task(request: Request, query_model: CommandQueryModel): notification_queue = Queue() # type: ignore transaction_id = str(uuid.uuid4()) if query_model.clientid is None else query_model.clientid register_notification_listener(notification_queue) return StreamingResponse(run_task(request, transaction_id, query_model.command, browser_manager, notification_queue, query_model.request_originator,query_model.llm_config, planner_max_chat_round=query_model.planner_max_chat_round, browser_nav_max_chat_round=query_model.browser_nav_max_chat_round), media_type="text/event-stream") def run_task(request: Request, transaction_id: str, command: str, playwright_manager: browserManager.PlaywrightManager, notification_queue: Queue, request_originator: str|None = None, llm_config: dict[str,Any]|None = None, # type: ignore planner_max_chat_round: int = 50, browser_nav_max_chat_round: int = 10): """ Run the task to process the command and generate events. Args: request (Request): The request object to detect client disconnect. transaction_id (str): The transaction ID to identify the request. command (str): The command to execute. playwright_manager (PlaywrightManager): The manager handling browser interactions and notifications. notification_queue (Queue): The queue to hold notifications for this request. request_originator (str|None): The originator of the request. llm_config (dict[str,Any]|None): The LLM configuration to use for the agents. planner_max_chat_rounds (int, optional): The maximum number of chat rounds for the planner. Defaults to 50. browser_nav_max_chat_round (int, optional): The maximum number of chat rounds for the browser navigation agent. Defaults to 10. Yields: str: JSON-encoded string representing a notification. """ async def event_generator(): task = asyncio.create_task(process_command(command, playwright_manager, planner_max_chat_round, browser_nav_max_chat_round, llm_config)) task_detail = f"transaction_id={transaction_id}, request_originator={request_originator}, command={command}" try: while not task.done() or not notification_queue.empty(): if await request.is_disconnected(): logger.info(f"Client disconnected. Cancelling the task: {task_detail}") task.cancel() break try: notification = notification_queue.get_nowait() # type: ignore notification["transaction_id"] = transaction_id # Include the transaction ID in the notification notification["request_originator"] = request_originator # Include the request originator in the notification yield f"data: {json.dumps(notification)}\n\n" # Using 'data: ' to follow the SSE format except Empty: await asyncio.sleep(0.1) except asyncio.CancelledError: logger.info(f"Task was cancelled due to client disconnection. {task_detail}") except Exception as e: logger.error(f"An error occurred while processing task: {task_detail}. Error: {e}") await task except asyncio.CancelledError: logger.info(f"Task was cancelled due to client disconnection. {task_detail}") await task return event_generator() async def process_command(command: str, playwright_manager: browserManager.PlaywrightManager, planner_max_chat_round: int, browser_nav_max_chat_round: int, llm_config:dict[str,Any]|None = None): """ Process the command and send notifications. Args: command (str): The command to process. playwright_manager (PlaywrightManager): The manager handling browser interactions and notifications. """ await playwright_manager.go_to_homepage() # Go to the homepage before processing the command current_url = await playwright_manager.get_current_url() await playwright_manager.notify_user("Processing command", MessageType.INFO) # Load the configuration using AgentsLLMConfig normalized_llm_config = None if llm_config is None: normalized_llm_config = AgentsLLMConfig() else: normalized_llm_config = AgentsLLMConfig(llm_config=llm_config) logger.info("Applied LLM config received via API.") # Retrieve planner agent and browser nav agent configurations planner_agent_config = normalized_llm_config.get_planner_agent_config() browser_nav_agent_config = normalized_llm_config.get_browser_nav_agent_config() ag = await AutogenWrapper.create(planner_agent_config, browser_nav_agent_config, planner_max_chat_round=planner_max_chat_round, browser_nav_max_chat_round=browser_nav_max_chat_round) command_exec_result = await ag.process_command(command, current_url) # type: ignore messages=ag.agents_map["planner_agent"].chat_messages messages_str_keys = {str(key): value for key, value in messages.items()} # type: ignore with open(os.path.join(SOURCE_LOG_FOLDER_PATH, 'chat_messages.json'), 'w', encoding='utf-8') as f: json.dump(messages_str_keys, f, ensure_ascii=False, indent=4) logger.debug("Chat messages saved") if is_terminating_message(command_exec_result.summary): await playwright_manager.notify_user("DONE", MessageType.DONE) else: await playwright_manager.notify_user("Max turns reached", MessageType.MAX_TURNS_REACHED) def register_notification_listener(notification_queue: Queue): # type: ignore """ Register the event generator as a listener in the NotificationManager. """ def listener(notification: dict[str, str]) -> None: notification["container_id"] = container_id # Include the container ID (or UUID) in the notification notification_queue.put(notification) # type: ignore browser_manager.notification_manager.register_listener(listener) if __name__ == "__main__": logger.info("**********Application Started**********") uvicorn.run("main:app", host=HOST, port=PORT, workers=WORKERS, reload=IS_DEBUG, log_level="info")