voxagent 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.
- voxagent/__init__.py +143 -0
- voxagent/_version.py +5 -0
- voxagent/agent/__init__.py +32 -0
- voxagent/agent/abort.py +178 -0
- voxagent/agent/core.py +902 -0
- voxagent/code/__init__.py +9 -0
- voxagent/mcp/__init__.py +16 -0
- voxagent/mcp/manager.py +188 -0
- voxagent/mcp/tool.py +152 -0
- voxagent/providers/__init__.py +110 -0
- voxagent/providers/anthropic.py +498 -0
- voxagent/providers/augment.py +293 -0
- voxagent/providers/auth.py +116 -0
- voxagent/providers/base.py +268 -0
- voxagent/providers/chatgpt.py +415 -0
- voxagent/providers/claudecode.py +162 -0
- voxagent/providers/cli_base.py +265 -0
- voxagent/providers/codex.py +183 -0
- voxagent/providers/failover.py +90 -0
- voxagent/providers/google.py +532 -0
- voxagent/providers/groq.py +96 -0
- voxagent/providers/ollama.py +425 -0
- voxagent/providers/openai.py +435 -0
- voxagent/providers/registry.py +175 -0
- voxagent/py.typed +1 -0
- voxagent/security/__init__.py +14 -0
- voxagent/security/events.py +75 -0
- voxagent/security/filter.py +169 -0
- voxagent/security/registry.py +87 -0
- voxagent/session/__init__.py +39 -0
- voxagent/session/compaction.py +237 -0
- voxagent/session/lock.py +103 -0
- voxagent/session/model.py +109 -0
- voxagent/session/storage.py +184 -0
- voxagent/streaming/__init__.py +52 -0
- voxagent/streaming/emitter.py +286 -0
- voxagent/streaming/events.py +255 -0
- voxagent/subagent/__init__.py +20 -0
- voxagent/subagent/context.py +124 -0
- voxagent/subagent/definition.py +172 -0
- voxagent/tools/__init__.py +32 -0
- voxagent/tools/context.py +50 -0
- voxagent/tools/decorator.py +175 -0
- voxagent/tools/definition.py +131 -0
- voxagent/tools/executor.py +109 -0
- voxagent/tools/policy.py +89 -0
- voxagent/tools/registry.py +89 -0
- voxagent/types/__init__.py +46 -0
- voxagent/types/messages.py +134 -0
- voxagent/types/run.py +176 -0
- voxagent-0.1.0.dist-info/METADATA +186 -0
- voxagent-0.1.0.dist-info/RECORD +53 -0
- voxagent-0.1.0.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
"""Tool decorator for auto-generating ToolDefinition from functions.
|
|
2
|
+
|
|
3
|
+
This module provides the @tool decorator that creates a ToolDefinition
|
|
4
|
+
from a function's signature, type hints, and docstring.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import inspect
|
|
10
|
+
import re
|
|
11
|
+
from typing import Any, Callable, TypeVar, Union, get_args, get_origin, get_type_hints
|
|
12
|
+
|
|
13
|
+
from voxagent.tools.definition import ToolDefinition
|
|
14
|
+
|
|
15
|
+
R = TypeVar("R")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def tool(
|
|
19
|
+
name: str | None = None,
|
|
20
|
+
description: str | None = None,
|
|
21
|
+
) -> Callable[[Callable[..., R]], ToolDefinition]:
|
|
22
|
+
"""Decorator to create a ToolDefinition from a function.
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
name: Tool name (defaults to function name)
|
|
26
|
+
description: Tool description (defaults to docstring first line)
|
|
27
|
+
|
|
28
|
+
Returns:
|
|
29
|
+
A decorator that converts a function to a ToolDefinition
|
|
30
|
+
|
|
31
|
+
Example:
|
|
32
|
+
@tool()
|
|
33
|
+
def get_weather(city: str) -> str:
|
|
34
|
+
'''Get weather for a city.'''
|
|
35
|
+
return f"Weather in {city}: Sunny"
|
|
36
|
+
|
|
37
|
+
# get_weather is now a ToolDefinition
|
|
38
|
+
assert get_weather.name == "get_weather"
|
|
39
|
+
assert get_weather.description == "Get weather for a city."
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
def decorator(fn: Callable[..., R]) -> ToolDefinition:
|
|
43
|
+
# Determine name
|
|
44
|
+
tool_name = name if name is not None else fn.__name__
|
|
45
|
+
|
|
46
|
+
# Validate name
|
|
47
|
+
if not re.match(r"^[a-zA-Z_][a-zA-Z0-9_]*$", tool_name):
|
|
48
|
+
raise ValueError(f"Invalid tool name: {tool_name}")
|
|
49
|
+
|
|
50
|
+
# Determine description from docstring
|
|
51
|
+
tool_description = description
|
|
52
|
+
if tool_description is None:
|
|
53
|
+
if fn.__doc__:
|
|
54
|
+
# Use first line of docstring
|
|
55
|
+
tool_description = fn.__doc__.strip().split("\n")[0].strip()
|
|
56
|
+
else:
|
|
57
|
+
tool_description = ""
|
|
58
|
+
|
|
59
|
+
# Check if async
|
|
60
|
+
is_async = inspect.iscoroutinefunction(fn)
|
|
61
|
+
|
|
62
|
+
# Build parameters schema from type hints
|
|
63
|
+
parameters = _build_parameters_schema(fn)
|
|
64
|
+
|
|
65
|
+
return ToolDefinition(
|
|
66
|
+
name=tool_name,
|
|
67
|
+
description=tool_description,
|
|
68
|
+
parameters=parameters,
|
|
69
|
+
execute=fn,
|
|
70
|
+
is_async=is_async,
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
return decorator
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _build_parameters_schema(fn: Callable[..., Any]) -> dict[str, Any]:
|
|
77
|
+
"""Build JSON Schema from function type hints."""
|
|
78
|
+
sig = inspect.signature(fn)
|
|
79
|
+
hints: dict[str, Any] = {}
|
|
80
|
+
try:
|
|
81
|
+
hints = get_type_hints(fn)
|
|
82
|
+
except Exception:
|
|
83
|
+
pass
|
|
84
|
+
|
|
85
|
+
properties: dict[str, Any] = {}
|
|
86
|
+
required: list[str] = []
|
|
87
|
+
|
|
88
|
+
for param_name, param in sig.parameters.items():
|
|
89
|
+
# Skip 'context' parameter (ToolContext)
|
|
90
|
+
if param_name == "context":
|
|
91
|
+
continue
|
|
92
|
+
|
|
93
|
+
# Skip *args and **kwargs
|
|
94
|
+
if param.kind in (
|
|
95
|
+
inspect.Parameter.VAR_POSITIONAL,
|
|
96
|
+
inspect.Parameter.VAR_KEYWORD,
|
|
97
|
+
):
|
|
98
|
+
continue
|
|
99
|
+
|
|
100
|
+
# Get type hint
|
|
101
|
+
type_hint = hints.get(param_name, Any)
|
|
102
|
+
|
|
103
|
+
# Convert type to JSON Schema
|
|
104
|
+
prop_schema = _type_to_json_schema(type_hint)
|
|
105
|
+
properties[param_name] = prop_schema
|
|
106
|
+
|
|
107
|
+
# Check if required (no default value)
|
|
108
|
+
if param.default is inspect.Parameter.empty:
|
|
109
|
+
required.append(param_name)
|
|
110
|
+
else:
|
|
111
|
+
# Add default to schema
|
|
112
|
+
if param.default is not None:
|
|
113
|
+
properties[param_name]["default"] = param.default
|
|
114
|
+
|
|
115
|
+
schema: dict[str, Any] = {
|
|
116
|
+
"type": "object",
|
|
117
|
+
"properties": properties,
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if required:
|
|
121
|
+
schema["required"] = required
|
|
122
|
+
else:
|
|
123
|
+
schema["required"] = []
|
|
124
|
+
|
|
125
|
+
return schema
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def _type_to_json_schema(type_hint: Any) -> dict[str, Any]:
|
|
129
|
+
"""Convert a Python type hint to JSON Schema."""
|
|
130
|
+
# Handle None/NoneType
|
|
131
|
+
if type_hint is type(None):
|
|
132
|
+
return {"type": "null"}
|
|
133
|
+
|
|
134
|
+
# Handle basic types
|
|
135
|
+
if type_hint is str:
|
|
136
|
+
return {"type": "string"}
|
|
137
|
+
if type_hint is int:
|
|
138
|
+
return {"type": "integer"}
|
|
139
|
+
if type_hint is float:
|
|
140
|
+
return {"type": "number"}
|
|
141
|
+
if type_hint is bool:
|
|
142
|
+
return {"type": "boolean"}
|
|
143
|
+
|
|
144
|
+
# Handle Optional (Union with None)
|
|
145
|
+
origin = get_origin(type_hint)
|
|
146
|
+
args = get_args(type_hint)
|
|
147
|
+
|
|
148
|
+
if origin is Union:
|
|
149
|
+
# Check if it's Optional (Union[X, None])
|
|
150
|
+
non_none_args = [a for a in args if a is not type(None)]
|
|
151
|
+
if len(non_none_args) == 1 and type(None) in args:
|
|
152
|
+
# It's Optional[X]
|
|
153
|
+
inner_schema = _type_to_json_schema(non_none_args[0])
|
|
154
|
+
inner_schema["nullable"] = True
|
|
155
|
+
return inner_schema
|
|
156
|
+
# General Union - use anyOf
|
|
157
|
+
return {"anyOf": [_type_to_json_schema(a) for a in args]}
|
|
158
|
+
|
|
159
|
+
# Handle list
|
|
160
|
+
if origin is list:
|
|
161
|
+
if args:
|
|
162
|
+
return {"type": "array", "items": _type_to_json_schema(args[0])}
|
|
163
|
+
return {"type": "array"}
|
|
164
|
+
|
|
165
|
+
# Handle dict
|
|
166
|
+
if origin is dict:
|
|
167
|
+
return {"type": "object"}
|
|
168
|
+
|
|
169
|
+
# Handle Any
|
|
170
|
+
if type_hint is Any:
|
|
171
|
+
return {}
|
|
172
|
+
|
|
173
|
+
# Default to object for unknown types
|
|
174
|
+
return {"type": "object"}
|
|
175
|
+
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
"""Tool definition classes.
|
|
2
|
+
|
|
3
|
+
This module provides ToolDefinition and ToolContext for the agent tool system.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import asyncio
|
|
9
|
+
import inspect
|
|
10
|
+
from typing import Any, Callable
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ToolContext:
|
|
14
|
+
"""Context passed to tool functions during execution.
|
|
15
|
+
|
|
16
|
+
Provides access to abort signals, dependencies, and other runtime context.
|
|
17
|
+
|
|
18
|
+
Attributes:
|
|
19
|
+
abort_signal: An asyncio.Event that can be used to signal cancellation.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
def __init__(self, abort_signal: asyncio.Event | None = None) -> None:
|
|
23
|
+
"""Initialize ToolContext.
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
abort_signal: Optional event for signaling tool cancellation.
|
|
27
|
+
"""
|
|
28
|
+
self.abort_signal = abort_signal
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class ToolDefinition:
|
|
32
|
+
"""Definition of a tool that can be called by an agent.
|
|
33
|
+
|
|
34
|
+
Attributes:
|
|
35
|
+
name: The tool name (alphanumeric and underscores only).
|
|
36
|
+
description: Human-readable description for the LLM.
|
|
37
|
+
parameters: JSON Schema for the tool's parameters.
|
|
38
|
+
execute: The callable that implements the tool.
|
|
39
|
+
is_async: Whether the execute function is async.
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
def __init__(
|
|
43
|
+
self,
|
|
44
|
+
name: str,
|
|
45
|
+
description: str,
|
|
46
|
+
execute: Callable[..., Any],
|
|
47
|
+
parameters: dict[str, Any] | None = None,
|
|
48
|
+
is_async: bool = False,
|
|
49
|
+
) -> None:
|
|
50
|
+
"""Initialize ToolDefinition.
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
name: Tool name (alphanumeric and underscores only).
|
|
54
|
+
description: Description for the LLM.
|
|
55
|
+
execute: The function that implements the tool.
|
|
56
|
+
parameters: JSON Schema for parameters. Defaults to empty dict.
|
|
57
|
+
is_async: Whether execute is an async function.
|
|
58
|
+
|
|
59
|
+
Raises:
|
|
60
|
+
ValueError: If name contains invalid characters.
|
|
61
|
+
"""
|
|
62
|
+
# Validate name
|
|
63
|
+
import re
|
|
64
|
+
|
|
65
|
+
if not re.match(r"^[a-zA-Z_][a-zA-Z0-9_]*$", name):
|
|
66
|
+
raise ValueError(
|
|
67
|
+
f"Tool name '{name}' is invalid. "
|
|
68
|
+
"Must contain only alphanumeric characters and underscores, "
|
|
69
|
+
"and cannot start with a digit."
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
self.name = name
|
|
73
|
+
self.description = description
|
|
74
|
+
self.execute = execute
|
|
75
|
+
self.parameters = parameters if parameters is not None else {}
|
|
76
|
+
self.is_async = is_async
|
|
77
|
+
|
|
78
|
+
async def run(self, params: dict[str, Any], context: ToolContext) -> Any:
|
|
79
|
+
"""Execute the tool with given parameters.
|
|
80
|
+
|
|
81
|
+
Args:
|
|
82
|
+
params: Dictionary of parameter values.
|
|
83
|
+
context: The ToolContext for this execution.
|
|
84
|
+
|
|
85
|
+
Returns:
|
|
86
|
+
The result from the tool execution.
|
|
87
|
+
"""
|
|
88
|
+
# Check if execute function accepts 'context' or 'ctx' parameter
|
|
89
|
+
sig = inspect.signature(self.execute)
|
|
90
|
+
accepts_context = "context" in sig.parameters
|
|
91
|
+
accepts_ctx = "ctx" in sig.parameters
|
|
92
|
+
|
|
93
|
+
call_params = dict(params)
|
|
94
|
+
if accepts_context:
|
|
95
|
+
call_params["context"] = context
|
|
96
|
+
elif accepts_ctx:
|
|
97
|
+
call_params["ctx"] = context
|
|
98
|
+
|
|
99
|
+
if self.is_async:
|
|
100
|
+
return await self.execute(**call_params)
|
|
101
|
+
else:
|
|
102
|
+
# Run sync function directly
|
|
103
|
+
return self.execute(**call_params)
|
|
104
|
+
|
|
105
|
+
def to_openai_schema(self) -> dict[str, Any]:
|
|
106
|
+
"""Convert to OpenAI function calling format.
|
|
107
|
+
|
|
108
|
+
Returns:
|
|
109
|
+
Dictionary in OpenAI's function calling format.
|
|
110
|
+
"""
|
|
111
|
+
return {
|
|
112
|
+
"type": "function",
|
|
113
|
+
"function": {
|
|
114
|
+
"name": self.name,
|
|
115
|
+
"description": self.description,
|
|
116
|
+
"parameters": self.parameters if self.parameters else {"type": "object", "properties": {}},
|
|
117
|
+
},
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
def to_anthropic_schema(self) -> dict[str, Any]:
|
|
121
|
+
"""Convert to Anthropic tool format.
|
|
122
|
+
|
|
123
|
+
Returns:
|
|
124
|
+
Dictionary in Anthropic's tool format.
|
|
125
|
+
"""
|
|
126
|
+
return {
|
|
127
|
+
"name": self.name,
|
|
128
|
+
"description": self.description,
|
|
129
|
+
"input_schema": self.parameters if self.parameters else {"type": "object", "properties": {}},
|
|
130
|
+
}
|
|
131
|
+
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"""Tool executor for voxagent."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from voxagent.providers.base import AbortSignal
|
|
9
|
+
from voxagent.tools.context import AbortError, ToolContext
|
|
10
|
+
from voxagent.tools.definition import ToolDefinition
|
|
11
|
+
from voxagent.types.messages import ToolResult
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
async def execute_tool(
|
|
15
|
+
name: str,
|
|
16
|
+
params: dict[str, Any],
|
|
17
|
+
tools: list[ToolDefinition],
|
|
18
|
+
abort_signal: AbortSignal,
|
|
19
|
+
tool_use_id: str,
|
|
20
|
+
deps: Any = None,
|
|
21
|
+
session_id: str | None = None,
|
|
22
|
+
run_id: str | None = None,
|
|
23
|
+
) -> ToolResult:
|
|
24
|
+
"""Execute a tool by name.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
name: Tool name to execute
|
|
28
|
+
params: Parameters to pass to the tool
|
|
29
|
+
tools: List of available tools
|
|
30
|
+
abort_signal: Signal to check for abort
|
|
31
|
+
tool_use_id: ID for the tool use (from LLM)
|
|
32
|
+
deps: Optional dependencies to inject
|
|
33
|
+
session_id: Current session ID
|
|
34
|
+
run_id: Current run ID
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
ToolResult with content or error
|
|
38
|
+
"""
|
|
39
|
+
# Find tool
|
|
40
|
+
tool = next((t for t in tools if t.name == name), None)
|
|
41
|
+
|
|
42
|
+
if tool is None:
|
|
43
|
+
return ToolResult(
|
|
44
|
+
tool_use_id=tool_use_id,
|
|
45
|
+
content=f"Unknown tool: {name}",
|
|
46
|
+
is_error=True,
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
# Check abort before execution
|
|
50
|
+
if abort_signal.aborted:
|
|
51
|
+
return ToolResult(
|
|
52
|
+
tool_use_id=tool_use_id,
|
|
53
|
+
content="Aborted",
|
|
54
|
+
is_error=True,
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
# Create context
|
|
58
|
+
context = ToolContext(
|
|
59
|
+
abort_signal=abort_signal,
|
|
60
|
+
deps=deps,
|
|
61
|
+
session_id=session_id,
|
|
62
|
+
run_id=run_id,
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
try:
|
|
66
|
+
# Execute tool
|
|
67
|
+
result = await tool.run(params, context)
|
|
68
|
+
|
|
69
|
+
# Sanitize result
|
|
70
|
+
content = _sanitize_result(result)
|
|
71
|
+
|
|
72
|
+
return ToolResult(
|
|
73
|
+
tool_use_id=tool_use_id,
|
|
74
|
+
content=content,
|
|
75
|
+
is_error=False,
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
except AbortError:
|
|
79
|
+
return ToolResult(
|
|
80
|
+
tool_use_id=tool_use_id,
|
|
81
|
+
content="Aborted",
|
|
82
|
+
is_error=True,
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
except Exception as e:
|
|
86
|
+
return ToolResult(
|
|
87
|
+
tool_use_id=tool_use_id,
|
|
88
|
+
content=_format_error(e),
|
|
89
|
+
is_error=True,
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _sanitize_result(result: Any) -> str:
|
|
94
|
+
"""Convert tool result to string."""
|
|
95
|
+
if result is None:
|
|
96
|
+
return ""
|
|
97
|
+
if isinstance(result, str):
|
|
98
|
+
return result
|
|
99
|
+
# JSON serialize dicts, lists, etc.
|
|
100
|
+
try:
|
|
101
|
+
return json.dumps(result)
|
|
102
|
+
except (TypeError, ValueError):
|
|
103
|
+
return str(result)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _format_error(e: Exception) -> str:
|
|
107
|
+
"""Format exception for tool result."""
|
|
108
|
+
return f"{type(e).__name__}: {e}"
|
|
109
|
+
|
voxagent/tools/policy.py
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"""Tool policy for voxagent."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pydantic import BaseModel, Field
|
|
6
|
+
|
|
7
|
+
from voxagent.tools.definition import ToolDefinition
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ToolPolicy(BaseModel):
|
|
11
|
+
"""Policy for filtering available tools.
|
|
12
|
+
|
|
13
|
+
Attributes:
|
|
14
|
+
allow_list: List of allowed tool names, or None to allow all.
|
|
15
|
+
deny_list: List of denied tool names.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
allow_list: list[str] | None = None # None = allow all
|
|
19
|
+
deny_list: list[str] = Field(default_factory=list)
|
|
20
|
+
|
|
21
|
+
def allows(self, tool_name: str) -> bool:
|
|
22
|
+
"""Check if this policy allows a tool.
|
|
23
|
+
|
|
24
|
+
Deny list takes precedence over allow list.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
tool_name: The name of the tool to check.
|
|
28
|
+
|
|
29
|
+
Returns:
|
|
30
|
+
True if the tool is allowed, False otherwise.
|
|
31
|
+
"""
|
|
32
|
+
# Deny list takes precedence
|
|
33
|
+
if tool_name in self.deny_list:
|
|
34
|
+
return False
|
|
35
|
+
# If allow_list is set, tool must be in it
|
|
36
|
+
if self.allow_list is not None and tool_name not in self.allow_list:
|
|
37
|
+
return False
|
|
38
|
+
return True
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def apply_tool_policies(
|
|
42
|
+
tools: list[ToolDefinition],
|
|
43
|
+
policies: list[ToolPolicy],
|
|
44
|
+
) -> list[ToolDefinition]:
|
|
45
|
+
"""Apply layered policies to filter tools.
|
|
46
|
+
|
|
47
|
+
Policies are applied in order:
|
|
48
|
+
- Allow lists are intersected (each policy can only restrict further)
|
|
49
|
+
- Deny lists are unioned (each policy can add more denials)
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
tools: List of tools to filter.
|
|
53
|
+
policies: List of policies to apply in order.
|
|
54
|
+
|
|
55
|
+
Returns:
|
|
56
|
+
Filtered list of tools.
|
|
57
|
+
"""
|
|
58
|
+
if not policies:
|
|
59
|
+
return tools
|
|
60
|
+
|
|
61
|
+
# Compute effective allow and deny lists
|
|
62
|
+
effective_allow: set[str] | None = None
|
|
63
|
+
effective_deny: set[str] = set()
|
|
64
|
+
|
|
65
|
+
for policy in policies:
|
|
66
|
+
# Intersect allow lists
|
|
67
|
+
if policy.allow_list is not None:
|
|
68
|
+
policy_allow = set(policy.allow_list)
|
|
69
|
+
if effective_allow is None:
|
|
70
|
+
effective_allow = policy_allow
|
|
71
|
+
else:
|
|
72
|
+
effective_allow = effective_allow.intersection(policy_allow)
|
|
73
|
+
|
|
74
|
+
# Union deny lists
|
|
75
|
+
effective_deny = effective_deny.union(policy.deny_list)
|
|
76
|
+
|
|
77
|
+
# Filter tools
|
|
78
|
+
result = []
|
|
79
|
+
for tool in tools:
|
|
80
|
+
# Check allow list (if set)
|
|
81
|
+
if effective_allow is not None and tool.name not in effective_allow:
|
|
82
|
+
continue
|
|
83
|
+
# Check deny list
|
|
84
|
+
if tool.name in effective_deny:
|
|
85
|
+
continue
|
|
86
|
+
result.append(tool)
|
|
87
|
+
|
|
88
|
+
return result
|
|
89
|
+
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"""Tool registry for voxagent."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from voxagent.tools.definition import ToolDefinition
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ToolNotFoundError(Exception):
|
|
9
|
+
"""Raised when a tool is not found in the registry."""
|
|
10
|
+
|
|
11
|
+
def __init__(self, name: str) -> None:
|
|
12
|
+
self.name = name
|
|
13
|
+
super().__init__(f"Tool not found: {name}")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class ToolAlreadyRegisteredError(Exception):
|
|
17
|
+
"""Raised when trying to register a tool that already exists."""
|
|
18
|
+
|
|
19
|
+
def __init__(self, name: str) -> None:
|
|
20
|
+
self.name = name
|
|
21
|
+
super().__init__(f"Tool already registered: {name}")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class ToolRegistry:
|
|
25
|
+
"""Registry for managing tool definitions."""
|
|
26
|
+
|
|
27
|
+
def __init__(self) -> None:
|
|
28
|
+
self._tools: dict[str, ToolDefinition] = {}
|
|
29
|
+
|
|
30
|
+
def register(self, tool: ToolDefinition, prefix: str | None = None) -> None:
|
|
31
|
+
"""Register a tool.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
tool: The tool definition to register
|
|
35
|
+
prefix: Optional namespace prefix (e.g., "mcp_server_name")
|
|
36
|
+
|
|
37
|
+
Raises:
|
|
38
|
+
ToolAlreadyRegisteredError: If tool with same name already registered
|
|
39
|
+
"""
|
|
40
|
+
name = f"{prefix}_{tool.name}" if prefix else tool.name
|
|
41
|
+
if name in self._tools:
|
|
42
|
+
raise ToolAlreadyRegisteredError(name)
|
|
43
|
+
self._tools[name] = tool
|
|
44
|
+
|
|
45
|
+
def unregister(self, name: str) -> None:
|
|
46
|
+
"""Unregister a tool by name.
|
|
47
|
+
|
|
48
|
+
Raises:
|
|
49
|
+
ToolNotFoundError: If tool not found
|
|
50
|
+
"""
|
|
51
|
+
if name not in self._tools:
|
|
52
|
+
raise ToolNotFoundError(name)
|
|
53
|
+
del self._tools[name]
|
|
54
|
+
|
|
55
|
+
def get(self, name: str) -> ToolDefinition | None:
|
|
56
|
+
"""Get a tool by name, returns None if not found."""
|
|
57
|
+
return self._tools.get(name)
|
|
58
|
+
|
|
59
|
+
def get_or_raise(self, name: str) -> ToolDefinition:
|
|
60
|
+
"""Get a tool by name.
|
|
61
|
+
|
|
62
|
+
Raises:
|
|
63
|
+
ToolNotFoundError: If tool not found
|
|
64
|
+
"""
|
|
65
|
+
tool = self._tools.get(name)
|
|
66
|
+
if tool is None:
|
|
67
|
+
raise ToolNotFoundError(name)
|
|
68
|
+
return tool
|
|
69
|
+
|
|
70
|
+
def list(self) -> list[ToolDefinition]:
|
|
71
|
+
"""List all registered tools."""
|
|
72
|
+
return list(self._tools.values())
|
|
73
|
+
|
|
74
|
+
def list_names(self) -> list[str]:
|
|
75
|
+
"""List all registered tool names."""
|
|
76
|
+
return list(self._tools.keys())
|
|
77
|
+
|
|
78
|
+
def clear(self) -> None:
|
|
79
|
+
"""Clear all registered tools."""
|
|
80
|
+
self._tools.clear()
|
|
81
|
+
|
|
82
|
+
def __contains__(self, name: str) -> bool:
|
|
83
|
+
"""Check if a tool is registered."""
|
|
84
|
+
return name in self._tools
|
|
85
|
+
|
|
86
|
+
def __len__(self) -> int:
|
|
87
|
+
"""Return number of registered tools."""
|
|
88
|
+
return len(self._tools)
|
|
89
|
+
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""Core type definitions.
|
|
2
|
+
|
|
3
|
+
This subpackage provides:
|
|
4
|
+
- Message types (user, assistant, system messages)
|
|
5
|
+
- ContentBlock types (text, image, tool_use, tool_result)
|
|
6
|
+
- ToolCall and ToolResult types for tool interactions
|
|
7
|
+
- ModelConfig and AgentConfig for agent configuration
|
|
8
|
+
- RunParams and RunResult for run lifecycle
|
|
9
|
+
- ToolPolicy and ToolMeta for tool management
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from voxagent.types.messages import (
|
|
13
|
+
ContentBlock,
|
|
14
|
+
ImageBlock,
|
|
15
|
+
Message,
|
|
16
|
+
TextBlock,
|
|
17
|
+
ToolCall,
|
|
18
|
+
ToolResult,
|
|
19
|
+
ToolResultBlock,
|
|
20
|
+
ToolUseBlock,
|
|
21
|
+
)
|
|
22
|
+
from voxagent.types.run import (
|
|
23
|
+
AgentConfig,
|
|
24
|
+
ModelConfig,
|
|
25
|
+
RunParams,
|
|
26
|
+
RunResult,
|
|
27
|
+
ToolMeta,
|
|
28
|
+
ToolPolicy,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
__all__ = [
|
|
32
|
+
"AgentConfig",
|
|
33
|
+
"ContentBlock",
|
|
34
|
+
"ImageBlock",
|
|
35
|
+
"Message",
|
|
36
|
+
"ModelConfig",
|
|
37
|
+
"RunParams",
|
|
38
|
+
"RunResult",
|
|
39
|
+
"TextBlock",
|
|
40
|
+
"ToolCall",
|
|
41
|
+
"ToolMeta",
|
|
42
|
+
"ToolPolicy",
|
|
43
|
+
"ToolResult",
|
|
44
|
+
"ToolResultBlock",
|
|
45
|
+
"ToolUseBlock",
|
|
46
|
+
]
|