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.
Files changed (53) hide show
  1. voxagent/__init__.py +143 -0
  2. voxagent/_version.py +5 -0
  3. voxagent/agent/__init__.py +32 -0
  4. voxagent/agent/abort.py +178 -0
  5. voxagent/agent/core.py +902 -0
  6. voxagent/code/__init__.py +9 -0
  7. voxagent/mcp/__init__.py +16 -0
  8. voxagent/mcp/manager.py +188 -0
  9. voxagent/mcp/tool.py +152 -0
  10. voxagent/providers/__init__.py +110 -0
  11. voxagent/providers/anthropic.py +498 -0
  12. voxagent/providers/augment.py +293 -0
  13. voxagent/providers/auth.py +116 -0
  14. voxagent/providers/base.py +268 -0
  15. voxagent/providers/chatgpt.py +415 -0
  16. voxagent/providers/claudecode.py +162 -0
  17. voxagent/providers/cli_base.py +265 -0
  18. voxagent/providers/codex.py +183 -0
  19. voxagent/providers/failover.py +90 -0
  20. voxagent/providers/google.py +532 -0
  21. voxagent/providers/groq.py +96 -0
  22. voxagent/providers/ollama.py +425 -0
  23. voxagent/providers/openai.py +435 -0
  24. voxagent/providers/registry.py +175 -0
  25. voxagent/py.typed +1 -0
  26. voxagent/security/__init__.py +14 -0
  27. voxagent/security/events.py +75 -0
  28. voxagent/security/filter.py +169 -0
  29. voxagent/security/registry.py +87 -0
  30. voxagent/session/__init__.py +39 -0
  31. voxagent/session/compaction.py +237 -0
  32. voxagent/session/lock.py +103 -0
  33. voxagent/session/model.py +109 -0
  34. voxagent/session/storage.py +184 -0
  35. voxagent/streaming/__init__.py +52 -0
  36. voxagent/streaming/emitter.py +286 -0
  37. voxagent/streaming/events.py +255 -0
  38. voxagent/subagent/__init__.py +20 -0
  39. voxagent/subagent/context.py +124 -0
  40. voxagent/subagent/definition.py +172 -0
  41. voxagent/tools/__init__.py +32 -0
  42. voxagent/tools/context.py +50 -0
  43. voxagent/tools/decorator.py +175 -0
  44. voxagent/tools/definition.py +131 -0
  45. voxagent/tools/executor.py +109 -0
  46. voxagent/tools/policy.py +89 -0
  47. voxagent/tools/registry.py +89 -0
  48. voxagent/types/__init__.py +46 -0
  49. voxagent/types/messages.py +134 -0
  50. voxagent/types/run.py +176 -0
  51. voxagent-0.1.0.dist-info/METADATA +186 -0
  52. voxagent-0.1.0.dist-info/RECORD +53 -0
  53. 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
+
@@ -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
+ ]