axio-tui 0.6.2__tar.gz → 0.7.0__tar.gz
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.
- {axio_tui-0.6.2 → axio_tui-0.7.0}/PKG-INFO +5 -5
- {axio_tui-0.6.2 → axio_tui-0.7.0}/README.md +2 -2
- {axio_tui-0.6.2 → axio_tui-0.7.0}/pyproject.toml +3 -3
- {axio_tui-0.6.2 → axio_tui-0.7.0}/src/axio_tui/app.py +8 -8
- {axio_tui-0.6.2 → axio_tui-0.7.0}/src/axio_tui/plugin.py +6 -6
- {axio_tui-0.6.2 → axio_tui-0.7.0}/src/axio_tui/screens.py +9 -9
- {axio_tui-0.6.2 → axio_tui-0.7.0}/src/axio_tui/tools.py +10 -10
- {axio_tui-0.6.2 → axio_tui-0.7.0}/tests/test_guard_screens.py +6 -4
- {axio_tui-0.6.2 → axio_tui-0.7.0}/tests/test_plugin.py +7 -7
- {axio_tui-0.6.2 → axio_tui-0.7.0}/tests/test_plugin_toggle.py +6 -4
- {axio_tui-0.6.2 → axio_tui-0.7.0}/tests/test_tools.py +16 -13
- {axio_tui-0.6.2 → axio_tui-0.7.0}/.github/workflows/publish.yml +0 -0
- {axio_tui-0.6.2 → axio_tui-0.7.0}/.github/workflows/tests.yml +0 -0
- {axio_tui-0.6.2 → axio_tui-0.7.0}/.gitignore +0 -0
- {axio_tui-0.6.2 → axio_tui-0.7.0}/LICENSE +0 -0
- {axio_tui-0.6.2 → axio_tui-0.7.0}/Makefile +0 -0
- {axio_tui-0.6.2 → axio_tui-0.7.0}/src/axio_tui/__init__.py +0 -0
- {axio_tui-0.6.2 → axio_tui-0.7.0}/src/axio_tui/__main__.py +0 -0
- {axio_tui-0.6.2 → axio_tui-0.7.0}/src/axio_tui/args.py +0 -0
- {axio_tui-0.6.2 → axio_tui-0.7.0}/src/axio_tui/prompt.py +0 -0
- {axio_tui-0.6.2 → axio_tui-0.7.0}/src/axio_tui/sqlite_config.py +0 -0
- {axio_tui-0.6.2 → axio_tui-0.7.0}/src/axio_tui/transport_registry.py +0 -0
- {axio_tui-0.6.2 → axio_tui-0.7.0}/tests/conftest.py +0 -0
- {axio_tui-0.6.2 → axio_tui-0.7.0}/tests/test_nav.py +0 -0
- {axio_tui-0.6.2 → axio_tui-0.7.0}/tests/test_sqlite_config.py +0 -0
- {axio_tui-0.6.2 → axio_tui-0.7.0}/tests/test_tool_status_widget.py +0 -0
- {axio_tui-0.6.2 → axio_tui-0.7.0}/tests/test_transport_registry.py +0 -0
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: axio-tui
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.7.0
|
|
4
4
|
Summary: Textual TUI application for Axio
|
|
5
|
-
Project-URL: Homepage, https://github.com/axio-agent
|
|
6
|
-
Project-URL: Repository, https://github.com/axio-agent
|
|
5
|
+
Project-URL: Homepage, https://github.com/mosquito/axio-agent
|
|
6
|
+
Project-URL: Repository, https://github.com/mosquito/axio-agent
|
|
7
7
|
License: MIT
|
|
8
8
|
License-File: LICENSE
|
|
9
9
|
Keywords: agent,ai,chat,llm,terminal,textual,tui
|
|
@@ -40,7 +40,7 @@ Description-Content-Type: text/markdown
|
|
|
40
40
|
[](https://pypi.org/project/axio-tui/)
|
|
41
41
|
[](LICENSE)
|
|
42
42
|
|
|
43
|
-
Textual TUI application for [axio](https://github.com/axio-agent
|
|
43
|
+
Textual TUI application for [axio](https://github.com/mosquito/axio-agent).
|
|
44
44
|
|
|
45
45
|
A full-featured terminal chat interface with session management, a plugin system for transports and tools, and a built-in SQLite context store. Pick your LLM backend, load your tools, and start chatting — all from the terminal.
|
|
46
46
|
|
|
@@ -150,7 +150,7 @@ textual-serve axio_tui.__main__:app
|
|
|
150
150
|
|
|
151
151
|
## Part of the axio ecosystem
|
|
152
152
|
|
|
153
|
-
[axio](https://github.com/axio-agent
|
|
153
|
+
[axio](https://github.com/mosquito/axio-agent) · [axio-transport-anthropic](https://github.com/mosquito/axio-agent) · [axio-tui-guards](https://github.com/mosquito/axio-agent) · [axio-tools-local](https://github.com/mosquito/axio-agent) · [axio-tools-mcp](https://github.com/mosquito/axio-agent) · [axio-tools-docker](https://github.com/mosquito/axio-agent)
|
|
154
154
|
|
|
155
155
|
## License
|
|
156
156
|
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
[](https://pypi.org/project/axio-tui/)
|
|
5
5
|
[](LICENSE)
|
|
6
6
|
|
|
7
|
-
Textual TUI application for [axio](https://github.com/axio-agent
|
|
7
|
+
Textual TUI application for [axio](https://github.com/mosquito/axio-agent).
|
|
8
8
|
|
|
9
9
|
A full-featured terminal chat interface with session management, a plugin system for transports and tools, and a built-in SQLite context store. Pick your LLM backend, load your tools, and start chatting — all from the terminal.
|
|
10
10
|
|
|
@@ -114,7 +114,7 @@ textual-serve axio_tui.__main__:app
|
|
|
114
114
|
|
|
115
115
|
## Part of the axio ecosystem
|
|
116
116
|
|
|
117
|
-
[axio](https://github.com/axio-agent
|
|
117
|
+
[axio](https://github.com/mosquito/axio-agent) · [axio-transport-anthropic](https://github.com/mosquito/axio-agent) · [axio-tui-guards](https://github.com/mosquito/axio-agent) · [axio-tools-local](https://github.com/mosquito/axio-agent) · [axio-tools-mcp](https://github.com/mosquito/axio-agent) · [axio-tools-docker](https://github.com/mosquito/axio-agent)
|
|
118
118
|
|
|
119
119
|
## License
|
|
120
120
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "axio-tui"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.7.0"
|
|
4
4
|
description = "Textual TUI application for Axio"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.12"
|
|
@@ -36,8 +36,8 @@ axio = "axio_tui.__main__:main"
|
|
|
36
36
|
|
|
37
37
|
|
|
38
38
|
[project.urls]
|
|
39
|
-
Homepage = "https://github.com/axio-agent
|
|
40
|
-
Repository = "https://github.com/axio-agent
|
|
39
|
+
Homepage = "https://github.com/mosquito/axio-agent"
|
|
40
|
+
Repository = "https://github.com/mosquito/axio-agent"
|
|
41
41
|
[build-system]
|
|
42
42
|
requires = ["hatchling"]
|
|
43
43
|
build-backend = "hatchling.build"
|
|
@@ -378,14 +378,14 @@ class AgentApp(App[None]):
|
|
|
378
378
|
self._last_input_tokens = 0
|
|
379
379
|
self._chat_model: ModelSpec | None = None
|
|
380
380
|
self._model_name = ""
|
|
381
|
-
self._all_tools: list[Tool] = []
|
|
381
|
+
self._all_tools: list[Tool[Any]] = []
|
|
382
382
|
self._disabled_plugins: set[str] = set()
|
|
383
383
|
self._disabled_transports: set[str] = set()
|
|
384
384
|
self._disabled_guards: set[str] = set()
|
|
385
385
|
self._guard_tool_map: dict[str, set[str]] = {}
|
|
386
386
|
self._guard_instances: dict[str, PermissionGuard] = {}
|
|
387
387
|
self._guard_classes: dict[str, type[PermissionGuard]] = {}
|
|
388
|
-
self._active_tools: list[Tool] = []
|
|
388
|
+
self._active_tools: list[Tool[Any]] = []
|
|
389
389
|
self._chat_transport: Any = None
|
|
390
390
|
self._selector_classes: dict[str, type] = {}
|
|
391
391
|
self._selector_instances: dict[str, Any] = {}
|
|
@@ -535,7 +535,7 @@ class AgentApp(App[None]):
|
|
|
535
535
|
self._chat_transport = chat_transport
|
|
536
536
|
|
|
537
537
|
# Discover tools and guards via entry points
|
|
538
|
-
tools: list[Tool] = discover_tools()
|
|
538
|
+
tools: list[Tool[Any]] = discover_tools()
|
|
539
539
|
self._guard_classes = discover_guards()
|
|
540
540
|
|
|
541
541
|
# Load disabled plugins from config
|
|
@@ -842,7 +842,7 @@ class AgentApp(App[None]):
|
|
|
842
842
|
return None
|
|
843
843
|
|
|
844
844
|
def _rebuild_agent_tools(self) -> None:
|
|
845
|
-
tools: list[Tool] = []
|
|
845
|
+
tools: list[Tool[Any]] = []
|
|
846
846
|
for t in self._all_tools:
|
|
847
847
|
if t.name in self._disabled_plugins:
|
|
848
848
|
continue
|
|
@@ -1005,7 +1005,7 @@ class AgentApp(App[None]):
|
|
|
1005
1005
|
return False
|
|
1006
1006
|
return self._last_input_tokens / self._chat_model.context_window > 0.7
|
|
1007
1007
|
|
|
1008
|
-
async def _compact_with_progress(self
|
|
1008
|
+
async def _compact_with_progress(self) -> list[Message] | None:
|
|
1009
1009
|
"""Run compaction while showing a live elapsed-time counter in the chat."""
|
|
1010
1010
|
if self._agent is None:
|
|
1011
1011
|
return None
|
|
@@ -1027,7 +1027,7 @@ class AgentApp(App[None]):
|
|
|
1027
1027
|
transport = self._transports.make_transport(cb.transport, cb.model)
|
|
1028
1028
|
else:
|
|
1029
1029
|
transport = self._agent.transport
|
|
1030
|
-
return await compact_context(self._chat_context, transport
|
|
1030
|
+
return await compact_context(self._chat_context, transport)
|
|
1031
1031
|
finally:
|
|
1032
1032
|
timer.stop()
|
|
1033
1033
|
await widget.remove()
|
|
@@ -1209,7 +1209,7 @@ class AgentApp(App[None]):
|
|
|
1209
1209
|
|
|
1210
1210
|
def _on_tools_plugin_dismissed(self) -> None:
|
|
1211
1211
|
# Rebuild tools list with all plugin tools
|
|
1212
|
-
tools: list[Tool] = discover_tools()
|
|
1212
|
+
tools: list[Tool[Any]] = discover_tools()
|
|
1213
1213
|
for plugin in self._tools_plugins.values():
|
|
1214
1214
|
tools.extend(plugin.all_tools)
|
|
1215
1215
|
self._all_tools = tools
|
|
@@ -1297,7 +1297,7 @@ class AgentApp(App[None]):
|
|
|
1297
1297
|
|
|
1298
1298
|
self._is_running = True
|
|
1299
1299
|
if self._should_compact():
|
|
1300
|
-
messages = await self._compact_with_progress(
|
|
1300
|
+
messages = await self._compact_with_progress()
|
|
1301
1301
|
if messages is not None:
|
|
1302
1302
|
await self._replace_context(messages)
|
|
1303
1303
|
self._last_input_tokens = 0
|
|
@@ -27,7 +27,7 @@ class ToolsPlugin(Protocol):
|
|
|
27
27
|
async def init(self, config: Any = None, global_config: Any = None) -> None: ...
|
|
28
28
|
|
|
29
29
|
@property
|
|
30
|
-
def all_tools(self) -> list[Tool]: ...
|
|
30
|
+
def all_tools(self) -> list[Tool[Any]]: ...
|
|
31
31
|
|
|
32
32
|
def settings_screen(self) -> Any: ...
|
|
33
33
|
|
|
@@ -42,7 +42,7 @@ TOOLS_SETTINGS_GROUP = "axio.tools.settings"
|
|
|
42
42
|
SELECTOR_GROUP = "axio.selector"
|
|
43
43
|
|
|
44
44
|
|
|
45
|
-
def _handler_description(handler: type[ToolHandler]) -> str:
|
|
45
|
+
def _handler_description(handler: type[ToolHandler[Any]]) -> str:
|
|
46
46
|
"""Extract handler docstring as tool description."""
|
|
47
47
|
doc = handler.__doc__
|
|
48
48
|
if doc:
|
|
@@ -50,9 +50,9 @@ def _handler_description(handler: type[ToolHandler]) -> str:
|
|
|
50
50
|
return handler.__name__
|
|
51
51
|
|
|
52
52
|
|
|
53
|
-
def discover_tools() -> list[Tool]:
|
|
53
|
+
def discover_tools() -> list[Tool[Any]]:
|
|
54
54
|
"""Load ToolHandler classes from 'axio.tools' entry points, build Tool objects."""
|
|
55
|
-
tools: list[Tool] = []
|
|
55
|
+
tools: list[Tool[Any]] = []
|
|
56
56
|
for ep in entry_points(group=TOOLS_GROUP):
|
|
57
57
|
try:
|
|
58
58
|
handler = ep.load()
|
|
@@ -74,9 +74,9 @@ def discover_tools() -> list[Tool]:
|
|
|
74
74
|
return tools
|
|
75
75
|
|
|
76
76
|
|
|
77
|
-
def discover_tools_by_package() -> dict[str, list[Tool]]:
|
|
77
|
+
def discover_tools_by_package() -> dict[str, list[Tool[Any]]]:
|
|
78
78
|
"""Return tools from 'axio.tools' entry points grouped by distribution package name."""
|
|
79
|
-
groups: dict[str, list[Tool]] = {}
|
|
79
|
+
groups: dict[str, list[Tool[Any]]] = {}
|
|
80
80
|
for ep in entry_points(group=TOOLS_GROUP):
|
|
81
81
|
try:
|
|
82
82
|
handler = ep.load()
|
|
@@ -247,13 +247,13 @@ class ToolSelectScreen(ModalScreen[set[str] | None]):
|
|
|
247
247
|
#tool-list { height: 1fr; }
|
|
248
248
|
"""
|
|
249
249
|
|
|
250
|
-
def __init__(self, groups: dict[str, list[Tool]], disabled: set[str]) -> None:
|
|
250
|
+
def __init__(self, groups: dict[str, list[Tool[Any]]], disabled: set[str]) -> None:
|
|
251
251
|
super().__init__()
|
|
252
252
|
self._groups = groups
|
|
253
|
-
self._all_tools: list[Tool] = [t for ts in groups.values() for t in ts]
|
|
253
|
+
self._all_tools: list[Tool[Any]] = [t for ts in groups.values() for t in ts]
|
|
254
254
|
self._disabled = set(disabled)
|
|
255
255
|
self._filter_query = ""
|
|
256
|
-
self._items: list[str | Tool] = []
|
|
256
|
+
self._items: list[str | Tool[Any]] = []
|
|
257
257
|
self._rebuild_items()
|
|
258
258
|
|
|
259
259
|
def _rebuild_items(self) -> None:
|
|
@@ -261,7 +261,7 @@ class ToolSelectScreen(ModalScreen[set[str] | None]):
|
|
|
261
261
|
if q:
|
|
262
262
|
self._items = [t for t in self._all_tools if q in t.name.lower() or q in t.description.lower()]
|
|
263
263
|
return
|
|
264
|
-
items: list[str | Tool] = []
|
|
264
|
+
items: list[str | Tool[Any]] = []
|
|
265
265
|
for pkg, tools in self._groups.items():
|
|
266
266
|
items.append(pkg)
|
|
267
267
|
items.extend(tools)
|
|
@@ -274,12 +274,12 @@ class ToolSelectScreen(ModalScreen[set[str] | None]):
|
|
|
274
274
|
mark = "[*]" if n_on == total else ("[ ]" if n_on == 0 else "[-]")
|
|
275
275
|
return f"{mark} ── {pkg} ({n_on}/{total}) ──"
|
|
276
276
|
|
|
277
|
-
def _tool_line(self, tool: Tool, *, indent: bool = False) -> str:
|
|
277
|
+
def _tool_line(self, tool: Tool[Any], *, indent: bool = False) -> str:
|
|
278
278
|
mark = "[ ]" if tool.name in self._disabled else "[*]"
|
|
279
279
|
prefix = " " if indent else ""
|
|
280
280
|
return f"{prefix}{mark} {tool.name:<22} {tool.description}"
|
|
281
281
|
|
|
282
|
-
def _format(self, item: str | Tool) -> str:
|
|
282
|
+
def _format(self, item: str | Tool[Any]) -> str:
|
|
283
283
|
if isinstance(item, str):
|
|
284
284
|
return self._group_header(item)
|
|
285
285
|
return self._tool_line(item, indent=not self._filter_query)
|
|
@@ -323,7 +323,7 @@ class ToolSelectScreen(ModalScreen[set[str] | None]):
|
|
|
323
323
|
self._rebuild_items()
|
|
324
324
|
self._refresh_list()
|
|
325
325
|
|
|
326
|
-
def _select(self, item: str | Tool) -> None:
|
|
326
|
+
def _select(self, item: str | Tool[Any]) -> None:
|
|
327
327
|
if isinstance(item, str):
|
|
328
328
|
tools = self._groups[item]
|
|
329
329
|
all_on = all(t.name not in self._disabled for t in tools)
|
|
@@ -400,7 +400,7 @@ class PluginHubScreen(ModalScreen[None]):
|
|
|
400
400
|
|
|
401
401
|
def __init__(
|
|
402
402
|
self,
|
|
403
|
-
tool_groups: dict[str, list[Tool]],
|
|
403
|
+
tool_groups: dict[str, list[Tool[Any]]],
|
|
404
404
|
transport_available: list[str],
|
|
405
405
|
transport_discovered: list[str],
|
|
406
406
|
transport_model_counts: dict[str, int],
|
|
@@ -419,7 +419,7 @@ class PluginHubScreen(ModalScreen[None]):
|
|
|
419
419
|
) -> None:
|
|
420
420
|
super().__init__()
|
|
421
421
|
self._tool_groups = tool_groups
|
|
422
|
-
self._all_tools: list[Tool] = [t for ts in tool_groups.values() for t in ts]
|
|
422
|
+
self._all_tools: list[Tool[Any]] = [t for ts in tool_groups.values() for t in ts]
|
|
423
423
|
self._transport_available = transport_available
|
|
424
424
|
self._transport_discovered = transport_discovered
|
|
425
425
|
self._transport_model_counts = transport_model_counts
|
|
@@ -19,7 +19,7 @@ def _short(value: Any, limit: int = 60) -> str:
|
|
|
19
19
|
return s if len(s) <= limit else s[:limit] + "..."
|
|
20
20
|
|
|
21
21
|
|
|
22
|
-
class StatusLine(ToolHandler):
|
|
22
|
+
class StatusLine(ToolHandler[Any]):
|
|
23
23
|
"""Set a short status message shown to the user in the TUI status bar.
|
|
24
24
|
Call this at the start of every turn before other tools to indicate
|
|
25
25
|
what you are about to do (e.g. "Reading project files",
|
|
@@ -31,13 +31,13 @@ class StatusLine(ToolHandler):
|
|
|
31
31
|
def __repr__(self) -> str:
|
|
32
32
|
return f"StatusLine(message={self.message!r})"
|
|
33
33
|
|
|
34
|
-
async def __call__(self) -> str:
|
|
34
|
+
async def __call__(self, context: Any) -> str:
|
|
35
35
|
if StatusLine._callback:
|
|
36
36
|
StatusLine._callback(self.message)
|
|
37
37
|
return "ok"
|
|
38
38
|
|
|
39
39
|
|
|
40
|
-
class Confirm(ToolHandler):
|
|
40
|
+
class Confirm(ToolHandler[Any]):
|
|
41
41
|
"""Submit a safety verdict for a pending tool call. Used by the
|
|
42
42
|
LLM-based permission guard. Verdict must be SAFE, RISKY, or DENY.
|
|
43
43
|
Provide a clear reason and a category for the action."""
|
|
@@ -49,7 +49,7 @@ class Confirm(ToolHandler):
|
|
|
49
49
|
def __repr__(self) -> str:
|
|
50
50
|
return f"Confirm(verdict={self.verdict!r}, category={self.category!r})"
|
|
51
51
|
|
|
52
|
-
async def __call__(self) -> str:
|
|
52
|
+
async def __call__(self, context: Any) -> str:
|
|
53
53
|
return self.verdict
|
|
54
54
|
|
|
55
55
|
|
|
@@ -142,7 +142,7 @@ SUBAGENT_SYSTEM_PROMPT = """\
|
|
|
142
142
|
</subagent_behavior>"""
|
|
143
143
|
|
|
144
144
|
|
|
145
|
-
class SubAgent(ToolHandler):
|
|
145
|
+
class SubAgent(ToolHandler[Any]):
|
|
146
146
|
"""Delegate a task to an independent sub-agent. The sub-agent receives
|
|
147
147
|
conversation context and has access to the same tools (except subagent).
|
|
148
148
|
Up to 3 sub-agents run concurrently. Use for parallelizing independent
|
|
@@ -157,11 +157,11 @@ class SubAgent(ToolHandler):
|
|
|
157
157
|
def __repr__(self) -> str:
|
|
158
158
|
return f"SubAgent(task={_short(self.task)!r})"
|
|
159
159
|
|
|
160
|
-
async def __call__(self) -> str:
|
|
160
|
+
async def __call__(self, context: Any) -> str:
|
|
161
161
|
if SubAgent._factory is None:
|
|
162
162
|
return "SubAgent is not configured"
|
|
163
|
-
agent,
|
|
164
|
-
return await agent.run(self.task,
|
|
163
|
+
agent, store = await SubAgent._factory()
|
|
164
|
+
return await agent.run(self.task, store)
|
|
165
165
|
|
|
166
166
|
|
|
167
167
|
_IMAGE_MEDIA_TYPES: dict[str, str] = {
|
|
@@ -173,7 +173,7 @@ _IMAGE_MEDIA_TYPES: dict[str, str] = {
|
|
|
173
173
|
}
|
|
174
174
|
|
|
175
175
|
|
|
176
|
-
class VisionAnalyze(ToolHandler):
|
|
176
|
+
class VisionAnalyze(ToolHandler[Any]):
|
|
177
177
|
"""Analyze an image file using a vision-capable model. Supports
|
|
178
178
|
JPEG, PNG, GIF, and WebP formats. Provide a path to the image and
|
|
179
179
|
an optional prompt describing what to look for. Returns the model's
|
|
@@ -186,7 +186,7 @@ class VisionAnalyze(ToolHandler):
|
|
|
186
186
|
def __repr__(self) -> str:
|
|
187
187
|
return f"VisionAnalyze(path={_short(self.path)!r})"
|
|
188
188
|
|
|
189
|
-
async def __call__(self) -> str:
|
|
189
|
+
async def __call__(self, context: Any) -> str:
|
|
190
190
|
import os
|
|
191
191
|
|
|
192
192
|
if VisionAnalyze._transport is None:
|
|
@@ -2,19 +2,21 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
5
7
|
from axio.tool import Tool, ToolHandler
|
|
6
8
|
|
|
7
9
|
from axio_tui.screens import GuardSelectScreen, GuardToolsScreen, PluginHubScreen, ToolDetailScreen
|
|
8
10
|
|
|
9
11
|
|
|
10
|
-
class _StubHandler(ToolHandler):
|
|
12
|
+
class _StubHandler(ToolHandler[Any]):
|
|
11
13
|
"""Stub tool handler."""
|
|
12
14
|
|
|
13
|
-
async def __call__(self) -> str:
|
|
15
|
+
async def __call__(self, context: Any) -> str:
|
|
14
16
|
return ""
|
|
15
17
|
|
|
16
18
|
|
|
17
|
-
def _make_tools(*names: str) -> list[Tool]:
|
|
19
|
+
def _make_tools(*names: str) -> list[Tool[Any]]:
|
|
18
20
|
return [Tool(name=n, description=f"Description for {n}", handler=_StubHandler) for n in names]
|
|
19
21
|
|
|
20
22
|
|
|
@@ -109,7 +111,7 @@ class TestGuardSelectScreen:
|
|
|
109
111
|
|
|
110
112
|
|
|
111
113
|
def _make_hub(
|
|
112
|
-
tools: list[Tool],
|
|
114
|
+
tools: list[Tool[Any]],
|
|
113
115
|
disabled_plugins: set[str] = frozenset(), # type: ignore[assignment]
|
|
114
116
|
disabled_guards: set[str] = frozenset(), # type: ignore[assignment]
|
|
115
117
|
guard_tool_map: dict[str, set[str]] | None = None,
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
-
from typing import ClassVar
|
|
5
|
+
from typing import Any, ClassVar
|
|
6
6
|
from unittest.mock import MagicMock, patch
|
|
7
7
|
|
|
8
8
|
from axio.permission import PermissionGuard
|
|
@@ -11,29 +11,29 @@ from axio.tool import ToolHandler
|
|
|
11
11
|
from axio_tui.plugin import discover_guards, discover_tools, discover_transports
|
|
12
12
|
|
|
13
13
|
|
|
14
|
-
class _EchoHandler(ToolHandler):
|
|
14
|
+
class _EchoHandler(ToolHandler[Any]):
|
|
15
15
|
"""Echo back input text."""
|
|
16
16
|
|
|
17
17
|
text: str = ""
|
|
18
18
|
|
|
19
|
-
async def __call__(self) -> str:
|
|
19
|
+
async def __call__(self, context: Any) -> str:
|
|
20
20
|
return self.text
|
|
21
21
|
|
|
22
22
|
|
|
23
|
-
class _NoDocHandler(ToolHandler):
|
|
23
|
+
class _NoDocHandler(ToolHandler[Any]):
|
|
24
24
|
text: str = ""
|
|
25
25
|
|
|
26
|
-
async def __call__(self) -> str:
|
|
26
|
+
async def __call__(self, context: Any) -> str:
|
|
27
27
|
return self.text
|
|
28
28
|
|
|
29
29
|
|
|
30
|
-
class _ConcurrentHandler(ToolHandler):
|
|
30
|
+
class _ConcurrentHandler(ToolHandler[Any]):
|
|
31
31
|
"""Handler with concurrency limit."""
|
|
32
32
|
|
|
33
33
|
_tool_concurrency: ClassVar[int | None] = 2
|
|
34
34
|
text: str = ""
|
|
35
35
|
|
|
36
|
-
async def __call__(self) -> str:
|
|
36
|
+
async def __call__(self, context: Any) -> str:
|
|
37
37
|
return self.text
|
|
38
38
|
|
|
39
39
|
|
|
@@ -2,24 +2,26 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
5
7
|
import pytest
|
|
6
8
|
from axio.tool import Tool, ToolHandler
|
|
7
9
|
|
|
8
10
|
from axio_tui.screens import ToolSelectScreen
|
|
9
11
|
|
|
10
12
|
|
|
11
|
-
class _StubHandler(ToolHandler):
|
|
13
|
+
class _StubHandler(ToolHandler[Any]):
|
|
12
14
|
"""Stub tool handler."""
|
|
13
15
|
|
|
14
|
-
async def __call__(self) -> str:
|
|
16
|
+
async def __call__(self, context: Any) -> str:
|
|
15
17
|
return ""
|
|
16
18
|
|
|
17
19
|
|
|
18
|
-
def _make_tools(*names: str) -> list[Tool]:
|
|
20
|
+
def _make_tools(*names: str) -> list[Tool[Any]]:
|
|
19
21
|
return [Tool(name=n, description=f"Description for {n}", handler=_StubHandler) for n in names]
|
|
20
22
|
|
|
21
23
|
|
|
22
|
-
def _screen(tools: list[Tool], disabled: set[str]) -> ToolSelectScreen:
|
|
24
|
+
def _screen(tools: list[Tool[Any]], disabled: set[str]) -> ToolSelectScreen:
|
|
23
25
|
return ToolSelectScreen(groups={"pkg": tools}, disabled=disabled)
|
|
24
26
|
|
|
25
27
|
|
|
@@ -5,6 +5,7 @@ from __future__ import annotations
|
|
|
5
5
|
import os
|
|
6
6
|
from collections.abc import AsyncIterator
|
|
7
7
|
from pathlib import Path
|
|
8
|
+
from typing import Any
|
|
8
9
|
|
|
9
10
|
import pytest
|
|
10
11
|
from axio.agent import Agent
|
|
@@ -20,13 +21,13 @@ from axio_tui.tools import Confirm, StatusLine, SubAgent, VisionAnalyze
|
|
|
20
21
|
class TestStatusLine:
|
|
21
22
|
async def test_returns_ok(self) -> None:
|
|
22
23
|
handler = StatusLine(message="working")
|
|
23
|
-
assert await handler() == "ok"
|
|
24
|
+
assert await handler({}) == "ok"
|
|
24
25
|
|
|
25
26
|
|
|
26
27
|
class TestConfirm:
|
|
27
28
|
async def test_returns_verdict(self) -> None:
|
|
28
29
|
handler = Confirm(verdict="SAFE", reason="harmless", category="read")
|
|
29
|
-
assert await handler() == "SAFE"
|
|
30
|
+
assert await handler({}) == "SAFE"
|
|
30
31
|
|
|
31
32
|
async def test_repr(self) -> None:
|
|
32
33
|
handler = Confirm(verdict="DENY", reason="bad", category="exec")
|
|
@@ -36,7 +37,7 @@ class TestConfirm:
|
|
|
36
37
|
|
|
37
38
|
|
|
38
39
|
class TestSubAgent:
|
|
39
|
-
def _make_agent(self, transport: StubTransport, tools: list[Tool] | None = None) -> Agent:
|
|
40
|
+
def _make_agent(self, transport: StubTransport, tools: list[Tool[Any]] | None = None) -> Agent:
|
|
40
41
|
return Agent(system="test", tools=tools or [], transport=transport, max_iterations=5)
|
|
41
42
|
|
|
42
43
|
async def test_returns_subagent_result(self) -> None:
|
|
@@ -49,7 +50,7 @@ class TestSubAgent:
|
|
|
49
50
|
SubAgent._factory = factory
|
|
50
51
|
try:
|
|
51
52
|
handler = SubAgent(task="do something")
|
|
52
|
-
result = await handler()
|
|
53
|
+
result = await handler({})
|
|
53
54
|
assert result == "sub-result"
|
|
54
55
|
finally:
|
|
55
56
|
SubAgent._factory = None
|
|
@@ -57,7 +58,7 @@ class TestSubAgent:
|
|
|
57
58
|
async def test_no_factory_returns_error(self) -> None:
|
|
58
59
|
SubAgent._factory = None
|
|
59
60
|
handler = SubAgent(task="do something")
|
|
60
|
-
result = await handler()
|
|
61
|
+
result = await handler({})
|
|
61
62
|
assert result == "SubAgent is not configured"
|
|
62
63
|
|
|
63
64
|
async def test_context_forking(self) -> None:
|
|
@@ -80,7 +81,7 @@ class TestSubAgent:
|
|
|
80
81
|
SubAgent._factory = factory
|
|
81
82
|
try:
|
|
82
83
|
handler = SubAgent(task="check context")
|
|
83
|
-
await handler()
|
|
84
|
+
await handler({})
|
|
84
85
|
assert len(received_context) == 1
|
|
85
86
|
history = await received_context[0].get_history()
|
|
86
87
|
assert history[0].content[0].text == "hello" # type: ignore[attr-defined]
|
|
@@ -97,7 +98,7 @@ class TestSubAgent:
|
|
|
97
98
|
try:
|
|
98
99
|
handler = SubAgent(task="boom")
|
|
99
100
|
with pytest.raises(RuntimeError, match="factory failed"):
|
|
100
|
-
await handler()
|
|
101
|
+
await handler({})
|
|
101
102
|
finally:
|
|
102
103
|
SubAgent._factory = None
|
|
103
104
|
|
|
@@ -152,7 +153,7 @@ class TestVisionAnalyze:
|
|
|
152
153
|
async def test_no_transport_returns_error(self) -> None:
|
|
153
154
|
VisionAnalyze._transport = None
|
|
154
155
|
handler = VisionAnalyze(path="img.png")
|
|
155
|
-
result = await handler()
|
|
156
|
+
result = await handler({})
|
|
156
157
|
assert "not configured" in result
|
|
157
158
|
|
|
158
159
|
async def test_file_not_found(self, tmp_path: Path) -> None:
|
|
@@ -161,7 +162,7 @@ class TestVisionAnalyze:
|
|
|
161
162
|
try:
|
|
162
163
|
os.chdir(tmp_path)
|
|
163
164
|
handler = VisionAnalyze(path="missing.png")
|
|
164
|
-
result = await handler()
|
|
165
|
+
result = await handler({})
|
|
165
166
|
assert "File not found" in result
|
|
166
167
|
finally:
|
|
167
168
|
os.chdir(old_cwd)
|
|
@@ -174,7 +175,7 @@ class TestVisionAnalyze:
|
|
|
174
175
|
try:
|
|
175
176
|
os.chdir(tmp_path)
|
|
176
177
|
handler = VisionAnalyze(path="file.bmp")
|
|
177
|
-
result = await handler()
|
|
178
|
+
result = await handler({})
|
|
178
179
|
assert "Unsupported image format" in result
|
|
179
180
|
finally:
|
|
180
181
|
os.chdir(old_cwd)
|
|
@@ -188,7 +189,7 @@ class TestVisionAnalyze:
|
|
|
188
189
|
try:
|
|
189
190
|
os.chdir(tmp_path)
|
|
190
191
|
handler = VisionAnalyze(path="photo.png", prompt="What is this?")
|
|
191
|
-
result = await handler()
|
|
192
|
+
result = await handler({})
|
|
192
193
|
assert result == "A red pixel"
|
|
193
194
|
finally:
|
|
194
195
|
os.chdir(old_cwd)
|
|
@@ -202,7 +203,9 @@ class TestVisionAnalyze:
|
|
|
202
203
|
captured: list[list[Message]] = []
|
|
203
204
|
|
|
204
205
|
class CapturingTransport(StubTransport):
|
|
205
|
-
def stream(
|
|
206
|
+
def stream(
|
|
207
|
+
self, messages: list[Message], tools: list[Tool[Any]], system: str
|
|
208
|
+
) -> AsyncIterator[StreamEvent]:
|
|
206
209
|
captured.append(messages)
|
|
207
210
|
return super().stream(messages, tools, system)
|
|
208
211
|
|
|
@@ -212,7 +215,7 @@ class TestVisionAnalyze:
|
|
|
212
215
|
try:
|
|
213
216
|
os.chdir(tmp_path)
|
|
214
217
|
handler = VisionAnalyze(path="test.jpg", prompt="Describe it")
|
|
215
|
-
await handler()
|
|
218
|
+
await handler({})
|
|
216
219
|
assert len(captured) == 1
|
|
217
220
|
msg = captured[0][0]
|
|
218
221
|
assert len(msg.content) == 2
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|