diff --git a/examples/mcp/mcp-sse-weather.py b/examples/mcp/mcp-sse-weather.py new file mode 100644 index 000000000..bcd48b77b --- /dev/null +++ b/examples/mcp/mcp-sse-weather.py @@ -0,0 +1,9 @@ +from praisonaiagents import Agent, MCP + +search_agent = Agent( + instructions="""You are a weather agent that can provide weather information for a given city.""", + llm="openai/gpt-4o-mini", + tools=MCP("http://localhost:8080/sse") +) + +search_agent.start("What is the weather in London?") \ No newline at end of file diff --git a/src/praisonai-agents/multi-agent-api.py b/src/praisonai-agents/multi-agent-api.py new file mode 100644 index 000000000..e7dbf643e --- /dev/null +++ b/src/praisonai-agents/multi-agent-api.py @@ -0,0 +1,29 @@ +from praisonaiagents import Agent + +# Create a weather agent +weather_agent = Agent( + instructions="""You are a weather agent that can provide weather information for a given city.""", + llm="gpt-4o-mini" +) + +# Create a stock market agent +stock_agent = Agent( + instructions="""You are a stock market agent that can provide information about stock prices and market trends.""", + llm="gpt-4o-mini" +) + +# Create a travel agent +travel_agent = Agent( + instructions="""You are a travel agent that can provide recommendations for destinations, hotels, and activities.""", + llm="gpt-4o-mini" +) + +# Register the first two agents with blocking=False so they don't block execution +weather_agent.launch(path="/weather", port=3030, blocking=False) +stock_agent.launch(path="/stock", port=3030, blocking=False) + +# Register the last agent with blocking=True to keep the server running +# This must be the last launch() call +travel_agent.launch(path="/travel", port=3030, blocking=True) + +# The script will block at the last launch() call until the user presses Ctrl+C diff --git a/src/praisonai-agents/praisonaiagents/agent/agent.py b/src/praisonai-agents/praisonaiagents/agent/agent.py index 922320d86..0e1e02e0d 100644 --- a/src/praisonai-agents/praisonaiagents/agent/agent.py +++ b/src/praisonai-agents/praisonaiagents/agent/agent.py @@ -22,9 +22,16 @@ import uuid from dataclasses import dataclass +# Don't import FastAPI dependencies here - use lazy loading instead + if TYPE_CHECKING: from ..task.task import Task +# Shared variables for API server +_shared_app = None +_server_started = False +_registered_agents = {} + @dataclass class ChatCompletionMessage: content: str @@ -1399,4 +1406,157 @@ async def execute_tool_async(self, function_name: str, arguments: Dict[str, Any] except Exception as e: logging.error(f"Error in execute_tool_async: {str(e)}", exc_info=True) - return {"error": f"Error in execute_tool_async: {str(e)}"} \ No newline at end of file + return {"error": f"Error in execute_tool_async: {str(e)}"} + + def launch(self, path: str = '/', port: int = 8000, host: str = '0.0.0.0', autostart: bool = True, debug: bool = False, blocking: bool = True): + """ + Launch the agent as an HTTP API endpoint. + + Args: + path: API endpoint path (default: '/') + port: Server port (default: 8000) + host: Server host (default: '0.0.0.0') + autostart: Whether to start the server automatically (default: True) + debug: Enable debug mode for uvicorn (default: False) + blocking: If True, blocks the main thread to keep the server running (default: True) + + Returns: + None + """ + global _server_started, _registered_agents, _shared_app + + # Try to import FastAPI dependencies - lazy loading + try: + import uvicorn + from fastapi import FastAPI, HTTPException, Request + from fastapi.responses import JSONResponse + from pydantic import BaseModel + + # Define the request model here since we need pydantic + class AgentQuery(BaseModel): + query: str + + except ImportError as e: + # Check which specific module is missing + missing_module = str(e).split("No module named '")[-1].rstrip("'") + display_error(f"Missing dependency: {missing_module}. Required for launch() method.") + logging.error(f"Missing dependency: {missing_module}. Required for launch() method.") + print(f"\nTo add API capabilities, install the required dependencies:") + print(f"pip install {missing_module}") + print("\nOr install all API dependencies with:") + print("pip install 'praisonaiagents[api]'") + return None + + # Initialize shared FastAPI app if not already created + if _shared_app is None: + _shared_app = FastAPI( + title="PraisonAI Agents API", + description="API for interacting with PraisonAI Agents" + ) + + # Add a root endpoint with a welcome message + @_shared_app.get("/") + async def root(): + return {"message": "Welcome to PraisonAI Agents API. See /docs for usage."} + + # Normalize path to ensure it starts with / + if not path.startswith('/'): + path = f'/{path}' + + # Check if path is already registered by another agent + if path in _registered_agents and _registered_agents[path] != self.agent_id: + existing_agent = _registered_agents[path] + logging.warning(f"Path '{path}' is already registered by another agent. Please use a different path.") + print(f"⚠️ Warning: Path '{path}' is already registered by another agent.") + # Use a modified path to avoid conflicts + original_path = path + path = f"{path}_{self.agent_id[:6]}" + logging.warning(f"Using '{path}' instead of '{original_path}'") + print(f"🔄 Using '{path}' instead") + + # Register the agent to this path + _registered_agents[path] = self.agent_id + + # Define the endpoint handler + @_shared_app.post(path) + async def handle_agent_query(request: Request, query_data: Optional[AgentQuery] = None): + # Handle both direct JSON with query field and form data + if query_data is None: + try: + request_data = await request.json() + if "query" not in request_data: + raise HTTPException(status_code=400, detail="Missing 'query' field in request") + query = request_data["query"] + except: + # Fallback to form data or query params + form_data = await request.form() + if "query" in form_data: + query = form_data["query"] + else: + raise HTTPException(status_code=400, detail="Missing 'query' field in request") + else: + query = query_data.query + + try: + # Use async version if available, otherwise use sync version + if asyncio.iscoroutinefunction(self.chat): + response = await self.achat(query) + else: + # Run sync function in a thread to avoid blocking + loop = asyncio.get_event_loop() + response = await loop.run_in_executor(None, lambda: self.chat(query)) + + return {"response": response} + except Exception as e: + logging.error(f"Error processing query: {str(e)}", exc_info=True) + return JSONResponse( + status_code=500, + content={"error": f"Error processing query: {str(e)}"} + ) + + print(f"🚀 Agent '{self.name}' available at http://{host}:{port}{path}") + + # Start the server if this is the first launch call and autostart is True + if autostart and not _server_started: + _server_started = True + + # Add healthcheck endpoint + @_shared_app.get("/health") + async def healthcheck(): + return {"status": "ok", "agents": list(_registered_agents.keys())} + + # Start the server in a separate thread to not block execution + import threading + def run_server(): + try: + uvicorn.run(_shared_app, host=host, port=port, log_level="debug" if debug else "info") + except Exception as e: + logging.error(f"Error starting server: {str(e)}", exc_info=True) + print(f"❌ Error starting server: {str(e)}") + + server_thread = threading.Thread(target=run_server, daemon=True) + server_thread.start() + + # Give the server a moment to start up + import time + time.sleep(0.5) + + print(f"✅ FastAPI server started at http://{host}:{port}") + print(f"📚 API documentation available at http://{host}:{port}/docs") + + # If blocking is True, keep the main thread alive + if blocking: + print("\nServer is running in blocking mode. Press Ctrl+C to stop...") + try: + while True: + time.sleep(1) + except KeyboardInterrupt: + print("\nServer stopped") + else: + # Note for non-blocking mode + print("\nNote: Server is running in a background thread. To keep it alive, either:") + print("1. Set blocking=True when calling launch()") + print("2. Keep your main application running") + print("3. Use a loop in your code to prevent the program from exiting") + + return None \ No newline at end of file diff --git a/src/praisonai-agents/pyproject.toml b/src/praisonai-agents/pyproject.toml index c025923ad..d43e276e8 100644 --- a/src/praisonai-agents/pyproject.toml +++ b/src/praisonai-agents/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "praisonaiagents" -version = "0.0.77" +version = "0.0.78" description = "Praison AI agents for completing complex tasks with Self Reflection Agents" authors = [ { name="Mervin Praison" } @@ -38,10 +38,17 @@ llm = [ "pydantic>=2.4.2" ] +# Add API dependencies +api = [ + "fastapi>=0.115.0", + "uvicorn>=0.34.0" +] + # Combined features all = [ "praisonaiagents[memory]", "praisonaiagents[knowledge]", "praisonaiagents[llm]", - "praisonaiagents[mcp]" + "praisonaiagents[mcp]", + "praisonaiagents[api]" ] \ No newline at end of file diff --git a/src/praisonai-agents/simple-api-mcp.py b/src/praisonai-agents/simple-api-mcp.py new file mode 100644 index 000000000..857361d4a --- /dev/null +++ b/src/praisonai-agents/simple-api-mcp.py @@ -0,0 +1,8 @@ +from praisonaiagents import Agent, MCP + +search_agent = Agent( + instructions="""You are a weather agent that can provide weather information for a given city.""", + llm="openai/gpt-4o-mini", + tools=MCP("http://localhost:8080/sse") +) +search_agent.launch(path="/weather", port=3030) \ No newline at end of file diff --git a/src/praisonai-agents/simple-api.py b/src/praisonai-agents/simple-api.py new file mode 100644 index 000000000..47b56b36f --- /dev/null +++ b/src/praisonai-agents/simple-api.py @@ -0,0 +1,4 @@ +from praisonaiagents import Agent + +agent = Agent(instructions="""You are a helpful assistant.""", llm="gpt-4o-mini") +agent.launch(path="/ask", port=3030) \ No newline at end of file diff --git a/src/praisonai-agents/uv.lock b/src/praisonai-agents/uv.lock index 4a00693dc..707765b10 100644 --- a/src/praisonai-agents/uv.lock +++ b/src/praisonai-agents/uv.lock @@ -1883,7 +1883,7 @@ wheels = [ [[package]] name = "praisonaiagents" -version = "0.0.77" +version = "0.0.78" source = { editable = "." } dependencies = [ { name = "mcp" }, @@ -1896,11 +1896,17 @@ dependencies = [ all = [ { name = "chonkie" }, { name = "chromadb" }, + { name = "fastapi" }, { name = "litellm" }, { name = "markitdown" }, { name = "mcp" }, { name = "mem0ai" }, { name = "pydantic" }, + { name = "uvicorn" }, +] +api = [ + { name = "fastapi" }, + { name = "uvicorn" }, ] knowledge = [ { name = "chonkie" }, @@ -1924,12 +1930,14 @@ requires-dist = [ { name = "chonkie", marker = "extra == 'knowledge'", specifier = ">=1.0.2" }, { name = "chromadb", marker = "extra == 'knowledge'", specifier = "==0.5.23" }, { name = "chromadb", marker = "extra == 'memory'", specifier = ">=0.5.23" }, + { name = "fastapi", marker = "extra == 'api'", specifier = ">=0.115.0" }, { name = "litellm", marker = "extra == 'llm'", specifier = ">=1.50.0" }, { name = "markitdown", extras = ["all"], marker = "extra == 'knowledge'" }, { name = "mcp", specifier = ">=1.6.0" }, { name = "mcp", marker = "extra == 'mcp'", specifier = ">=1.6.0" }, { name = "mem0ai", marker = "extra == 'knowledge'", specifier = ">=0.1.0" }, { name = "openai" }, + { name = "praisonaiagents", extras = ["api"], marker = "extra == 'all'" }, { name = "praisonaiagents", extras = ["knowledge"], marker = "extra == 'all'" }, { name = "praisonaiagents", extras = ["llm"], marker = "extra == 'all'" }, { name = "praisonaiagents", extras = ["mcp"], marker = "extra == 'all'" }, @@ -1937,6 +1945,7 @@ requires-dist = [ { name = "pydantic" }, { name = "pydantic", marker = "extra == 'llm'", specifier = ">=2.4.2" }, { name = "rich" }, + { name = "uvicorn", marker = "extra == 'api'", specifier = ">=0.34.0" }, ] [[package]]