webagents 0.1.0__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 (94) hide show
  1. webagents/__init__.py +18 -0
  2. webagents/__main__.py +55 -0
  3. webagents/agents/__init__.py +13 -0
  4. webagents/agents/core/__init__.py +19 -0
  5. webagents/agents/core/base_agent.py +1834 -0
  6. webagents/agents/core/handoffs.py +293 -0
  7. webagents/agents/handoffs/__init__.py +0 -0
  8. webagents/agents/interfaces/__init__.py +0 -0
  9. webagents/agents/lifecycle/__init__.py +0 -0
  10. webagents/agents/skills/__init__.py +109 -0
  11. webagents/agents/skills/base.py +136 -0
  12. webagents/agents/skills/core/__init__.py +8 -0
  13. webagents/agents/skills/core/guardrails/__init__.py +0 -0
  14. webagents/agents/skills/core/llm/__init__.py +0 -0
  15. webagents/agents/skills/core/llm/anthropic/__init__.py +1 -0
  16. webagents/agents/skills/core/llm/litellm/__init__.py +10 -0
  17. webagents/agents/skills/core/llm/litellm/skill.py +538 -0
  18. webagents/agents/skills/core/llm/openai/__init__.py +1 -0
  19. webagents/agents/skills/core/llm/xai/__init__.py +1 -0
  20. webagents/agents/skills/core/mcp/README.md +375 -0
  21. webagents/agents/skills/core/mcp/__init__.py +15 -0
  22. webagents/agents/skills/core/mcp/skill.py +731 -0
  23. webagents/agents/skills/core/memory/__init__.py +11 -0
  24. webagents/agents/skills/core/memory/long_term_memory/__init__.py +10 -0
  25. webagents/agents/skills/core/memory/long_term_memory/memory_skill.py +639 -0
  26. webagents/agents/skills/core/memory/short_term_memory/__init__.py +9 -0
  27. webagents/agents/skills/core/memory/short_term_memory/skill.py +341 -0
  28. webagents/agents/skills/core/memory/vector_memory/skill.py +447 -0
  29. webagents/agents/skills/core/planning/__init__.py +9 -0
  30. webagents/agents/skills/core/planning/planner.py +343 -0
  31. webagents/agents/skills/ecosystem/__init__.py +0 -0
  32. webagents/agents/skills/ecosystem/crewai/__init__.py +1 -0
  33. webagents/agents/skills/ecosystem/database/__init__.py +1 -0
  34. webagents/agents/skills/ecosystem/filesystem/__init__.py +0 -0
  35. webagents/agents/skills/ecosystem/google/__init__.py +0 -0
  36. webagents/agents/skills/ecosystem/google/calendar/__init__.py +6 -0
  37. webagents/agents/skills/ecosystem/google/calendar/skill.py +306 -0
  38. webagents/agents/skills/ecosystem/n8n/__init__.py +0 -0
  39. webagents/agents/skills/ecosystem/openai_agents/__init__.py +0 -0
  40. webagents/agents/skills/ecosystem/web/__init__.py +0 -0
  41. webagents/agents/skills/ecosystem/zapier/__init__.py +0 -0
  42. webagents/agents/skills/robutler/__init__.py +11 -0
  43. webagents/agents/skills/robutler/auth/README.md +63 -0
  44. webagents/agents/skills/robutler/auth/__init__.py +17 -0
  45. webagents/agents/skills/robutler/auth/skill.py +354 -0
  46. webagents/agents/skills/robutler/crm/__init__.py +18 -0
  47. webagents/agents/skills/robutler/crm/skill.py +368 -0
  48. webagents/agents/skills/robutler/discovery/README.md +281 -0
  49. webagents/agents/skills/robutler/discovery/__init__.py +16 -0
  50. webagents/agents/skills/robutler/discovery/skill.py +230 -0
  51. webagents/agents/skills/robutler/kv/__init__.py +6 -0
  52. webagents/agents/skills/robutler/kv/skill.py +80 -0
  53. webagents/agents/skills/robutler/message_history/__init__.py +9 -0
  54. webagents/agents/skills/robutler/message_history/skill.py +270 -0
  55. webagents/agents/skills/robutler/messages/__init__.py +0 -0
  56. webagents/agents/skills/robutler/nli/__init__.py +13 -0
  57. webagents/agents/skills/robutler/nli/skill.py +687 -0
  58. webagents/agents/skills/robutler/notifications/__init__.py +5 -0
  59. webagents/agents/skills/robutler/notifications/skill.py +141 -0
  60. webagents/agents/skills/robutler/payments/__init__.py +41 -0
  61. webagents/agents/skills/robutler/payments/exceptions.py +255 -0
  62. webagents/agents/skills/robutler/payments/skill.py +610 -0
  63. webagents/agents/skills/robutler/storage/__init__.py +10 -0
  64. webagents/agents/skills/robutler/storage/files/__init__.py +9 -0
  65. webagents/agents/skills/robutler/storage/files/skill.py +445 -0
  66. webagents/agents/skills/robutler/storage/json/__init__.py +9 -0
  67. webagents/agents/skills/robutler/storage/json/skill.py +336 -0
  68. webagents/agents/skills/robutler/storage/kv/skill.py +88 -0
  69. webagents/agents/skills/robutler/storage.py +389 -0
  70. webagents/agents/tools/__init__.py +0 -0
  71. webagents/agents/tools/decorators.py +426 -0
  72. webagents/agents/tracing/__init__.py +0 -0
  73. webagents/agents/workflows/__init__.py +0 -0
  74. webagents/scripts/__init__.py +0 -0
  75. webagents/server/__init__.py +28 -0
  76. webagents/server/context/__init__.py +0 -0
  77. webagents/server/context/context_vars.py +121 -0
  78. webagents/server/core/__init__.py +0 -0
  79. webagents/server/core/app.py +843 -0
  80. webagents/server/core/middleware.py +69 -0
  81. webagents/server/core/models.py +98 -0
  82. webagents/server/core/monitoring.py +59 -0
  83. webagents/server/endpoints/__init__.py +0 -0
  84. webagents/server/interfaces/__init__.py +0 -0
  85. webagents/server/middleware.py +330 -0
  86. webagents/server/models.py +92 -0
  87. webagents/server/monitoring.py +659 -0
  88. webagents/utils/__init__.py +0 -0
  89. webagents/utils/logging.py +359 -0
  90. webagents-0.1.0.dist-info/METADATA +230 -0
  91. webagents-0.1.0.dist-info/RECORD +94 -0
  92. webagents-0.1.0.dist-info/WHEEL +4 -0
  93. webagents-0.1.0.dist-info/entry_points.txt +2 -0
  94. webagents-0.1.0.dist-info/licenses/LICENSE +20 -0
