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,69 @@
1
+ """
2
+ FastAPI Middleware - WebAgents V2.0
3
+
4
+ Request logging and rate limiting middleware for the WebAgents server.
5
+ """
6
+
7
+ import time
8
+ import logging
9
+ from typing import Dict, Any, Optional
10
+ from dataclasses import dataclass
11
+ from fastapi import Request, Response
12
+ from starlette.middleware.base import BaseHTTPMiddleware
13
+
14
+ # Get logger for middleware
15
+ middleware_logger = logging.getLogger("webagents.server.middleware")
16
+
17
+
18
+ @dataclass
19
+ class RateLimitRule:
20
+ """Rate limiting rule configuration"""
21
+ requests_per_minute: int = 60
22
+ requests_per_hour: int = 1000
23
+ burst_limit: int = 10
24
+
25
+
26
+ class RequestLoggingMiddleware(BaseHTTPMiddleware):
27
+ """Middleware for logging HTTP requests"""
28
+
29
+ def __init__(self, app, timeout: float = 300.0):
30
+ super().__init__(app)
31
+ self.timeout = timeout
32
+
33
+ async def dispatch(self, request: Request, call_next):
34
+ start_time = time.time()
35
+
36
+ # Log request using proper logging (INFO level for important requests)
37
+ middleware_logger.info(f"{request.method} {request.url.path}")
38
+
39
+ # Process request
40
+ response = await call_next(request)
41
+
42
+ # Log response
43
+ duration = time.time() - start_time
44
+
45
+ # Use different log levels based on status code
46
+ if response.status_code >= 400:
47
+ middleware_logger.warning(f"{response.status_code} - {duration:.3f}s")
48
+ else:
49
+ middleware_logger.info(f"{response.status_code} - {duration:.3f}s")
50
+
51
+ return response
52
+
53
+
54
+ class RateLimitMiddleware(BaseHTTPMiddleware):
55
+ """Middleware for rate limiting requests"""
56
+
57
+ def __init__(self, app, default_rule: RateLimitRule, user_rules: Dict[str, RateLimitRule]):
58
+ super().__init__(app)
59
+ self.default_rule = default_rule
60
+ self.user_rules = user_rules
61
+ self.request_counts = {}
62
+
63
+ async def dispatch(self, request: Request, call_next):
64
+ # Simple rate limiting (in production, use Redis or similar)
65
+ client_ip = request.client.host if request.client else "unknown"
66
+
67
+ # For now, just pass through - rate limiting would be implemented here
68
+ response = await call_next(request)
69
+ return response
@@ -0,0 +1,98 @@
1
+ """
2
+ FastAPI Request/Response Models - WebAgents V2.0
3
+
4
+ Pydantic models for OpenAI-compatible API endpoints and server responses.
5
+ """
6
+
7
+ from typing import List, Dict, Any, Optional, Union
8
+ from pydantic import BaseModel, Field
9
+
10
+
11
+ class ChatMessage(BaseModel):
12
+ """OpenAI-compatible chat message"""
13
+ role: str = Field(..., description="Message role: 'system', 'user', 'assistant', or 'tool'")
14
+ content: Optional[str] = Field(None, description="Message content")
15
+ tool_calls: Optional[List[Dict[str, Any]]] = Field(None, description="Tool calls in the message")
16
+ tool_call_id: Optional[str] = Field(None, description="Tool call ID for tool responses")
17
+
18
+
19
+ class ToolFunction(BaseModel):
20
+ """OpenAI-compatible tool function definition"""
21
+ name: str = Field(..., description="Function name")
22
+ description: str = Field(..., description="Function description")
23
+ parameters: Dict[str, Any] = Field(..., description="JSON schema for function parameters")
24
+
25
+
26
+ class Tool(BaseModel):
27
+ """OpenAI-compatible tool definition"""
28
+ type: str = Field("function", description="Tool type")
29
+ function: ToolFunction = Field(..., description="Function definition")
30
+
31
+
32
+ class ChatCompletionRequest(BaseModel):
33
+ """OpenAI-compatible chat completion request"""
34
+ model: str = Field(..., description="Model name")
35
+ messages: List[ChatMessage] = Field(..., description="List of messages")
36
+ tools: Optional[List[Tool]] = Field(None, description="Available tools")
37
+ tool_choice: Optional[Union[str, Dict[str, Any]]] = Field(None, description="Tool choice strategy")
38
+ temperature: Optional[float] = Field(None, ge=0, le=2, description="Sampling temperature")
39
+ max_tokens: Optional[int] = Field(None, gt=0, description="Maximum tokens to generate")
40
+ stream: bool = Field(False, description="Whether to stream the response")
41
+ top_p: Optional[float] = Field(None, ge=0, le=1, description="Nucleus sampling parameter")
42
+ frequency_penalty: Optional[float] = Field(None, ge=-2, le=2, description="Frequency penalty")
43
+ presence_penalty: Optional[float] = Field(None, ge=-2, le=2, description="Presence penalty")
44
+ stop: Optional[Union[str, List[str]]] = Field(None, description="Stop sequences")
45
+
46
+
47
+ class Usage(BaseModel):
48
+ """OpenAI-compatible usage statistics"""
49
+ prompt_tokens: int = Field(..., description="Number of tokens in the prompt")
50
+ completion_tokens: int = Field(..., description="Number of tokens in the completion")
51
+ total_tokens: int = Field(..., description="Total number of tokens used")
52
+
53
+
54
+ class ChatCompletionChoice(BaseModel):
55
+ """OpenAI-compatible chat completion choice"""
56
+ index: int = Field(..., description="Choice index")
57
+ message: ChatMessage = Field(..., description="Generated message")
58
+ finish_reason: str = Field(..., description="Reason for completion finish")
59
+
60
+
61
+ class ChatCompletionResponse(BaseModel):
62
+ """OpenAI-compatible chat completion response"""
63
+ id: str = Field(..., description="Completion ID")
64
+ object: str = Field("chat.completion", description="Object type")
65
+ created: int = Field(..., description="Unix timestamp of creation")
66
+ model: str = Field(..., description="Model used for completion")
67
+ choices: List[ChatCompletionChoice] = Field(..., description="List of completion choices")
68
+ usage: Usage = Field(..., description="Token usage statistics")
69
+
70
+
71
+ class AgentInfoResponse(BaseModel):
72
+ """Agent information response"""
73
+ name: str = Field(..., description="Agent name")
74
+ instructions: str = Field(..., description="Agent instructions")
75
+ model: str = Field(..., description="Model identifier")
76
+ endpoints: Dict[str, str] = Field(..., description="Available endpoints")
77
+
78
+
79
+ class HealthResponse(BaseModel):
80
+ """Health check response"""
81
+ status: str = Field(..., description="Health status")
82
+ version: str = Field(..., description="Server version")
83
+ uptime_seconds: float = Field(..., description="Server uptime in seconds")
84
+ agents_count: int = Field(..., description="Number of registered agents")
85
+ dynamic_agents_enabled: bool = Field(..., description="Whether dynamic agents are enabled")
86
+
87
+
88
+ class AgentListResponse(BaseModel):
89
+ """Agent list response"""
90
+ agents: List[Dict[str, Any]] = Field(..., description="List of available agents")
91
+ total_count: int = Field(..., description="Total number of agents")
92
+
93
+
94
+ class ServerStatsResponse(BaseModel):
95
+ """Server statistics response"""
96
+ server: Dict[str, Any] = Field(..., description="Server information")
97
+ agents: Dict[str, Any] = Field(..., description="Agent statistics")
98
+ performance: Optional[Dict[str, Any]] = Field(None, description="Performance metrics")
@@ -0,0 +1,59 @@
1
+ """
2
+ Server Monitoring - WebAgents V2.0
3
+
4
+ Prometheus metrics and structured logging for the WebAgents server.
5
+ """
6
+
7
+ import time
8
+ from typing import Dict, Any, Optional
9
+ from dataclasses import dataclass
10
+
11
+
12
+ @dataclass
13
+ class PrometheusMetrics:
14
+ """Prometheus metrics collector"""
15
+ enable_prometheus: bool = True
16
+
17
+ def set_server_info(self, **kwargs):
18
+ """Set server information metrics"""
19
+ pass
20
+
21
+ def get_metrics_response(self) -> str:
22
+ """Get Prometheus metrics response"""
23
+ return "# Prometheus metrics would be here\n"
24
+
25
+
26
+ @dataclass
27
+ class MonitoringSystem:
28
+ """Complete monitoring system"""
29
+ enable_prometheus: bool
30
+ enable_structured_logging: bool
31
+ prometheus: PrometheusMetrics
32
+
33
+ def get_performance_stats(self) -> Dict[str, Any]:
34
+ """Get performance statistics"""
35
+ return {
36
+ "uptime": time.time(),
37
+ "memory_usage": "unknown",
38
+ "cpu_usage": "unknown"
39
+ }
40
+
41
+ def update_system_metrics(self, **kwargs):
42
+ """Update system metrics"""
43
+ pass
44
+
45
+
46
+ def initialize_monitoring(
47
+ enable_prometheus: bool = True,
48
+ enable_structured_logging: bool = True,
49
+ metrics_port: int = 9090
50
+ ) -> MonitoringSystem:
51
+ """Initialize monitoring system"""
52
+
53
+ prometheus = PrometheusMetrics(enable_prometheus=enable_prometheus)
54
+
55
+ return MonitoringSystem(
56
+ enable_prometheus=enable_prometheus,
57
+ enable_structured_logging=enable_structured_logging,
58
+ prometheus=prometheus
59
+ )
File without changes
File without changes
@@ -0,0 +1,330 @@
1
+ """
2
+ Server Middleware - WebAgents V2.0
3
+
4
+ Production-ready middleware for request timeout, rate limiting,
5
+ and comprehensive request management.
6
+ """
7
+
8
+ import time
9
+ import asyncio
10
+ from typing import Dict, Any, Optional, Tuple, Callable
11
+ from datetime import datetime, timedelta
12
+ from collections import defaultdict
13
+ from dataclasses import dataclass
14
+ import uuid
15
+
16
+ from fastapi import Request, HTTPException
17
+ from fastapi.responses import JSONResponse
18
+ from starlette.middleware.base import BaseHTTPMiddleware
19
+
20
+ from .context.context_vars import create_context, set_context
21
+
22
+
23
+ @dataclass
24
+ class RateLimitRule:
25
+ """Rate limiting rule configuration"""
26
+ requests_per_minute: int = 60
27
+ requests_per_hour: int = 1000
28
+ requests_per_day: int = 10000
29
+ burst_limit: int = 10 # Max requests in burst window
30
+ burst_window_seconds: int = 1 # Burst window duration
31
+
32
+
33
+ @dataclass
34
+ class ClientUsage:
35
+ """Track client usage for rate limiting"""
36
+ client_id: str
37
+ minute_count: int = 0
38
+ hour_count: int = 0
39
+ day_count: int = 0
40
+ burst_count: int = 0
41
+ last_minute_reset: datetime = None
42
+ last_hour_reset: datetime = None
43
+ last_day_reset: datetime = None
44
+ last_burst_reset: datetime = None
45
+
46
+ def __post_init__(self):
47
+ now = datetime.utcnow()
48
+ if self.last_minute_reset is None:
49
+ self.last_minute_reset = now
50
+ if self.last_hour_reset is None:
51
+ self.last_hour_reset = now
52
+ if self.last_day_reset is None:
53
+ self.last_day_reset = now
54
+ if self.last_burst_reset is None:
55
+ self.last_burst_reset = now
56
+
57
+
58
+ class TimeoutMiddleware(BaseHTTPMiddleware):
59
+ """Request timeout middleware"""
60
+
61
+ def __init__(self, app, timeout_seconds: float = 300.0):
62
+ super().__init__(app)
63
+ self.timeout_seconds = timeout_seconds
64
+
65
+ async def dispatch(self, request: Request, call_next):
66
+ try:
67
+ # Apply timeout to request processing
68
+ response = await asyncio.wait_for(
69
+ call_next(request),
70
+ timeout=self.timeout_seconds
71
+ )
72
+ return response
73
+
74
+ except asyncio.TimeoutError:
75
+ return JSONResponse(
76
+ status_code=408,
77
+ content={
78
+ "error": {
79
+ "type": "timeout_error",
80
+ "message": f"Request timeout after {self.timeout_seconds} seconds",
81
+ "code": "request_timeout"
82
+ }
83
+ }
84
+ )
85
+
86
+
87
+ class RateLimitMiddleware(BaseHTTPMiddleware):
88
+ """Comprehensive rate limiting middleware"""
89
+
90
+ def __init__(
91
+ self,
92
+ app,
93
+ default_rule: RateLimitRule = None,
94
+ user_rules: Dict[str, RateLimitRule] = None,
95
+ enable_rate_limiting: bool = True
96
+ ):
97
+ super().__init__(app)
98
+ self.default_rule = default_rule or RateLimitRule()
99
+ self.user_rules = user_rules or {}
100
+ self.enable_rate_limiting = enable_rate_limiting
101
+
102
+ # In-memory storage for client usage (production should use Redis)
103
+ self.client_usage: Dict[str, ClientUsage] = {}
104
+
105
+ # Exempt paths from rate limiting
106
+ self.exempt_paths = {'/health', '/health/detailed', '/'}
107
+
108
+ def _get_client_id(self, request: Request) -> str:
109
+ """Extract client identifier from request"""
110
+ # Priority order for client identification:
111
+ # 1. User ID header (if authenticated)
112
+ # 2. API key (if provided)
113
+ # 3. IP address (fallback)
114
+
115
+ user_id = request.headers.get("x-user-id")
116
+ if user_id and user_id != "anonymous":
117
+ return f"user:{user_id}"
118
+
119
+ api_key = request.headers.get("authorization")
120
+ if api_key:
121
+ # Hash for privacy but keep unique
122
+ return f"api:{hash(api_key) % 1000000}"
123
+
124
+ # Fallback to IP address
125
+ client_ip = request.client.host if request.client else "unknown"
126
+ return f"ip:{client_ip}"
127
+
128
+ def _get_rate_limit_rule(self, client_id: str, request: Request) -> RateLimitRule:
129
+ """Get rate limit rule for client"""
130
+
131
+ # Check for user-specific rules
132
+ user_id = request.headers.get("x-user-id")
133
+ if user_id and user_id in self.user_rules:
134
+ return self.user_rules[user_id]
135
+
136
+ # Check for agent owner rules
137
+ agent_owner = request.headers.get("x-agent-owner-id")
138
+ if agent_owner and agent_owner in self.user_rules:
139
+ return self.user_rules[agent_owner]
140
+
141
+ # Default rule
142
+ return self.default_rule
143
+
144
+ def _reset_counters_if_needed(self, usage: ClientUsage) -> None:
145
+ """Reset usage counters if time windows have expired"""
146
+ now = datetime.utcnow()
147
+
148
+ # Reset burst counter
149
+ if now - usage.last_burst_reset >= timedelta(seconds=1):
150
+ usage.burst_count = 0
151
+ usage.last_burst_reset = now
152
+
153
+ # Reset minute counter
154
+ if now - usage.last_minute_reset >= timedelta(minutes=1):
155
+ usage.minute_count = 0
156
+ usage.last_minute_reset = now
157
+
158
+ # Reset hour counter
159
+ if now - usage.last_hour_reset >= timedelta(hours=1):
160
+ usage.hour_count = 0
161
+ usage.last_hour_reset = now
162
+
163
+ # Reset day counter
164
+ if now - usage.last_day_reset >= timedelta(days=1):
165
+ usage.day_count = 0
166
+ usage.last_day_reset = now
167
+
168
+ def _check_rate_limits(self, client_id: str, rule: RateLimitRule) -> Tuple[bool, str, int]:
169
+ """
170
+ Check if client has exceeded rate limits
171
+
172
+ Returns:
173
+ Tuple of (allowed: bool, error_message: str, retry_after_seconds: int)
174
+ """
175
+
176
+ # Get or create client usage
177
+ if client_id not in self.client_usage:
178
+ self.client_usage[client_id] = ClientUsage(client_id=client_id)
179
+
180
+ usage = self.client_usage[client_id]
181
+ self._reset_counters_if_needed(usage)
182
+
183
+ # Check burst limit (most restrictive)
184
+ if usage.burst_count >= rule.burst_limit:
185
+ return False, f"Burst limit exceeded ({rule.burst_limit} requests per second)", 1
186
+
187
+ # Check per-minute limit
188
+ if usage.minute_count >= rule.requests_per_minute:
189
+ remaining_seconds = 60 - (datetime.utcnow() - usage.last_minute_reset).seconds
190
+ return False, f"Rate limit exceeded ({rule.requests_per_minute} requests per minute)", remaining_seconds
191
+
192
+ # Check per-hour limit
193
+ if usage.hour_count >= rule.requests_per_hour:
194
+ remaining_seconds = 3600 - (datetime.utcnow() - usage.last_hour_reset).seconds
195
+ return False, f"Rate limit exceeded ({rule.requests_per_hour} requests per hour)", remaining_seconds
196
+
197
+ # Check per-day limit
198
+ if usage.day_count >= rule.requests_per_day:
199
+ remaining_seconds = 86400 - (datetime.utcnow() - usage.last_day_reset).seconds
200
+ return False, f"Rate limit exceeded ({rule.requests_per_day} requests per day)", remaining_seconds
201
+
202
+ return True, "", 0
203
+
204
+ def _increment_counters(self, client_id: str) -> None:
205
+ """Increment usage counters for client"""
206
+ usage = self.client_usage[client_id]
207
+ usage.burst_count += 1
208
+ usage.minute_count += 1
209
+ usage.hour_count += 1
210
+ usage.day_count += 1
211
+
212
+ def _cleanup_old_entries(self) -> None:
213
+ """Clean up old client usage entries to prevent memory leaks"""
214
+ now = datetime.utcnow()
215
+ cutoff = now - timedelta(days=2) # Keep 2 days of history
216
+
217
+ # Remove entries older than cutoff
218
+ old_clients = [
219
+ client_id for client_id, usage in self.client_usage.items()
220
+ if usage.last_day_reset < cutoff
221
+ ]
222
+
223
+ for client_id in old_clients:
224
+ del self.client_usage[client_id]
225
+
226
+ async def dispatch(self, request: Request, call_next):
227
+ # Skip rate limiting if disabled
228
+ if not self.enable_rate_limiting:
229
+ return await call_next(request)
230
+
231
+ # Skip rate limiting for exempt paths
232
+ if request.url.path in self.exempt_paths:
233
+ return await call_next(request)
234
+
235
+ # Get client ID and rate limit rule
236
+ client_id = self._get_client_id(request)
237
+ rule = self._get_rate_limit_rule(client_id, request)
238
+
239
+ # Check rate limits
240
+ allowed, error_message, retry_after = self._check_rate_limits(client_id, rule)
241
+
242
+ if not allowed:
243
+ # Return rate limit error
244
+ response = JSONResponse(
245
+ status_code=429,
246
+ content={
247
+ "error": {
248
+ "type": "rate_limit_exceeded",
249
+ "message": error_message,
250
+ "code": "too_many_requests",
251
+ "retry_after": retry_after
252
+ }
253
+ },
254
+ headers={"Retry-After": str(retry_after)}
255
+ )
256
+ return response
257
+
258
+ # Increment counters and continue
259
+ self._increment_counters(client_id)
260
+
261
+ # Periodic cleanup (every 1000 requests)
262
+ if len(self.client_usage) % 1000 == 0:
263
+ self._cleanup_old_entries()
264
+
265
+ # Add rate limit headers to response
266
+ response = await call_next(request)
267
+
268
+ # Add rate limit info headers
269
+ usage = self.client_usage[client_id]
270
+ response.headers["X-RateLimit-Limit-Minute"] = str(rule.requests_per_minute)
271
+ response.headers["X-RateLimit-Remaining-Minute"] = str(max(0, rule.requests_per_minute - usage.minute_count))
272
+ response.headers["X-RateLimit-Limit-Hour"] = str(rule.requests_per_hour)
273
+ response.headers["X-RateLimit-Remaining-Hour"] = str(max(0, rule.requests_per_hour - usage.hour_count))
274
+ response.headers["X-RateLimit-Limit-Day"] = str(rule.requests_per_day)
275
+ response.headers["X-RateLimit-Remaining-Day"] = str(max(0, rule.requests_per_day - usage.day_count))
276
+
277
+ return response
278
+
279
+
280
+ class RequestLoggingMiddleware(BaseHTTPMiddleware):
281
+ """Request logging and metrics middleware"""
282
+
283
+ def __init__(self, app, enable_logging: bool = True):
284
+ super().__init__(app)
285
+ self.enable_logging = enable_logging
286
+ self.request_count = 0
287
+ self.error_count = 0
288
+
289
+ async def dispatch(self, request: Request, call_next):
290
+ if not self.enable_logging:
291
+ return await call_next(request)
292
+
293
+ start_time = time.time()
294
+ self.request_count += 1
295
+ request_id = str(uuid.uuid4())[:8]
296
+
297
+ # Log request start
298
+ print(f"[{request_id}] {request.method} {request.url.path} - Started")
299
+
300
+ try:
301
+ response = await call_next(request)
302
+
303
+ # Calculate duration
304
+ duration_ms = (time.time() - start_time) * 1000
305
+
306
+ # Log successful request
307
+ print(f"[{request_id}] {request.method} {request.url.path} - {response.status_code} ({duration_ms:.1f}ms)")
308
+
309
+ # Add request ID header
310
+ response.headers["X-Request-ID"] = request_id
311
+
312
+ return response
313
+
314
+ except Exception as e:
315
+ self.error_count += 1
316
+ duration_ms = (time.time() - start_time) * 1000
317
+
318
+ # Log error
319
+ print(f"[{request_id}] {request.method} {request.url.path} - ERROR ({duration_ms:.1f}ms): {str(e)}")
320
+
321
+ # Re-raise exception
322
+ raise
323
+
324
+ def get_stats(self) -> Dict[str, Any]:
325
+ """Get middleware statistics"""
326
+ return {
327
+ "total_requests": self.request_count,
328
+ "error_count": self.error_count,
329
+ "success_rate": (self.request_count - self.error_count) / max(1, self.request_count)
330
+ }
@@ -0,0 +1,92 @@
1
+ """
2
+ OpenAI-Compatible Request/Response Models for WebAgents V2.0 Server
3
+ """
4
+
5
+ from pydantic import BaseModel, Field
6
+ from typing import List, Dict, Any, Optional, Union
7
+ from datetime import datetime
8
+
9
+
10
+ class ChatMessage(BaseModel):
11
+ """OpenAI-compatible chat message"""
12
+ role: str = Field(..., description="Role of the message sender")
13
+ content: Optional[str] = Field(None, description="Message content (can be null for tool calls)")
14
+ name: Optional[str] = Field(None, description="Name of the sender")
15
+ tool_calls: Optional[List[Dict[str, Any]]] = Field(None, description="Tool calls in message")
16
+ tool_call_id: Optional[str] = Field(None, description="Tool call ID for tool responses")
17
+
18
+
19
+ class ChatCompletionRequest(BaseModel):
20
+ """OpenAI-compatible chat completion request"""
21
+ model: Optional[str] = Field(None, description="Model name (agent name used as model)")
22
+ messages: List[ChatMessage] = Field(..., description="List of messages in conversation")
23
+ stream: Optional[bool] = Field(False, description="Whether to stream the response")
24
+ tools: Optional[List[Dict[str, Any]]] = Field(None, description="External tools to make available")
25
+ temperature: Optional[float] = Field(None, description="Sampling temperature")
26
+ max_tokens: Optional[int] = Field(None, description="Maximum tokens in response")
27
+ top_p: Optional[float] = Field(None, description="Nucleus sampling parameter")
28
+ frequency_penalty: Optional[float] = Field(None, description="Frequency penalty")
29
+ presence_penalty: Optional[float] = Field(None, description="Presence penalty")
30
+ stop: Optional[Union[str, List[str]]] = Field(None, description="Stop sequences")
31
+
32
+
33
+ class OpenAIUsage(BaseModel):
34
+ """OpenAI-compatible usage information"""
35
+ prompt_tokens: int = Field(..., description="Number of prompt tokens")
36
+ completion_tokens: int = Field(..., description="Number of completion tokens")
37
+ total_tokens: int = Field(..., description="Total number of tokens")
38
+
39
+
40
+ class OpenAIChoice(BaseModel):
41
+ """OpenAI-compatible choice in response"""
42
+ index: int = Field(..., description="Index of the choice")
43
+ message: ChatMessage = Field(..., description="Generated message")
44
+ finish_reason: Optional[str] = Field(None, description="Reason for finishing")
45
+
46
+
47
+ class OpenAIResponse(BaseModel):
48
+ """OpenAI-compatible chat completion response"""
49
+ id: str = Field(..., description="Unique completion ID")
50
+ object: str = Field(default="chat.completion", description="Object type")
51
+ created: int = Field(..., description="Unix timestamp of creation")
52
+ model: str = Field(..., description="Model used for completion")
53
+ choices: List[OpenAIChoice] = Field(..., description="List of choices")
54
+ usage: OpenAIUsage = Field(..., description="Token usage information")
55
+ system_fingerprint: Optional[str] = Field(None, description="System fingerprint")
56
+
57
+
58
+ class OpenAIStreamChunk(BaseModel):
59
+ """OpenAI-compatible streaming chunk"""
60
+ id: str = Field(..., description="Unique completion ID")
61
+ object: str = Field(default="chat.completion.chunk", description="Object type")
62
+ created: int = Field(..., description="Unix timestamp of creation")
63
+ model: str = Field(..., description="Model used for completion")
64
+ choices: List[Dict[str, Any]] = Field(..., description="Streaming choices with delta")
65
+ usage: Optional[OpenAIUsage] = Field(None, description="Token usage (final chunk only)")
66
+
67
+
68
+ class AgentInfoResponse(BaseModel):
69
+ """Agent information response"""
70
+ name: str = Field(..., description="Agent name")
71
+ description: str = Field(..., description="Agent description")
72
+ capabilities: List[str] = Field(..., description="List of agent capabilities")
73
+ skills: List[str] = Field(..., description="List of configured skills")
74
+ tools: List[str] = Field(..., description="List of available tools")
75
+ model: Optional[str] = Field(None, description="Primary LLM model")
76
+ pricing: Dict[str, Any] = Field(default_factory=dict, description="Pricing information")
77
+
78
+
79
+ class ServerInfo(BaseModel):
80
+ """Server information response"""
81
+ message: str = Field(default="WebAgents V2 Server", description="Server message")
82
+ version: str = Field(default="2.0.0", description="Server version")
83
+ agents: List[str] = Field(..., description="List of available agents")
84
+ endpoints: Dict[str, str] = Field(..., description="Available endpoints")
85
+
86
+
87
+ class HealthResponse(BaseModel):
88
+ """Health check response"""
89
+ status: str = Field(..., description="Health status")
90
+ version: str = Field(default="2.0.0", description="Server version")
91
+ timestamp: datetime = Field(..., description="Health check timestamp")
92
+ agents: Optional[Dict[str, Dict[str, Any]]] = Field(None, description="Agent health status")