aury-agent 0.0.4__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.
- aury/__init__.py +2 -0
- aury/agents/__init__.py +55 -0
- aury/agents/a2a/__init__.py +168 -0
- aury/agents/backends/__init__.py +196 -0
- aury/agents/backends/artifact/__init__.py +9 -0
- aury/agents/backends/artifact/memory.py +130 -0
- aury/agents/backends/artifact/types.py +133 -0
- aury/agents/backends/code/__init__.py +65 -0
- aury/agents/backends/file/__init__.py +11 -0
- aury/agents/backends/file/local.py +66 -0
- aury/agents/backends/file/types.py +40 -0
- aury/agents/backends/invocation/__init__.py +8 -0
- aury/agents/backends/invocation/memory.py +81 -0
- aury/agents/backends/invocation/types.py +110 -0
- aury/agents/backends/memory/__init__.py +8 -0
- aury/agents/backends/memory/memory.py +179 -0
- aury/agents/backends/memory/types.py +136 -0
- aury/agents/backends/message/__init__.py +9 -0
- aury/agents/backends/message/memory.py +122 -0
- aury/agents/backends/message/types.py +124 -0
- aury/agents/backends/sandbox.py +275 -0
- aury/agents/backends/session/__init__.py +8 -0
- aury/agents/backends/session/memory.py +93 -0
- aury/agents/backends/session/types.py +124 -0
- aury/agents/backends/shell/__init__.py +11 -0
- aury/agents/backends/shell/local.py +110 -0
- aury/agents/backends/shell/types.py +55 -0
- aury/agents/backends/shell.py +209 -0
- aury/agents/backends/snapshot/__init__.py +19 -0
- aury/agents/backends/snapshot/git.py +95 -0
- aury/agents/backends/snapshot/hybrid.py +125 -0
- aury/agents/backends/snapshot/memory.py +86 -0
- aury/agents/backends/snapshot/types.py +59 -0
- aury/agents/backends/state/__init__.py +29 -0
- aury/agents/backends/state/composite.py +49 -0
- aury/agents/backends/state/file.py +57 -0
- aury/agents/backends/state/memory.py +52 -0
- aury/agents/backends/state/sqlite.py +262 -0
- aury/agents/backends/state/types.py +178 -0
- aury/agents/backends/subagent/__init__.py +165 -0
- aury/agents/cli/__init__.py +41 -0
- aury/agents/cli/chat.py +239 -0
- aury/agents/cli/config.py +236 -0
- aury/agents/cli/extensions.py +460 -0
- aury/agents/cli/main.py +189 -0
- aury/agents/cli/session.py +337 -0
- aury/agents/cli/workflow.py +276 -0
- aury/agents/context_providers/__init__.py +66 -0
- aury/agents/context_providers/artifact.py +299 -0
- aury/agents/context_providers/base.py +177 -0
- aury/agents/context_providers/memory.py +70 -0
- aury/agents/context_providers/message.py +130 -0
- aury/agents/context_providers/skill.py +50 -0
- aury/agents/context_providers/subagent.py +46 -0
- aury/agents/context_providers/tool.py +68 -0
- aury/agents/core/__init__.py +83 -0
- aury/agents/core/base.py +573 -0
- aury/agents/core/context.py +797 -0
- aury/agents/core/context_builder.py +303 -0
- aury/agents/core/event_bus/__init__.py +15 -0
- aury/agents/core/event_bus/bus.py +203 -0
- aury/agents/core/factory.py +169 -0
- aury/agents/core/isolator.py +97 -0
- aury/agents/core/logging.py +95 -0
- aury/agents/core/parallel.py +194 -0
- aury/agents/core/runner.py +139 -0
- aury/agents/core/services/__init__.py +5 -0
- aury/agents/core/services/file_session.py +144 -0
- aury/agents/core/services/message.py +53 -0
- aury/agents/core/services/session.py +53 -0
- aury/agents/core/signals.py +109 -0
- aury/agents/core/state.py +363 -0
- aury/agents/core/types/__init__.py +107 -0
- aury/agents/core/types/action.py +176 -0
- aury/agents/core/types/artifact.py +135 -0
- aury/agents/core/types/block.py +736 -0
- aury/agents/core/types/message.py +350 -0
- aury/agents/core/types/recall.py +144 -0
- aury/agents/core/types/session.py +257 -0
- aury/agents/core/types/subagent.py +154 -0
- aury/agents/core/types/tool.py +205 -0
- aury/agents/eval/__init__.py +331 -0
- aury/agents/hitl/__init__.py +57 -0
- aury/agents/hitl/ask_user.py +242 -0
- aury/agents/hitl/compaction.py +230 -0
- aury/agents/hitl/exceptions.py +87 -0
- aury/agents/hitl/permission.py +617 -0
- aury/agents/hitl/revert.py +216 -0
- aury/agents/llm/__init__.py +31 -0
- aury/agents/llm/adapter.py +367 -0
- aury/agents/llm/openai.py +294 -0
- aury/agents/llm/provider.py +476 -0
- aury/agents/mcp/__init__.py +153 -0
- aury/agents/memory/__init__.py +46 -0
- aury/agents/memory/compaction.py +394 -0
- aury/agents/memory/manager.py +465 -0
- aury/agents/memory/processor.py +177 -0
- aury/agents/memory/store.py +187 -0
- aury/agents/memory/types.py +137 -0
- aury/agents/messages/__init__.py +40 -0
- aury/agents/messages/config.py +47 -0
- aury/agents/messages/raw_store.py +224 -0
- aury/agents/messages/store.py +118 -0
- aury/agents/messages/types.py +88 -0
- aury/agents/middleware/__init__.py +31 -0
- aury/agents/middleware/base.py +341 -0
- aury/agents/middleware/chain.py +342 -0
- aury/agents/middleware/message.py +129 -0
- aury/agents/middleware/message_container.py +126 -0
- aury/agents/middleware/raw_message.py +153 -0
- aury/agents/middleware/truncation.py +139 -0
- aury/agents/middleware/types.py +81 -0
- aury/agents/plugin.py +162 -0
- aury/agents/react/__init__.py +4 -0
- aury/agents/react/agent.py +1923 -0
- aury/agents/sandbox/__init__.py +23 -0
- aury/agents/sandbox/local.py +239 -0
- aury/agents/sandbox/remote.py +200 -0
- aury/agents/sandbox/types.py +115 -0
- aury/agents/skill/__init__.py +16 -0
- aury/agents/skill/loader.py +180 -0
- aury/agents/skill/types.py +83 -0
- aury/agents/tool/__init__.py +39 -0
- aury/agents/tool/builtin/__init__.py +23 -0
- aury/agents/tool/builtin/ask_user.py +155 -0
- aury/agents/tool/builtin/bash.py +107 -0
- aury/agents/tool/builtin/delegate.py +726 -0
- aury/agents/tool/builtin/edit.py +121 -0
- aury/agents/tool/builtin/plan.py +277 -0
- aury/agents/tool/builtin/read.py +91 -0
- aury/agents/tool/builtin/thinking.py +111 -0
- aury/agents/tool/builtin/yield_result.py +130 -0
- aury/agents/tool/decorator.py +252 -0
- aury/agents/tool/set.py +204 -0
- aury/agents/usage/__init__.py +12 -0
- aury/agents/usage/tracker.py +236 -0
- aury/agents/workflow/__init__.py +85 -0
- aury/agents/workflow/adapter.py +268 -0
- aury/agents/workflow/dag.py +116 -0
- aury/agents/workflow/dsl.py +575 -0
- aury/agents/workflow/executor.py +659 -0
- aury/agents/workflow/expression.py +136 -0
- aury/agents/workflow/parser.py +182 -0
- aury/agents/workflow/state.py +145 -0
- aury/agents/workflow/types.py +86 -0
- aury_agent-0.0.4.dist-info/METADATA +90 -0
- aury_agent-0.0.4.dist-info/RECORD +149 -0
- aury_agent-0.0.4.dist-info/WHEEL +4 -0
- aury_agent-0.0.4.dist-info/entry_points.txt +2 -0
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
"""Yield result tool - return control to parent agent.
|
|
2
|
+
|
|
3
|
+
Only available in DELEGATED mode sub-agents.
|
|
4
|
+
Dynamically injected when control is transferred.
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
from ...core.logging import tool_logger as logger
|
|
11
|
+
from ...core.types.tool import BaseTool, ToolContext, ToolResult
|
|
12
|
+
from ...core.types.block import BlockEvent, BlockKind, BlockOp
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class YieldResultTool(BaseTool):
|
|
16
|
+
"""Return control to parent agent with results.
|
|
17
|
+
|
|
18
|
+
This tool is only available to sub-agents running in DELEGATED mode.
|
|
19
|
+
It is dynamically injected when control is transferred via delegate().
|
|
20
|
+
|
|
21
|
+
When called:
|
|
22
|
+
1. Pops current frame from control_stack
|
|
23
|
+
2. Passes result to parent agent
|
|
24
|
+
3. Resumes parent invocation
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
_name = "yield_result"
|
|
28
|
+
|
|
29
|
+
def __init__(self, parent_invocation_id: str):
|
|
30
|
+
"""Initialize with parent context.
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
parent_invocation_id: ID of the parent invocation to return to
|
|
34
|
+
"""
|
|
35
|
+
self.parent_invocation_id = parent_invocation_id
|
|
36
|
+
|
|
37
|
+
@property
|
|
38
|
+
def name(self) -> str:
|
|
39
|
+
return self._name
|
|
40
|
+
|
|
41
|
+
@property
|
|
42
|
+
def description(self) -> str:
|
|
43
|
+
return """Return control to the parent agent with task results.
|
|
44
|
+
|
|
45
|
+
Use this when the delegated task is complete and you want to
|
|
46
|
+
return the results to the agent that delegated to you.
|
|
47
|
+
|
|
48
|
+
This will end your current session and resume the parent."""
|
|
49
|
+
|
|
50
|
+
@property
|
|
51
|
+
def parameters(self) -> dict[str, Any]:
|
|
52
|
+
return {
|
|
53
|
+
"type": "object",
|
|
54
|
+
"properties": {
|
|
55
|
+
"result": {
|
|
56
|
+
"type": "string",
|
|
57
|
+
"description": "Summary of what was accomplished",
|
|
58
|
+
},
|
|
59
|
+
"data": {
|
|
60
|
+
"type": "object",
|
|
61
|
+
"description": "Structured data to pass back to parent",
|
|
62
|
+
},
|
|
63
|
+
"status": {
|
|
64
|
+
"type": "string",
|
|
65
|
+
"enum": ["completed", "failed", "cancelled"],
|
|
66
|
+
"description": "Status of the delegated task",
|
|
67
|
+
"default": "completed",
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
"required": ["result"],
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async def execute(
|
|
74
|
+
self,
|
|
75
|
+
params: dict[str, Any],
|
|
76
|
+
ctx: ToolContext,
|
|
77
|
+
) -> ToolResult:
|
|
78
|
+
"""Execute yield - return to parent."""
|
|
79
|
+
result_text = params.get("result", "")
|
|
80
|
+
data = params.get("data", {})
|
|
81
|
+
status = params.get("status", "completed")
|
|
82
|
+
|
|
83
|
+
logger.info(
|
|
84
|
+
"Yielding result to parent",
|
|
85
|
+
extra={
|
|
86
|
+
"parent_invocation_id": self.parent_invocation_id,
|
|
87
|
+
"status": status,
|
|
88
|
+
},
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
# Emit YIELD block
|
|
92
|
+
await self._emit_yield_block(ctx, result_text, data, status)
|
|
93
|
+
|
|
94
|
+
# Note: Real implementation would:
|
|
95
|
+
# 1. Pop current frame from session.control_stack
|
|
96
|
+
# 2. Serialize result for parent
|
|
97
|
+
# 3. Update state to trigger parent resumption
|
|
98
|
+
# 4. End current invocation
|
|
99
|
+
|
|
100
|
+
return ToolResult(output=f"Returning to parent agent with status: {status}\nResult: {result_text}")
|
|
101
|
+
|
|
102
|
+
async def _emit_yield_block(
|
|
103
|
+
self,
|
|
104
|
+
ctx: ToolContext,
|
|
105
|
+
result: str,
|
|
106
|
+
data: dict[str, Any],
|
|
107
|
+
status: str,
|
|
108
|
+
) -> None:
|
|
109
|
+
"""Emit YIELD block."""
|
|
110
|
+
emit = getattr(ctx, 'emit', None)
|
|
111
|
+
if emit is None:
|
|
112
|
+
return
|
|
113
|
+
|
|
114
|
+
block = BlockEvent(
|
|
115
|
+
kind=BlockKind.YIELD,
|
|
116
|
+
op=BlockOp.APPLY,
|
|
117
|
+
data={
|
|
118
|
+
"result": result,
|
|
119
|
+
"data": data,
|
|
120
|
+
"status": status,
|
|
121
|
+
"parent_invocation_id": self.parent_invocation_id,
|
|
122
|
+
},
|
|
123
|
+
session_id=ctx.session_id,
|
|
124
|
+
invocation_id=ctx.invocation_id,
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
await emit(block)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
__all__ = ["YieldResultTool"]
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
"""Tool decorators for creating tools from functions."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import asyncio
|
|
5
|
+
import inspect
|
|
6
|
+
from functools import wraps
|
|
7
|
+
from typing import Any, Callable, get_type_hints, get_origin, get_args, Union
|
|
8
|
+
|
|
9
|
+
from ..core.types.tool import BaseTool, ToolContext, ToolResult, ToolInfo
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _type_to_schema(t: type) -> dict[str, Any]:
|
|
13
|
+
"""Convert Python type to JSON Schema."""
|
|
14
|
+
# Handle None/NoneType
|
|
15
|
+
if t is type(None):
|
|
16
|
+
return {"type": "null"}
|
|
17
|
+
|
|
18
|
+
# Handle Optional (Union with None)
|
|
19
|
+
origin = get_origin(t)
|
|
20
|
+
if origin is Union:
|
|
21
|
+
args = get_args(t)
|
|
22
|
+
# Filter out None
|
|
23
|
+
non_none = [a for a in args if a is not type(None)]
|
|
24
|
+
if len(non_none) == 1:
|
|
25
|
+
# Optional[X] -> X with nullable
|
|
26
|
+
schema = _type_to_schema(non_none[0])
|
|
27
|
+
return schema
|
|
28
|
+
# Union of multiple types
|
|
29
|
+
return {"oneOf": [_type_to_schema(a) for a in non_none]}
|
|
30
|
+
|
|
31
|
+
# Handle list
|
|
32
|
+
if origin is list:
|
|
33
|
+
args = get_args(t)
|
|
34
|
+
if args:
|
|
35
|
+
return {"type": "array", "items": _type_to_schema(args[0])}
|
|
36
|
+
return {"type": "array"}
|
|
37
|
+
|
|
38
|
+
# Handle dict
|
|
39
|
+
if origin is dict:
|
|
40
|
+
args = get_args(t)
|
|
41
|
+
if len(args) == 2:
|
|
42
|
+
return {
|
|
43
|
+
"type": "object",
|
|
44
|
+
"additionalProperties": _type_to_schema(args[1]),
|
|
45
|
+
}
|
|
46
|
+
return {"type": "object"}
|
|
47
|
+
|
|
48
|
+
# Basic types
|
|
49
|
+
type_map = {
|
|
50
|
+
str: {"type": "string"},
|
|
51
|
+
int: {"type": "integer"},
|
|
52
|
+
float: {"type": "number"},
|
|
53
|
+
bool: {"type": "boolean"},
|
|
54
|
+
list: {"type": "array"},
|
|
55
|
+
dict: {"type": "object"},
|
|
56
|
+
Any: {},
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return type_map.get(t, {"type": "string"})
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _parse_docstring(docstring: str | None) -> tuple[str, dict[str, str]]:
|
|
63
|
+
"""Parse docstring to extract description and param descriptions.
|
|
64
|
+
|
|
65
|
+
Returns:
|
|
66
|
+
Tuple of (main description, {param_name: param_description})
|
|
67
|
+
"""
|
|
68
|
+
if not docstring:
|
|
69
|
+
return "", {}
|
|
70
|
+
|
|
71
|
+
lines = docstring.strip().split("\n")
|
|
72
|
+
description_lines = []
|
|
73
|
+
param_docs: dict[str, str] = {}
|
|
74
|
+
|
|
75
|
+
in_params = False
|
|
76
|
+
current_param = None
|
|
77
|
+
|
|
78
|
+
for line in lines:
|
|
79
|
+
stripped = line.strip()
|
|
80
|
+
|
|
81
|
+
# Check for param section
|
|
82
|
+
if stripped.lower().startswith(("args:", "arguments:", "parameters:", "params:")):
|
|
83
|
+
in_params = True
|
|
84
|
+
continue
|
|
85
|
+
|
|
86
|
+
if stripped.lower().startswith(("returns:", "return:", "raises:", "yields:")):
|
|
87
|
+
in_params = False
|
|
88
|
+
current_param = None
|
|
89
|
+
continue
|
|
90
|
+
|
|
91
|
+
if in_params:
|
|
92
|
+
# Check for param definition: "param_name: description" or "param_name (type): description"
|
|
93
|
+
if ":" in stripped and not stripped.startswith(" "):
|
|
94
|
+
parts = stripped.split(":", 1)
|
|
95
|
+
param_name = parts[0].strip()
|
|
96
|
+
# Remove type annotation if present
|
|
97
|
+
if "(" in param_name:
|
|
98
|
+
param_name = param_name.split("(")[0].strip()
|
|
99
|
+
param_docs[param_name] = parts[1].strip() if len(parts) > 1 else ""
|
|
100
|
+
current_param = param_name
|
|
101
|
+
elif current_param and stripped:
|
|
102
|
+
# Continuation of previous param description
|
|
103
|
+
param_docs[current_param] += " " + stripped
|
|
104
|
+
else:
|
|
105
|
+
description_lines.append(stripped)
|
|
106
|
+
|
|
107
|
+
description = " ".join(description_lines).strip()
|
|
108
|
+
return description, param_docs
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def tool(
|
|
112
|
+
func: Callable | None = None,
|
|
113
|
+
*,
|
|
114
|
+
name: str | None = None,
|
|
115
|
+
description: str | None = None,
|
|
116
|
+
parameters: dict[str, Any] | None = None,
|
|
117
|
+
) -> Callable[[Callable], BaseTool] | BaseTool:
|
|
118
|
+
"""Decorator to create a Tool from a function.
|
|
119
|
+
|
|
120
|
+
The function can be sync or async. Parameters are automatically converted
|
|
121
|
+
to JSON Schema based on type hints, unless manually specified.
|
|
122
|
+
|
|
123
|
+
Args:
|
|
124
|
+
name: Tool name (defaults to function name)
|
|
125
|
+
description: Tool description (defaults to docstring)
|
|
126
|
+
parameters: Manual JSON Schema for parameters (optional)
|
|
127
|
+
|
|
128
|
+
Returns:
|
|
129
|
+
Decorated function as a Tool
|
|
130
|
+
|
|
131
|
+
Example:
|
|
132
|
+
@tool
|
|
133
|
+
def greet(name: str) -> str:
|
|
134
|
+
return f"Hello, {name}!"
|
|
135
|
+
|
|
136
|
+
@tool(name="calculator", description="Perform calculations")
|
|
137
|
+
def calculate(expression: str) -> str:
|
|
138
|
+
return str(eval(expression))
|
|
139
|
+
|
|
140
|
+
@tool(parameters={"type": "object", "properties": {...}})
|
|
141
|
+
def custom_tool(arg: str) -> str:
|
|
142
|
+
return arg
|
|
143
|
+
"""
|
|
144
|
+
def decorator(fn: Callable) -> BaseTool:
|
|
145
|
+
tool_name = name or fn.__name__
|
|
146
|
+
|
|
147
|
+
# Parse docstring for descriptions
|
|
148
|
+
doc_desc, param_docs = _parse_docstring(fn.__doc__)
|
|
149
|
+
tool_desc = description or doc_desc or f"Tool: {tool_name}"
|
|
150
|
+
|
|
151
|
+
# Use manual parameters or auto-generate
|
|
152
|
+
if parameters is not None:
|
|
153
|
+
parameters_schema = parameters
|
|
154
|
+
else:
|
|
155
|
+
# Get type hints and signature
|
|
156
|
+
try:
|
|
157
|
+
hints = get_type_hints(fn)
|
|
158
|
+
except Exception:
|
|
159
|
+
hints = {}
|
|
160
|
+
|
|
161
|
+
sig = inspect.signature(fn)
|
|
162
|
+
|
|
163
|
+
# Build parameters schema
|
|
164
|
+
properties: dict[str, Any] = {}
|
|
165
|
+
required: list[str] = []
|
|
166
|
+
|
|
167
|
+
for param_name, param in sig.parameters.items():
|
|
168
|
+
# Skip special parameters
|
|
169
|
+
if param_name in ("self", "cls", "ctx", "context"):
|
|
170
|
+
continue
|
|
171
|
+
|
|
172
|
+
# Get type
|
|
173
|
+
param_type = hints.get(param_name, str)
|
|
174
|
+
schema = _type_to_schema(param_type)
|
|
175
|
+
|
|
176
|
+
# Add description from docstring
|
|
177
|
+
if param_name in param_docs:
|
|
178
|
+
schema["description"] = param_docs[param_name]
|
|
179
|
+
|
|
180
|
+
properties[param_name] = schema
|
|
181
|
+
|
|
182
|
+
# Check if required
|
|
183
|
+
if param.default is inspect.Parameter.empty:
|
|
184
|
+
required.append(param_name)
|
|
185
|
+
|
|
186
|
+
parameters_schema = {
|
|
187
|
+
"type": "object",
|
|
188
|
+
"properties": properties,
|
|
189
|
+
"required": required,
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
sig = inspect.signature(fn)
|
|
193
|
+
|
|
194
|
+
# Check if function accepts ctx
|
|
195
|
+
accepts_ctx = "ctx" in sig.parameters or "context" in sig.parameters
|
|
196
|
+
ctx_param_name = "ctx" if "ctx" in sig.parameters else "context"
|
|
197
|
+
|
|
198
|
+
class FunctionTool:
|
|
199
|
+
"""Tool created from decorated function."""
|
|
200
|
+
|
|
201
|
+
@property
|
|
202
|
+
def name(self) -> str:
|
|
203
|
+
return tool_name
|
|
204
|
+
|
|
205
|
+
@property
|
|
206
|
+
def description(self) -> str:
|
|
207
|
+
return tool_desc
|
|
208
|
+
|
|
209
|
+
@property
|
|
210
|
+
def parameters(self) -> dict[str, Any]:
|
|
211
|
+
return parameters_schema
|
|
212
|
+
|
|
213
|
+
async def execute(
|
|
214
|
+
self,
|
|
215
|
+
params: dict[str, Any],
|
|
216
|
+
ctx: ToolContext,
|
|
217
|
+
) -> ToolResult:
|
|
218
|
+
"""Execute the wrapped function."""
|
|
219
|
+
try:
|
|
220
|
+
# Add context if function accepts it
|
|
221
|
+
if accepts_ctx:
|
|
222
|
+
params = {**params, ctx_param_name: ctx}
|
|
223
|
+
|
|
224
|
+
# Call function
|
|
225
|
+
result = fn(**params)
|
|
226
|
+
|
|
227
|
+
# Await if coroutine
|
|
228
|
+
if asyncio.iscoroutine(result):
|
|
229
|
+
result = await result
|
|
230
|
+
|
|
231
|
+
# Convert result to ToolResult
|
|
232
|
+
if isinstance(result, ToolResult):
|
|
233
|
+
return result
|
|
234
|
+
|
|
235
|
+
return ToolResult(output=str(result))
|
|
236
|
+
|
|
237
|
+
except Exception as e:
|
|
238
|
+
return ToolResult.error(str(e))
|
|
239
|
+
|
|
240
|
+
def get_info(self) -> ToolInfo:
|
|
241
|
+
return ToolInfo(
|
|
242
|
+
name=tool_name,
|
|
243
|
+
description=tool_desc,
|
|
244
|
+
parameters=parameters_schema,
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
return FunctionTool()
|
|
248
|
+
|
|
249
|
+
# Support both @tool and @tool(...) usage
|
|
250
|
+
if func is not None:
|
|
251
|
+
return decorator(func)
|
|
252
|
+
return decorator
|
aury/agents/tool/set.py
ADDED
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
"""ToolSet - A collection of tools."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from typing import Any, Callable
|
|
5
|
+
|
|
6
|
+
from ..core.types.tool import BaseTool, ToolInfo
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class ToolSet:
|
|
10
|
+
"""A collection of tools with lookup, filtering, and merging capabilities.
|
|
11
|
+
|
|
12
|
+
ToolSet provides:
|
|
13
|
+
- Fast lookup by tool ID
|
|
14
|
+
- Lazy instantiation with caching
|
|
15
|
+
- Filtering (include/exclude)
|
|
16
|
+
- Merging multiple sets
|
|
17
|
+
|
|
18
|
+
Usage:
|
|
19
|
+
# From list
|
|
20
|
+
tools = ToolSet.from_list([tool1, tool2])
|
|
21
|
+
|
|
22
|
+
# Manual construction
|
|
23
|
+
tools = ToolSet()
|
|
24
|
+
tools.add(my_tool)
|
|
25
|
+
|
|
26
|
+
# Filtering
|
|
27
|
+
subset = tools.filtered(include=["read_file", "write_file"])
|
|
28
|
+
|
|
29
|
+
# Merging
|
|
30
|
+
combined = tools.merge(other_tools)
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
def __init__(self) -> None:
|
|
34
|
+
self._tools: dict[str, Callable[[], BaseTool]] = {}
|
|
35
|
+
self._instances: dict[str, BaseTool] = {} # Cached instances
|
|
36
|
+
|
|
37
|
+
@classmethod
|
|
38
|
+
def from_list(cls, tools: list[BaseTool]) -> "ToolSet":
|
|
39
|
+
"""Create ToolSet from a list of tools.
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
tools: List of tool instances
|
|
43
|
+
|
|
44
|
+
Returns:
|
|
45
|
+
New ToolSet containing all tools
|
|
46
|
+
"""
|
|
47
|
+
ts = cls()
|
|
48
|
+
for tool in tools:
|
|
49
|
+
ts.add(tool)
|
|
50
|
+
return ts
|
|
51
|
+
|
|
52
|
+
def add(
|
|
53
|
+
self,
|
|
54
|
+
tool: BaseTool | Callable[[], BaseTool],
|
|
55
|
+
replace: bool = False,
|
|
56
|
+
) -> None:
|
|
57
|
+
"""Add a tool to the set.
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
tool: Tool instance or factory function
|
|
61
|
+
replace: Allow replacing existing tool
|
|
62
|
+
"""
|
|
63
|
+
# If it's a BaseTool instance, wrap it in a lambda
|
|
64
|
+
if isinstance(tool, BaseTool):
|
|
65
|
+
tool_name = tool.name
|
|
66
|
+
factory: Callable[[], BaseTool] = lambda t=tool: t
|
|
67
|
+
else:
|
|
68
|
+
# It's a factory, call it once to get the name
|
|
69
|
+
instance = tool()
|
|
70
|
+
tool_name = instance.name
|
|
71
|
+
factory = tool
|
|
72
|
+
|
|
73
|
+
if tool_name in self._tools and not replace:
|
|
74
|
+
raise ValueError(f"Tool already registered: {tool_name}")
|
|
75
|
+
|
|
76
|
+
self._tools[tool_name] = factory
|
|
77
|
+
# Clear cached instance
|
|
78
|
+
if tool_name in self._instances:
|
|
79
|
+
del self._instances[tool_name]
|
|
80
|
+
|
|
81
|
+
def remove(self, tool_id: str) -> bool:
|
|
82
|
+
"""Remove a tool from the set.
|
|
83
|
+
|
|
84
|
+
Args:
|
|
85
|
+
tool_id: Tool identifier to remove
|
|
86
|
+
|
|
87
|
+
Returns:
|
|
88
|
+
True if tool was removed, False if not found
|
|
89
|
+
"""
|
|
90
|
+
if tool_id in self._tools:
|
|
91
|
+
del self._tools[tool_id]
|
|
92
|
+
if tool_id in self._instances:
|
|
93
|
+
del self._instances[tool_id]
|
|
94
|
+
return True
|
|
95
|
+
return False
|
|
96
|
+
|
|
97
|
+
def get(self, tool_id: str, cached: bool = True) -> BaseTool:
|
|
98
|
+
"""Get a tool instance.
|
|
99
|
+
|
|
100
|
+
Args:
|
|
101
|
+
tool_id: Tool identifier
|
|
102
|
+
cached: Whether to use cached instance
|
|
103
|
+
|
|
104
|
+
Returns:
|
|
105
|
+
BaseTool instance
|
|
106
|
+
|
|
107
|
+
Raises:
|
|
108
|
+
KeyError: If tool not found
|
|
109
|
+
"""
|
|
110
|
+
if tool_id not in self._tools:
|
|
111
|
+
raise KeyError(f"Tool not found: {tool_id}")
|
|
112
|
+
|
|
113
|
+
if cached and tool_id in self._instances:
|
|
114
|
+
return self._instances[tool_id]
|
|
115
|
+
|
|
116
|
+
instance = self._tools[tool_id]()
|
|
117
|
+
if cached:
|
|
118
|
+
self._instances[tool_id] = instance
|
|
119
|
+
|
|
120
|
+
return instance
|
|
121
|
+
|
|
122
|
+
def has(self, tool_id: str) -> bool:
|
|
123
|
+
"""Check if tool is registered."""
|
|
124
|
+
return tool_id in self._tools
|
|
125
|
+
|
|
126
|
+
def all(self, cached: bool = True) -> list[BaseTool]:
|
|
127
|
+
"""Get all registered tools.
|
|
128
|
+
|
|
129
|
+
Args:
|
|
130
|
+
cached: Whether to use cached instances
|
|
131
|
+
"""
|
|
132
|
+
return [self.get(tid, cached=cached) for tid in self._tools]
|
|
133
|
+
|
|
134
|
+
def all_info(self) -> list[ToolInfo]:
|
|
135
|
+
"""Get info for all registered tools."""
|
|
136
|
+
return [tool.get_info() for tool in self.all()]
|
|
137
|
+
|
|
138
|
+
def ids(self) -> list[str]:
|
|
139
|
+
"""Get all registered tool IDs."""
|
|
140
|
+
return list(self._tools.keys())
|
|
141
|
+
|
|
142
|
+
def filtered(
|
|
143
|
+
self,
|
|
144
|
+
include: list[str] | None = None,
|
|
145
|
+
exclude: list[str] | None = None,
|
|
146
|
+
) -> "ToolSet":
|
|
147
|
+
"""Create a filtered copy of this set.
|
|
148
|
+
|
|
149
|
+
Args:
|
|
150
|
+
include: Only include these tools (None = all)
|
|
151
|
+
exclude: Exclude these tools
|
|
152
|
+
|
|
153
|
+
Returns:
|
|
154
|
+
New ToolSet with filtered tools
|
|
155
|
+
"""
|
|
156
|
+
filtered = ToolSet()
|
|
157
|
+
|
|
158
|
+
for tool_id, factory in self._tools.items():
|
|
159
|
+
# Check include filter
|
|
160
|
+
if include is not None and tool_id not in include:
|
|
161
|
+
continue
|
|
162
|
+
|
|
163
|
+
# Check exclude filter
|
|
164
|
+
if exclude is not None and tool_id in exclude:
|
|
165
|
+
continue
|
|
166
|
+
|
|
167
|
+
filtered._tools[tool_id] = factory
|
|
168
|
+
|
|
169
|
+
return filtered
|
|
170
|
+
|
|
171
|
+
def merge(self, other: "ToolSet", replace: bool = False) -> "ToolSet":
|
|
172
|
+
"""Merge another ToolSet into this one.
|
|
173
|
+
|
|
174
|
+
Args:
|
|
175
|
+
other: ToolSet to merge
|
|
176
|
+
replace: Allow replacing existing tools
|
|
177
|
+
|
|
178
|
+
Returns:
|
|
179
|
+
Self for chaining
|
|
180
|
+
"""
|
|
181
|
+
for tool_id, factory in other._tools.items():
|
|
182
|
+
if tool_id not in self._tools or replace:
|
|
183
|
+
self._tools[tool_id] = factory
|
|
184
|
+
return self
|
|
185
|
+
|
|
186
|
+
def copy(self) -> "ToolSet":
|
|
187
|
+
"""Create a copy of this set."""
|
|
188
|
+
new = ToolSet()
|
|
189
|
+
new._tools = dict(self._tools)
|
|
190
|
+
return new
|
|
191
|
+
|
|
192
|
+
def clear(self) -> None:
|
|
193
|
+
"""Clear all tools."""
|
|
194
|
+
self._tools.clear()
|
|
195
|
+
self._instances.clear()
|
|
196
|
+
|
|
197
|
+
def __len__(self) -> int:
|
|
198
|
+
return len(self._tools)
|
|
199
|
+
|
|
200
|
+
def __contains__(self, tool_id: str) -> bool:
|
|
201
|
+
return tool_id in self._tools
|
|
202
|
+
|
|
203
|
+
def __iter__(self):
|
|
204
|
+
return iter(self.all())
|