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.
- webagents/__init__.py +18 -0
- webagents/__main__.py +55 -0
- webagents/agents/__init__.py +13 -0
- webagents/agents/core/__init__.py +19 -0
- webagents/agents/core/base_agent.py +1834 -0
- webagents/agents/core/handoffs.py +293 -0
- webagents/agents/handoffs/__init__.py +0 -0
- webagents/agents/interfaces/__init__.py +0 -0
- webagents/agents/lifecycle/__init__.py +0 -0
- webagents/agents/skills/__init__.py +109 -0
- webagents/agents/skills/base.py +136 -0
- webagents/agents/skills/core/__init__.py +8 -0
- webagents/agents/skills/core/guardrails/__init__.py +0 -0
- webagents/agents/skills/core/llm/__init__.py +0 -0
- webagents/agents/skills/core/llm/anthropic/__init__.py +1 -0
- webagents/agents/skills/core/llm/litellm/__init__.py +10 -0
- webagents/agents/skills/core/llm/litellm/skill.py +538 -0
- webagents/agents/skills/core/llm/openai/__init__.py +1 -0
- webagents/agents/skills/core/llm/xai/__init__.py +1 -0
- webagents/agents/skills/core/mcp/README.md +375 -0
- webagents/agents/skills/core/mcp/__init__.py +15 -0
- webagents/agents/skills/core/mcp/skill.py +731 -0
- webagents/agents/skills/core/memory/__init__.py +11 -0
- webagents/agents/skills/core/memory/long_term_memory/__init__.py +10 -0
- webagents/agents/skills/core/memory/long_term_memory/memory_skill.py +639 -0
- webagents/agents/skills/core/memory/short_term_memory/__init__.py +9 -0
- webagents/agents/skills/core/memory/short_term_memory/skill.py +341 -0
- webagents/agents/skills/core/memory/vector_memory/skill.py +447 -0
- webagents/agents/skills/core/planning/__init__.py +9 -0
- webagents/agents/skills/core/planning/planner.py +343 -0
- webagents/agents/skills/ecosystem/__init__.py +0 -0
- webagents/agents/skills/ecosystem/crewai/__init__.py +1 -0
- webagents/agents/skills/ecosystem/database/__init__.py +1 -0
- webagents/agents/skills/ecosystem/filesystem/__init__.py +0 -0
- webagents/agents/skills/ecosystem/google/__init__.py +0 -0
- webagents/agents/skills/ecosystem/google/calendar/__init__.py +6 -0
- webagents/agents/skills/ecosystem/google/calendar/skill.py +306 -0
- webagents/agents/skills/ecosystem/n8n/__init__.py +0 -0
- webagents/agents/skills/ecosystem/openai_agents/__init__.py +0 -0
- webagents/agents/skills/ecosystem/web/__init__.py +0 -0
- webagents/agents/skills/ecosystem/zapier/__init__.py +0 -0
- webagents/agents/skills/robutler/__init__.py +11 -0
- webagents/agents/skills/robutler/auth/README.md +63 -0
- webagents/agents/skills/robutler/auth/__init__.py +17 -0
- webagents/agents/skills/robutler/auth/skill.py +354 -0
- webagents/agents/skills/robutler/crm/__init__.py +18 -0
- webagents/agents/skills/robutler/crm/skill.py +368 -0
- webagents/agents/skills/robutler/discovery/README.md +281 -0
- webagents/agents/skills/robutler/discovery/__init__.py +16 -0
- webagents/agents/skills/robutler/discovery/skill.py +230 -0
- webagents/agents/skills/robutler/kv/__init__.py +6 -0
- webagents/agents/skills/robutler/kv/skill.py +80 -0
- webagents/agents/skills/robutler/message_history/__init__.py +9 -0
- webagents/agents/skills/robutler/message_history/skill.py +270 -0
- webagents/agents/skills/robutler/messages/__init__.py +0 -0
- webagents/agents/skills/robutler/nli/__init__.py +13 -0
- webagents/agents/skills/robutler/nli/skill.py +687 -0
- webagents/agents/skills/robutler/notifications/__init__.py +5 -0
- webagents/agents/skills/robutler/notifications/skill.py +141 -0
- webagents/agents/skills/robutler/payments/__init__.py +41 -0
- webagents/agents/skills/robutler/payments/exceptions.py +255 -0
- webagents/agents/skills/robutler/payments/skill.py +610 -0
- webagents/agents/skills/robutler/storage/__init__.py +10 -0
- webagents/agents/skills/robutler/storage/files/__init__.py +9 -0
- webagents/agents/skills/robutler/storage/files/skill.py +445 -0
- webagents/agents/skills/robutler/storage/json/__init__.py +9 -0
- webagents/agents/skills/robutler/storage/json/skill.py +336 -0
- webagents/agents/skills/robutler/storage/kv/skill.py +88 -0
- webagents/agents/skills/robutler/storage.py +389 -0
- webagents/agents/tools/__init__.py +0 -0
- webagents/agents/tools/decorators.py +426 -0
- webagents/agents/tracing/__init__.py +0 -0
- webagents/agents/workflows/__init__.py +0 -0
- webagents/scripts/__init__.py +0 -0
- webagents/server/__init__.py +28 -0
- webagents/server/context/__init__.py +0 -0
- webagents/server/context/context_vars.py +121 -0
- webagents/server/core/__init__.py +0 -0
- webagents/server/core/app.py +843 -0
- webagents/server/core/middleware.py +69 -0
- webagents/server/core/models.py +98 -0
- webagents/server/core/monitoring.py +59 -0
- webagents/server/endpoints/__init__.py +0 -0
- webagents/server/interfaces/__init__.py +0 -0
- webagents/server/middleware.py +330 -0
- webagents/server/models.py +92 -0
- webagents/server/monitoring.py +659 -0
- webagents/utils/__init__.py +0 -0
- webagents/utils/logging.py +359 -0
- webagents-0.1.0.dist-info/METADATA +230 -0
- webagents-0.1.0.dist-info/RECORD +94 -0
- webagents-0.1.0.dist-info/WHEEL +4 -0
- webagents-0.1.0.dist-info/entry_points.txt +2 -0
- webagents-0.1.0.dist-info/licenses/LICENSE +20 -0
@@ -0,0 +1,426 @@
|
|
1
|
+
"""
|
2
|
+
Tool, Hook, Handoff, and HTTP Decorators - WebAgents V2.0
|
3
|
+
|
4
|
+
Decorators for automatic registration of tools, hooks, handoffs, and HTTP handlers with BaseAgent.
|
5
|
+
Supports context injection and scope-based access control.
|
6
|
+
"""
|
7
|
+
|
8
|
+
import inspect
|
9
|
+
import functools
|
10
|
+
from typing import Dict, Any, List, Optional, Callable, Union
|
11
|
+
from dataclasses import dataclass
|
12
|
+
|
13
|
+
|
14
|
+
def tool(func: Optional[Callable] = None, *, name: Optional[str] = None, description: Optional[str] = None, scope: Union[str, List[str]] = "all"):
|
15
|
+
"""Decorator to mark functions as tools for automatic registration
|
16
|
+
|
17
|
+
Can be used as:
|
18
|
+
@tool
|
19
|
+
def my_tool(param: str) -> str: ...
|
20
|
+
|
21
|
+
Or:
|
22
|
+
@tool(name="custom", description="Custom tool", scope="owner")
|
23
|
+
def my_tool(param: str) -> str: ...
|
24
|
+
|
25
|
+
Args:
|
26
|
+
name: Optional override for tool name (defaults to function name)
|
27
|
+
description: Tool description (defaults to function docstring)
|
28
|
+
scope: Access scope - "all", "owner", "admin", or list of scopes
|
29
|
+
|
30
|
+
The decorated function can optionally receive Context via dependency injection:
|
31
|
+
|
32
|
+
@tool(scope="owner")
|
33
|
+
def my_tool(self, param: str, context: Context = None) -> str:
|
34
|
+
# Context automatically injected if parameter exists
|
35
|
+
if context:
|
36
|
+
user_id = context.peer_user_id
|
37
|
+
# ... use context
|
38
|
+
return result
|
39
|
+
"""
|
40
|
+
def decorator(f: Callable) -> Callable:
|
41
|
+
# Generate OpenAI-compatible tool schema
|
42
|
+
sig = inspect.signature(f)
|
43
|
+
parameters = {}
|
44
|
+
required = []
|
45
|
+
|
46
|
+
for param_name, param in sig.parameters.items():
|
47
|
+
# Skip 'self' and 'context' parameters from schema
|
48
|
+
if param_name in ('self', 'context'):
|
49
|
+
continue
|
50
|
+
|
51
|
+
param_type = "string" # Default type
|
52
|
+
param_desc = f"Parameter {param_name}"
|
53
|
+
|
54
|
+
# Try to infer type from annotation
|
55
|
+
if param.annotation != inspect.Parameter.empty:
|
56
|
+
if param.annotation == int:
|
57
|
+
param_type = "integer"
|
58
|
+
elif param.annotation == float:
|
59
|
+
param_type = "number"
|
60
|
+
elif param.annotation == bool:
|
61
|
+
param_type = "boolean"
|
62
|
+
elif param.annotation == list:
|
63
|
+
param_type = "array"
|
64
|
+
elif param.annotation == dict:
|
65
|
+
param_type = "object"
|
66
|
+
|
67
|
+
parameters[param_name] = {
|
68
|
+
"type": param_type,
|
69
|
+
"description": param_desc
|
70
|
+
}
|
71
|
+
|
72
|
+
# Mark as required if no default value
|
73
|
+
if param.default == inspect.Parameter.empty:
|
74
|
+
required.append(param_name)
|
75
|
+
|
76
|
+
# Create OpenAI tool schema
|
77
|
+
tool_schema = {
|
78
|
+
"type": "function",
|
79
|
+
"function": {
|
80
|
+
"name": name or f.__name__,
|
81
|
+
"description": description or f.__doc__ or f"Tool: {f.__name__}",
|
82
|
+
"parameters": {
|
83
|
+
"type": "object",
|
84
|
+
"properties": parameters,
|
85
|
+
"required": required
|
86
|
+
}
|
87
|
+
}
|
88
|
+
}
|
89
|
+
|
90
|
+
# Mark function with metadata for BaseAgent discovery
|
91
|
+
f._webagents_is_tool = True
|
92
|
+
f._webagents_tool_definition = tool_schema
|
93
|
+
f._tool_scope = scope
|
94
|
+
f._tool_scope_was_set = func is None # If func is None, decorator was called with params
|
95
|
+
f._tool_name = name or f.__name__
|
96
|
+
f._tool_description = description or f.__doc__ or f"Tool: {f.__name__}"
|
97
|
+
|
98
|
+
# Check if function expects context injection
|
99
|
+
has_context_param = 'context' in sig.parameters
|
100
|
+
|
101
|
+
if has_context_param:
|
102
|
+
@functools.wraps(f)
|
103
|
+
async def async_wrapper(*args, **kwargs):
|
104
|
+
# Inject context if requested and not provided
|
105
|
+
if 'context' not in kwargs:
|
106
|
+
from ...server.context.context_vars import get_context
|
107
|
+
context = get_context()
|
108
|
+
kwargs['context'] = context
|
109
|
+
# Call original function and return directly; BaseAgent will log usage
|
110
|
+
return await f(*args, **kwargs) if inspect.iscoroutinefunction(f) else f(*args, **kwargs)
|
111
|
+
|
112
|
+
@functools.wraps(f)
|
113
|
+
def sync_wrapper(*args, **kwargs):
|
114
|
+
# Inject context if requested and not provided
|
115
|
+
if 'context' not in kwargs:
|
116
|
+
from ...server.context.context_vars import get_context
|
117
|
+
context = get_context()
|
118
|
+
kwargs['context'] = context
|
119
|
+
# Call original function and return directly; BaseAgent will log usage
|
120
|
+
return f(*args, **kwargs)
|
121
|
+
|
122
|
+
# Return appropriate wrapper based on function type
|
123
|
+
if inspect.iscoroutinefunction(f):
|
124
|
+
wrapper = async_wrapper
|
125
|
+
else:
|
126
|
+
wrapper = sync_wrapper
|
127
|
+
else:
|
128
|
+
# No context injection needed; return function directly
|
129
|
+
wrapper = f
|
130
|
+
|
131
|
+
# Copy metadata to wrapper
|
132
|
+
wrapper._webagents_is_tool = True
|
133
|
+
wrapper._webagents_tool_definition = tool_schema
|
134
|
+
wrapper._tool_scope = scope
|
135
|
+
wrapper._tool_scope_was_set = func is None # If func is None, decorator was called with params
|
136
|
+
wrapper._tool_name = name or f.__name__
|
137
|
+
wrapper._tool_description = description or f.__doc__ or f"Tool: {f.__name__}"
|
138
|
+
|
139
|
+
return wrapper
|
140
|
+
|
141
|
+
if func is None:
|
142
|
+
# Called with arguments: @tool(name="...", ...)
|
143
|
+
return decorator
|
144
|
+
else:
|
145
|
+
# Called without arguments: @tool
|
146
|
+
return decorator(func)
|
147
|
+
|
148
|
+
|
149
|
+
def hook(event: str, priority: int = 50, scope: Union[str, List[str]] = "all"):
|
150
|
+
"""Decorator to mark functions as lifecycle hooks for automatic registration
|
151
|
+
|
152
|
+
Args:
|
153
|
+
event: Lifecycle event name (on_connection, on_chunk, on_message, etc.)
|
154
|
+
priority: Execution priority (lower numbers execute first)
|
155
|
+
scope: Access scope - "all", "owner", "admin", or list of scopes
|
156
|
+
|
157
|
+
Hook functions should accept and return Context:
|
158
|
+
|
159
|
+
@hook("on_message", priority=10, scope="owner")
|
160
|
+
async def my_hook(self, context: Context) -> Context:
|
161
|
+
# Process context
|
162
|
+
return context
|
163
|
+
"""
|
164
|
+
def decorator(func: Callable) -> Callable:
|
165
|
+
# Mark function with metadata for BaseAgent discovery
|
166
|
+
func._webagents_is_hook = True
|
167
|
+
func._hook_event_type = event
|
168
|
+
func._hook_priority = priority
|
169
|
+
func._hook_scope = scope
|
170
|
+
|
171
|
+
return func
|
172
|
+
|
173
|
+
return decorator
|
174
|
+
|
175
|
+
|
176
|
+
def prompt(priority: int = 50, scope: Union[str, List[str]] = "all"):
|
177
|
+
"""Decorator to mark functions as system prompt providers for automatic registration
|
178
|
+
|
179
|
+
Args:
|
180
|
+
priority: Execution priority (lower numbers execute first)
|
181
|
+
scope: Access scope - "all", "owner", "admin", or list of scopes
|
182
|
+
|
183
|
+
Prompt functions should accept context and return a string to be added to the system prompt:
|
184
|
+
|
185
|
+
@prompt(priority=10, scope="owner")
|
186
|
+
def my_prompt(self, context: Context) -> str:
|
187
|
+
# Generate dynamic prompt content
|
188
|
+
return f"Current user: {context.user_id}"
|
189
|
+
|
190
|
+
@prompt(priority=20)
|
191
|
+
async def async_prompt(self, context: Context) -> str:
|
192
|
+
# Async prompt generation
|
193
|
+
data = await some_async_call()
|
194
|
+
return f"Dynamic data: {data}"
|
195
|
+
"""
|
196
|
+
def decorator(func: Callable) -> Callable:
|
197
|
+
# Mark function with metadata for BaseAgent discovery
|
198
|
+
func._webagents_is_prompt = True
|
199
|
+
func._prompt_priority = priority
|
200
|
+
func._prompt_scope = scope
|
201
|
+
|
202
|
+
# Check if function expects context injection
|
203
|
+
sig = inspect.signature(func)
|
204
|
+
has_context_param = 'context' in sig.parameters
|
205
|
+
|
206
|
+
if has_context_param:
|
207
|
+
@functools.wraps(func)
|
208
|
+
async def async_wrapper(*args, **kwargs):
|
209
|
+
# Inject context if requested and not provided
|
210
|
+
if 'context' not in kwargs:
|
211
|
+
from ...server.context.context_vars import get_context
|
212
|
+
context = get_context()
|
213
|
+
kwargs['context'] = context
|
214
|
+
|
215
|
+
# Call original function
|
216
|
+
if inspect.iscoroutinefunction(func):
|
217
|
+
return await func(*args, **kwargs)
|
218
|
+
else:
|
219
|
+
return func(*args, **kwargs)
|
220
|
+
|
221
|
+
@functools.wraps(func)
|
222
|
+
def sync_wrapper(*args, **kwargs):
|
223
|
+
# Inject context if requested and not provided
|
224
|
+
if 'context' not in kwargs:
|
225
|
+
from ...server.context.context_vars import get_context
|
226
|
+
context = get_context()
|
227
|
+
kwargs['context'] = context
|
228
|
+
|
229
|
+
return func(*args, **kwargs)
|
230
|
+
|
231
|
+
# Return appropriate wrapper based on function type
|
232
|
+
if inspect.iscoroutinefunction(func):
|
233
|
+
wrapper = async_wrapper
|
234
|
+
else:
|
235
|
+
wrapper = sync_wrapper
|
236
|
+
else:
|
237
|
+
# No context injection needed
|
238
|
+
wrapper = func
|
239
|
+
|
240
|
+
# Copy metadata to wrapper
|
241
|
+
wrapper._webagents_is_prompt = True
|
242
|
+
wrapper._prompt_priority = priority
|
243
|
+
wrapper._prompt_scope = scope
|
244
|
+
|
245
|
+
return wrapper
|
246
|
+
|
247
|
+
return decorator
|
248
|
+
|
249
|
+
|
250
|
+
def handoff(name: Optional[str] = None, handoff_type: str = "agent", description: Optional[str] = None,
|
251
|
+
scope: Union[str, List[str]] = "all"):
|
252
|
+
"""Decorator to mark functions as handoffs for automatic registration
|
253
|
+
|
254
|
+
Args:
|
255
|
+
name: Optional override for handoff name (defaults to function name)
|
256
|
+
handoff_type: Type of handoff - "agent", "llm", "pipeline", etc.
|
257
|
+
description: Handoff description (defaults to function docstring)
|
258
|
+
scope: Access scope - "all", "owner", "admin", or list of scopes
|
259
|
+
|
260
|
+
Handoff functions should return HandoffResult:
|
261
|
+
|
262
|
+
@handoff(handoff_type="agent", scope=["admin"])
|
263
|
+
async def escalate_to_admin(self, issue: str, context: Context = None) -> HandoffResult:
|
264
|
+
# Process handoff
|
265
|
+
return HandoffResult(result="escalated", handoff_type="agent")
|
266
|
+
"""
|
267
|
+
def decorator(func: Callable) -> Callable:
|
268
|
+
# Mark function with metadata for BaseAgent discovery
|
269
|
+
func._webagents_is_handoff = True
|
270
|
+
func._handoff_type = handoff_type
|
271
|
+
func._handoff_scope = scope
|
272
|
+
func._handoff_name = name or func.__name__
|
273
|
+
func._handoff_description = description or func.__doc__ or f"Handoff: {func.__name__}"
|
274
|
+
|
275
|
+
# Check if function expects context injection
|
276
|
+
sig = inspect.signature(func)
|
277
|
+
has_context_param = 'context' in sig.parameters
|
278
|
+
|
279
|
+
if has_context_param:
|
280
|
+
@functools.wraps(func)
|
281
|
+
async def async_wrapper(*args, **kwargs):
|
282
|
+
# Inject context if requested and not provided
|
283
|
+
if 'context' not in kwargs:
|
284
|
+
from ...server.context.context_vars import get_context
|
285
|
+
context = get_context()
|
286
|
+
kwargs['context'] = context
|
287
|
+
|
288
|
+
# Call original function
|
289
|
+
if inspect.iscoroutinefunction(func):
|
290
|
+
return await func(*args, **kwargs)
|
291
|
+
else:
|
292
|
+
return func(*args, **kwargs)
|
293
|
+
|
294
|
+
@functools.wraps(func)
|
295
|
+
def sync_wrapper(*args, **kwargs):
|
296
|
+
# Inject context if requested and not provided
|
297
|
+
if 'context' not in kwargs:
|
298
|
+
from ...server.context.context_vars import get_context
|
299
|
+
context = get_context()
|
300
|
+
kwargs['context'] = context
|
301
|
+
|
302
|
+
return func(*args, **kwargs)
|
303
|
+
|
304
|
+
# Return appropriate wrapper
|
305
|
+
if inspect.iscoroutinefunction(func):
|
306
|
+
wrapper = async_wrapper
|
307
|
+
else:
|
308
|
+
wrapper = sync_wrapper
|
309
|
+
else:
|
310
|
+
wrapper = func
|
311
|
+
|
312
|
+
# Copy metadata to wrapper
|
313
|
+
wrapper._webagents_is_handoff = True
|
314
|
+
wrapper._handoff_type = handoff_type
|
315
|
+
wrapper._handoff_scope = scope
|
316
|
+
wrapper._handoff_name = name or func.__name__
|
317
|
+
wrapper._handoff_description = description or func.__doc__ or f"Handoff: {func.__name__}"
|
318
|
+
|
319
|
+
return wrapper
|
320
|
+
|
321
|
+
return decorator
|
322
|
+
|
323
|
+
|
324
|
+
def http(subpath: str, method: str = "get", scope: Union[str, List[str]] = "all"):
|
325
|
+
"""Decorator to mark functions as HTTP handlers for automatic registration
|
326
|
+
|
327
|
+
Args:
|
328
|
+
subpath: URL path after agent name (e.g., "/myapi" -> /{agentname}/myapi)
|
329
|
+
Supports dynamic parameters: "/users/{user_id}/posts/{post_id}"
|
330
|
+
method: HTTP method - "get", "post", "put", "delete", etc. (default: "get")
|
331
|
+
scope: Access scope - "all", "owner", "admin", or list of scopes
|
332
|
+
|
333
|
+
HTTP handler functions receive FastAPI request arguments directly:
|
334
|
+
|
335
|
+
@http("/weather", method="get", scope="owner")
|
336
|
+
def get_weather(location: str, units: str = "celsius") -> dict:
|
337
|
+
# Function receives query parameters as arguments
|
338
|
+
return {"location": location, "temperature": 25, "units": units}
|
339
|
+
|
340
|
+
@http("/data", method="post")
|
341
|
+
async def post_data(request: Request, data: dict) -> dict:
|
342
|
+
# Function can receive Request object and body data
|
343
|
+
return {"received": data, "status": "success"}
|
344
|
+
|
345
|
+
@http("/users/{user_id}", method="get")
|
346
|
+
def get_user(user_id: str) -> dict:
|
347
|
+
# Function receives path parameters as arguments
|
348
|
+
return {"user_id": user_id, "name": f"User {user_id}"}
|
349
|
+
|
350
|
+
@http("/users/{user_id}/posts/{post_id}", method="get")
|
351
|
+
def get_user_post(user_id: str, post_id: str, include_comments: bool = False) -> dict:
|
352
|
+
# Function receives both path parameters and query parameters
|
353
|
+
return {
|
354
|
+
"user_id": user_id,
|
355
|
+
"post_id": post_id,
|
356
|
+
"include_comments": include_comments
|
357
|
+
}
|
358
|
+
|
359
|
+
Dynamic path parameters are automatically extracted by FastAPI and passed
|
360
|
+
to the handler function. Query parameters and JSON body data are also
|
361
|
+
automatically passed as function arguments.
|
362
|
+
"""
|
363
|
+
def decorator(func: Callable) -> Callable:
|
364
|
+
# Validate HTTP method
|
365
|
+
valid_methods = ["get", "post", "put", "delete", "patch", "head", "options"]
|
366
|
+
if method.lower() not in valid_methods:
|
367
|
+
raise ValueError(f"Invalid HTTP method '{method}'. Must be one of: {valid_methods}")
|
368
|
+
|
369
|
+
# Ensure subpath starts with /
|
370
|
+
normalized_subpath = subpath if subpath.startswith('/') else f'/{subpath}'
|
371
|
+
|
372
|
+
# Mark function with metadata for BaseAgent discovery
|
373
|
+
func._webagents_is_http = True
|
374
|
+
func._http_subpath = normalized_subpath
|
375
|
+
func._http_method = method.lower()
|
376
|
+
func._http_scope = scope
|
377
|
+
func._http_description = func.__doc__ or f"HTTP {method.upper()} handler for {normalized_subpath}"
|
378
|
+
|
379
|
+
# Check if function expects context injection
|
380
|
+
sig = inspect.signature(func)
|
381
|
+
has_context_param = 'context' in sig.parameters
|
382
|
+
|
383
|
+
if has_context_param:
|
384
|
+
@functools.wraps(func)
|
385
|
+
async def async_wrapper(*args, **kwargs):
|
386
|
+
# Inject context if requested and not provided
|
387
|
+
if 'context' not in kwargs:
|
388
|
+
from ...server.context.context_vars import get_context
|
389
|
+
context = get_context()
|
390
|
+
kwargs['context'] = context
|
391
|
+
|
392
|
+
# Call original function
|
393
|
+
if inspect.iscoroutinefunction(func):
|
394
|
+
return await func(*args, **kwargs)
|
395
|
+
else:
|
396
|
+
return func(*args, **kwargs)
|
397
|
+
|
398
|
+
@functools.wraps(func)
|
399
|
+
def sync_wrapper(*args, **kwargs):
|
400
|
+
# Inject context if requested and not provided
|
401
|
+
if 'context' not in kwargs:
|
402
|
+
from ...server.context.context_vars import get_context
|
403
|
+
context = get_context()
|
404
|
+
kwargs['context'] = context
|
405
|
+
|
406
|
+
return func(*args, **kwargs)
|
407
|
+
|
408
|
+
# Return appropriate wrapper based on function type
|
409
|
+
if inspect.iscoroutinefunction(func):
|
410
|
+
wrapper = async_wrapper
|
411
|
+
else:
|
412
|
+
wrapper = sync_wrapper
|
413
|
+
else:
|
414
|
+
# No context injection needed
|
415
|
+
wrapper = func
|
416
|
+
|
417
|
+
# Copy metadata to wrapper
|
418
|
+
wrapper._webagents_is_http = True
|
419
|
+
wrapper._http_subpath = normalized_subpath
|
420
|
+
wrapper._http_method = method.lower()
|
421
|
+
wrapper._http_scope = scope
|
422
|
+
wrapper._http_description = func.__doc__ or f"HTTP {method.upper()} handler for {normalized_subpath}"
|
423
|
+
|
424
|
+
return wrapper
|
425
|
+
|
426
|
+
return decorator
|
File without changes
|
File without changes
|
File without changes
|
@@ -0,0 +1,28 @@
|
|
1
|
+
"""
|
2
|
+
WebAgents V2.0 Server Package
|
3
|
+
|
4
|
+
FastAPI server implementation with OpenAI compatibility,
|
5
|
+
streaming support, and comprehensive agent management.
|
6
|
+
"""
|
7
|
+
|
8
|
+
# Import moved to avoid circular dependency
|
9
|
+
# from .core.app import WebAgentsServer, create_server
|
10
|
+
from .models import (
|
11
|
+
ChatCompletionRequest,
|
12
|
+
OpenAIResponse,
|
13
|
+
OpenAIStreamChunk,
|
14
|
+
AgentInfoResponse,
|
15
|
+
ServerInfo,
|
16
|
+
HealthResponse
|
17
|
+
)
|
18
|
+
|
19
|
+
__all__ = [
|
20
|
+
# 'WebAgentsServer',
|
21
|
+
# 'create_server',
|
22
|
+
'ChatCompletionRequest',
|
23
|
+
'OpenAIResponse',
|
24
|
+
'OpenAIStreamChunk',
|
25
|
+
'AgentInfoResponse',
|
26
|
+
'ServerInfo',
|
27
|
+
'HealthResponse'
|
28
|
+
]
|
File without changes
|
@@ -0,0 +1,121 @@
|
|
1
|
+
"""
|
2
|
+
Context Variables and Unified Context - WebAgents V2.0
|
3
|
+
|
4
|
+
Unified context management using Python's contextvars for thread-safe,
|
5
|
+
async-compatible context handling.
|
6
|
+
"""
|
7
|
+
|
8
|
+
import time
|
9
|
+
import uuid
|
10
|
+
from contextvars import ContextVar
|
11
|
+
from dataclasses import dataclass, field
|
12
|
+
from typing import Dict, Any, List, Optional, Union
|
13
|
+
from datetime import datetime
|
14
|
+
|
15
|
+
|
16
|
+
# Single ContextVar for unified context
|
17
|
+
CONTEXT: ContextVar['Context'] = ContextVar('webagents_context')
|
18
|
+
|
19
|
+
|
20
|
+
@dataclass
|
21
|
+
class Context:
|
22
|
+
"""Unified Context containing ALL request data AND agent capabilities
|
23
|
+
|
24
|
+
This replaces the previous multiple context variables with a single,
|
25
|
+
comprehensive context object that contains everything skills and
|
26
|
+
tools need access to.
|
27
|
+
"""
|
28
|
+
|
29
|
+
# Request identification
|
30
|
+
request_id: str = field(default_factory=lambda: str(uuid.uuid4()))
|
31
|
+
start_time: float = field(default_factory=time.time)
|
32
|
+
|
33
|
+
# Request data
|
34
|
+
messages: List[Dict[str, Any]] = field(default_factory=list)
|
35
|
+
stream: bool = False
|
36
|
+
|
37
|
+
agent: Optional[Any] = None # BaseAgent instance
|
38
|
+
request: Optional[Any] = None # Incoming HTTP request wrapper
|
39
|
+
|
40
|
+
# Authentication namespace (populated by AuthSkill)
|
41
|
+
auth: Optional[Any] = None
|
42
|
+
|
43
|
+
# Usage data
|
44
|
+
usage: List[Dict[str, Any]] = field(default_factory=list)
|
45
|
+
|
46
|
+
# Custom data storage (for skills/tools to store temporary data)
|
47
|
+
custom_data: Dict[str, Any] = field(default_factory=dict)
|
48
|
+
|
49
|
+
# --- Properties for clean access ---
|
50
|
+
|
51
|
+
@property
|
52
|
+
def skills(self) -> Dict[str, Any]:
|
53
|
+
"""Access to agent's skills"""
|
54
|
+
return self.agent.skills if self.agent else {}
|
55
|
+
|
56
|
+
@property
|
57
|
+
def tools(self) -> List[Dict[str, Any]]:
|
58
|
+
"""Access to agent's registered tools"""
|
59
|
+
return self.agent.get_all_tools() if self.agent else []
|
60
|
+
|
61
|
+
@property
|
62
|
+
def handoffs(self) -> List[Dict[str, Any]]:
|
63
|
+
"""Access to agent's registered handoffs"""
|
64
|
+
return self.agent.get_all_handoffs() if self.agent else []
|
65
|
+
|
66
|
+
@property
|
67
|
+
def auth_scope(self) -> str:
|
68
|
+
"""Determine user scope based on user context"""
|
69
|
+
return self.auth.scope.value if self.auth else 'all'
|
70
|
+
|
71
|
+
# --- Methods for data manipulation ---
|
72
|
+
|
73
|
+
def set(self, key: str, value: Any) -> None:
|
74
|
+
"""Store custom data"""
|
75
|
+
self.custom_data[key] = value
|
76
|
+
|
77
|
+
def get(self, key: str, default: Any = None) -> Any:
|
78
|
+
"""Retrieve custom data"""
|
79
|
+
return self.custom_data.get(key, default)
|
80
|
+
|
81
|
+
|
82
|
+
def add_message(self, message: Dict[str, Any]) -> None:
|
83
|
+
"""Add message to context"""
|
84
|
+
self.messages.append(message)
|
85
|
+
|
86
|
+
def update_agent_context(self, agent: Any, agent_name: str) -> None:
|
87
|
+
"""Update agent-related context"""
|
88
|
+
self.agent = agent
|
89
|
+
self.agent_name_resolved = agent_name
|
90
|
+
|
91
|
+
|
92
|
+
# Context management functions
|
93
|
+
def get_context() -> Optional[Context]:
|
94
|
+
"""Get current context from context variable"""
|
95
|
+
try:
|
96
|
+
return CONTEXT.get()
|
97
|
+
except LookupError:
|
98
|
+
return None
|
99
|
+
|
100
|
+
|
101
|
+
def set_context(context: Context) -> None:
|
102
|
+
"""Set current context in context variable"""
|
103
|
+
CONTEXT.set(context)
|
104
|
+
|
105
|
+
|
106
|
+
def create_context(
|
107
|
+
request_id: Optional[str] = None,
|
108
|
+
messages: Optional[List[Dict[str, Any]]] = None,
|
109
|
+
stream: bool = False,
|
110
|
+
request: Any = None,
|
111
|
+
agent: Any = None
|
112
|
+
) -> Context:
|
113
|
+
"""Create new context with provided data"""
|
114
|
+
context = Context(
|
115
|
+
request_id=request_id or str(uuid.uuid4()),
|
116
|
+
messages=messages or [],
|
117
|
+
stream=stream,
|
118
|
+
agent=agent,
|
119
|
+
request=request
|
120
|
+
)
|
121
|
+
return context
|
File without changes
|