voxagent 0.2.4__py3-none-any.whl → 0.2.5__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/_version.py +1 -1
- voxagent/code/__init__.py +3 -0
- voxagent/code/agent.py +72 -12
- voxagent/code/query.py +179 -0
- voxagent/code/sandbox.py +144 -4
- voxagent/code/virtual_fs.py +7 -17
- {voxagent-0.2.4.dist-info → voxagent-0.2.5.dist-info}/METADATA +1 -1
- {voxagent-0.2.4.dist-info → voxagent-0.2.5.dist-info}/RECORD +9 -8
- {voxagent-0.2.4.dist-info → voxagent-0.2.5.dist-info}/WHEEL +0 -0
voxagent/_version.py
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
__version__ = "0.2.
|
|
1
|
+
__version__ = "0.2.5"
|
|
2
2
|
__version_info__ = tuple(int(x) for x in __version__.split("."))
|
voxagent/code/__init__.py
CHANGED
|
@@ -30,6 +30,7 @@ from voxagent.code.tool_proxy import (
|
|
|
30
30
|
ToolProxyServer,
|
|
31
31
|
create_tool_proxy_pair,
|
|
32
32
|
)
|
|
33
|
+
from voxagent.code.query import QueryResult
|
|
33
34
|
|
|
34
35
|
__all__ = [
|
|
35
36
|
# Sandbox
|
|
@@ -52,4 +53,6 @@ __all__ = [
|
|
|
52
53
|
"ToolProxyClient",
|
|
53
54
|
"ToolProxyServer",
|
|
54
55
|
"create_tool_proxy_pair",
|
|
56
|
+
# Query
|
|
57
|
+
"QueryResult",
|
|
55
58
|
]
|
voxagent/code/agent.py
CHANGED
|
@@ -7,9 +7,12 @@ that calls tools via the virtual filesystem.
|
|
|
7
7
|
|
|
8
8
|
from __future__ import annotations
|
|
9
9
|
|
|
10
|
+
import asyncio
|
|
11
|
+
import multiprocessing
|
|
10
12
|
from typing import TYPE_CHECKING, Any
|
|
11
13
|
|
|
12
14
|
from voxagent.code.sandbox import SubprocessSandbox, SandboxResult
|
|
15
|
+
from voxagent.code.tool_proxy import ToolProxyServer
|
|
13
16
|
from voxagent.code.virtual_fs import VirtualFilesystem, ToolRegistry
|
|
14
17
|
from voxagent.tools.definition import ToolDefinition
|
|
15
18
|
|
|
@@ -32,6 +35,21 @@ You have access to a single tool: `execute_code`. Use it to write Python code th
|
|
|
32
35
|
- `call_tool(category, tool_name, **kwargs)` - Call a tool with arguments
|
|
33
36
|
- `print(*args)` - Output results (captured and returned to you)
|
|
34
37
|
|
|
38
|
+
### Query Functions (for efficient data processing)
|
|
39
|
+
- `query(data)` - Wrap tool result in QueryResult for chaining
|
|
40
|
+
- `tree(path?)` - Show full tool structure at a glance
|
|
41
|
+
- `search(keyword)` - Find tools by name
|
|
42
|
+
|
|
43
|
+
### QueryResult Methods (chainable)
|
|
44
|
+
- `.filter(**patterns)` - Filter by regex patterns on fields
|
|
45
|
+
- `.map(fields)` - Extract specific fields
|
|
46
|
+
- `.reduce(by?, count?)` - Aggregate/group results
|
|
47
|
+
- `.first()` - Get first result
|
|
48
|
+
- `.last()` - Get last result
|
|
49
|
+
- `.each(fn)` - Apply function to each result
|
|
50
|
+
- `.sort(by, reverse?)` - Sort results by field
|
|
51
|
+
- `.unique(by)` - Remove duplicates by field
|
|
52
|
+
|
|
35
53
|
### Workflow
|
|
36
54
|
1. **Explore**: `print(ls("tools/"))` to see categories
|
|
37
55
|
2. **Learn**: `print(read("tools/<category>/<tool>.py"))` to see tool signatures
|
|
@@ -51,6 +69,19 @@ result = call_tool("devices", "registry.py", device_type="light")
|
|
|
51
69
|
print("Devices:", result)
|
|
52
70
|
```
|
|
53
71
|
|
|
72
|
+
### Efficient Device Control Example
|
|
73
|
+
```python
|
|
74
|
+
# Instead of 12+ calls, use 2:
|
|
75
|
+
devices = call_tool("devices", "list_devices")
|
|
76
|
+
device = query(devices).filter(name="balcony").first()
|
|
77
|
+
call_tool("control", "turn_on_device", **device)
|
|
78
|
+
|
|
79
|
+
# Find all living room lights and turn them off
|
|
80
|
+
devices = call_tool("devices", "list_devices")
|
|
81
|
+
living_lights = query(devices).filter(room="living", type="light")
|
|
82
|
+
living_lights.each(lambda d: call_tool("control", "turn_off_device", device_id=d["device_id"]))
|
|
83
|
+
```
|
|
84
|
+
|
|
54
85
|
### Rules
|
|
55
86
|
1. Always use `print()` to show results
|
|
56
87
|
2. Explore before assuming - use `ls()` and `read()` first
|
|
@@ -83,13 +114,13 @@ class CodeModeConfig:
|
|
|
83
114
|
|
|
84
115
|
class CodeModeExecutor:
|
|
85
116
|
"""Executes code for an agent in code mode.
|
|
86
|
-
|
|
117
|
+
|
|
87
118
|
This class:
|
|
88
119
|
1. Manages the sandbox and virtual filesystem
|
|
89
120
|
2. Provides the execute_code tool implementation
|
|
90
|
-
3. Routes tool calls from sandbox to real implementations
|
|
121
|
+
3. Routes tool calls from sandbox to real implementations via queue-based proxy
|
|
91
122
|
"""
|
|
92
|
-
|
|
123
|
+
|
|
93
124
|
def __init__(
|
|
94
125
|
self,
|
|
95
126
|
config: CodeModeConfig,
|
|
@@ -97,19 +128,28 @@ class CodeModeExecutor:
|
|
|
97
128
|
):
|
|
98
129
|
self.config = config
|
|
99
130
|
self.tool_registry = tool_registry
|
|
131
|
+
|
|
132
|
+
# Create tool proxy queues (queues are picklable, client/server objects are not)
|
|
133
|
+
self._tool_request_queue: multiprocessing.Queue[Any] = multiprocessing.Queue()
|
|
134
|
+
self._tool_response_queue: multiprocessing.Queue[Any] = multiprocessing.Queue()
|
|
135
|
+
|
|
136
|
+
# Create proxy server for main process
|
|
137
|
+
self._proxy_server = ToolProxyServer(
|
|
138
|
+
self._tool_request_queue,
|
|
139
|
+
self._tool_response_queue,
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
# Create sandbox (queues passed during execute)
|
|
100
143
|
self.sandbox = SubprocessSandbox(
|
|
101
144
|
timeout_seconds=config.timeout_seconds,
|
|
102
145
|
memory_limit_mb=config.memory_limit_mb,
|
|
103
146
|
)
|
|
104
147
|
|
|
105
|
-
# Tool
|
|
148
|
+
# Tool implementations registry (kept for backward compatibility)
|
|
106
149
|
self._tool_implementations: dict[str, Any] = {}
|
|
107
150
|
|
|
108
|
-
# Create virtual filesystem
|
|
109
|
-
self.virtual_fs = VirtualFilesystem(
|
|
110
|
-
registry=tool_registry,
|
|
111
|
-
tool_caller=self.call_tool,
|
|
112
|
-
)
|
|
151
|
+
# Create virtual filesystem (no longer needs tool_caller)
|
|
152
|
+
self.virtual_fs = VirtualFilesystem(registry=tool_registry)
|
|
113
153
|
|
|
114
154
|
def register_tool_implementation(
|
|
115
155
|
self,
|
|
@@ -120,6 +160,8 @@ class CodeModeExecutor:
|
|
|
120
160
|
"""Register a real tool implementation for the proxy."""
|
|
121
161
|
key = f"{category}.{tool_name}"
|
|
122
162
|
self._tool_implementations[key] = implementation
|
|
163
|
+
# Also register with proxy server for queue-based communication
|
|
164
|
+
self._proxy_server.register_implementation(category, tool_name, implementation)
|
|
123
165
|
|
|
124
166
|
async def execute_code(self, code: str) -> str:
|
|
125
167
|
"""Execute Python code in the sandbox.
|
|
@@ -132,11 +174,29 @@ class CodeModeExecutor:
|
|
|
132
174
|
Returns:
|
|
133
175
|
Captured output or error message
|
|
134
176
|
"""
|
|
135
|
-
# Build globals with virtual filesystem functions (ls, read
|
|
177
|
+
# Build globals with virtual filesystem functions (ls, read)
|
|
136
178
|
globals_dict = self.virtual_fs.get_sandbox_globals()
|
|
137
179
|
|
|
138
|
-
#
|
|
139
|
-
|
|
180
|
+
# Start proxy server task to handle tool calls from subprocess
|
|
181
|
+
server_task = asyncio.create_task(
|
|
182
|
+
self._proxy_server.run_until_complete(timeout=self.config.timeout_seconds + 5)
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
try:
|
|
186
|
+
# Execute in sandbox with queues for tool proxy
|
|
187
|
+
result = await self.sandbox.execute(
|
|
188
|
+
code,
|
|
189
|
+
globals_dict,
|
|
190
|
+
tool_request_queue=self._tool_request_queue,
|
|
191
|
+
tool_response_queue=self._tool_response_queue,
|
|
192
|
+
)
|
|
193
|
+
finally:
|
|
194
|
+
# Stop server
|
|
195
|
+
self._proxy_server.stop()
|
|
196
|
+
try:
|
|
197
|
+
await asyncio.wait_for(server_task, timeout=1.0)
|
|
198
|
+
except asyncio.TimeoutError:
|
|
199
|
+
pass
|
|
140
200
|
|
|
141
201
|
# Format output
|
|
142
202
|
if result.success:
|
voxagent/code/query.py
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
"""Query utilities for Code Mode sandbox.
|
|
2
|
+
|
|
3
|
+
Provides functional-style data pipelines for efficient tool result processing.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import re
|
|
9
|
+
from typing import Any, Callable, Iterator
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class QueryResult:
|
|
13
|
+
"""Chainable wrapper for query results.
|
|
14
|
+
|
|
15
|
+
Enables functional-style data pipelines:
|
|
16
|
+
filter("tools/devices/list_devices", name="balcony").map(["device_id", "name"]).first()
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
def __init__(self, data: list[dict[str, Any]]) -> None:
|
|
20
|
+
"""Initialize with list of dicts."""
|
|
21
|
+
self._data = data if isinstance(data, list) else [data] if data else []
|
|
22
|
+
|
|
23
|
+
def __iter__(self) -> Iterator[dict[str, Any]]:
|
|
24
|
+
"""Allow iteration over results."""
|
|
25
|
+
return iter(self._data)
|
|
26
|
+
|
|
27
|
+
def __len__(self) -> int:
|
|
28
|
+
"""Return number of results."""
|
|
29
|
+
return len(self._data)
|
|
30
|
+
|
|
31
|
+
def __repr__(self) -> str:
|
|
32
|
+
"""String representation."""
|
|
33
|
+
return f"QueryResult({len(self._data)} items)"
|
|
34
|
+
|
|
35
|
+
@property
|
|
36
|
+
def data(self) -> list[dict[str, Any]]:
|
|
37
|
+
"""Get raw data."""
|
|
38
|
+
return self._data
|
|
39
|
+
|
|
40
|
+
def filter(self, **patterns: str) -> "QueryResult":
|
|
41
|
+
"""Filter results by regex patterns on field values.
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
**patterns: Field name -> regex pattern pairs
|
|
45
|
+
|
|
46
|
+
Returns:
|
|
47
|
+
New QueryResult with matching items
|
|
48
|
+
|
|
49
|
+
Example:
|
|
50
|
+
result.filter(name="balcony", room="living.*")
|
|
51
|
+
"""
|
|
52
|
+
if not patterns:
|
|
53
|
+
return self
|
|
54
|
+
|
|
55
|
+
filtered = []
|
|
56
|
+
for item in self._data:
|
|
57
|
+
match = True
|
|
58
|
+
for field, pattern in patterns.items():
|
|
59
|
+
value = str(item.get(field, ""))
|
|
60
|
+
if not re.search(pattern, value, re.IGNORECASE):
|
|
61
|
+
match = False
|
|
62
|
+
break
|
|
63
|
+
if match:
|
|
64
|
+
filtered.append(item)
|
|
65
|
+
|
|
66
|
+
return QueryResult(filtered)
|
|
67
|
+
|
|
68
|
+
def map(self, fields: list[str] | str) -> "QueryResult":
|
|
69
|
+
"""Extract specific fields from each result.
|
|
70
|
+
|
|
71
|
+
Args:
|
|
72
|
+
fields: Field name or list of field names to extract
|
|
73
|
+
|
|
74
|
+
Returns:
|
|
75
|
+
New QueryResult with only specified fields
|
|
76
|
+
|
|
77
|
+
Example:
|
|
78
|
+
result.map(["device_id", "name"])
|
|
79
|
+
result.map("device_id") # Returns list of values
|
|
80
|
+
"""
|
|
81
|
+
if isinstance(fields, str):
|
|
82
|
+
# Single field - return list of values wrapped in dicts
|
|
83
|
+
return QueryResult([{fields: item.get(fields)} for item in self._data])
|
|
84
|
+
|
|
85
|
+
mapped = []
|
|
86
|
+
for item in self._data:
|
|
87
|
+
mapped.append({f: item.get(f) for f in fields if f in item})
|
|
88
|
+
return QueryResult(mapped)
|
|
89
|
+
|
|
90
|
+
def reduce(self, by: str | None = None, count: bool = False) -> dict[str, Any]:
|
|
91
|
+
"""Aggregate results.
|
|
92
|
+
|
|
93
|
+
Args:
|
|
94
|
+
by: Field to group by (optional)
|
|
95
|
+
count: If True, return counts per group
|
|
96
|
+
|
|
97
|
+
Returns:
|
|
98
|
+
Aggregated dict
|
|
99
|
+
|
|
100
|
+
Example:
|
|
101
|
+
result.reduce(by="room", count=True) # {"living": 3, "bedroom": 2}
|
|
102
|
+
"""
|
|
103
|
+
if by is None:
|
|
104
|
+
if count:
|
|
105
|
+
return {"count": len(self._data)}
|
|
106
|
+
return {"items": self._data}
|
|
107
|
+
|
|
108
|
+
groups: dict[str, list[dict[str, Any]]] = {}
|
|
109
|
+
for item in self._data:
|
|
110
|
+
key = str(item.get(by, "unknown"))
|
|
111
|
+
if key not in groups:
|
|
112
|
+
groups[key] = []
|
|
113
|
+
groups[key].append(item)
|
|
114
|
+
|
|
115
|
+
if count:
|
|
116
|
+
return {k: len(v) for k, v in groups.items()}
|
|
117
|
+
return groups
|
|
118
|
+
|
|
119
|
+
def first(self) -> dict[str, Any] | None:
|
|
120
|
+
"""Get first result or None.
|
|
121
|
+
|
|
122
|
+
Returns:
|
|
123
|
+
First item or None if empty
|
|
124
|
+
"""
|
|
125
|
+
return self._data[0] if self._data else None
|
|
126
|
+
|
|
127
|
+
def last(self) -> dict[str, Any] | None:
|
|
128
|
+
"""Get last result or None."""
|
|
129
|
+
return self._data[-1] if self._data else None
|
|
130
|
+
|
|
131
|
+
def each(self, fn: Callable[[dict[str, Any]], Any]) -> list[Any]:
|
|
132
|
+
"""Apply function to each result.
|
|
133
|
+
|
|
134
|
+
Args:
|
|
135
|
+
fn: Function to apply to each item
|
|
136
|
+
|
|
137
|
+
Returns:
|
|
138
|
+
List of function results
|
|
139
|
+
|
|
140
|
+
Example:
|
|
141
|
+
result.each(lambda d: call_tool("control", "turn_on", **d))
|
|
142
|
+
"""
|
|
143
|
+
return [fn(item) for item in self._data]
|
|
144
|
+
|
|
145
|
+
def sort(self, by: str, reverse: bool = False) -> "QueryResult":
|
|
146
|
+
"""Sort results by field.
|
|
147
|
+
|
|
148
|
+
Args:
|
|
149
|
+
by: Field name to sort by
|
|
150
|
+
reverse: If True, sort descending
|
|
151
|
+
|
|
152
|
+
Returns:
|
|
153
|
+
New QueryResult with sorted items
|
|
154
|
+
"""
|
|
155
|
+
sorted_data = sorted(
|
|
156
|
+
self._data,
|
|
157
|
+
key=lambda x: str(x.get(by, "")),
|
|
158
|
+
reverse=reverse
|
|
159
|
+
)
|
|
160
|
+
return QueryResult(sorted_data)
|
|
161
|
+
|
|
162
|
+
def unique(self, by: str) -> "QueryResult":
|
|
163
|
+
"""Remove duplicates based on field value.
|
|
164
|
+
|
|
165
|
+
Args:
|
|
166
|
+
by: Field to check for uniqueness
|
|
167
|
+
|
|
168
|
+
Returns:
|
|
169
|
+
New QueryResult with unique items
|
|
170
|
+
"""
|
|
171
|
+
seen: set[str] = set()
|
|
172
|
+
unique_items = []
|
|
173
|
+
for item in self._data:
|
|
174
|
+
key = str(item.get(by, ""))
|
|
175
|
+
if key not in seen:
|
|
176
|
+
seen.add(key)
|
|
177
|
+
unique_items.append(item)
|
|
178
|
+
return QueryResult(unique_items)
|
|
179
|
+
|
voxagent/code/sandbox.py
CHANGED
|
@@ -91,6 +91,8 @@ def _execute_in_subprocess(
|
|
|
91
91
|
globals_dict: dict[str, Any],
|
|
92
92
|
result_queue: multiprocessing.Queue, # type: ignore[type-arg]
|
|
93
93
|
memory_limit_mb: int,
|
|
94
|
+
tool_request_queue: "multiprocessing.Queue[Any] | None" = None,
|
|
95
|
+
tool_response_queue: "multiprocessing.Queue[Any] | None" = None,
|
|
94
96
|
) -> None:
|
|
95
97
|
"""Subprocess entry point for sandboxed execution."""
|
|
96
98
|
# Import here to avoid loading in main process
|
|
@@ -257,6 +259,114 @@ def _execute_in_subprocess(
|
|
|
257
259
|
"_write_": lambda x: x,
|
|
258
260
|
**globals_dict,
|
|
259
261
|
}
|
|
262
|
+
|
|
263
|
+
# Create call_tool function using proxy if queues are provided
|
|
264
|
+
if tool_request_queue is not None and tool_response_queue is not None:
|
|
265
|
+
from voxagent.code.tool_proxy import ToolProxyClient
|
|
266
|
+
proxy_client = ToolProxyClient(tool_request_queue, tool_response_queue)
|
|
267
|
+
|
|
268
|
+
def call_tool(category: str, tool_name: str, **kwargs: Any) -> Any:
|
|
269
|
+
proxy = proxy_client.create_tool_proxy(category, tool_name)
|
|
270
|
+
return proxy(**kwargs)
|
|
271
|
+
|
|
272
|
+
exec_globals["call_tool"] = call_tool
|
|
273
|
+
|
|
274
|
+
# Add query functions for efficient data processing
|
|
275
|
+
from voxagent.code.query import QueryResult
|
|
276
|
+
import re as _re
|
|
277
|
+
|
|
278
|
+
def query(data: list | dict) -> QueryResult:
|
|
279
|
+
"""Wrap data in QueryResult for chaining."""
|
|
280
|
+
if isinstance(data, dict) and "devices" in data:
|
|
281
|
+
# Handle list_devices response format
|
|
282
|
+
return QueryResult(data["devices"])
|
|
283
|
+
if isinstance(data, dict) and "items" in data:
|
|
284
|
+
return QueryResult(data["items"])
|
|
285
|
+
if isinstance(data, list):
|
|
286
|
+
return QueryResult(data)
|
|
287
|
+
return QueryResult([data] if data else [])
|
|
288
|
+
|
|
289
|
+
def tree(path: str = "tools") -> str:
|
|
290
|
+
"""Show full tool structure with signatures.
|
|
291
|
+
|
|
292
|
+
Args:
|
|
293
|
+
path: Starting path (default: "tools")
|
|
294
|
+
|
|
295
|
+
Returns:
|
|
296
|
+
Tree-formatted string showing all tools
|
|
297
|
+
"""
|
|
298
|
+
lines = []
|
|
299
|
+
path = path.rstrip("/")
|
|
300
|
+
|
|
301
|
+
# Get ls function from globals_dict
|
|
302
|
+
ls_func = globals_dict.get("ls")
|
|
303
|
+
if ls_func is None:
|
|
304
|
+
return "(ls function not available)"
|
|
305
|
+
|
|
306
|
+
# Get root entries
|
|
307
|
+
entries = ls_func(path)
|
|
308
|
+
if isinstance(entries, str):
|
|
309
|
+
# Handle error message
|
|
310
|
+
return entries
|
|
311
|
+
for entry in entries:
|
|
312
|
+
if entry == "__index__.md":
|
|
313
|
+
continue
|
|
314
|
+
if entry.endswith("/"):
|
|
315
|
+
# It's a category
|
|
316
|
+
cat_name = entry.rstrip("/")
|
|
317
|
+
lines.append(f"📁 {cat_name}/")
|
|
318
|
+
# Get tools in category
|
|
319
|
+
cat_entries = ls_func(f"{path}/{cat_name}")
|
|
320
|
+
if isinstance(cat_entries, list):
|
|
321
|
+
for tool in cat_entries:
|
|
322
|
+
if tool != "__index__.md":
|
|
323
|
+
lines.append(f" 📄 {tool}")
|
|
324
|
+
else:
|
|
325
|
+
lines.append(f"📄 {entry}")
|
|
326
|
+
|
|
327
|
+
return "\n".join(lines) if lines else "(empty)"
|
|
328
|
+
|
|
329
|
+
def search(query_str: str) -> list:
|
|
330
|
+
"""Search for tools by keyword.
|
|
331
|
+
|
|
332
|
+
Args:
|
|
333
|
+
query_str: Search term (matches tool names)
|
|
334
|
+
|
|
335
|
+
Returns:
|
|
336
|
+
List of matching tool paths
|
|
337
|
+
"""
|
|
338
|
+
results = []
|
|
339
|
+
pattern = _re.compile(query_str, _re.IGNORECASE)
|
|
340
|
+
|
|
341
|
+
# Get ls function from globals_dict
|
|
342
|
+
ls_func = globals_dict.get("ls")
|
|
343
|
+
if ls_func is None:
|
|
344
|
+
return []
|
|
345
|
+
|
|
346
|
+
# Search all categories
|
|
347
|
+
categories = ls_func("tools")
|
|
348
|
+
if isinstance(categories, str):
|
|
349
|
+
return []
|
|
350
|
+
for cat in categories:
|
|
351
|
+
if cat == "__index__.md" or not cat.endswith("/"):
|
|
352
|
+
continue
|
|
353
|
+
cat_name = cat.rstrip("/")
|
|
354
|
+
tools = ls_func(f"tools/{cat_name}")
|
|
355
|
+
if isinstance(tools, str):
|
|
356
|
+
continue
|
|
357
|
+
for tool in tools:
|
|
358
|
+
if tool == "__index__.md":
|
|
359
|
+
continue
|
|
360
|
+
if pattern.search(tool) or pattern.search(cat_name):
|
|
361
|
+
results.append(f"tools/{cat_name}/{tool}")
|
|
362
|
+
|
|
363
|
+
return results
|
|
364
|
+
|
|
365
|
+
exec_globals["query"] = query
|
|
366
|
+
exec_globals["tree"] = tree
|
|
367
|
+
exec_globals["search"] = search
|
|
368
|
+
exec_globals["QueryResult"] = QueryResult
|
|
369
|
+
|
|
260
370
|
exec(byte_code, exec_globals)
|
|
261
371
|
# Get the _print object that was created during execution and call it
|
|
262
372
|
_print_obj = exec_globals.get("_print")
|
|
@@ -298,18 +408,48 @@ class SubprocessSandbox(CodeSandbox):
|
|
|
298
408
|
self.tool_proxy_client = tool_proxy_client
|
|
299
409
|
|
|
300
410
|
async def execute(
|
|
301
|
-
self,
|
|
411
|
+
self,
|
|
412
|
+
code: str,
|
|
413
|
+
globals_dict: dict[str, Any] | None = None,
|
|
414
|
+
tool_request_queue: "multiprocessing.Queue[Any] | None" = None,
|
|
415
|
+
tool_response_queue: "multiprocessing.Queue[Any] | None" = None,
|
|
302
416
|
) -> SandboxResult:
|
|
303
|
-
"""Execute code in subprocess with RestrictedPython.
|
|
417
|
+
"""Execute code in subprocess with RestrictedPython.
|
|
418
|
+
|
|
419
|
+
Args:
|
|
420
|
+
code: Python source code to execute
|
|
421
|
+
globals_dict: Optional globals to inject (e.g., ls, read functions)
|
|
422
|
+
tool_request_queue: Queue for tool call requests from subprocess
|
|
423
|
+
tool_response_queue: Queue for tool call responses to subprocess
|
|
424
|
+
|
|
425
|
+
Returns:
|
|
426
|
+
SandboxResult with output or error
|
|
427
|
+
"""
|
|
428
|
+
import asyncio
|
|
429
|
+
|
|
304
430
|
start_time = time.monotonic()
|
|
305
431
|
|
|
306
432
|
result_queue: multiprocessing.Queue[SandboxResult] = multiprocessing.Queue()
|
|
307
433
|
process = multiprocessing.Process(
|
|
308
434
|
target=_execute_in_subprocess,
|
|
309
|
-
args=(
|
|
435
|
+
args=(
|
|
436
|
+
code,
|
|
437
|
+
globals_dict or {},
|
|
438
|
+
result_queue,
|
|
439
|
+
self.memory_limit_mb,
|
|
440
|
+
tool_request_queue,
|
|
441
|
+
tool_response_queue,
|
|
442
|
+
),
|
|
310
443
|
)
|
|
311
444
|
process.start()
|
|
312
|
-
|
|
445
|
+
|
|
446
|
+
# Use non-blocking wait to allow event loop to run proxy server
|
|
447
|
+
# Poll the process status instead of blocking on join()
|
|
448
|
+
poll_interval = 0.01 # 10ms
|
|
449
|
+
elapsed = 0.0
|
|
450
|
+
while process.is_alive() and elapsed < self.timeout_seconds:
|
|
451
|
+
await asyncio.sleep(poll_interval)
|
|
452
|
+
elapsed = time.monotonic() - start_time
|
|
313
453
|
|
|
314
454
|
execution_time_ms = (time.monotonic() - start_time) * 1000
|
|
315
455
|
|
voxagent/code/virtual_fs.py
CHANGED
|
@@ -95,14 +95,12 @@ class ToolRegistry:
|
|
|
95
95
|
return []
|
|
96
96
|
|
|
97
97
|
|
|
98
|
-
# Type alias for tool caller function
|
|
99
|
-
ToolCaller = Any # Callable[[str, str, ...], Any]
|
|
100
|
-
|
|
101
|
-
|
|
102
98
|
class VirtualFilesystem:
|
|
103
99
|
"""Virtual filesystem for tool discovery.
|
|
104
100
|
|
|
105
101
|
Provides ls() and read() functions that can be injected into the sandbox.
|
|
102
|
+
The call_tool function is now injected directly by the sandbox using
|
|
103
|
+
the tool proxy pattern for reliable cross-process communication.
|
|
106
104
|
|
|
107
105
|
Directory structure:
|
|
108
106
|
tools/
|
|
@@ -116,13 +114,8 @@ class VirtualFilesystem:
|
|
|
116
114
|
└── temperature.py
|
|
117
115
|
"""
|
|
118
116
|
|
|
119
|
-
def __init__(
|
|
120
|
-
self,
|
|
121
|
-
registry: ToolRegistry,
|
|
122
|
-
tool_caller: ToolCaller | None = None,
|
|
123
|
-
) -> None:
|
|
117
|
+
def __init__(self, registry: ToolRegistry) -> None:
|
|
124
118
|
self._registry = registry
|
|
125
|
-
self._tool_caller = tool_caller
|
|
126
119
|
|
|
127
120
|
def ls(self, path: str) -> list[str]:
|
|
128
121
|
"""List directory contents.
|
|
@@ -223,15 +216,12 @@ class VirtualFilesystem:
|
|
|
223
216
|
"""Get globals dict to inject into sandbox.
|
|
224
217
|
|
|
225
218
|
Returns:
|
|
226
|
-
Dict with ls
|
|
219
|
+
Dict with ls and read functions bound to this filesystem.
|
|
220
|
+
Note: call_tool is now injected directly by the sandbox using
|
|
221
|
+
the tool proxy pattern for reliable cross-process communication.
|
|
227
222
|
"""
|
|
228
|
-
|
|
223
|
+
return {
|
|
229
224
|
"ls": self.ls,
|
|
230
225
|
"read": self.read,
|
|
231
226
|
}
|
|
232
227
|
|
|
233
|
-
if self._tool_caller is not None:
|
|
234
|
-
globals_dict["call_tool"] = self._tool_caller
|
|
235
|
-
|
|
236
|
-
return globals_dict
|
|
237
|
-
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: voxagent
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.5
|
|
4
4
|
Summary: A lightweight, model-agnostic LLM provider abstraction with streaming and tool support
|
|
5
5
|
Project-URL: Homepage, https://github.com/lensator/voxagent
|
|
6
6
|
Project-URL: Documentation, https://github.com/lensator/voxagent#readme
|
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
voxagent/__init__.py,sha256=YMYC95iwWXK26hicGYmd2erNOInrYtohwUVOuNmpTCs,3927
|
|
2
|
-
voxagent/_version.py,sha256=
|
|
2
|
+
voxagent/_version.py,sha256=BQIRef51oDZ39-xgLzB-7zzZTwre-GSkFQ2C7tG_vg4,87
|
|
3
3
|
voxagent/py.typed,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
|
|
4
4
|
voxagent/agent/__init__.py,sha256=eASoU7Zhvw8BtJ-iUqVN06S4fMLkHwDgUZbHeH2AUOM,755
|
|
5
5
|
voxagent/agent/abort.py,sha256=2Wnnxq8Dcn7wQkKPHrba2o0OeOdzF4NNsl-usgE4CJw,5191
|
|
6
6
|
voxagent/agent/core.py,sha256=ddjDoWcDqiacRtNzef5wORR2f5uHqL1Y1gOmzoeXRFY,33339
|
|
7
|
-
voxagent/code/__init__.py,sha256=
|
|
8
|
-
voxagent/code/agent.py,sha256=
|
|
9
|
-
voxagent/code/
|
|
7
|
+
voxagent/code/__init__.py,sha256=WEmr8IcpVwM8eykSa4XDwPAwl5QmtzlygOLuzVA8JzE,1454
|
|
8
|
+
voxagent/code/agent.py,sha256=NbdhT1Zx99N76YzUqL1A49wmYViFuOp4l0zIoftGpIM,12128
|
|
9
|
+
voxagent/code/query.py,sha256=qqRxT9_4gJ28lYXY6Ygdzio8eAvWasuswVmVy0cNMjc,5462
|
|
10
|
+
voxagent/code/sandbox.py,sha256=sJvKL-qrfzXZDQTwDMPUZrtGCb_hznIiomTNb2wG7UM,16859
|
|
10
11
|
voxagent/code/tool_proxy.py,sha256=wZvRqXoz2SfYTHLpe8tIkpJL6b8fmlU96DSGeYw-HfM,8091
|
|
11
|
-
voxagent/code/virtual_fs.py,sha256=
|
|
12
|
+
voxagent/code/virtual_fs.py,sha256=L4Z_Kmoalwj4lJDhRxQOTOblC7Zrx8lv7slvHeHRILM,7362
|
|
12
13
|
voxagent/mcp/__init__.py,sha256=_3Rsn7nIuivdWLv0MzpyjRGsPuCgr4LrXCge6FCb3nE,470
|
|
13
14
|
voxagent/mcp/manager.py,sha256=sECOhw-f6HB6NV-mBqcgJzsEt28acdQI_O2cT-41Rxw,6606
|
|
14
15
|
voxagent/mcp/tool.py,sha256=YQQqXNcanDDr5tkL1Z5OjNsDI5dWMEzm_SlJ4pOcUpk,5147
|
|
@@ -52,6 +53,6 @@ voxagent/tools/registry.py,sha256=MNJzgcmKT0AoMWIky9TJY4WVhzn5dkmjIHsUiZ3mv3U,25
|
|
|
52
53
|
voxagent/types/__init__.py,sha256=3VunuprKKEpOR9Cg-UITHJXds_xQ-tfqQb4S7wD3nP4,933
|
|
53
54
|
voxagent/types/messages.py,sha256=c6hNi9w6C8gbFoFm5fFge35vwJGywaoR_OiPQprfyVs,3494
|
|
54
55
|
voxagent/types/run.py,sha256=4vYq0pCqH7_7SWbMb1SplWj4TLiE3DELDYMi0HefFmo,5071
|
|
55
|
-
voxagent-0.2.
|
|
56
|
-
voxagent-0.2.
|
|
57
|
-
voxagent-0.2.
|
|
56
|
+
voxagent-0.2.5.dist-info/METADATA,sha256=V29VGrxDI7moTtP7ps104Io9vF3jrZxcBn4xFJQly5c,5685
|
|
57
|
+
voxagent-0.2.5.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
58
|
+
voxagent-0.2.5.dist-info/RECORD,,
|
|
File without changes
|