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,9 @@
1
+ """Code execution sandbox for token optimization.
2
+
3
+ This subpackage provides:
4
+ - Sandboxed code execution (subprocess, Pyodide)
5
+ - Tool file generation from MCP servers
6
+ - Skills persistence and discovery
7
+ - PII tokenization for privacy
8
+ """
9
+
@@ -0,0 +1,16 @@
1
+ """Model Context Protocol (MCP) integration.
2
+
3
+ This subpackage provides:
4
+ - MCPToolDefinition: ToolDefinition subclass for MCP tools
5
+ - MCPServerManager: Manages MCP server lifecycle and tool extraction
6
+ - sanitize_tool_name: Utility to sanitize MCP tool names
7
+ """
8
+
9
+ from voxagent.mcp.manager import MCPServerManager
10
+ from voxagent.mcp.tool import MCPToolDefinition, sanitize_tool_name
11
+
12
+ __all__ = [
13
+ "MCPServerManager",
14
+ "MCPToolDefinition",
15
+ "sanitize_tool_name",
16
+ ]
@@ -0,0 +1,188 @@
1
+ """MCP Server Manager for voxagent.
2
+
3
+ This module provides MCPServerManager, which handles:
4
+ - Connecting to MCP servers
5
+ - Extracting tools from MCP servers
6
+ - Converting MCP tools to ToolDefinition format
7
+ - Managing server lifecycle (connect/disconnect)
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import logging
13
+ from typing import Any
14
+
15
+ from voxagent.mcp.tool import MCPToolDefinition, sanitize_tool_name
16
+ from voxagent.tools.definition import ToolDefinition
17
+
18
+ logger = logging.getLogger(__name__)
19
+
20
+
21
+ class MCPServerManager:
22
+ """Manages MCP server connections and tool extraction.
23
+
24
+ This class handles the lifecycle of MCP servers and provides methods
25
+ to extract and convert their tools to voxagent's ToolDefinition format.
26
+ """
27
+
28
+ def __init__(self) -> None:
29
+ """Initialize the MCPServerManager."""
30
+ self._servers: list[Any] = []
31
+ self._connected_servers: list[Any] = []
32
+ self._tools: list[ToolDefinition] = []
33
+
34
+ async def add_servers(self, servers: list[Any]) -> None:
35
+ """Add MCP servers to manage.
36
+
37
+ Args:
38
+ servers: List of MCP server instances (PydanticAI MCP classes).
39
+ """
40
+ self._servers.extend(servers)
41
+
42
+ async def connect_all(self) -> list[ToolDefinition]:
43
+ """Connect to all MCP servers and extract their tools.
44
+
45
+ Returns:
46
+ List of ToolDefinition objects from all connected servers.
47
+ """
48
+ self._tools = []
49
+ self._connected_servers = []
50
+
51
+ for server in self._servers:
52
+ try:
53
+ # Enter the async context manager
54
+ await server.__aenter__()
55
+ self._connected_servers.append(server)
56
+
57
+ # Extract tools from this server
58
+ tools = await self._extract_tools(server)
59
+ self._tools.extend(tools)
60
+
61
+ server_name = getattr(server, "name", None) or getattr(
62
+ server, "tool_prefix", "unknown"
63
+ )
64
+ logger.info(
65
+ f"Connected to MCP server '{server_name}' with {len(tools)} tools"
66
+ )
67
+
68
+ except Exception as e:
69
+ server_name = getattr(server, "name", None) or getattr(
70
+ server, "tool_prefix", "unknown"
71
+ )
72
+ logger.warning(f"Failed to connect to MCP server '{server_name}': {e}")
73
+ # Continue with other servers
74
+
75
+ return self._tools
76
+
77
+ async def disconnect_all(self) -> None:
78
+ """Disconnect from all connected MCP servers.
79
+
80
+ Note: PydanticAI MCP servers use anyio cancel scopes, which must be
81
+ entered and exited from the same task. If called from a different task,
82
+ the disconnect will be skipped to avoid RuntimeError.
83
+ """
84
+ for server in self._connected_servers:
85
+ try:
86
+ await server.__aexit__(None, None, None)
87
+ except RuntimeError as e:
88
+ # anyio cancel scope errors when called from different task
89
+ if "cancel scope" in str(e).lower() or "different task" in str(e).lower():
90
+ server_name = getattr(server, "name", None) or getattr(
91
+ server, "tool_prefix", "unknown"
92
+ )
93
+ logger.debug(
94
+ f"MCP server '{server_name}' disconnect skipped (task mismatch): {e}"
95
+ )
96
+ else:
97
+ raise
98
+ except Exception as e:
99
+ server_name = getattr(server, "name", None) or getattr(
100
+ server, "tool_prefix", "unknown"
101
+ )
102
+ logger.warning(f"Error disconnecting from MCP server '{server_name}': {e}")
103
+
104
+ self._connected_servers = []
105
+ self._tools = []
106
+
107
+ async def _extract_tools(self, server: Any) -> list[ToolDefinition]:
108
+ """Extract tools from an MCP server and convert to ToolDefinition.
109
+
110
+ Args:
111
+ server: The connected MCP server instance.
112
+
113
+ Returns:
114
+ List of ToolDefinition objects.
115
+ """
116
+ tools: list[ToolDefinition] = []
117
+
118
+ try:
119
+ # Get the server name/prefix for tool naming
120
+ server_name = getattr(server, "name", None) or getattr(
121
+ server, "tool_prefix", None
122
+ )
123
+
124
+ # List tools from the MCP server
125
+ mcp_tools = await server.list_tools()
126
+
127
+ for mcp_tool in mcp_tools:
128
+ try:
129
+ tool_def = self._convert_mcp_tool(mcp_tool, server, server_name)
130
+ tools.append(tool_def)
131
+ except Exception as e:
132
+ tool_name = getattr(mcp_tool, "name", "unknown")
133
+ logger.warning(f"Failed to convert MCP tool '{tool_name}': {e}")
134
+
135
+ except Exception as e:
136
+ logger.warning(f"Failed to list tools from MCP server: {e}")
137
+
138
+ return tools
139
+
140
+ def _convert_mcp_tool(
141
+ self, mcp_tool: Any, server: Any, server_name: str | None
142
+ ) -> MCPToolDefinition:
143
+ """Convert an MCP tool to MCPToolDefinition.
144
+
145
+ Args:
146
+ mcp_tool: The MCP tool object from list_tools().
147
+ server: The MCP server instance.
148
+ server_name: The server name/prefix.
149
+
150
+ Returns:
151
+ MCPToolDefinition instance.
152
+ """
153
+ # Get tool attributes
154
+ original_name = mcp_tool.name
155
+ description = mcp_tool.description or f"MCP tool: {original_name}"
156
+
157
+ # Build the full tool name with prefix
158
+ if server_name:
159
+ full_name = f"{server_name}_{sanitize_tool_name(original_name)}"
160
+ else:
161
+ full_name = sanitize_tool_name(original_name)
162
+
163
+ # Get input schema (parameters)
164
+ parameters: dict[str, Any] = {"type": "object", "properties": {}}
165
+ if hasattr(mcp_tool, "inputSchema") and mcp_tool.inputSchema:
166
+ parameters = mcp_tool.inputSchema
167
+ elif hasattr(mcp_tool, "input_schema") and mcp_tool.input_schema:
168
+ parameters = mcp_tool.input_schema
169
+
170
+ return MCPToolDefinition(
171
+ name=full_name,
172
+ description=description,
173
+ parameters=parameters,
174
+ mcp_server=server,
175
+ original_tool_name=original_name,
176
+ server_name=server_name or "unknown",
177
+ )
178
+
179
+ @property
180
+ def tools(self) -> list[ToolDefinition]:
181
+ """Get all extracted tools."""
182
+ return self._tools
183
+
184
+ @property
185
+ def connected_server_count(self) -> int:
186
+ """Get the number of connected servers."""
187
+ return len(self._connected_servers)
188
+
voxagent/mcp/tool.py ADDED
@@ -0,0 +1,152 @@
1
+ """MCP Tool Definition for voxagent.
2
+
3
+ This module provides MCPToolDefinition, a ToolDefinition subclass that wraps
4
+ MCP server tools and forwards execution calls to the MCP server.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import re
10
+ from typing import TYPE_CHECKING, Any
11
+
12
+ from voxagent.tools.definition import ToolContext, ToolDefinition
13
+
14
+ if TYPE_CHECKING:
15
+ pass
16
+
17
+
18
+ class MCPToolDefinition(ToolDefinition):
19
+ """A ToolDefinition that wraps an MCP server tool.
20
+
21
+ This class forwards tool execution to the MCP server that provides the tool.
22
+ It stores a reference to the MCP server and the original tool name.
23
+ """
24
+
25
+ def __init__(
26
+ self,
27
+ name: str,
28
+ description: str,
29
+ parameters: dict[str, Any],
30
+ mcp_server: Any,
31
+ original_tool_name: str,
32
+ server_name: str,
33
+ ) -> None:
34
+ """Initialize MCPToolDefinition.
35
+
36
+ Args:
37
+ name: The sanitized tool name (alphanumeric and underscores).
38
+ description: Tool description for the LLM.
39
+ parameters: JSON Schema for the tool's parameters.
40
+ mcp_server: The MCP server instance that provides this tool.
41
+ original_tool_name: The original tool name from the MCP server.
42
+ server_name: The name of the MCP server (for logging/debugging).
43
+ """
44
+ # Don't call parent __init__ with execute - we override run() instead
45
+ # Validate name manually
46
+ if not re.match(r"^[a-zA-Z_][a-zA-Z0-9_]*$", name):
47
+ raise ValueError(
48
+ f"Tool name '{name}' is invalid. "
49
+ "Must contain only alphanumeric characters and underscores, "
50
+ "and cannot start with a digit."
51
+ )
52
+
53
+ self.name = name
54
+ self.description = description
55
+ self.parameters = parameters
56
+ self.is_async = True # MCP calls are always async
57
+
58
+ # MCP-specific attributes
59
+ self._mcp_server = mcp_server
60
+ self._original_tool_name = original_tool_name
61
+ self._server_name = server_name
62
+
63
+ # Set execute to None - we override run() instead
64
+ self.execute = self._execute_mcp_tool
65
+
66
+ async def _execute_mcp_tool(self, **params: Any) -> Any:
67
+ """Execute the MCP tool by calling the MCP server.
68
+
69
+ This is called by the parent's run() method, but we override run()
70
+ to handle MCP-specific execution.
71
+ """
72
+ # This shouldn't be called directly - run() handles execution
73
+ raise NotImplementedError("Use run() instead")
74
+
75
+ async def run(self, params: dict[str, Any], context: ToolContext) -> Any:
76
+ """Execute the tool by forwarding to the MCP server.
77
+
78
+ Args:
79
+ params: Dictionary of parameter values.
80
+ context: The ToolContext for this execution.
81
+
82
+ Returns:
83
+ The result from the MCP server tool execution.
84
+ """
85
+ try:
86
+ # Call the MCP server's direct_call_tool method
87
+ # This bypasses PydanticAI's context requirements
88
+ result = await self._mcp_server.direct_call_tool(
89
+ self._original_tool_name,
90
+ params,
91
+ )
92
+
93
+ # MCP returns a list of content items, extract text
94
+ if hasattr(result, "content") and result.content:
95
+ # Concatenate all text content
96
+ texts = []
97
+ for item in result.content:
98
+ if hasattr(item, "text"):
99
+ texts.append(item.text)
100
+ elif isinstance(item, dict) and "text" in item:
101
+ texts.append(item["text"])
102
+ else:
103
+ texts.append(str(item))
104
+ return "\n".join(texts) if texts else str(result)
105
+
106
+ return str(result)
107
+
108
+ except Exception as e:
109
+ # Return error as string - the executor will handle it
110
+ return f"MCP tool error ({self._server_name}/{self._original_tool_name}): {e}"
111
+
112
+ @property
113
+ def mcp_server(self) -> Any:
114
+ """Get the MCP server instance."""
115
+ return self._mcp_server
116
+
117
+ @property
118
+ def original_tool_name(self) -> str:
119
+ """Get the original MCP tool name."""
120
+ return self._original_tool_name
121
+
122
+ @property
123
+ def server_name(self) -> str:
124
+ """Get the MCP server name."""
125
+ return self._server_name
126
+
127
+
128
+ def sanitize_tool_name(name: str) -> str:
129
+ """Sanitize a tool name to be valid for voxagent.
130
+
131
+ MCP tool names may contain dashes or other characters that are not
132
+ valid in Python identifiers. This function converts them to underscores.
133
+
134
+ Args:
135
+ name: The original tool name.
136
+
137
+ Returns:
138
+ A sanitized tool name with only alphanumeric characters and underscores.
139
+ """
140
+ # Replace dashes and other invalid characters with underscores
141
+ sanitized = re.sub(r"[^a-zA-Z0-9_]", "_", name)
142
+
143
+ # Ensure it doesn't start with a digit
144
+ if sanitized and sanitized[0].isdigit():
145
+ sanitized = "_" + sanitized
146
+
147
+ # Ensure it's not empty
148
+ if not sanitized:
149
+ sanitized = "unnamed_tool"
150
+
151
+ return sanitized
152
+
@@ -0,0 +1,110 @@
1
+ """LLM provider implementations.
2
+
3
+ This subpackage contains unified interfaces for various LLM providers:
4
+ - OpenAI (GPT-4, GPT-4o)
5
+ - Anthropic (Claude)
6
+ - Google (Gemini)
7
+ - Groq (Llama, Mixtral)
8
+ - Ollama (local models)
9
+ - ChatGPT (ChatGPT Plus backend)
10
+ - Augment (Auggie CLI)
11
+ - Codex (OpenAI Codex CLI)
12
+ - ClaudeCode (Claude Code CLI)
13
+ """
14
+
15
+ from voxagent.providers.anthropic import AnthropicProvider
16
+ from voxagent.providers.augment import AugmentProvider
17
+ from voxagent.providers.auth import (
18
+ AuthProfile,
19
+ AuthProfileManager,
20
+ FailoverError,
21
+ FailoverReason,
22
+ )
23
+ from voxagent.providers.base import (
24
+ AbortSignal,
25
+ BaseProvider,
26
+ ErrorChunk,
27
+ MessageEndChunk,
28
+ StreamChunk,
29
+ TextDeltaChunk,
30
+ ToolUseChunk,
31
+ )
32
+ from voxagent.providers.chatgpt import ChatGPTProvider
33
+ from voxagent.providers.claudecode import ClaudeCodeProvider
34
+ from voxagent.providers.cli_base import CLINotFoundError, CLIProvider
35
+ from voxagent.providers.codex import CodexProvider
36
+ from voxagent.providers.failover import (
37
+ FailoverExhaustedError,
38
+ NoProfilesAvailableError,
39
+ run_with_failover,
40
+ )
41
+ from voxagent.providers.google import GoogleProvider
42
+ from voxagent.providers.groq import GroqProvider
43
+ from voxagent.providers.ollama import OllamaProvider
44
+ from voxagent.providers.openai import OpenAIProvider
45
+ from voxagent.providers.registry import (
46
+ InvalidModelStringError,
47
+ ProviderNotFoundError,
48
+ ProviderRegistry,
49
+ get_default_registry,
50
+ )
51
+
52
+ __all__ = [
53
+ "AbortSignal",
54
+ "AnthropicProvider",
55
+ "AugmentProvider",
56
+ "AuthProfile",
57
+ "AuthProfileManager",
58
+ "BaseProvider",
59
+ "ChatGPTProvider",
60
+ "CLINotFoundError",
61
+ "CLIProvider",
62
+ "ClaudeCodeProvider",
63
+ "CodexProvider",
64
+ "ErrorChunk",
65
+ "FailoverError",
66
+ "FailoverExhaustedError",
67
+ "FailoverReason",
68
+ "GoogleProvider",
69
+ "GroqProvider",
70
+ "InvalidModelStringError",
71
+ "MessageEndChunk",
72
+ "NoProfilesAvailableError",
73
+ "OllamaProvider",
74
+ "OpenAIProvider",
75
+ "ProviderNotFoundError",
76
+ "ProviderRegistry",
77
+ "StreamChunk",
78
+ "TextDeltaChunk",
79
+ "ToolUseChunk",
80
+ "get_default_registry",
81
+ "run_with_failover",
82
+ ]
83
+
84
+
85
+ # =============================================================================
86
+ # Register default providers
87
+ # =============================================================================
88
+
89
+ def _register_default_providers() -> None:
90
+ """Register all built-in providers with the default registry."""
91
+ registry = get_default_registry()
92
+
93
+ # Only register if not already registered (idempotent)
94
+ if not registry.list_providers():
95
+ # HTTP API providers
96
+ registry.register("openai", OpenAIProvider)
97
+ registry.register("anthropic", AnthropicProvider)
98
+ registry.register("google", GoogleProvider)
99
+ registry.register("groq", GroqProvider)
100
+ registry.register("ollama", OllamaProvider)
101
+ registry.register("chatgpt", ChatGPTProvider)
102
+ # CLI-wrapped providers
103
+ registry.register("augment", AugmentProvider)
104
+ registry.register("codex", CodexProvider)
105
+ registry.register("claudecode", ClaudeCodeProvider)
106
+
107
+
108
+ # Auto-register providers on module import
109
+ _register_default_providers()
110
+