agent-mcp 0.1.2__py3-none-any.whl → 0.1.4__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. agent_mcp/__init__.py +16 -0
  2. agent_mcp/camel_mcp_adapter.py +521 -0
  3. agent_mcp/cli.py +47 -0
  4. agent_mcp/crewai_mcp_adapter.py +281 -0
  5. agent_mcp/enhanced_mcp_agent.py +601 -0
  6. agent_mcp/heterogeneous_group_chat.py +798 -0
  7. agent_mcp/langchain_mcp_adapter.py +458 -0
  8. agent_mcp/langgraph_mcp_adapter.py +325 -0
  9. agent_mcp/mcp_agent.py +658 -0
  10. agent_mcp/mcp_decorator.py +257 -0
  11. agent_mcp/mcp_langgraph.py +733 -0
  12. agent_mcp/mcp_transaction.py +97 -0
  13. agent_mcp/mcp_transport.py +706 -0
  14. agent_mcp/mcp_transport_enhanced.py +46 -0
  15. agent_mcp/proxy_agent.py +24 -0
  16. agent_mcp-0.1.4.dist-info/METADATA +333 -0
  17. agent_mcp-0.1.4.dist-info/RECORD +49 -0
  18. {agent_mcp-0.1.2.dist-info → agent_mcp-0.1.4.dist-info}/WHEEL +1 -1
  19. agent_mcp-0.1.4.dist-info/entry_points.txt +2 -0
  20. agent_mcp-0.1.4.dist-info/top_level.txt +3 -0
  21. demos/__init__.py +1 -0
  22. demos/basic/__init__.py +1 -0
  23. demos/basic/framework_examples.py +108 -0
  24. demos/basic/langchain_camel_demo.py +272 -0
  25. demos/basic/simple_chat.py +355 -0
  26. demos/basic/simple_integration_example.py +51 -0
  27. demos/collaboration/collaborative_task_example.py +437 -0
  28. demos/collaboration/group_chat_example.py +130 -0
  29. demos/collaboration/simplified_crewai_example.py +39 -0
  30. demos/langgraph/autonomous_langgraph_network.py +808 -0
  31. demos/langgraph/langgraph_agent_network.py +415 -0
  32. demos/langgraph/langgraph_collaborative_task.py +619 -0
  33. demos/langgraph/langgraph_example.py +227 -0
  34. demos/langgraph/run_langgraph_examples.py +213 -0
  35. demos/network/agent_network_example.py +381 -0
  36. demos/network/email_agent.py +130 -0
  37. demos/network/email_agent_demo.py +46 -0
  38. demos/network/heterogeneous_network_example.py +216 -0
  39. demos/network/multi_framework_example.py +199 -0
  40. demos/utils/check_imports.py +49 -0
  41. demos/workflows/autonomous_agent_workflow.py +248 -0
  42. demos/workflows/mcp_features_demo.py +353 -0
  43. demos/workflows/run_agent_collaboration_demo.py +63 -0
  44. demos/workflows/run_agent_collaboration_with_logs.py +396 -0
  45. demos/workflows/show_agent_interactions.py +107 -0
  46. demos/workflows/simplified_autonomous_demo.py +74 -0
  47. functions/main.py +144 -0
  48. functions/mcp_network_server.py +513 -0
  49. functions/utils.py +47 -0
  50. agent_mcp-0.1.2.dist-info/METADATA +0 -475
  51. agent_mcp-0.1.2.dist-info/RECORD +0 -5
  52. agent_mcp-0.1.2.dist-info/entry_points.txt +0 -2
  53. agent_mcp-0.1.2.dist-info/top_level.txt +0 -1
