akgentic-tool 1.2.2__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.
- akgentic/__init__.py +10 -0
- akgentic/tool/__init__.py +91 -0
- akgentic/tool/core.py +377 -0
- akgentic/tool/errors.py +22 -0
- akgentic/tool/event.py +149 -0
- akgentic/tool/knowledge_graph/__init__.py +94 -0
- akgentic/tool/knowledge_graph/kg_actor.py +1114 -0
- akgentic/tool/knowledge_graph/kg_tool.py +378 -0
- akgentic/tool/knowledge_graph/models.py +379 -0
- akgentic/tool/mcp/__init__.py +31 -0
- akgentic/tool/mcp/mcp.py +269 -0
- akgentic/tool/mcp/oauth_handler.py +250 -0
- akgentic/tool/planning/README.md +32 -0
- akgentic/tool/planning/__init__.py +17 -0
- akgentic/tool/planning/planning.py +334 -0
- akgentic/tool/planning/planning_actor.py +425 -0
- akgentic/tool/py.typed +2 -0
- akgentic/tool/sandbox/__init__.py +33 -0
- akgentic/tool/sandbox/actor.py +231 -0
- akgentic/tool/sandbox/bwrap.py +114 -0
- akgentic/tool/sandbox/docker.py +113 -0
- akgentic/tool/sandbox/local.py +148 -0
- akgentic/tool/sandbox/sandbox.Dockerfile +24 -0
- akgentic/tool/sandbox/seatbelt.py +172 -0
- akgentic/tool/sandbox/tool.py +213 -0
- akgentic/tool/search/__init__.py +10 -0
- akgentic/tool/search/search.py +250 -0
- akgentic/tool/team/__init__.py +17 -0
- akgentic/tool/team/team.py +544 -0
- akgentic/tool/vector.py +267 -0
- akgentic/tool/vector_store/__init__.py +57 -0
- akgentic/tool/vector_store/actor.py +552 -0
- akgentic/tool/vector_store/embedding_actor.py +192 -0
- akgentic/tool/vector_store/inmemory.py +286 -0
- akgentic/tool/vector_store/protocol.py +202 -0
- akgentic/tool/vector_store/tool.py +113 -0
- akgentic/tool/vector_store/weaviate.py +287 -0
- akgentic/tool/workspace/__init__.py +73 -0
- akgentic/tool/workspace/edit.py +373 -0
- akgentic/tool/workspace/readers.py +182 -0
- akgentic/tool/workspace/tool.py +1220 -0
- akgentic/tool/workspace/workspace.py +160 -0
- akgentic_tool-1.2.2.dist-info/METADATA +650 -0
- akgentic_tool-1.2.2.dist-info/RECORD +46 -0
- akgentic_tool-1.2.2.dist-info/WHEEL +4 -0
- akgentic_tool-1.2.2.dist-info/licenses/LICENSE +661 -0
akgentic/__init__.py
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"""Akgentic namespace package.
|
|
2
|
+
|
|
3
|
+
This __init__.py file marks the akgentic directory as a namespace package,
|
|
4
|
+
allowing multiple packages to contribute to the akgentic namespace.
|
|
5
|
+
|
|
6
|
+
Do not add any code here. Each module (core, llm, team, tool, etc.) will
|
|
7
|
+
have its own subpackage that extends this namespace.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
__path__ = __import__("pkgutil").extend_path(__path__, __name__)
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"""akgentic-tool public API."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING, Any
|
|
6
|
+
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from .knowledge_graph.models import KnowledgeGraphStateEvent as KnowledgeGraphStateEvent
|
|
9
|
+
|
|
10
|
+
# Submodules with their own __init__ files
|
|
11
|
+
from . import mcp, planning, sandbox, search, team, workspace # noqa: F401
|
|
12
|
+
from .core import ( # noqa: F401
|
|
13
|
+
COMMAND,
|
|
14
|
+
SYSTEM_PROMPT,
|
|
15
|
+
TOOL_CALL,
|
|
16
|
+
BaseToolParam,
|
|
17
|
+
Channels,
|
|
18
|
+
ToolCard,
|
|
19
|
+
ToolFactory,
|
|
20
|
+
)
|
|
21
|
+
from .errors import RetriableError, ToolObserverGone # noqa: F401
|
|
22
|
+
from .event import ( # noqa: F401
|
|
23
|
+
ActorToolObserver,
|
|
24
|
+
TeamManagementToolObserver,
|
|
25
|
+
ToolObserver,
|
|
26
|
+
ToolStateEvent,
|
|
27
|
+
ToolStatePayload,
|
|
28
|
+
)
|
|
29
|
+
from .sandbox.bwrap import BwrapSandboxActor # noqa: F401
|
|
30
|
+
from .sandbox.seatbelt import SeatbeltSandboxActor # noqa: F401
|
|
31
|
+
from .sandbox.tool import ExecTool # noqa: F401
|
|
32
|
+
from .workspace.tool import WorkspaceTool # noqa: F401
|
|
33
|
+
|
|
34
|
+
try:
|
|
35
|
+
from .vector import EmbeddingService, VectorEntry, VectorIndex # noqa: F401
|
|
36
|
+
|
|
37
|
+
_VECTOR_SEARCH_AVAILABLE = True
|
|
38
|
+
except ImportError:
|
|
39
|
+
_VECTOR_SEARCH_AVAILABLE = False
|
|
40
|
+
|
|
41
|
+
__all__ = [
|
|
42
|
+
# Core abstractions
|
|
43
|
+
"BaseToolParam",
|
|
44
|
+
"ToolCard",
|
|
45
|
+
"ToolFactory",
|
|
46
|
+
# Expose channel constants
|
|
47
|
+
"COMMAND",
|
|
48
|
+
"SYSTEM_PROMPT",
|
|
49
|
+
"TOOL_CALL",
|
|
50
|
+
"Channels",
|
|
51
|
+
# Errors
|
|
52
|
+
"RetriableError",
|
|
53
|
+
"ToolObserverGone",
|
|
54
|
+
# Events and observers
|
|
55
|
+
"ToolObserver",
|
|
56
|
+
"ActorToolObserver",
|
|
57
|
+
"TeamManagementToolObserver",
|
|
58
|
+
"ToolStateEvent",
|
|
59
|
+
"ToolStatePayload",
|
|
60
|
+
"KnowledgeGraphStateEvent",
|
|
61
|
+
# Submodules
|
|
62
|
+
"mcp",
|
|
63
|
+
"planning",
|
|
64
|
+
"sandbox",
|
|
65
|
+
"search",
|
|
66
|
+
"team",
|
|
67
|
+
"workspace",
|
|
68
|
+
"BwrapSandboxActor",
|
|
69
|
+
"ExecTool",
|
|
70
|
+
"SeatbeltSandboxActor",
|
|
71
|
+
"WorkspaceTool",
|
|
72
|
+
]
|
|
73
|
+
|
|
74
|
+
if _VECTOR_SEARCH_AVAILABLE:
|
|
75
|
+
__all__ += ["VectorEntry", "EmbeddingService", "VectorIndex"]
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def __getattr__(name: str) -> Any:
|
|
79
|
+
"""Lazy re-export of the KG delta payload (Story 17.1).
|
|
80
|
+
|
|
81
|
+
``KnowledgeGraphStateEvent`` lives in ``akgentic.tool.knowledge_graph.models``
|
|
82
|
+
and pulls the ``[vector_search]`` optional dependency chain when imported.
|
|
83
|
+
Exposing it via module ``__getattr__`` keeps the bare ``akgentic.tool``
|
|
84
|
+
import cheap (see ``test_tool_import_does_not_trigger_kg_import``) while
|
|
85
|
+
still honoring AC #5 of Story 17.1.
|
|
86
|
+
"""
|
|
87
|
+
if name == "KnowledgeGraphStateEvent":
|
|
88
|
+
from .knowledge_graph.models import KnowledgeGraphStateEvent
|
|
89
|
+
|
|
90
|
+
return KnowledgeGraphStateEvent
|
|
91
|
+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
akgentic/tool/core.py
ADDED
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
"""Tool abstractions and factory for the akgentic tool package.
|
|
2
|
+
|
|
3
|
+
Defines the core contracts:
|
|
4
|
+
- ``BaseToolParam``: base for capability parameter models.
|
|
5
|
+
- ``ToolCard``: abstract base — tool configuration + callable factory in one class.
|
|
6
|
+
- ``ToolFactory``: resolves ``ToolCard`` instances into callable tools, prompts, and toolsets.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import functools
|
|
10
|
+
import weakref
|
|
11
|
+
from abc import ABC, abstractmethod
|
|
12
|
+
from collections import deque
|
|
13
|
+
from enum import StrEnum
|
|
14
|
+
from typing import Any, Callable, TypeVar
|
|
15
|
+
|
|
16
|
+
from pydantic import PrivateAttr
|
|
17
|
+
|
|
18
|
+
from akgentic.core.utils import SerializableBaseModel
|
|
19
|
+
from akgentic.tool.errors import RetriableError, ToolObserverGone
|
|
20
|
+
from akgentic.tool.event import ToolObserver
|
|
21
|
+
|
|
22
|
+
T = TypeVar("T", bound="BaseToolParam")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _resolve(value: "T | bool", cls: "type[T]") -> "T | None":
|
|
26
|
+
"""Resolve a ``ParamModel | bool`` field to a ``ParamModel`` or ``None``.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
value: ``True`` (enable with defaults), ``False`` (disable), or a
|
|
30
|
+
``BaseToolParam`` instance (enable with custom parameters).
|
|
31
|
+
cls: The param model class to instantiate when *value* is ``True``.
|
|
32
|
+
|
|
33
|
+
Returns:
|
|
34
|
+
A param model instance, or ``None`` if the capability is disabled.
|
|
35
|
+
"""
|
|
36
|
+
if value is True:
|
|
37
|
+
return cls()
|
|
38
|
+
if value is False:
|
|
39
|
+
return None
|
|
40
|
+
return value # already a ParamModel instance
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class Channels(StrEnum):
|
|
44
|
+
"""Valid channel names for capability exposure."""
|
|
45
|
+
|
|
46
|
+
SYSTEM_PROMPT = "system_prompt"
|
|
47
|
+
"""Expose as a system prompt injected into the LLM context."""
|
|
48
|
+
|
|
49
|
+
TOOL_CALL = "tool_call"
|
|
50
|
+
"""Expose as a callable tool for the LLM."""
|
|
51
|
+
|
|
52
|
+
COMMAND = "command"
|
|
53
|
+
"""Expose as a programmatic command for inter-agent orchestration."""
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
# Backward-compatible module-level aliases
|
|
57
|
+
SYSTEM_PROMPT = Channels.SYSTEM_PROMPT
|
|
58
|
+
TOOL_CALL = Channels.TOOL_CALL
|
|
59
|
+
COMMAND = Channels.COMMAND
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class BaseToolParam(SerializableBaseModel):
|
|
63
|
+
"""Base for capability parameter models.
|
|
64
|
+
|
|
65
|
+
Provides common fields that control how a capability is exposed
|
|
66
|
+
and how its description can be customized.
|
|
67
|
+
|
|
68
|
+
Each subclass can override the default ``expose`` set to declare the channels
|
|
69
|
+
it participates in. Use the module-level channel constants:
|
|
70
|
+
|
|
71
|
+
- ``TOOL_CALL``: callable tool invoked by the LLM (default).
|
|
72
|
+
- ``SYSTEM_PROMPT``: prompt injected into the LLM context.
|
|
73
|
+
- ``COMMAND``: programmatic call for inter-agent orchestration.
|
|
74
|
+
"""
|
|
75
|
+
|
|
76
|
+
instructions: str | None = None
|
|
77
|
+
"""Additional instructions appended to the default tool docstring.
|
|
78
|
+
|
|
79
|
+
When set, the factory appends these instructions to the built-in docstring
|
|
80
|
+
under a structured header. When ``None``, only the default docstring is used.
|
|
81
|
+
"""
|
|
82
|
+
|
|
83
|
+
expose: set[Channels] = {TOOL_CALL}
|
|
84
|
+
"""Set of channels this capability is exposed through.
|
|
85
|
+
|
|
86
|
+
Defaults to ``{TOOL_CALL}``. Override in subclasses or at instantiation.
|
|
87
|
+
Use ``Channels`` enum members or module-level aliases: ``TOOL_CALL``, ``SYSTEM_PROMPT``,
|
|
88
|
+
``COMMAND``.
|
|
89
|
+
"""
|
|
90
|
+
|
|
91
|
+
def format_docstring(self, original: str | None) -> str | None:
|
|
92
|
+
"""Format the tool docstring with optional additional instructions.
|
|
93
|
+
|
|
94
|
+
Args:
|
|
95
|
+
original: The original docstring from the tool callable.
|
|
96
|
+
|
|
97
|
+
Returns:
|
|
98
|
+
The formatted docstring, or the original if no instructions are set.
|
|
99
|
+
"""
|
|
100
|
+
if not self.instructions:
|
|
101
|
+
return original
|
|
102
|
+
|
|
103
|
+
base_doc = original or ""
|
|
104
|
+
return f"{base_doc}\n\nAdditional Instructions:\n{self.instructions}"
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
class ToolCard(SerializableBaseModel, ABC):
|
|
108
|
+
"""Abstract base: tool configuration + callable factory in one class.
|
|
109
|
+
|
|
110
|
+
Subclasses define typed fields for their capabilities and implement
|
|
111
|
+
the factory methods that produce LLM-callable functions.
|
|
112
|
+
|
|
113
|
+
Attributes:
|
|
114
|
+
name: Human-readable tool provider name.
|
|
115
|
+
description: Natural-language description of tool capabilities.
|
|
116
|
+
"""
|
|
117
|
+
|
|
118
|
+
name: str
|
|
119
|
+
description: str
|
|
120
|
+
# Runtime-only, WEAK: a tool/closure/registry must never pin its agent.
|
|
121
|
+
_observer_ref: "weakref.ref[ToolObserver] | None" = PrivateAttr(default=None)
|
|
122
|
+
|
|
123
|
+
@property
|
|
124
|
+
def depends_on(self) -> list[str]:
|
|
125
|
+
"""Class-name list of ToolCards that MUST be wired before this one.
|
|
126
|
+
|
|
127
|
+
Default: no dependencies. Subclasses may override as a property
|
|
128
|
+
whose return value depends on instance fields (e.g. the value of
|
|
129
|
+
a ``vector_store`` field on consumer tools). The string is matched
|
|
130
|
+
against ``type(card).__name__`` by ``ToolFactory``'s topological
|
|
131
|
+
sort. Not a Pydantic field — does not appear in ``model_dump`` and
|
|
132
|
+
cannot be set via ``model_validate``.
|
|
133
|
+
"""
|
|
134
|
+
return []
|
|
135
|
+
|
|
136
|
+
def observer(self, observer: ToolObserver) -> "ToolCard":
|
|
137
|
+
"""Attach an observer (held weakly) and perform runtime setup.
|
|
138
|
+
|
|
139
|
+
Follows the same pattern as ``BaseState.observer()``.
|
|
140
|
+
Override for setup that requires the observer (e.g., actor proxies).
|
|
141
|
+
All methods can then access the observer via ``self._observer``.
|
|
142
|
+
|
|
143
|
+
The observer is stored through a ``weakref`` so a tool, its closures, and
|
|
144
|
+
its command registry can never pin a stopped owning agent in memory.
|
|
145
|
+
|
|
146
|
+
Args:
|
|
147
|
+
observer: Optional observer for tool call events.
|
|
148
|
+
|
|
149
|
+
Returns:
|
|
150
|
+
Self, enabling method chaining.
|
|
151
|
+
"""
|
|
152
|
+
self._observer = observer
|
|
153
|
+
return self
|
|
154
|
+
|
|
155
|
+
@property
|
|
156
|
+
def _observer(self) -> "ToolObserver":
|
|
157
|
+
"""Live observer for synchronous, in-life use. Raises if the agent has stopped."""
|
|
158
|
+
obs = self._observer_or_none()
|
|
159
|
+
if obs is None:
|
|
160
|
+
raise ToolObserverGone("tool used after its owning agent was stopped")
|
|
161
|
+
return obs
|
|
162
|
+
|
|
163
|
+
@_observer.setter
|
|
164
|
+
def _observer(self, observer: ToolObserver) -> None:
|
|
165
|
+
"""Store the observer weakly (backward-compatible ``self._observer = observer``)."""
|
|
166
|
+
self._observer_ref = weakref.ref(observer)
|
|
167
|
+
|
|
168
|
+
def _observer_or_none(self) -> "ToolObserver | None":
|
|
169
|
+
"""Return the live observer, or ``None`` if unset or already collected."""
|
|
170
|
+
return self._observer_ref() if self._observer_ref is not None else None
|
|
171
|
+
|
|
172
|
+
@abstractmethod
|
|
173
|
+
def get_tools(self) -> list[Callable]:
|
|
174
|
+
"""Return callable tool functions for LLM agents.
|
|
175
|
+
|
|
176
|
+
Use ``self._observer`` when tool callables need to emit events.
|
|
177
|
+
"""
|
|
178
|
+
...
|
|
179
|
+
|
|
180
|
+
def get_system_prompts(self) -> list[Callable]:
|
|
181
|
+
"""Return system prompt callables injected into LLM context.
|
|
182
|
+
|
|
183
|
+
Use ``self._observer`` when prompts need runtime data.
|
|
184
|
+
"""
|
|
185
|
+
return []
|
|
186
|
+
|
|
187
|
+
def get_commands(self) -> dict[type["BaseToolParam"], Callable]:
|
|
188
|
+
"""Return callable commands for programmatic invocation.
|
|
189
|
+
|
|
190
|
+
Commands are methods exposed for inter-agent orchestration
|
|
191
|
+
(e.g., ``hire_member``, ``fire_member``). Unlike tools (invoked by
|
|
192
|
+
the LLM), commands are called programmatically by other agents
|
|
193
|
+
or system components via ``proxy_call`` or similar mechanisms.
|
|
194
|
+
|
|
195
|
+
Returns:
|
|
196
|
+
Dict mapping param class (e.g., ``HireTeamMember``) to callable.
|
|
197
|
+
"""
|
|
198
|
+
return {}
|
|
199
|
+
|
|
200
|
+
def get_toolsets(self) -> list[Any]:
|
|
201
|
+
"""Return runtime toolset objects (e.g., MCP servers)."""
|
|
202
|
+
return []
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def _topological_sort(cards: list[ToolCard]) -> list[ToolCard]:
|
|
206
|
+
"""Return ``cards`` topologically sorted by ``ToolCard.depends_on``.
|
|
207
|
+
|
|
208
|
+
Dependency keys are matched against ``type(card).__name__``. The sort uses
|
|
209
|
+
Kahn's algorithm with a FIFO queue seeded in input order, which produces a
|
|
210
|
+
deterministic ordering: independent nodes retain their relative input order.
|
|
211
|
+
|
|
212
|
+
Duplicate class names in ``cards`` (e.g. two ``VectorStoreTool`` instances
|
|
213
|
+
with different configuration) are permitted — later entries overwrite
|
|
214
|
+
earlier entries in the internal name→card map. Dependency relationships
|
|
215
|
+
are at the class level, not per-instance.
|
|
216
|
+
|
|
217
|
+
Args:
|
|
218
|
+
cards: Tool cards to sort. Input order is preserved for independent
|
|
219
|
+
nodes.
|
|
220
|
+
|
|
221
|
+
Returns:
|
|
222
|
+
A new list containing the same cards in dependency-respecting order
|
|
223
|
+
(prerequisites before dependents).
|
|
224
|
+
|
|
225
|
+
Raises:
|
|
226
|
+
ValueError: If a declared dependency is not present in ``cards``
|
|
227
|
+
(message names both the dependent and the missing class), or if
|
|
228
|
+
the dependency graph contains a cycle (message contains ``"cycle"``
|
|
229
|
+
and lists the class names involved).
|
|
230
|
+
"""
|
|
231
|
+
# Name → card map. Later duplicates overwrite earlier entries — dependency
|
|
232
|
+
# relationships are at the class level, not per-instance.
|
|
233
|
+
by_name: dict[str, ToolCard] = {type(card).__name__: card for card in cards}
|
|
234
|
+
|
|
235
|
+
# Validate every declared dependency is present.
|
|
236
|
+
for card in cards:
|
|
237
|
+
for dep in card.depends_on:
|
|
238
|
+
if dep not in by_name:
|
|
239
|
+
raise ValueError(
|
|
240
|
+
f"{type(card).__name__} depends on {dep} but it was not "
|
|
241
|
+
f"found in the tool list"
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
# Build in-degree map keyed by class name (not instance — duplicates collapse).
|
|
245
|
+
in_degree: dict[str, int] = {name: 0 for name in by_name}
|
|
246
|
+
# Reverse adjacency: dep_name → list of class names that depend on it.
|
|
247
|
+
dependents: dict[str, list[str]] = {name: [] for name in by_name}
|
|
248
|
+
for name, card in by_name.items():
|
|
249
|
+
for dep in card.depends_on:
|
|
250
|
+
in_degree[name] += 1
|
|
251
|
+
dependents[dep].append(name)
|
|
252
|
+
|
|
253
|
+
# Seed the queue with zero-in-degree names in the order they appeared in
|
|
254
|
+
# the input (FIFO → deterministic for the same input). We iterate cards to
|
|
255
|
+
# preserve input order, skipping duplicates.
|
|
256
|
+
queue: deque[str] = deque()
|
|
257
|
+
seen: set[str] = set()
|
|
258
|
+
for card in cards:
|
|
259
|
+
name = type(card).__name__
|
|
260
|
+
if name in seen:
|
|
261
|
+
continue
|
|
262
|
+
seen.add(name)
|
|
263
|
+
if in_degree[name] == 0:
|
|
264
|
+
queue.append(name)
|
|
265
|
+
|
|
266
|
+
ordered_names: list[str] = []
|
|
267
|
+
while queue:
|
|
268
|
+
name = queue.popleft()
|
|
269
|
+
ordered_names.append(name)
|
|
270
|
+
for dependent in dependents[name]:
|
|
271
|
+
in_degree[dependent] -= 1
|
|
272
|
+
if in_degree[dependent] == 0:
|
|
273
|
+
queue.append(dependent)
|
|
274
|
+
|
|
275
|
+
if len(ordered_names) < len(by_name):
|
|
276
|
+
remaining = sorted(set(by_name) - set(ordered_names))
|
|
277
|
+
raise ValueError(
|
|
278
|
+
f"ToolCard dependency cycle detected: {remaining}"
|
|
279
|
+
)
|
|
280
|
+
|
|
281
|
+
# Map sorted names back to ToolCard instances. Preserve input order for
|
|
282
|
+
# duplicate class names: emit instances in the order they appeared in the
|
|
283
|
+
# input, grouped by their class's position in the sorted name order.
|
|
284
|
+
by_name_instances: dict[str, list[ToolCard]] = {name: [] for name in by_name}
|
|
285
|
+
for card in cards:
|
|
286
|
+
by_name_instances[type(card).__name__].append(card)
|
|
287
|
+
ordered: list[ToolCard] = []
|
|
288
|
+
for name in ordered_names:
|
|
289
|
+
ordered.extend(by_name_instances[name])
|
|
290
|
+
return ordered
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
class ToolFactory:
|
|
294
|
+
"""Resolves ``ToolCard`` instances into callable tools, prompts, and toolsets."""
|
|
295
|
+
|
|
296
|
+
def __init__(
|
|
297
|
+
self,
|
|
298
|
+
tool_cards: list[ToolCard],
|
|
299
|
+
observer: ToolObserver | None = None,
|
|
300
|
+
retry_exception: type[Exception] | None = None,
|
|
301
|
+
) -> None:
|
|
302
|
+
"""Create a factory for one or more tool cards.
|
|
303
|
+
|
|
304
|
+
Topologically sorts ``tool_cards`` by their ``depends_on`` class
|
|
305
|
+
attribute, then attaches the observer to every card in dependency order
|
|
306
|
+
(triggers runtime setup in ``ToolCard.observer()``). Prerequisites are
|
|
307
|
+
wired before dependents, so a consumer card's ``observer()`` can safely
|
|
308
|
+
look up actors or resources created by its prerequisites.
|
|
309
|
+
|
|
310
|
+
Args:
|
|
311
|
+
tool_cards: Tool cards to resolve into callable tools. The caller's
|
|
312
|
+
list is not mutated; a new dependency-ordered list is stored on
|
|
313
|
+
``self.tool_cards``. Aggregators (``get_tools``,
|
|
314
|
+
``get_system_prompts``, ``get_commands``, ``get_toolsets``)
|
|
315
|
+
iterate in this dependency order.
|
|
316
|
+
observer: Optional observer notified by tool implementations during
|
|
317
|
+
tool calls.
|
|
318
|
+
retry_exception: Optional exception class to raise when a tool raises
|
|
319
|
+
``RetriableError``. Injected by the integration layer (e.g., ModelRetry
|
|
320
|
+
from pydantic-ai) to keep the tool module framework-agnostic.
|
|
321
|
+
|
|
322
|
+
Raises:
|
|
323
|
+
ValueError: If the dependency graph is invalid — either a card
|
|
324
|
+
declares ``depends_on`` for a class not present in
|
|
325
|
+
``tool_cards``, or a cycle exists. Raised before any observer
|
|
326
|
+
is attached (fail fast at team creation).
|
|
327
|
+
"""
|
|
328
|
+
self.tool_cards = _topological_sort(tool_cards)
|
|
329
|
+
self.observer = observer
|
|
330
|
+
self._retry_exception = retry_exception
|
|
331
|
+
|
|
332
|
+
if self.observer is not None:
|
|
333
|
+
for card in self.tool_cards:
|
|
334
|
+
card.observer(self.observer)
|
|
335
|
+
|
|
336
|
+
def _wrap_with_retry(self, fn: Callable) -> Callable:
|
|
337
|
+
"""Wrap a tool callable to convert ``RetriableError`` into retry_exception."""
|
|
338
|
+
assert self._retry_exception is not None
|
|
339
|
+
retry_exc = self._retry_exception
|
|
340
|
+
|
|
341
|
+
@functools.wraps(fn)
|
|
342
|
+
def wrapper(*args, **kwargs):
|
|
343
|
+
try:
|
|
344
|
+
return fn(*args, **kwargs)
|
|
345
|
+
except RetriableError as e:
|
|
346
|
+
raise retry_exc(str(e)) from e
|
|
347
|
+
|
|
348
|
+
return wrapper
|
|
349
|
+
|
|
350
|
+
def get_tools(self) -> list[Callable]:
|
|
351
|
+
"""Return tool callables aggregated from all tool cards."""
|
|
352
|
+
tools = [t for card in self.tool_cards for t in card.get_tools()]
|
|
353
|
+
if self._retry_exception is not None:
|
|
354
|
+
tools = [self._wrap_with_retry(t) for t in tools]
|
|
355
|
+
return tools
|
|
356
|
+
|
|
357
|
+
def get_system_prompts(self) -> list[Callable]:
|
|
358
|
+
"""Return system prompt callables aggregated from all tool cards."""
|
|
359
|
+
return [p for card in self.tool_cards for p in card.get_system_prompts()]
|
|
360
|
+
|
|
361
|
+
def get_commands(self) -> dict[type[BaseToolParam], Callable]:
|
|
362
|
+
"""Return command callables aggregated from all tool cards.
|
|
363
|
+
|
|
364
|
+
Returns:
|
|
365
|
+
Dict mapping param class to callable, merged from all tool cards.
|
|
366
|
+
"""
|
|
367
|
+
commands: dict[type[BaseToolParam], Callable] = {}
|
|
368
|
+
for card in self.tool_cards:
|
|
369
|
+
commands.update(card.get_commands())
|
|
370
|
+
|
|
371
|
+
if self._retry_exception is not None:
|
|
372
|
+
commands = {k: self._wrap_with_retry(v) for k, v in commands.items()}
|
|
373
|
+
return commands
|
|
374
|
+
|
|
375
|
+
def get_toolsets(self) -> list[Any]:
|
|
376
|
+
"""Return toolset instances aggregated from all tool cards."""
|
|
377
|
+
return [ts for card in self.tool_cards for ts in card.get_toolsets()]
|
akgentic/tool/errors.py
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""Tool-layer exceptions for error signaling.
|
|
2
|
+
|
|
3
|
+
These exceptions are framework-agnostic — the integration layer (e.g., akgentic-team)
|
|
4
|
+
translates them into the appropriate retry mechanism (e.g., pydantic-ai ModelRetry).
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class RetriableError(Exception):
|
|
9
|
+
"""Raised by tool implementations when the LLM should retry with corrected input.
|
|
10
|
+
|
|
11
|
+
Tools raise this to signal a recoverable error. The consuming framework
|
|
12
|
+
(via ToolFactory's retry_exception injection) converts it to the appropriate
|
|
13
|
+
retry mechanism.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
pass
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class ToolObserverGone(RuntimeError): # noqa: N818 — signals a lifecycle state, not a failure
|
|
20
|
+
"""A tool callable ran after its owning agent was stopped."""
|
|
21
|
+
|
|
22
|
+
pass
|
akgentic/tool/event.py
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import uuid
|
|
4
|
+
from typing import TYPE_CHECKING, Protocol, TypeAlias, runtime_checkable
|
|
5
|
+
|
|
6
|
+
from akgentic.core.actor_address import ActorAddress
|
|
7
|
+
from akgentic.core.agent import AkgentType
|
|
8
|
+
from akgentic.core.messages import Message
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from akgentic.tool.knowledge_graph.models import KnowledgeGraphStateEvent
|
|
12
|
+
|
|
13
|
+
# Union of tool-specific delta payloads carried by ``ToolStateEvent`` (ADR-024).
|
|
14
|
+
# Defined as a ``TypeAlias`` so future stateful tools (e.g. ``VectorStoreStateEvent``)
|
|
15
|
+
# can extend the union without touching ``ToolStateEvent``. Uses a string forward
|
|
16
|
+
# reference to avoid the ``event.py → knowledge_graph.models`` import cycle.
|
|
17
|
+
ToolStatePayload: TypeAlias = "KnowledgeGraphStateEvent"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class ToolStateEvent(Message):
|
|
21
|
+
"""Generic tool-state event envelope (ADR-024, Story 17.1).
|
|
22
|
+
|
|
23
|
+
Wraps a tool-specific delta payload so any stateful tool actor can broadcast
|
|
24
|
+
typed state changes on the existing orchestrator event stream. Inherits
|
|
25
|
+
``team_id``, ``timestamp``, ``id``, ``sender``, and ``display_type`` from
|
|
26
|
+
:class:`akgentic.core.messages.Message` without override.
|
|
27
|
+
|
|
28
|
+
Attributes:
|
|
29
|
+
tool_id: Tool-actor name emitting the event (e.g. ``"#KnowledgeGraphTool"``).
|
|
30
|
+
seq: Per-tool monotonic sequence number (starts at 1, enforced in Story 17.2).
|
|
31
|
+
payload: Tool-specific delta payload (see :data:`ToolStatePayload`).
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
tool_id: str
|
|
35
|
+
seq: int
|
|
36
|
+
payload: ToolStatePayload
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@runtime_checkable
|
|
40
|
+
class ToolObserver(Protocol):
|
|
41
|
+
"""Basic observer protocol for tool interactions.
|
|
42
|
+
|
|
43
|
+
This protocol defines the minimal interface required for tools that only
|
|
44
|
+
need to emit events. Tools requiring actor-aware features should use
|
|
45
|
+
ActorToolObserver instead.
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
def notify_event(self, event: object) -> None:
|
|
49
|
+
"""Called when a tool domain event is emitted.
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
event: Domain event object
|
|
53
|
+
"""
|
|
54
|
+
...
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@runtime_checkable
|
|
58
|
+
class ActorToolObserver(ToolObserver, Protocol):
|
|
59
|
+
"""Actor-aware observer protocol for tool interactions.
|
|
60
|
+
|
|
61
|
+
Extends ToolObserver with actor-specific capabilities needed by tools
|
|
62
|
+
that interact with the actor system (e.g., PlanningTool).
|
|
63
|
+
"""
|
|
64
|
+
|
|
65
|
+
@property
|
|
66
|
+
def myAddress(self) -> ActorAddress: # noqa: N802
|
|
67
|
+
"""Get the current actor's address."""
|
|
68
|
+
...
|
|
69
|
+
|
|
70
|
+
@property
|
|
71
|
+
def orchestrator(self) -> ActorAddress | None:
|
|
72
|
+
"""Get the orchestrator address."""
|
|
73
|
+
...
|
|
74
|
+
|
|
75
|
+
@property
|
|
76
|
+
def team_id(self) -> uuid.UUID:
|
|
77
|
+
"""Get the team id."""
|
|
78
|
+
...
|
|
79
|
+
|
|
80
|
+
def proxy_ask(
|
|
81
|
+
self,
|
|
82
|
+
actor: ActorAddress,
|
|
83
|
+
actor_type: type[AkgentType] | None = None,
|
|
84
|
+
timeout: int | None = None,
|
|
85
|
+
) -> AkgentType:
|
|
86
|
+
"""Get a proxy to another actor.
|
|
87
|
+
|
|
88
|
+
Args:
|
|
89
|
+
actor: Address of the target actor
|
|
90
|
+
actor_type: Optional expected type of the target actor for better type checking
|
|
91
|
+
timeout: Optional timeout for the proxy ask
|
|
92
|
+
|
|
93
|
+
Returns:
|
|
94
|
+
Proxy object to interact with the target actor
|
|
95
|
+
"""
|
|
96
|
+
...
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
@runtime_checkable
|
|
100
|
+
class TeamManagementToolObserver(ActorToolObserver, Protocol):
|
|
101
|
+
"""Observer protocol for team management tools.
|
|
102
|
+
|
|
103
|
+
Extends ActorToolObserver with team-specific capabilities needed by
|
|
104
|
+
TeamTool for hiring, firing, and managing team members within the
|
|
105
|
+
actor system.
|
|
106
|
+
"""
|
|
107
|
+
|
|
108
|
+
def createActor( # noqa: N802
|
|
109
|
+
self,
|
|
110
|
+
actor_class: type[AkgentType],
|
|
111
|
+
*,
|
|
112
|
+
config: object,
|
|
113
|
+
) -> ActorAddress:
|
|
114
|
+
"""Create a child actor with the given config.
|
|
115
|
+
|
|
116
|
+
Args:
|
|
117
|
+
actor_class: The actor class to instantiate
|
|
118
|
+
config: Configuration object for the actor
|
|
119
|
+
|
|
120
|
+
Returns:
|
|
121
|
+
Address of the newly created actor
|
|
122
|
+
"""
|
|
123
|
+
...
|
|
124
|
+
|
|
125
|
+
def on_hire(self, address: ActorAddress) -> None:
|
|
126
|
+
"""Hook called after hiring a team member.
|
|
127
|
+
|
|
128
|
+
Handles agent-specific concerns such as:
|
|
129
|
+
- Tracking child in agent's children list
|
|
130
|
+
- Updating local caches
|
|
131
|
+
- Any agent-specific bookkeeping
|
|
132
|
+
|
|
133
|
+
Args:
|
|
134
|
+
address: ActorAddress of hired agent
|
|
135
|
+
"""
|
|
136
|
+
...
|
|
137
|
+
|
|
138
|
+
def on_fire(self, address: ActorAddress) -> None:
|
|
139
|
+
"""Hook called after firing a team member.
|
|
140
|
+
|
|
141
|
+
Handles agent-specific concerns such as:
|
|
142
|
+
- Removing from children tracking
|
|
143
|
+
- Clearing from local caches
|
|
144
|
+
- Any agent-specific cleanup
|
|
145
|
+
|
|
146
|
+
Args:
|
|
147
|
+
address: ActorAddress of fired agent
|
|
148
|
+
"""
|
|
149
|
+
...
|