@@ -0,0 +1,843 @@
1
+ """
2
+ FastAPI Server - WebAgents V2.0
3
+
4
+ Production FastAPI server with OpenAI compatibility, dynamic agent routing,
5
+ and comprehensive monitoring.
6
+ """
7
+
8
+ import asyncio
9
+ import time
10
+ import uuid
11
+ from datetime import datetime
12
+ from typing import List, Dict, Any, Optional, Callable, Union, Awaitable
13
+ import inspect
14
+
15
+ import uvicorn
16
+ from fastapi import FastAPI, HTTPException, Request, Response, APIRouter
17
+ from fastapi.middleware.cors import CORSMiddleware
18
+ from ..monitoring import CONTENT_TYPE_LATEST
19
+
20
+ from .models import (
21
+ ChatCompletionRequest, ChatCompletionResponse, AgentInfoResponse,
22
+ HealthResponse, AgentListResponse, ServerStatsResponse
23
+ )
24
+ from .middleware import RequestLoggingMiddleware, RateLimitMiddleware, RateLimitRule
25
+ from .monitoring import initialize_monitoring
26
+ from ..context.context_vars import Context, set_context, create_context
27
+ from ...agents.core.base_agent import BaseAgent
28
+ from ...utils.logging import get_logger
29
+
30
+
31
+ class WebAgentsServer:
32
+ """
33
+ FastAPI server for AI agents with OpenAI compatibility and production monitoring
34
+
35
+ Features:
36
+ - OpenAI-compatible /chat/completions endpoint
37
+ - Streaming and non-streaming support
38
+ - Dynamic agent routing via provided resolver function
39
+ - Context management middleware
40
+ - Health and discovery endpoints
41
+ - Prometheus metrics collection
42
+ - Structured logging and request tracing
43
+ """
44
+
45
+ def __init__(
46
+ self,
47
+ agents: List[BaseAgent] = None,
48
+ dynamic_agents: Optional[Union[Callable[[str], BaseAgent], Callable[[str], Awaitable[Optional[BaseAgent]]]]] = None,
49
+ enable_cors: bool = True,
50
+ title: str = "WebAgents V2 Server",
51
+ description: str = "AI Agent Server with OpenAI Compatibility",
52
+ version: str = "2.0.0",
53
+ url_prefix: str = "",
54
+ # Middleware configuration
55
+ request_timeout: float = 300.0,
56
+ enable_rate_limiting: bool = True,
57
+ default_rate_limit: RateLimitRule = None,
58
+ user_rate_limits: Dict[str, RateLimitRule] = None,
59
+ enable_request_logging: bool = True,
60
+ # Monitoring configuration
61
+ enable_monitoring: bool = True,
62
+ enable_prometheus: bool = True,
63
+ enable_structured_logging: bool = True,
64
+ metrics_port: int = 9090
65
+ ):
66
+ """
67
+ Initialize WebAgents server
68
+
69
+ Args:
70
+ agents: List of static Agent instances (optional)
71
+ dynamic_agents: Optional function (sync or async) that takes agent_name: str and returns
72
+ BaseAgent or Optional[BaseAgent]. Server does not manage how this works internally.
73
+ enable_cors: Whether to enable CORS middleware
74
+ title: FastAPI app title
75
+ description: FastAPI app description
76
+ version: Server version
77
+ url_prefix: URL prefix for all routes (e.g., "/agents" makes all routes "/agents/...")
78
+ request_timeout: Request timeout in seconds (default: 300.0)
79
+ enable_rate_limiting: Whether to enable rate limiting (default: True)
80
+ default_rate_limit: Default rate limit rule for all clients
81
+ user_rate_limits: Per-user rate limit overrides
82
+ enable_request_logging: Whether to enable request logging (default: True)
83
+ enable_monitoring: Whether to enable monitoring system (default: True)
84
+ enable_prometheus: Whether to enable Prometheus metrics (default: True)
85
+ enable_structured_logging: Whether to enable structured logging (default: True)
86
+ metrics_port: Port for Prometheus metrics endpoint (default: 9090)
87
+ """
88
+ self.app = FastAPI(
89
+ title=title,
90
+ description=description,
91
+ version=version
92
+ )
93
+
94
+ self.version = version
95
+ self.url_prefix = url_prefix.rstrip("/") # Remove trailing slash if present
96
+
97
+ # Create API router with prefix
98
+ self.router = APIRouter(prefix=self.url_prefix)
99
+
100
+ # Store agents by name for quick lookup
101
+ self.static_agents = {agent.name: agent for agent in (agents or [])}
102
+
103
+ # Store dynamic agent resolver (server doesn't manage how it works)
104
+ self.dynamic_agents = dynamic_agents
105
+
106
+ # Store middleware configuration
107
+ self.request_timeout = request_timeout
108
+ self.enable_rate_limiting = enable_rate_limiting
109
+ self.default_rate_limit = default_rate_limit or RateLimitRule()
110
+ self.user_rate_limits = user_rate_limits or {}
111
+ self.enable_request_logging = enable_request_logging
112
+
113
+ # Initialize monitoring system
114
+ self.enable_monitoring = enable_monitoring
115
+ if enable_monitoring:
116
+ self.monitoring = initialize_monitoring(
117
+ enable_prometheus=enable_prometheus,
118
+ enable_structured_logging=enable_structured_logging,
119
+ metrics_port=metrics_port
120
+ )
121
+
122
+ # Set server info in metrics
123
+ self.monitoring.prometheus.set_server_info(
124
+ version=version,
125
+ agents_count=len(self.static_agents),
126
+ dynamic_agents_enabled=str(self.dynamic_agents is not None),
127
+ prometheus_enabled=str(enable_prometheus),
128
+ structured_logging_enabled=str(enable_structured_logging)
129
+ )
130
+ else:
131
+ self.monitoring = None
132
+
133
+ # Server startup time
134
+ self.startup_time = datetime.utcnow()
135
+
136
+ # Initialize logger
137
+ self.logger = get_logger('server.core.app')
138
+
139
+ # Initialize middleware and endpoints
140
+ self._setup_middleware()
141
+ self._create_endpoints()
142
+
143
+ print(f"🚀 WebAgents V2 Server initialized")
144
+ print(f" URL prefix: {self.url_prefix or '(none)'}")
145
+ print(f" Static agents: {len(self.static_agents)}")
146
+ print(f" Dynamic agents: {'✅ Enabled' if self.dynamic_agents else '❌ Disabled'}")
147
+ print(f" Monitoring: {'✅ Enabled' if self.monitoring else '❌ Disabled'}")
148
+
149
+ def _setup_middleware(self):
150
+ """Set up FastAPI middleware"""
151
+
152
+ # CORS middleware
153
+ self.app.add_middleware(
154
+ CORSMiddleware,
155
+ allow_origins=["*"],
156
+ allow_credentials=True,
157
+ allow_methods=["*"],
158
+ allow_headers=["*"],
159
+ )
160
+
161
+ # Request timeout and logging middleware
162
+ if self.enable_request_logging:
163
+ self.app.add_middleware(
164
+ RequestLoggingMiddleware,
165
+ timeout=self.request_timeout
166
+ )
167
+
168
+ # Rate limiting middleware
169
+ if self.enable_rate_limiting:
170
+ self.app.add_middleware(
171
+ RateLimitMiddleware,
172
+ default_rule=self.default_rate_limit,
173
+ user_rules=self.user_rate_limits
174
+ )
175
+
176
+ def _create_endpoints(self):
177
+ """Create all FastAPI endpoints"""
178
+
179
+ # Health check endpoint
180
+ @self.router.get("/health", response_model=HealthResponse)
181
+ async def health_check():
182
+ """Health check endpoint"""
183
+ uptime_seconds = (datetime.utcnow() - self.startup_time).total_seconds()
184
+
185
+ return HealthResponse(
186
+ status="healthy",
187
+ version=self.version,
188
+ uptime_seconds=uptime_seconds,
189
+ agents_count=len(self.static_agents),
190
+ dynamic_agents_enabled=self.dynamic_agents is not None
191
+ )
192
+
193
+ # Server info endpoint
194
+ @self.router.get("/info")
195
+ async def server_info():
196
+ """Get server information"""
197
+ uptime_seconds = (datetime.utcnow() - self.startup_time).total_seconds()
198
+
199
+ # Build endpoints with prefix
200
+ endpoints = {
201
+ "health": f"{self.url_prefix}/health",
202
+ "info": f"{self.url_prefix}/info",
203
+ "stats": f"{self.url_prefix}/stats"
204
+ }
205
+
206
+ if self.monitoring and self.monitoring.enable_prometheus:
207
+ endpoints["metrics"] = f"{self.url_prefix}/metrics"
208
+
209
+ return {
210
+ "name": "WebAgents V2 Server",
211
+ "version": self.version,
212
+ "status": "running",
213
+ "uptime_seconds": uptime_seconds,
214
+ "static_agents_count": len(self.static_agents),
215
+ "dynamic_agents_enabled": self.dynamic_agents is not None,
216
+ "monitoring_enabled": self.monitoring is not None,
217
+ "endpoints": endpoints
218
+ }
219
+
220
+ # Server stats endpoint
221
+ @self.router.get("/stats")
222
+ async def server_stats():
223
+ """Get comprehensive server statistics"""
224
+ uptime_seconds = (datetime.utcnow() - self.startup_time).total_seconds()
225
+
226
+ stats = {
227
+ "server": {
228
+ "name": "WebAgents V2 Server",
229
+ "version": self.version,
230
+ "uptime_seconds": uptime_seconds,
231
+ "startup_time": self.startup_time.isoformat()
232
+ },
233
+ "agents": {
234
+ "static_count": len(self.static_agents),
235
+ "static_names": list(self.static_agents.keys())
236
+ },
237
+ "dynamic_agents": {
238
+ "enabled": self.dynamic_agents is not None
239
+ }
240
+ }
241
+
242
+ # Add monitoring performance stats
243
+ if self.monitoring:
244
+ stats["performance"] = self.monitoring.get_performance_stats()
245
+
246
+ # Update system metrics
247
+ self.monitoring.update_system_metrics(
248
+ active_agents=len(self.static_agents),
249
+ dynamic_cache_size=0 # Server doesn't know about caching
250
+ )
251
+
252
+ return stats
253
+
254
+ # Agents listing endpoint
255
+ @self.router.get("/agents")
256
+ async def list_agents():
257
+ """List all available agents (static and dynamic)"""
258
+ agents_list = []
259
+
260
+ # Add static agents
261
+ for agent_name, agent in self.static_agents.items():
262
+ agents_list.append({
263
+ "name": agent_name,
264
+ "type": "static",
265
+ "instructions": agent.instructions,
266
+ "scopes": agent.scopes,
267
+ "tools_count": len(agent.get_tools_for_scope("all")),
268
+ "http_handlers_count": len(agent.get_all_http_handlers()),
269
+ "status": "active"
270
+ })
271
+
272
+ # Add dynamic agents info if available
273
+ dynamic_agents_available = []
274
+ if self.dynamic_agents:
275
+ try:
276
+ # Try to get available dynamic agents
277
+ # Note: This depends on dynamic agent implementation
278
+ dynamic_agents_available = [] # Placeholder - would need dynamic agent resolver to list
279
+ except Exception:
280
+ pass
281
+
282
+ return {
283
+ "agents": agents_list,
284
+ "static_count": len(self.static_agents),
285
+ "dynamic_count": len(dynamic_agents_available),
286
+ "total_count": len(agents_list) + len(dynamic_agents_available)
287
+ }
288
+
289
+ # Prometheus metrics endpoint
290
+ if self.monitoring and self.monitoring.enable_prometheus:
291
+ @self.router.get("/metrics")
292
+ async def prometheus_metrics():
293
+ """Prometheus metrics endpoint"""
294
+ metrics_data = self.monitoring.get_metrics_response()
295
+ return Response(
296
+ content=metrics_data,
297
+ media_type=CONTENT_TYPE_LATEST
298
+ )
299
+
300
+ # Static agent endpoints
301
+ for agent_name in self.static_agents.keys():
302
+ self._create_agent_endpoints(agent_name, is_dynamic=False)
303
+
304
+ # Dynamic agent endpoints (if resolver available)
305
+ if self.dynamic_agents:
306
+ @self.router.get("/{agent_name}", response_model=AgentInfoResponse)
307
+ async def dynamic_agent_info(agent_name: str):
308
+ return await self._handle_agent_info(agent_name, is_dynamic=True)
309
+
310
+ @self.router.post("/{agent_name}/chat/completions")
311
+ async def dynamic_chat_completion(agent_name: str, request: ChatCompletionRequest, raw_request: Request = None):
312
+ return await self._handle_chat_completion(agent_name, request, raw_request, is_dynamic=True)
313
+
314
+ @self.router.get("/{agent_name}/health")
315
+ async def dynamic_agent_health(agent_name: str):
316
+ """Dynamic agent health check"""
317
+ try:
318
+ agent = await self._resolve_agent(agent_name, is_dynamic=True)
319
+ return {
320
+ "agent_name": agent.name,
321
+ "status": "healthy",
322
+ "type": "dynamic_agent",
323
+ "instructions_preview": agent.instructions[:100] + "..." if len(agent.instructions) > 100 else agent.instructions
324
+ }
325
+ except HTTPException:
326
+ raise
327
+ except Exception as e:
328
+ raise HTTPException(status_code=500, detail=f"Agent health check failed: {str(e)}")
329
+
330
+ # Generic dynamic HTTP handler dispatcher for @http handlers on dynamic agents.
331
+ # This allows dynamic agents to expose custom HTTP endpoints without static registration.
332
+ @self.router.api_route("/{agent_name}/{request_path:path}", methods=[
333
+ "GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS", "HEAD"
334
+ ])
335
+ async def dynamic_agent_http_dispatch(
336
+ agent_name: str,
337
+ request_path: str,
338
+ request: Request
339
+ ):
340
+ import re
341
+ import inspect as _inspect
342
+ import asyncio as _asyncio
343
+
344
+ # Avoid handling reserved built-in endpoints; let their specific routes match first
345
+ reserved_suffixes = {
346
+ "chat/completions", "health", "", "info", "metrics", "stats", "agents"
347
+ }
348
+ if request_path in reserved_suffixes:
349
+ raise HTTPException(status_code=404, detail="Not found")
350
+
351
+ # Resolve the dynamic agent
352
+ agent = await self._resolve_agent(agent_name, is_dynamic=True)
353
+ # Ensure skills are initialized so skill methods have agent context
354
+ try:
355
+ if hasattr(agent, '_ensure_skills_initialized'):
356
+ await agent._ensure_skills_initialized()
357
+ except Exception as e:
358
+ raise HTTPException(status_code=500, detail=f"Failed to initialize agent skills: {e}")
359
+
360
+ # Iterate agent's http handlers and find a matching subpath and method
361
+ try:
362
+ http_handlers = agent.get_all_http_handlers()
363
+ except Exception as e:
364
+ raise HTTPException(status_code=500, detail=f"Failed to load HTTP handlers: {e}")
365
+
366
+ request_method = request.method.lower()
367
+ # Normalize request_path (no leading slash)
368
+ normalized_path = request_path.lstrip("/")
369
+
370
+ # Build a matcher for handler subpaths supporting path params like {id}
371
+ def build_regex(subpath: str):
372
+ # Remove leading slash for comparison
373
+ sp = subpath.lstrip("/")
374
+ # Find parameter names and build regex pattern
375
+ param_names = re.findall(r"\{([^}]+)\}", sp)
376
+ # Replace {param} with a capture group that matches a single path segment
377
+ pattern = re.sub(r"\{[^}]+\}", r"([^/]+)", sp)
378
+ # Anchor full match
379
+ pattern = f"^{pattern}$"
380
+ return re.compile(pattern), param_names
381
+
382
+ # Attempt to match in declaration order
383
+ for handler_config in http_handlers:
384
+ try:
385
+ subpath = handler_config.get('subpath') or ''
386
+ method = (handler_config.get('method') or 'get').lower()
387
+ handler_func = handler_config.get('function')
388
+
389
+ if method != request_method:
390
+ continue
391
+
392
+ regex, param_names = build_regex(subpath)
393
+ match = regex.match(normalized_path)
394
+ if not match:
395
+ continue
396
+
397
+ # Extract path params from capture groups
398
+ path_param_values = match.groups()
399
+ path_params = {name: value for name, value in zip(param_names, path_param_values)}
400
+
401
+ # Extract query params
402
+ query_params = dict(request.query_params)
403
+
404
+ # Extract JSON body for methods that commonly have a body
405
+ body_data = {}
406
+ if request.method in ["POST", "PUT", "PATCH"]:
407
+ try:
408
+ body_data = await request.json()
409
+ except Exception:
410
+ body_data = {}
411
+
412
+ # Combine and filter parameters by handler signature
413
+ combined_params = {**path_params, **query_params, **body_data}
414
+ sig = _inspect.signature(handler_func)
415
+ filtered_params = {}
416
+ for param_name in sig.parameters:
417
+ if param_name in ('self', 'context'):
418
+ continue
419
+ if param_name in combined_params:
420
+ filtered_params[param_name] = combined_params[param_name]
421
+
422
+ # Set minimal request context for handlers that depend on it (e.g., owner scope/User ID)
423
+ try:
424
+ ctx = create_context(messages=[], stream=False, agent=agent, request=request)
425
+ set_context(ctx)
426
+ except Exception:
427
+ pass
428
+
429
+ # Call the handler function (async or sync)
430
+ if _inspect.iscoroutinefunction(handler_func):
431
+ result = await handler_func(**filtered_params)
432
+ else:
433
+ # Run sync handler directly
434
+ result = handler_func(**filtered_params)
435
+
436
+ return result
437
+ except HTTPException:
438
+ raise
439
+ except Exception as e:
440
+ # If a handler matched but failed, surface the error
441
+ # Otherwise continue searching other handlers
442
+ last_error = str(e)
443
+ return Response(status_code=500, content=str(last_error))
444
+
445
+ # No matching handler found
446
+ raise HTTPException(status_code=404, detail=f"No HTTP handler for path '/{normalized_path}' and method {request.method} on agent '{agent_name}'")
447
+
448
+ # Include the router in the main app
449
+ self.app.include_router(self.router)
450
+
451
+ def _create_agent_endpoints(self, agent_name: str, is_dynamic: bool = False):
452
+ """Create endpoints for a specific agent"""
453
+
454
+ @self.router.get(f"/{agent_name}", response_model=AgentInfoResponse)
455
+ async def agent_info():
456
+ return await self._handle_agent_info(agent_name, is_dynamic=is_dynamic)
457
+
458
+ @self.router.post(f"/{agent_name}/chat/completions")
459
+ async def chat_completion(request: ChatCompletionRequest, raw_request: Request = None):
460
+ return await self._handle_chat_completion(agent_name, request, raw_request, is_dynamic=is_dynamic)
461
+
462
+ @self.router.get(f"/{agent_name}/health")
463
+ async def agent_health():
464
+ """Agent health check"""
465
+ try:
466
+ agent = await self._resolve_agent(agent_name, is_dynamic=is_dynamic)
467
+ return {
468
+ "agent_name": agent.name,
469
+ "status": "healthy",
470
+ "type": "static_agent" if not is_dynamic else "dynamic_agent",
471
+ "instructions_preview": agent.instructions[:100] + "..." if len(agent.instructions) > 100 else agent.instructions
472
+ }
473
+ except HTTPException:
474
+ raise
475
+ except Exception as e:
476
+ raise HTTPException(status_code=500, detail=f"Agent health check failed: {str(e)}")
477
+
478
+ # Register HTTP handlers if agent has any
479
+ if not is_dynamic:
480
+ agent = self.static_agents.get(agent_name)
481
+ if agent:
482
+ self._register_agent_http_handlers(agent_name, agent)
483
+
484
+ def _register_agent_http_handlers(self, agent_name: str, agent: BaseAgent):
485
+ """Register agent's HTTP handlers as FastAPI routes with dynamic parameter support"""
486
+ import inspect # Import at method level to ensure availability
487
+ import re
488
+ import asyncio
489
+
490
+ try:
491
+ http_handlers = agent.get_all_http_handlers()
492
+
493
+ for handler_config in http_handlers:
494
+ subpath = handler_config['subpath']
495
+ method = handler_config['method'].lower()
496
+ handler_func = handler_config['function']
497
+ scope = handler_config.get('scope', 'all')
498
+ description = handler_config.get('description', '')
499
+
500
+ # Create full path: /{agent_name}{subpath}
501
+ full_path = f"/{agent_name}{subpath}"
502
+
503
+ # Extract path parameter names from subpath (e.g., {param}, {user_id})
504
+ path_params = re.findall(r'\{([^}]+)\}', subpath)
505
+
506
+ def create_http_wrapper(handler_func, scope, method, path_param_names):
507
+ # Create dynamic function based on path parameters
508
+ if path_param_names:
509
+ # Create a wrapper that accepts path parameters as individual arguments
510
+ async def http_wrapper(request: Request, **kwargs):
511
+ try:
512
+ # Extract path parameters from kwargs (FastAPI automatically extracts them)
513
+ path_params = {name: kwargs.get(name) for name in path_param_names if name in kwargs}
514
+
515
+ # Extract query parameters
516
+ query_params = dict(request.query_params)
517
+
518
+ # Extract JSON body if present
519
+ body_data = {}
520
+ if request.method in ["POST", "PUT", "PATCH"]:
521
+ try:
522
+ body_data = await request.json()
523
+ except:
524
+ pass
525
+
526
+ # Combine all parameters: path params, query params, body data
527
+ all_params = {**path_params, **query_params, **body_data}
528
+
529
+ # Get function signature to handle parameters properly
530
+ sig = inspect.signature(handler_func)
531
+
532
+ # Filter parameters to only include those expected by the function
533
+ filtered_params = {}
534
+ for param_name in sig.parameters:
535
+ if param_name in ('self', 'context'):
536
+ continue
537
+ if param_name in all_params:
538
+ filtered_params[param_name] = all_params[param_name]
539
+
540
+ # Call the handler function
541
+ if asyncio.iscoroutinefunction(handler_func):
542
+ result = await handler_func(**filtered_params)
543
+ else:
544
+ result = handler_func(**filtered_params)
545
+
546
+ return result
547
+
548
+ except Exception as e:
549
+ raise HTTPException(status_code=500, detail=str(e))
550
+
551
+ # Now we need to dynamically add the correct path parameters to the wrapper signature
552
+ # Create a new function with the correct signature
553
+
554
+ # Build parameter list for the new function
555
+ params = [inspect.Parameter('request', inspect.Parameter.POSITIONAL_OR_KEYWORD, annotation=Request)]
556
+ for param_name in path_param_names:
557
+ params.append(inspect.Parameter(param_name, inspect.Parameter.POSITIONAL_OR_KEYWORD, annotation=str))
558
+
559
+ # Create new signature
560
+ new_sig = inspect.Signature(params)
561
+
562
+ # Create a wrapper with the correct signature
563
+ def make_wrapper():
564
+ async def wrapper(*args, **kwargs):
565
+ # Convert args to kwargs based on signature
566
+ bound_args = new_sig.bind(*args, **kwargs)
567
+ bound_args.apply_defaults()
568
+ return await http_wrapper(**bound_args.arguments)
569
+ wrapper.__signature__ = new_sig
570
+ return wrapper
571
+
572
+ return make_wrapper()
573
+ else:
574
+ # No path parameters, simpler wrapper
575
+ async def http_wrapper(request: Request):
576
+ try:
577
+ # Extract query parameters
578
+ query_params = dict(request.query_params)
579
+
580
+ # Extract JSON body if present
581
+ body_data = {}
582
+ if request.method in ["POST", "PUT", "PATCH"]:
583
+ try:
584
+ body_data = await request.json()
585
+ except:
586
+ pass
587
+
588
+ # Combine all parameters: query params, body data
589
+ all_params = {**query_params, **body_data}
590
+
591
+ # Get function signature to handle parameters properly
592
+ sig = inspect.signature(handler_func)
593
+
594
+ # Filter parameters to only include those expected by the function
595
+ filtered_params = {}
596
+ for param_name in sig.parameters:
597
+ if param_name in ('self', 'context'):
598
+ continue
599
+ if param_name in all_params:
600
+ filtered_params[param_name] = all_params[param_name]
601
+
602
+ # Call the handler function
603
+ if asyncio.iscoroutinefunction(handler_func):
604
+ result = await handler_func(**filtered_params)
605
+ else:
606
+ result = handler_func(**filtered_params)
607
+
608
+ return result
609
+
610
+ except Exception as e:
611
+ raise HTTPException(status_code=500, detail=str(e))
612
+
613
+ return http_wrapper
614
+
615
+ # Create the wrapper
616
+ wrapper = create_http_wrapper(handler_func, scope, method, path_params)
617
+
618
+ # Register with FastAPI based on HTTP method (using router instead of app directly)
619
+ # FastAPI will automatically handle path parameter extraction
620
+ if method == "get":
621
+ self.router.get(full_path, summary=description)(wrapper)
622
+ elif method == "post":
623
+ self.router.post(full_path, summary=description)(wrapper)
624
+ elif method == "put":
625
+ self.router.put(full_path, summary=description)(wrapper)
626
+ elif method == "delete":
627
+ self.router.delete(full_path, summary=description)(wrapper)
628
+ elif method == "patch":
629
+ self.router.patch(full_path, summary=description)(wrapper)
630
+ elif method == "head":
631
+ self.router.head(full_path, summary=description)(wrapper)
632
+ elif method == "options":
633
+ self.router.options(full_path, summary=description)(wrapper)
634
+
635
+ print(f"📡 Registered HTTP endpoint: {method.upper()} {self.url_prefix}{full_path}")
636
+
637
+ except Exception as e:
638
+ print(f"⚠️ Error registering HTTP handlers for agent '{agent_name}': {e}")
639
+
640
+ async def _handle_agent_info(self, agent_name: str, is_dynamic: bool = False) -> AgentInfoResponse:
641
+ """Handle agent info requests"""
642
+ agent = await self._resolve_agent(agent_name, is_dynamic=is_dynamic)
643
+
644
+ return AgentInfoResponse(
645
+ name=agent.name,
646
+ instructions=agent.instructions,
647
+ model="webagents-v2", # Generic model identifier
648
+ endpoints={
649
+ "chat_completions": f"{self.url_prefix}/{agent_name}/chat/completions",
650
+ "health": f"{self.url_prefix}/{agent_name}/health"
651
+ }
652
+ )
653
+
654
+ async def _handle_chat_completion(self, agent_name: str, request: ChatCompletionRequest, raw_request: Request = None, is_dynamic: bool = False):
655
+ """Handle chat completion requests"""
656
+
657
+ # Resolve the agent
658
+ agent = await self._resolve_agent(agent_name, is_dynamic=is_dynamic)
659
+
660
+ # Create context for this request
661
+ context = create_context(
662
+ messages=request.messages,
663
+ stream=request.stream,
664
+ agent=agent,
665
+ request=raw_request
666
+ )
667
+
668
+ set_context(context)
669
+
670
+ try:
671
+ # Convert Pydantic objects to dictionaries for LiteLLM compatibility
672
+ messages_dict = []
673
+ for msg in request.messages:
674
+ if hasattr(msg, 'model_dump'):
675
+ # Pydantic v2
676
+ msg_dict = msg.model_dump()
677
+ elif hasattr(msg, 'dict'):
678
+ # Pydantic v1
679
+ msg_dict = msg.dict()
680
+ else:
681
+ # Already a dict
682
+ msg_dict = msg
683
+
684
+ # Ensure required fields exist
685
+ if 'role' not in msg_dict:
686
+ msg_dict['role'] = 'user'
687
+ if 'content' not in msg_dict:
688
+ msg_dict['content'] = ''
689
+
690
+ messages_dict.append(msg_dict)
691
+
692
+ # Convert tools to dictionaries if present
693
+ tools_dict = None
694
+ if request.tools:
695
+ tools_dict = []
696
+ for tool in request.tools:
697
+ if hasattr(tool, 'model_dump'):
698
+ tools_dict.append(tool.model_dump())
699
+ elif hasattr(tool, 'dict'):
700
+ tools_dict.append(tool.dict())
701
+ else:
702
+ tools_dict.append(tool)
703
+
704
+ if request.stream:
705
+ # Handle streaming response
706
+ return await self._stream_response(agent, request, messages_dict, tools_dict)
707
+ else:
708
+ # Handle non-streaming response
709
+ response = await agent.run(
710
+ messages=messages_dict,
711
+ tools=tools_dict,
712
+ stream=False
713
+ )
714
+ return response
715
+
716
+ except Exception as e:
717
+ # Check if this is a payment-related error with custom status code
718
+ self.logger.error(f"🚨 Agent execution error: {type(e).__name__}: {str(e)}")
719
+
720
+ if hasattr(e, 'status_code') and hasattr(e, 'detail'):
721
+ self.logger.error(f" - Found status_code: {getattr(e, 'status_code', None)}, detail present: {hasattr(e, 'detail')}")
722
+ self.logger.error(f" - Raising HTTPException with status_code={getattr(e, 'status_code', 500)}")
723
+ raise HTTPException(
724
+ status_code=getattr(e, 'status_code', 500),
725
+ detail=getattr(e, 'detail')
726
+ )
727
+ else:
728
+ self.logger.error(f" - No status_code/detail attributes, defaulting to 500")
729
+ raise HTTPException(
730
+ status_code=500,
731
+ detail=f"Error executing agent '{agent_name}': {str(e)}"
732
+ )
733
+
734
+ async def _stream_response(self, agent: BaseAgent, request: ChatCompletionRequest, messages_dict: List[Dict[str, Any]], tools_dict: Optional[List[Dict[str, Any]]]):
735
+ """Handle streaming response"""
736
+ from fastapi.responses import StreamingResponse
737
+ import json
738
+
739
+ async def generate():
740
+ try:
741
+ async for chunk in agent.run_streaming(
742
+ messages=messages_dict,
743
+ tools=tools_dict
744
+ ):
745
+ # Properly serialize chunk to JSON for SSE format
746
+ try:
747
+ chunk_json = json.dumps(chunk)
748
+ yield f"data: {chunk_json}\n\n"
749
+ except Exception as json_error:
750
+ self.logger.error(f"Failed to serialize streaming chunk: {json_error}, chunk: {chunk}")
751
+ # Skip malformed chunks instead of breaking the stream
752
+ continue
753
+ yield "data: [DONE]\n\n"
754
+ except Exception as e:
755
+ self.logger.error(f"Streaming error: {e}")
756
+ error_chunk = {
757
+ "error": {
758
+ "message": str(e),
759
+ "type": "server_error"
760
+ }
761
+ }
762
+ error_json = json.dumps(error_chunk)
763
+ yield f"data: {error_json}\n\n"
764
+
765
+ return StreamingResponse(
766
+ generate(),
767
+ media_type="text/event-stream",
768
+ headers={
769
+ "Cache-Control": "no-cache",
770
+ "Connection": "keep-alive",
771
+ "Access-Control-Allow-Origin": "*",
772
+ }
773
+ )
774
+
775
+ async def _resolve_agent(self, agent_name: str, is_dynamic: bool = False) -> BaseAgent:
776
+ """Resolve agent by name from static or dynamic sources"""
777
+
778
+ # Try static agents first
779
+ agent = self.static_agents.get(agent_name)
780
+ if agent:
781
+ return agent
782
+
783
+ # If not looking for dynamic agents, stop here
784
+ if not is_dynamic:
785
+ raise HTTPException(status_code=404, detail=f"Agent '{agent_name}' not found")
786
+
787
+ # Try dynamic resolver if available
788
+ if self.dynamic_agents:
789
+ try:
790
+ if asyncio.iscoroutinefunction(self.dynamic_agents):
791
+ agent = await self.dynamic_agents(agent_name)
792
+ else:
793
+ agent = self.dynamic_agents(agent_name)
794
+
795
+ if agent and agent is not False:
796
+ return agent
797
+ except Exception as e:
798
+ print(f"Error resolving dynamic agent '{agent_name}': {e}")
799
+
800
+ # Agent not found
801
+ raise HTTPException(status_code=404, detail=f"Agent '{agent_name}' not found")
802
+
803
+ # Convenience property to access the FastAPI app
804
+ @property
805
+ def fastapi_app(self) -> FastAPI:
806
+ """Get the underlying FastAPI application"""
807
+ return self.app
808
+
809
+
810
+ # Factory function for easy server creation
811
+ def create_server(
812
+ title: str = "WebAgents V2 Server",
813
+ description: str = "AI Agent Server with OpenAI Compatibility",
814
+ version: str = "2.0.0",
815
+ agents: List[BaseAgent] = None,
816
+ dynamic_agents: Optional[Union[Callable[[str], BaseAgent], Callable[[str], Awaitable[Optional[BaseAgent]]]]] = None,
817
+ url_prefix: str = "",
818
+ **kwargs
819
+ ) -> WebAgentsServer:
820
+ """
821
+ Create a WebAgents server instance
822
+
823
+ Args:
824
+ title: Server title
825
+ description: Server description
826
+ version: Server version
827
+ agents: List of static agents
828
+ dynamic_agents: Optional dynamic agent resolver function (sync or async)
829
+ url_prefix: URL prefix for all routes (e.g., "/agents")
830
+ **kwargs: Additional server configuration
831
+
832
+ Returns:
833
+ Configured WebAgentsServer instance
834
+ """
835
+ return WebAgentsServer(
836
+ title=title,
837
+ description=description,
838
+ version=version,
839
+ agents=agents or [],
840
+ dynamic_agents=dynamic_agents,
841
+ url_prefix=url_prefix,
842
+ **kwargs
843
+ )