functions/main.py ADDED
@@ -0,0 +1,144 @@
1
+ #!/usr/bin/env python
2
+ # -*- coding: utf-8 -*-
3
+ import sys
4
+ import os
5
+ import asyncio
6
+ import uuid
7
+
8
+ # Adjust path for Cloud Functions environment if necessary
9
+ if os.environ.get('FUNCTIONS_EMULATOR') != 'true' and '/workspace' not in sys.path:
10
+ sys.path.insert(0, '/workspace')
11
+
12
+ import firebase_functions.options
13
+ from firebase_functions.https_fn import on_request, Request
14
+ # from firebase_functions.https_fn import AsgiRequestAdapter # Not available in 0.4.2
15
+ # from firebase_functions.storage_fn import StorageObjectData, on_object_finalized # Not needed
16
+
17
+ # Import your FastAPI app and Mangum
18
+ from fastapi import FastAPI
19
+ from mcp_network_server import router as network_router, find_firebase_key
20
+ from mangum import Mangum
21
+ import base64 # Needed for body encoding
22
+ import urllib.parse # Needed for query string parsing
23
+
24
+ firebase_functions.options.set_global_options(region=firebase_functions.options.SupportedRegion.EUROPE_WEST1)
25
+
26
+ # Initialize Firebase once
27
+ find_firebase_key()
28
+
29
+ # Create a FastAPI app
30
+ app = FastAPI()
31
+
32
+ # --- Root endpoint (Define BEFORE including router) ---
33
+ @app.get("/")
34
+ async def root():
35
+ """Basic health check endpoint"""
36
+ return {"message": "MCP Network Server is running!"}
37
+
38
+ # Include the router from the network server
39
+ app.include_router(network_router) # This adds routes like /register, /message/*, etc.
40
+
41
+ # Create a Mangum handler
42
+ handler = Mangum(app, lifespan="off") # lifespan="off" can sometimes help in simple cases
43
+
44
+ @on_request()
45
+ def mcp_server(req: Request) -> any:
46
+ """Handles incoming HTTPS requests by passing them to the FastAPI app via Mangum.
47
+
48
+ Manually constructs an AWS API Gateway V1 event dictionary for Mangum.
49
+ """
50
+ # Ensure an asyncio event loop exists for the current thread
51
+ try:
52
+ loop = asyncio.get_event_loop_policy().get_event_loop()
53
+ except RuntimeError:
54
+ loop = asyncio.new_event_loop()
55
+ asyncio.set_event_loop(loop)
56
+ print("Created new asyncio event loop for this thread.")
57
+
58
+ # Manually construct an event dictionary resembling AWS API Gateway v1 format
59
+ try:
60
+ query_string = urllib.parse.urlencode(req.args)
61
+ body = req.get_data()
62
+ is_base64_encoded = False
63
+ try:
64
+ # Attempt to decode as UTF-8. If it fails, assume binary and base64 encode.
65
+ body_str = body.decode('utf-8')
66
+ except UnicodeDecodeError:
67
+ body = base64.b64encode(body)
68
+ body_str = body.decode('utf-8')
69
+ is_base64_encoded = True
70
+
71
+ # Process multi-value headers correctly by iterating over headers
72
+ multi_value_headers_dict = {}
73
+ for k, v in req.headers:
74
+ # Normalize header keys to lowercase for consistency
75
+ key_lower = k.lower()
76
+ if key_lower not in multi_value_headers_dict:
77
+ multi_value_headers_dict[key_lower] = []
78
+ multi_value_headers_dict[key_lower].append(v)
79
+
80
+ # Use the lowercase version for single-value headers as well for consistency
81
+ single_value_headers_dict = {k.lower(): v for k, v in req.headers}
82
+
83
+ event = {
84
+ "httpMethod": req.method,
85
+ "path": req.path,
86
+ "queryStringParameters": req.args.to_dict(), # Simple dict for single values
87
+ "headers": single_value_headers_dict, # Simple dict with lowercase keys
88
+ "body": body_str,
89
+ "isBase64Encoded": is_base64_encoded,
90
+ # Minimal required context keys (might need more for complex apps)
91
+ "requestContext": {
92
+ "httpMethod": req.method,
93
+ "path": req.path,
94
+ "requestId": "firebase-function-invocation-" + str(uuid.uuid4()), # More unique ID
95
+ "stage": "prod", # Dummy value
96
+ "apiId": "dummyApiId", # Dummy value
97
+ # Add source IP if available and non-internal
98
+ "identity": {
99
+ "sourceIp": req.remote_addr if req.remote_addr else "127.0.0.1"
100
+ }
101
+ },
102
+ "multiValueQueryStringParameters": {k: vlist for k, vlist in req.args.lists()},
103
+ "multiValueHeaders": multi_value_headers_dict, # Use the correctly processed dict
104
+ "resource": req.path, # Set resource to match the actual path
105
+ "pathParameters": None, # Assuming no path parameters captured by Firebase Function trigger
106
+ }
107
+
108
+ # Minimal context object (often empty is fine)
109
+ context = {}
110
+
111
+ # Call Mangum handler
112
+ response_data = handler(event, context)
113
+
114
+ # Convert Mangum's response dict back to a Firebase/Flask Response object
115
+ response_headers = response_data.get("headers", {})
116
+ if "multiValueHeaders" in response_data:
117
+ flat_headers = {}
118
+ for key, values in response_data["multiValueHeaders"].items():
119
+ # Use the last value for simplicity, matching typical WSGI behavior
120
+ if values:
121
+ flat_headers[key] = values[-1]
122
+ response_headers = flat_headers # Use the flattened headers
123
+
124
+ response_body = response_data.get("body", "")
125
+ if response_data.get("isBase64Encoded", False):
126
+ # Decode body if Mangum base64 encoded it
127
+ response_body = base64.b64decode(response_body).decode('utf-8')
128
+
129
+ return {
130
+ "statusCode": response_data.get("statusCode", 500),
131
+ "headers": response_headers,
132
+ "body": response_body,
133
+ }
134
+
135
+ except Exception as e:
136
+ # Log the detailed error for debugging
137
+ import traceback
138
+ print(f"Error constructing event or calling handler: {e}")
139
+ traceback.print_exc()
140
+ # Return a generic 500 error
141
+ from flask import Response
142
+ return Response("Internal Server Error", status=500)
143
+
144
+ # Storage trigger placeholder - Removed
@@ -0,0 +1,513 @@
1
+ """
2
+ MCP Network Server - Central hub for agent communication.
3
+ """
4
+
5
+ from fastapi import Request, Depends, HTTPException, APIRouter, Response
6
+ from fastapi.security import HTTPBearer
7
+ from fastapi.responses import StreamingResponse, JSONResponse
8
+ from firebase_admin import initialize_app, get_app, credentials
9
+ from firebase_admin import firestore
10
+ import uvicorn
11
+ import asyncio
12
+ import time
13
+ from datetime import datetime, timedelta, timezone
14
+ #from google.cloud.firestore_v1.types.document import DocumentSnapshot
15
+ #from google.cloud._helpers import _datetime_to_pb_timestamp
16
+ from typing import Dict, Set, Any, Optional, Union
17
+ import json
18
+ import os
19
+ import sys
20
+ from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
21
+ import jwt
22
+ from functools import lru_cache
23
+ from dotenv import load_dotenv
24
+ from utils import convert_timestamps_to_isoformat
25
+ import logging
26
+ from fastapi import Query
27
+ from pydantic import BaseModel
28
+ from typing import List
29
+
30
+ # Set up logging
31
+ logger = logging.getLogger(__name__)
32
+
33
+ # Try to load .env file for local development
34
+ load_dotenv()
35
+
36
+ # Check if we're running in Firebase Cloud Functions
37
+ try:
38
+ import firebase_functions
39
+ IS_FIREBASE = True
40
+ except ImportError:
41
+ IS_FIREBASE = False
42
+
43
+ # Initialize Firebase
44
+ db = None
45
+
46
+ def find_firebase_key():
47
+ """Find and validate the Firebase key file location."""
48
+ # The exact path that Firebase Functions emulator expects
49
+ key_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'firebase', 'firebase-key.json')
50
+ print(f"Using key path: {key_path}")
51
+
52
+ if not os.path.exists(key_path):
53
+ raise FileNotFoundError(
54
+ f"\nFirebase key not found at {key_path}\n"
55
+ "Please ensure firebase-key.json exists in the functions/firebase/ directory.\n"
56
+ )
57
+ return key_path
58
+
59
+ try:
60
+ # Check if we're running in Firebase Functions
61
+ if IS_FIREBASE:
62
+ print("Running in Firebase Functions environment")
63
+ initialize_app()
64
+ db = firestore.client()
65
+ print("Initialized Firebase Admin using default credentials (sync client)")
66
+ else:
67
+ # Get the key path
68
+ key_path = find_firebase_key()
69
+
70
+ # Initialize Firebase with the key
71
+ print(f"Initializing Firebase Admin with key: {key_path}")
72
+ os.environ['GOOGLE_APPLICATION_CREDENTIALS'] = key_path
73
+ cred = credentials.Certificate(key_path)
74
+ initialize_app(cred)
75
+ db = firestore.client()
76
+ print("Firestore client obtained successfully (sync client)")
77
+
78
+ except Exception as e:
79
+ print(f"CRITICAL Error during Firebase Admin initialization: {e}", file=sys.stderr)
80
+ print("Please ensure you have valid Firebase credentials set up.", file=sys.stderr)
81
+ raise # Re-raise to prevent starting with invalid credentials
82
+
83
+ # Configuration management
84
+ @lru_cache()
85
+ def get_config():
86
+ """Get configuration using os.getenv for both local and Firebase"""
87
+ if IS_FIREBASE:
88
+ print("Loading config using os.getenv (Firebase environment)")
89
+ # Firebase Functions V2 automatically loads `firebase functions:config:set`
90
+ # values into environment variables, converting keys like `api.openai_key`
91
+ # to `API_OPENAI_KEY`.
92
+ openai_api_key = os.getenv('API_OPENAI_KEY')
93
+ gemini_api_key = os.getenv('API_GEMINI_KEY')
94
+ jwt_secret = os.getenv('JWT_SECRET')
95
+ server_port = int(os.getenv('SERVER_PORT', '8000')) # Default if not set in config
96
+ server_host = os.getenv('SERVER_HOST', '0.0.0.0') # Default if not set
97
+ jwt_expiration_minutes = int(os.getenv('JWT_EXPIRATION_MINUTES', '60')) # Default if not set
98
+ else:
99
+ # Local development uses .env file loaded by dotenv
100
+ print("Loading config from local .env file using os.getenv")
101
+ openai_api_key = os.getenv('OPENAI_API_KEY') # Standard name for .env
102
+ gemini_api_key = os.getenv('GOOGLE_GEMINI_API_KEY') # Standard name for .env
103
+ jwt_secret = os.getenv('JWT_SECRET')
104
+ server_port = int(os.getenv('PORT', '8000')) # Local often uses PORT
105
+ server_host = os.getenv('HOST', '0.0.0.0')
106
+ jwt_expiration_minutes = int(os.getenv('JWT_EXPIRATION_MINUTES', '60'))
107
+
108
+ # --- Logging ---
109
+ # Use consistent logging regardless of environment
110
+ env_type = "Firebase" if IS_FIREBASE else "Local"
111
+ print(f"{env_type} Env Config: API_OPENAI_KEY retrieved: {'present' if openai_api_key else 'MISSING'}", file=sys.stderr)
112
+ print(f"{env_type} Env Config: API_GEMINI_KEY retrieved: {'present' if gemini_api_key else 'MISSING'}", file=sys.stderr)
113
+ print(f"{env_type} Env Config: JWT_SECRET retrieved: {'present' if jwt_secret else 'MISSING'}", file=sys.stderr)
114
+ print(f"{env_type} Env Config: SERVER_PORT retrieved: {server_port}", file=sys.stderr)
115
+ print(f"{env_type} Env Config: SERVER_HOST retrieved: {server_host}", file=sys.stderr)
116
+ print(f"{env_type} Env Config: JWT_EXPIRATION_MINUTES retrieved: {jwt_expiration_minutes}", file=sys.stderr)
117
+
118
+ # --- Check JWT Secret --- (Crucial check)
119
+ if not jwt_secret:
120
+ print(f"CRITICAL ERROR in get_config ({env_type}): JWT_SECRET is missing! Check environment variables / Firebase config.", file=sys.stderr)
121
+ # In a real scenario, you might raise an error or prevent startup
122
+ # raise ValueError("JWT_SECRET is required but not found")
123
+
124
+ return {
125
+ 'openai_api_key': openai_api_key,
126
+ 'gemini_api_key': gemini_api_key,
127
+ 'server_port': server_port,
128
+ 'server_host': server_host,
129
+ 'jwt_secret': jwt_secret,
130
+ 'jwt_expiration_minutes': jwt_expiration_minutes
131
+ }
132
+
133
+ # JWT Configuration
134
+ JWT_ALGORITHM = os.getenv("JWT_ALGORITHM", "HS256")
135
+
136
+ router = APIRouter()
137
+ security = HTTPBearer()
138
+
139
+ class MessageQueue:
140
+ def __init__(self):
141
+ self.messages_ref = db.collection('messages')
142
+
143
+ def push_message(self, target_id: str, message: dict):
144
+ """Push a message to the target's queue"""
145
+ try:
146
+ # Always set timestamp to ensure consistency
147
+ message['timestamp'] = datetime.utcnow().replace(tzinfo=timezone.utc)
148
+
149
+ # Add acknowledged flag
150
+ message['acknowledged'] = False
151
+
152
+ # Create a new document reference with auto-generated ID
153
+ queue_ref = self.messages_ref.document(target_id).collection('queue')
154
+ doc_ref = queue_ref.document()
155
+ message_id = doc_ref.id
156
+
157
+ # Add the ID to the message before saving
158
+ message['id'] = message_id
159
+
160
+ # Save the message
161
+ doc_ref.set(message)
162
+ print(f"Pushed message {message_id} to {target_id}")
163
+ return message_id
164
+ except Exception as e:
165
+ print(f"Error pushing message to {target_id}: {e}")
166
+ raise HTTPException(status_code=500, detail=f"Error pushing message: {e}")
167
+
168
+ def get_messages(self, agent_id: str, last_message_id: str = None) -> list:
169
+ """Get all unacknowledged messages for an agent after the last_message_id"""
170
+ messages = []
171
+ try:
172
+ print(f"\n====== FETCHING MESSAGES FOR {agent_id} ======")
173
+ print(f"Last message ID: {last_message_id}")
174
+
175
+ # Build the query
176
+ query = self.messages_ref.document(agent_id).collection('queue')
177
+
178
+ # Only get unacknowledged messages
179
+ query = query.where('acknowledged', '==', False)
180
+
181
+ # Order by timestamp descending to get newest first
182
+ query = query.order_by('timestamp', direction=firestore.Query.DESCENDING)
183
+
184
+ # Get current time in UTC
185
+ current_time = datetime.utcnow().replace(tzinfo=timezone.utc)
186
+ cutoff_time = current_time - timedelta(minutes=1) # Only get messages from last 1 minute
187
+
188
+ # Add timestamp filter to only get recent messages
189
+ query = query.where('timestamp', '>', cutoff_time)
190
+
191
+ # If we have a last_message_id, get the timestamp of that message
192
+ if last_message_id:
193
+ try:
194
+ # Correctly get the document from the collection reference
195
+ last_msg_ref = self.messages_ref.document(agent_id).collection('queue').document(last_message_id)
196
+ last_msg = last_msg_ref.get()
197
+ if last_msg.exists:
198
+ last_timestamp = last_msg.to_dict().get('timestamp')
199
+ if last_timestamp:
200
+ # Only get messages after the last one
201
+ query = query.where('timestamp', '>', last_timestamp)
202
+ except Exception as e:
203
+ print(f"Error getting last message {last_message_id}: {e}")
204
+
205
+ # Limit to 10 messages at a time
206
+ query = query.limit(10)
207
+
208
+ # Process documents
209
+ for doc in query.stream():
210
+ msg_data = doc.to_dict()
211
+ msg_data['id'] = doc.id
212
+ msg_data = convert_timestamps_to_isoformat(msg_data)
213
+ messages.append(msg_data)
214
+
215
+ # Log message details
216
+ print(f"\n----- Message {msg_data['id']} -----")
217
+ print(f"Type: {msg_data.get('type', 'unknown')}")
218
+ print(f"Task ID: {msg_data.get('task_id', 'unknown')}")
219
+ print(f"Acknowledged: {msg_data.get('acknowledged', False)}")
220
+ print(f"Timestamp: {msg_data.get('timestamp', 'unknown')}")
221
+ if 'content' in msg_data:
222
+ print(f"Content: {json.dumps(msg_data['content'], indent=2)}")
223
+ if 'description' in msg_data:
224
+ print(f"Description: {msg_data['description']}")
225
+ if 'reply_to' in msg_data:
226
+ print(f"Reply To: {msg_data['reply_to']}")
227
+ print("-" * 40)
228
+
229
+ print(f"Found {len(messages)} messages")
230
+ print("====== END MESSAGES ======\n")
231
+ return messages
232
+
233
+ except Exception as e:
234
+ print(f"Error getting messages for {agent_id}: {e}")
235
+ return []
236
+
237
+ def delete_message(self, target_id: str, message_id: str):
238
+ """Delete a message from the target's queue"""
239
+ try:
240
+ # Use synchronous delete
241
+ self.messages_ref.document(target_id).collection('queue').document(message_id).delete()
242
+ print(f"Deleted message {message_id} for {target_id}")
243
+ except Exception as e:
244
+ print(f"Error deleting message {message_id} for {target_id}: {e}")
245
+ raise HTTPException(status_code=500, detail=f"Error deleting message: {e}")
246
+
247
+ def acknowledge_message(self, target_id: str, message_id: str):
248
+ """Mark a message as acknowledged"""
249
+ try:
250
+ # Use synchronous update
251
+ doc_ref = self.messages_ref.document(target_id).collection('queue').document(message_id)
252
+ doc_ref.update({'acknowledged': True, 'acknowledged_at': datetime.utcnow().replace(tzinfo=timezone.utc)}) # doc_ref.update({'acknowledged': True, 'acknowledged_at': firestore.SERVER_TIMESTAMP})
253
+ print(f"Acknowledged message {message_id} for {target_id}")
254
+ except Exception as e:
255
+ print(f"Error acknowledging message {message_id} for {target_id}: {e}")
256
+ raise HTTPException(status_code=500, detail=f"Error acknowledging message: {e}")
257
+
258
+
259
+ class AgentRegistry:
260
+ def __init__(self, db):
261
+ self.agents_ref = db.collection('agents')
262
+
263
+ def register(self, agent_id: str, info: dict):
264
+ """Register an agent"""
265
+ try:
266
+ agent_data = {
267
+ 'info': info,
268
+ 'registered_at': datetime.utcnow().isoformat(),
269
+ 'last_heartbeat': datetime.utcnow().isoformat(),
270
+ 'last_seen': datetime.utcnow().isoformat() # Keep this for backward compatibility
271
+ }
272
+ # Run Firestore operations in a thread to avoid blocking
273
+ self.agents_ref.document(agent_id).set(agent_data)
274
+ print(f"Agent {agent_id} registered successfully")
275
+ return agent_id
276
+ except Exception as e:
277
+ print(f"Error registering agent {agent_id}: {e}")
278
+ raise e
279
+
280
+ def get_agent_info(self, agent_id: str):
281
+ """Get agent info"""
282
+ try:
283
+ doc = self.agents_ref.document(agent_id).get()
284
+ if doc.exists:
285
+ return doc.to_dict().get('info')
286
+ return None
287
+ except Exception as e:
288
+ print(f"Error getting agent info for {agent_id}: {e}")
289
+ return None
290
+
291
+ def heartbeat(self, agent_id: str):
292
+ """Update agent's heartbeat"""
293
+ try:
294
+ self.agents_ref.document(agent_id).update({
295
+ 'last_heartbeat': datetime.utcnow().isoformat(),
296
+ 'last_seen': datetime.utcnow().isoformat()
297
+ })
298
+ except Exception as e:
299
+ print(f"Error updating heartbeat for {agent_id}: {e}")
300
+
301
+ def list_agents(self):
302
+ """List all agents"""
303
+ try:
304
+ docs = self.agents_ref.stream()
305
+ return [{"id": doc.id, **doc.to_dict()} for doc in docs]
306
+ except Exception as e:
307
+ print(f"Error listing agents: {e}")
308
+ return []
309
+
310
+ # Initialize services
311
+ message_queue = MessageQueue()
312
+ agent_registry = AgentRegistry(db)
313
+
314
+ async def verify_token(credentials: HTTPAuthorizationCredentials = Depends(security)):
315
+ try:
316
+ print(f"Verifying token: {credentials.credentials[:10]}...") # Only print first 10 chars for security
317
+ # Get secret from config
318
+ config = get_config()
319
+ jwt_secret = config.get('jwt_secret')
320
+ if not jwt_secret:
321
+ print("ERROR in verify_token: JWT Secret not found in config")
322
+ raise HTTPException(status_code=500, detail="Server configuration error: JWT secret missing.")
323
+
324
+ token_data = jwt.decode(credentials.credentials, jwt_secret, algorithms=[JWT_ALGORITHM])
325
+ print(f"Token decoded successfully: {token_data}")
326
+ agent_id = token_data.get("agent_id")
327
+ if not agent_id:
328
+ print("Token missing agent_id")
329
+ raise HTTPException(status_code=401, detail="Invalid token")
330
+ return token_data
331
+ except Exception as e:
332
+ print(f"Token verification failed: {str(e)}")
333
+ raise HTTPException(status_code=401, detail="Invalid token")
334
+
335
+ @router.post("/register")
336
+ async def register_agent(request: Request):
337
+ """Register an agent"""
338
+ try:
339
+ # Await request.json()
340
+ data = await request.json()
341
+ agent_id = data.get('agent_id')
342
+ info = data.get('info', {})
343
+
344
+ if not agent_id:
345
+ raise HTTPException(status_code=400, detail="Missing agent_id")
346
+
347
+ # Register agent
348
+ agent_registry.register(agent_id, info)
349
+
350
+ # Generate token
351
+ expiration = datetime.utcnow() + timedelta(minutes=int(os.getenv('JWT_EXPIRATION_MINUTES', '60')))
352
+ token_data = {
353
+ 'agent_id': agent_id,
354
+ 'type': None,
355
+ 'exp': expiration
356
+ }
357
+ print(f"Generating token with data: {token_data}")
358
+ token = jwt.encode(
359
+ token_data,
360
+ get_config().get('jwt_secret'),
361
+ algorithm=JWT_ALGORITHM
362
+ )
363
+ print(f"Generated token: {token[:10]}...")
364
+
365
+ response = {
366
+ 'status': 'registered',
367
+ 'agent_id': agent_id,
368
+ 'token': token
369
+ }
370
+ return response
371
+ except Exception as e:
372
+ print(f"Registration error: {str(e)}")
373
+ raise HTTPException(status_code=500, detail=str(e))
374
+
375
+ @router.post("/message/{target_id}")
376
+ async def handle_message(
377
+ target_id: str,
378
+ request: Request,
379
+ token_data: dict = Depends(verify_token)
380
+ ):
381
+ """Handle message delivery to target agent"""
382
+ # Verify target exists
383
+ target = agent_registry.get_agent_info(target_id)
384
+ if not target:
385
+ raise HTTPException(status_code=404, detail="Agent not found")
386
+
387
+ # Await request.json()
388
+ message = await request.json()
389
+ message["from"] = token_data["agent_id"]
390
+
391
+ # Store message
392
+ message_id = message_queue.push_message(target_id, message)
393
+
394
+ return {"status": "delivered", "message_id": message_id}
395
+
396
+ @router.get("/messages/{agent_id}")
397
+ async def get_messages_endpoint(agent_id: str, last_message_id: Optional[str] = Query(None), token_data: dict = Depends(verify_token)):
398
+ """Endpoint for clients to poll for new messages.
399
+
400
+ Args:
401
+ agent_id: The ID of the agent requesting messages.
402
+ last_message_id: The ID of the last message the client received (optional).
403
+ token_data: Decoded JWT token data.
404
+
405
+ Returns:
406
+ A list of new messages.
407
+
408
+ Raises:
409
+ HTTPException: 403 if token doesn't match agent_id.
410
+ HTTPException: 500 if there's an error fetching messages.
411
+ """
412
+ # Verify the token belongs to the agent asking for messages
413
+ if token_data["agent_id"] != agent_id:
414
+ logger.warning(f"Auth Error: Token agent ({token_data['agent_id']}) mismatch with requested agent_id ({agent_id}) for polling")
415
+ raise HTTPException(status_code=403, detail="Token does not match requested agent ID")
416
+
417
+ logger.info(f"Polling request for {agent_id}. Last message ID: {last_message_id}")
418
+
419
+ try:
420
+ message_queue = MessageQueue() # Instantiate locally for the request
421
+ # Run the synchronous Firestore call in a thread pool executor
422
+ loop = asyncio.get_event_loop()
423
+ messages = await loop.run_in_executor(
424
+ None, # Use default executor
425
+ message_queue.get_messages,
426
+ agent_id,
427
+ last_message_id
428
+ )
429
+
430
+ logger.info(f"Found {len(messages)} messages for {agent_id} after {last_message_id}")
431
+
432
+ # Return empty list if no messages
433
+ if not messages:
434
+ logger.debug(f"No messages found for agent {agent_id}")
435
+ return []
436
+
437
+ # Unwrap Firestore documents into a consistent format
438
+ formatted_messages = []
439
+ for msg in messages:
440
+ # Convert Firestore document to dict if needed
441
+ msg_dict = msg.to_dict() if hasattr(msg, 'to_dict') else msg
442
+ logger.info(f"Processing message for {agent_id}: {json.dumps(msg_dict, indent=2)}")
443
+
444
+ # Skip invalid messages
445
+ if not isinstance(msg_dict, dict):
446
+ logger.warning(f"Skipping invalid message format for {agent_id}: {msg_dict}")
447
+ continue
448
+
449
+ # Get message ID
450
+ msg_id = msg_dict.get('id')
451
+ if not msg_id and hasattr(msg, 'id'):
452
+ msg_id = msg.id
453
+ logger.debug(f"Using Firestore document ID for message: {msg_id}")
454
+
455
+ # Skip messages without ID
456
+ if not msg_id:
457
+ logger.warning(f"Skipping message without ID for {agent_id}: {msg_dict}")
458
+ continue
459
+
460
+ # Format message with required fields
461
+ formatted_msg = {
462
+ 'id': msg_id,
463
+ 'type': msg_dict.get('type', 'message'), # Default to 'message' type
464
+ 'content': msg_dict.get('content', {'text': 'No content provided'}), # Always provide content
465
+ 'timestamp': msg_dict.get('timestamp', datetime.utcnow().isoformat()), # Default to current time
466
+ 'from': msg_dict.get('from', 'unknown'), # Default to unknown sender
467
+ }
468
+
469
+ # Include optional fields if present
470
+ for field in ['task_id', 'description', 'reply_to', 'result']:
471
+ if field in msg_dict:
472
+ formatted_msg[field] = msg_dict[field]
473
+
474
+ logger.info(f"Formatted message for {agent_id}: {json.dumps(formatted_msg, indent=2)}")
475
+ formatted_messages.append(formatted_msg)
476
+
477
+ return formatted_messages
478
+ except Exception as e:
479
+ logger.error(f"Error fetching messages for {agent_id}: {e}", exc_info=True)
480
+ raise HTTPException(status_code=500, detail=f"Failed to fetch messages: {str(e)}")
481
+
482
+ @router.post("/message/{agent_id}/acknowledge/{message_id}")
483
+ async def acknowledge_message(
484
+ agent_id: str,
485
+ message_id: str,
486
+ request: Request,
487
+ token_data: dict = Depends(verify_token)
488
+ ):
489
+ """Acknowledge message receipt and processing"""
490
+ print(f"[{agent_id}] Received acknowledgment request for message {message_id}")
491
+
492
+ if token_data["agent_id"] != agent_id:
493
+ print(f"[{agent_id}] Authorization failed for message {message_id}. Token agent_id: {token_data['agent_id']}")
494
+ raise HTTPException(status_code=403, detail="Not authorized")
495
+
496
+ try:
497
+ # Mark message as acknowledged
498
+ print(f"[{agent_id}] Attempting to acknowledge message {message_id}")
499
+ message_queue.acknowledge_message(agent_id, message_id)
500
+ print(f"[{agent_id}] Successfully acknowledged message {message_id}")
501
+ return {"status": "acknowledged", "message_id": message_id}
502
+ except Exception as e:
503
+ print(f"[{agent_id}] Error acknowledging message {message_id}: {e}")
504
+ raise HTTPException(status_code=500, detail=str(e))
505
+
506
+ @router.get("/agents")
507
+ async def list_agents(token_data: dict = Depends(verify_token)):
508
+ """List all connected agents"""
509
+ agents = agent_registry.list_agents()
510
+ return {"agents": agents}
511
+
512
+ if __name__ == "__main__":
513
+ uvicorn.run(router, host=get_config()['server_host'], port=get_config()['server_port'])