agentify-core 0.1.2__tar.gz → 0.1.4__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.
- {agentify_core-0.1.2/agentify_core.egg-info → agentify_core-0.1.4}/PKG-INFO +14 -17
- {agentify_core-0.1.2 → agentify_core-0.1.4}/README.md +13 -3
- agentify_core-0.1.4/README_PYPI.md +104 -0
- {agentify_core-0.1.2 → agentify_core-0.1.4}/agentify/__init__.py +1 -1
- {agentify_core-0.1.2 → agentify_core-0.1.4}/agentify/core/agent.py +78 -5
- {agentify_core-0.1.2 → agentify_core-0.1.4}/agentify/core/config.py +1 -1
- agentify_core-0.1.4/agentify/extensions/tools/__init__.py +15 -0
- agentify_core-0.1.4/agentify/extensions/tools/calculator.py +55 -0
- agentify_core-0.1.4/agentify/extensions/tools/filesystem.py +126 -0
- agentify_core-0.1.4/agentify/extensions/tools/planning.py +76 -0
- agentify_core-0.1.4/agentify/extensions/tools/time.py +22 -0
- agentify_core-0.1.4/agentify/extensions/tools/weather.py +52 -0
- {agentify_core-0.1.2 → agentify_core-0.1.4}/agentify/memory/interfaces.py +10 -0
- agentify_core-0.1.4/agentify/memory/stores/__init__.py +5 -0
- {agentify_core-0.1.2 → agentify_core-0.1.4}/agentify/multi_agent/hierarchical.py +1 -1
- {agentify_core-0.1.2 → agentify_core-0.1.4}/agentify/multi_agent/pipeline.py +2 -1
- {agentify_core-0.1.2 → agentify_core-0.1.4}/agentify/multi_agent/team.py +1 -1
- {agentify_core-0.1.2 → agentify_core-0.1.4}/agentify/multi_agent/tool_wrapper.py +86 -1
- {agentify_core-0.1.2 → agentify_core-0.1.4/agentify_core.egg-info}/PKG-INFO +14 -17
- {agentify_core-0.1.2 → agentify_core-0.1.4}/agentify_core.egg-info/SOURCES.txt +6 -1
- {agentify_core-0.1.2 → agentify_core-0.1.4}/pyproject.toml +5 -5
- agentify_core-0.1.4/tests/test_filesystem_tools.py +62 -0
- agentify_core-0.1.4/tests/test_planning_tool.py +55 -0
- agentify_core-0.1.2/agentify/extensions/tools/__init__.py +0 -9
- agentify_core-0.1.2/agentify/extensions/tools/calculator.py +0 -56
- agentify_core-0.1.2/agentify/extensions/tools/time.py +0 -21
- agentify_core-0.1.2/agentify/extensions/tools/weather.py +0 -51
- agentify_core-0.1.2/agentify/memory/stores/__init__.py +0 -6
- {agentify_core-0.1.2 → agentify_core-0.1.4}/LICENSE +0 -0
- {agentify_core-0.1.2 → agentify_core-0.1.4}/MANIFEST.in +0 -0
- {agentify_core-0.1.2 → agentify_core-0.1.4}/agentify/core/__init__.py +0 -0
- {agentify_core-0.1.2 → agentify_core-0.1.4}/agentify/core/callbacks.py +0 -0
- {agentify_core-0.1.2 → agentify_core-0.1.4}/agentify/core/tool.py +0 -0
- {agentify_core-0.1.2 → agentify_core-0.1.4}/agentify/extensions/__init__.py +0 -0
- {agentify_core-0.1.2 → agentify_core-0.1.4}/agentify/extensions/prompts/__init__.py +0 -0
- {agentify_core-0.1.2 → agentify_core-0.1.4}/agentify/extensions/prompts/assistant.py +0 -0
- {agentify_core-0.1.2 → agentify_core-0.1.4}/agentify/llm/__init__.py +0 -0
- {agentify_core-0.1.2 → agentify_core-0.1.4}/agentify/llm/client.py +0 -0
- {agentify_core-0.1.2 → agentify_core-0.1.4}/agentify/memory/__init__.py +0 -0
- {agentify_core-0.1.2 → agentify_core-0.1.4}/agentify/memory/policies.py +0 -0
- {agentify_core-0.1.2 → agentify_core-0.1.4}/agentify/memory/service.py +0 -0
- {agentify_core-0.1.2 → agentify_core-0.1.4}/agentify/memory/stores/in_memory_store.py +0 -0
- {agentify_core-0.1.2 → agentify_core-0.1.4}/agentify/memory/stores/redis_store.py +0 -0
- {agentify_core-0.1.2 → agentify_core-0.1.4}/agentify/multi_agent/__init__.py +0 -0
- {agentify_core-0.1.2 → agentify_core-0.1.4}/agentify/utils/__init__.py +0 -0
- {agentify_core-0.1.2 → agentify_core-0.1.4}/agentify/utils/style.py +0 -0
- {agentify_core-0.1.2 → agentify_core-0.1.4}/agentify_core.egg-info/dependency_links.txt +0 -0
- {agentify_core-0.1.2 → agentify_core-0.1.4}/agentify_core.egg-info/requires.txt +0 -0
- {agentify_core-0.1.2 → agentify_core-0.1.4}/agentify_core.egg-info/top_level.txt +0 -0
- {agentify_core-0.1.2 → agentify_core-0.1.4}/requirements.txt +0 -0
- {agentify_core-0.1.2 → agentify_core-0.1.4}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: agentify-core
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.4
|
|
4
4
|
Summary: Framework-agnostic AI agent library for building single and multi-agent systems
|
|
5
5
|
Author-email: Fabian M <fabian@example.com>
|
|
6
6
|
License: MIT
|
|
@@ -19,7 +19,7 @@ Classifier: Programming Language :: Python :: 3.11
|
|
|
19
19
|
Classifier: Programming Language :: Python :: 3.12
|
|
20
20
|
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
21
21
|
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
22
|
-
Requires-Python: >=3.
|
|
22
|
+
Requires-Python: >=3.10
|
|
23
23
|
Description-Content-Type: text/markdown
|
|
24
24
|
License-File: LICENSE
|
|
25
25
|
Requires-Dist: openai>=1.0.0
|
|
@@ -47,9 +47,10 @@ Dynamic: license-file
|
|
|
47
47
|
|
|
48
48
|
# Agentify
|
|
49
49
|
|
|
50
|
-
**
|
|
50
|
+
**Independent AI agent library based on the OpenAI SDK**
|
|
51
|
+
|
|
52
|
+
Agentify is a Python library for building and orchestrating AI agents, from simple assistants to complex multi-agent systems. It targets the OpenAI-compatible Chat Completions interface, enabling support for multiple providers through a configurable `base_url` (OpenAI, Azure OpenAI, DeepSeek, Gemini, etc.). Agentify offers a streamlined, independent set of primitives for memory, tools, and coordination so you can focus on product logic without being tied to heavy frameworks.
|
|
51
53
|
|
|
52
|
-
Agentify is a Python library for building and orchestrating AI agents, from simple assistants to complex multi-agent systems. It focuses on a small set of composable primitives for LLM integration, memory, tools and coordination, so you can focus on product logic instead of framework details.
|
|
53
54
|
|
|
54
55
|
## Why Agentify?
|
|
55
56
|
|
|
@@ -116,7 +117,7 @@ agent = BaseAgent(
|
|
|
116
117
|
)
|
|
117
118
|
|
|
118
119
|
# 3. Run a conversation
|
|
119
|
-
response = agent.
|
|
120
|
+
response = agent.run(user_input="Hello! How can you help me?")
|
|
120
121
|
```
|
|
121
122
|
|
|
122
123
|
## Composable Flows
|
|
@@ -133,22 +134,18 @@ Because all flows share the same `run()` interface, you can build Teams made of
|
|
|
133
134
|
Agentify supports both **strict workflows** (fixed, pre-defined Pipelines and Hierarchies) and **dynamic agentic flows**, where a supervisor/router agent decides at runtime which agent, Team or Pipeline to call next.
|
|
134
135
|
|
|
135
136
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
Check out the [examples](examples/) directory for detailed implementations:
|
|
137
|
+
## Learn More
|
|
139
138
|
|
|
140
|
-
|
|
141
|
-
* [Multi-Agent Teams](examples/multi_agent/team/)
|
|
142
|
-
* [Sequential Pipelines](examples/multi_agent/pipeline/)
|
|
143
|
-
* [Hierarchical Structures](examples/multi_agent/hierarchical/)
|
|
139
|
+
For detailed documentation, examples, and API reference, visit the [GitHub repository](https://github.com/fa8i/Agentify).
|
|
144
140
|
|
|
141
|
+
## Contributing
|
|
145
142
|
|
|
146
|
-
|
|
143
|
+
Contributions are welcome! Please visit the [repository](https://github.com/fa8i/Agentify) to report issues or submit pull requests.
|
|
147
144
|
|
|
148
|
-
|
|
145
|
+
## License
|
|
149
146
|
|
|
147
|
+
MIT License - see the repository for details.
|
|
150
148
|
|
|
151
|
-
##
|
|
149
|
+
## Author
|
|
152
150
|
|
|
153
|
-
|
|
154
|
-
- **Issues**: https://github.com/fa8i/Agentify/issues
|
|
151
|
+
**Fabian Melchor** - [fabianmp_98@hotmail.com](mailto:fabianmp_98@hotmail.com)
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
# Agentify
|
|
2
2
|
|
|
3
|
-
**
|
|
3
|
+
**Independent AI agent library based on the OpenAI SDK**
|
|
4
|
+
|
|
5
|
+
Agentify is a Python library for building and orchestrating AI agents, from simple assistants to complex multi-agent systems. It targets the OpenAI-compatible Chat Completions interface, enabling support for multiple providers through a configurable `base_url` (OpenAI, Azure OpenAI, DeepSeek, Gemini, etc.). Agentify offers a streamlined, independent set of primitives for memory, tools, and coordination so you can focus on product logic without being tied to heavy frameworks.
|
|
4
6
|
|
|
5
|
-
Agentify is a Python library for building and orchestrating AI agents, from simple assistants to complex multi-agent systems. It focuses on a small set of composable primitives for LLM integration, memory, tools and coordination, so you can focus on product logic instead of framework details.
|
|
6
7
|
|
|
7
8
|
## Why Agentify?
|
|
8
9
|
|
|
@@ -69,7 +70,7 @@ agent = BaseAgent(
|
|
|
69
70
|
)
|
|
70
71
|
|
|
71
72
|
# 3. Run a conversation
|
|
72
|
-
response = agent.
|
|
73
|
+
response = agent.run(user_input="Hello! How can you help me?")
|
|
73
74
|
```
|
|
74
75
|
|
|
75
76
|
## Composable Flows
|
|
@@ -86,6 +87,15 @@ Because all flows share the same `run()` interface, you can build Teams made of
|
|
|
86
87
|
Agentify supports both **strict workflows** (fixed, pre-defined Pipelines and Hierarchies) and **dynamic agentic flows**, where a supervisor/router agent decides at runtime which agent, Team or Pipeline to call next.
|
|
87
88
|
|
|
88
89
|
|
|
90
|
+
## Documentation
|
|
91
|
+
|
|
92
|
+
- [Getting Started](docs/getting_started.md) - Installation and first steps
|
|
93
|
+
- [Core Concepts](docs/core_concepts.md) - Agents, memory, and tools
|
|
94
|
+
- [Multi-Agent Systems](docs/multi_agent.md) - Teams, pipelines, and hierarchies
|
|
95
|
+
- [Advanced Features](docs/advanced.md) - Vision, streaming, hooks, and more
|
|
96
|
+
- [API Reference](docs/api_reference.md) - Complete API documentation
|
|
97
|
+
|
|
98
|
+
|
|
89
99
|
### More Examples
|
|
90
100
|
|
|
91
101
|
Check out the [examples](examples/) directory for detailed implementations:
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# Agentify
|
|
2
|
+
|
|
3
|
+
**Independent AI agent library based on the OpenAI SDK**
|
|
4
|
+
|
|
5
|
+
Agentify is a Python library for building and orchestrating AI agents, from simple assistants to complex multi-agent systems. It targets the OpenAI-compatible Chat Completions interface, enabling support for multiple providers through a configurable `base_url` (OpenAI, Azure OpenAI, DeepSeek, Gemini, etc.). Agentify offers a streamlined, independent set of primitives for memory, tools, and coordination so you can focus on product logic without being tied to heavy frameworks.
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
## Why Agentify?
|
|
9
|
+
|
|
10
|
+
- **Built for production**: clear abstractions, explicit configuration, error handling and extension points that map well to real deployments.
|
|
11
|
+
- **Orchestration-first design**: a uniform `run()` interface for agents, teams, pipelines and hierarchies makes it straightforward to compose and refactor flows.
|
|
12
|
+
- **Providers**: switch between OpenAI, Gemini, Azure OpenAI, DeepSeek, Claude and others without changing your agent code.
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
## Key Features
|
|
16
|
+
|
|
17
|
+
- **Agents and multi-agent patterns**
|
|
18
|
+
Single Agents with tools and memory, supervisor–worker Multi-Agent Teams, Sequential Pipelines where output flows from step to step, Hierarchical Structures for complex delegation, and Dynamic Flows where a controller decides at runtime which sub-agents or teams to invoke.
|
|
19
|
+
|
|
20
|
+
- **Memory service and isolation**
|
|
21
|
+
Pluggable backends (in-memory, Redis, …) with per-use-case policies (TTL, maximum messages, etc.), plus optional memory isolation so each agent can maintain its own conversation history for scalability and privacy.
|
|
22
|
+
|
|
23
|
+
- **Reasoning Models**
|
|
24
|
+
Configure the model's thinking depth, safely merge `model_kwargs`, automatically store
|
|
25
|
+
"Chain of Thought" in conversation history, and log reasoning steps in real-time for visibility.
|
|
26
|
+
|
|
27
|
+
- **Tools and actions**
|
|
28
|
+
Type-annotated tool interface, straightforward registration of custom tools.
|
|
29
|
+
|
|
30
|
+
- **Observability hooks**
|
|
31
|
+
Callback system for logging, monitoring and debugging agent behaviour across complex flows.
|
|
32
|
+
|
|
33
|
+
- **I/O capabilities**
|
|
34
|
+
Streaming support for real-time responses and vision/image models for multimodal interactions.
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
## Installation
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
pip install agentify-core
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
For optional features:
|
|
44
|
+
```bash
|
|
45
|
+
pip install agentify-core[all] # Installs all optional dependencies
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Quick Start
|
|
49
|
+
|
|
50
|
+
```python
|
|
51
|
+
from agentify import BaseAgent, AgentConfig, MemoryService, MemoryAddress
|
|
52
|
+
from agentify.memory.stores import InMemoryStore
|
|
53
|
+
|
|
54
|
+
# 1. Create memory service
|
|
55
|
+
memory = MemoryService(store=InMemoryStore(), log_enabled=True, max_log_length=100)
|
|
56
|
+
addr = MemoryAddress(conversation_id="session_1")
|
|
57
|
+
|
|
58
|
+
# 2. Create an Agent
|
|
59
|
+
agent = BaseAgent(
|
|
60
|
+
config=AgentConfig(
|
|
61
|
+
name="ReasoningAgent",
|
|
62
|
+
system_prompt="You are a helpful assistant.",
|
|
63
|
+
provider="openai",
|
|
64
|
+
model_name="gpt-5",
|
|
65
|
+
reasoning_effort="high", # optional param:"low", "medium", "high"
|
|
66
|
+
model_kwargs={"max_completion_tokens": 5000} # Pass model-specific params
|
|
67
|
+
),
|
|
68
|
+
memory=memory,
|
|
69
|
+
memory_address=addr
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
# 3. Run a conversation
|
|
73
|
+
response = agent.run(user_input="Hello! How can you help me?")
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## Composable Flows
|
|
77
|
+
|
|
78
|
+
Agentify provides powerful primitives that can be combined to build arbitrarily complex systems:
|
|
79
|
+
|
|
80
|
+
* **BaseAgent**: The fundamental unit of work.
|
|
81
|
+
* **Teams**: A group of agents managed by a supervisor.
|
|
82
|
+
* **Pipelines**: A sequence of steps where output passes from one to the next.
|
|
83
|
+
* **Hierarchies**: Tree structures for massive delegation.
|
|
84
|
+
|
|
85
|
+
Because all flows share the same `run()` interface, you can build Teams made of Pipelines, Pipelines made of Teams, and deeply nested Hierarchies.
|
|
86
|
+
|
|
87
|
+
Agentify supports both **strict workflows** (fixed, pre-defined Pipelines and Hierarchies) and **dynamic agentic flows**, where a supervisor/router agent decides at runtime which agent, Team or Pipeline to call next.
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
## Learn More
|
|
91
|
+
|
|
92
|
+
For detailed documentation, examples, and API reference, visit the [GitHub repository](https://github.com/fa8i/Agentify).
|
|
93
|
+
|
|
94
|
+
## Contributing
|
|
95
|
+
|
|
96
|
+
Contributions are welcome! Please visit the [repository](https://github.com/fa8i/Agentify) to report issues or submit pull requests.
|
|
97
|
+
|
|
98
|
+
## License
|
|
99
|
+
|
|
100
|
+
MIT License - see the repository for details.
|
|
101
|
+
|
|
102
|
+
## Author
|
|
103
|
+
|
|
104
|
+
**Fabian Melchor** - [fabianmp_98@hotmail.com](mailto:fabianmp_98@hotmail.com)
|
|
@@ -5,7 +5,8 @@ import time
|
|
|
5
5
|
import uuid
|
|
6
6
|
import base64
|
|
7
7
|
from io import BytesIO
|
|
8
|
-
|
|
8
|
+
import inspect
|
|
9
|
+
from typing import Any, Dict, Generator, List, Optional, Union, Iterator, Callable
|
|
9
10
|
|
|
10
11
|
from PIL import Image
|
|
11
12
|
from openai import RateLimitError
|
|
@@ -21,7 +22,23 @@ logger = logging.getLogger(__name__)
|
|
|
21
22
|
|
|
22
23
|
|
|
23
24
|
class BaseAgent:
|
|
24
|
-
"""
|
|
25
|
+
"""AI Agent core class based on chat completions interface.
|
|
26
|
+
|
|
27
|
+
This class provides a unified interface for interacting with various LLM providers
|
|
28
|
+
that are compatible with the OpenAI SDK format.
|
|
29
|
+
|
|
30
|
+
Attributes:
|
|
31
|
+
pre_hooks (List[Callable]): Functions to execute before the agent loop starts.
|
|
32
|
+
Supported arguments for injection:
|
|
33
|
+
- `agent`: The BaseAgent instance.
|
|
34
|
+
- `user_input`: The user's input string.
|
|
35
|
+
|
|
36
|
+
post_hooks (List[Callable]): Functions to execute after the agent loop finishes.
|
|
37
|
+
Supported arguments for injection:
|
|
38
|
+
- `agent`: The BaseAgent instance.
|
|
39
|
+
- `user_input`: The user's input string.
|
|
40
|
+
- `response`: The final accumulated response string.
|
|
41
|
+
"""
|
|
25
42
|
|
|
26
43
|
def __init__(
|
|
27
44
|
self,
|
|
@@ -32,11 +49,15 @@ class BaseAgent:
|
|
|
32
49
|
client_factory: Optional[LLMClientFactory] = None,
|
|
33
50
|
tools: Optional[List[Tool]] = None,
|
|
34
51
|
image_config: Optional[ImageConfig] = None,
|
|
52
|
+
pre_hooks: Optional[List[Callable]] = None,
|
|
53
|
+
post_hooks: Optional[List[Callable]] = None,
|
|
35
54
|
) -> None:
|
|
36
55
|
self.config = config
|
|
37
56
|
self.memory = memory
|
|
38
57
|
self.memory_address = memory_address
|
|
39
58
|
self.image_config = image_config or ImageConfig()
|
|
59
|
+
self.pre_hooks = pre_hooks or []
|
|
60
|
+
self.post_hooks = post_hooks or []
|
|
40
61
|
|
|
41
62
|
# Decouple callbacks from config to avoid mutation of shared config
|
|
42
63
|
self.callbacks = list(self.config.callbacks) if self.config.callbacks else []
|
|
@@ -196,6 +217,28 @@ class BaseAgent:
|
|
|
196
217
|
for m in messages[1:]:
|
|
197
218
|
self.memory.append_history(a, m)
|
|
198
219
|
|
|
220
|
+
# Hook Execution
|
|
221
|
+
def _execute_hook(self, hook: Callable, **kwargs: Any) -> None:
|
|
222
|
+
"""Execute a hook injecting only the arguments it declares."""
|
|
223
|
+
try:
|
|
224
|
+
sig = inspect.signature(hook)
|
|
225
|
+
# Filter kwargs to only those present in the hook's signature
|
|
226
|
+
# If the hook accepts **kwargs, pass everything
|
|
227
|
+
has_var_keyword = any(
|
|
228
|
+
p.kind == inspect.Parameter.VAR_KEYWORD for p in sig.parameters.values()
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
if has_var_keyword:
|
|
232
|
+
hook_kwargs = kwargs
|
|
233
|
+
else:
|
|
234
|
+
hook_kwargs = {
|
|
235
|
+
k: v for k, v in kwargs.items() if k in sig.parameters
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
hook(**hook_kwargs)
|
|
239
|
+
except Exception as e:
|
|
240
|
+
logger.error(f"Error executing hook '{hook.__name__}': {e}", exc_info=True)
|
|
241
|
+
|
|
199
242
|
# Core Logic
|
|
200
243
|
|
|
201
244
|
def _get_llm_response(
|
|
@@ -518,6 +561,9 @@ class BaseAgent:
|
|
|
518
561
|
for cb in self.callbacks:
|
|
519
562
|
cb.on_agent_start(self.config.name, user_input)
|
|
520
563
|
|
|
564
|
+
for hook in self.pre_hooks:
|
|
565
|
+
self._execute_hook(hook, agent=self, user_input=user_input)
|
|
566
|
+
|
|
521
567
|
user_content = self._build_user_content(
|
|
522
568
|
user_input,
|
|
523
569
|
image_path=image_path,
|
|
@@ -526,7 +572,14 @@ class BaseAgent:
|
|
|
526
572
|
if user_content is not None:
|
|
527
573
|
self.add(role="user", content=user_content, addr=addr)
|
|
528
574
|
|
|
529
|
-
|
|
575
|
+
accumulated_response: List[str] = []
|
|
576
|
+
|
|
577
|
+
iteration_count = 0
|
|
578
|
+
while True:
|
|
579
|
+
if self.config.max_tool_iter is not None and iteration_count >= self.config.max_tool_iter:
|
|
580
|
+
break
|
|
581
|
+
iteration_count += 1
|
|
582
|
+
|
|
530
583
|
response_or_stream = self._get_llm_response(addr=addr)
|
|
531
584
|
|
|
532
585
|
current_turn_content_parts: List[str] = []
|
|
@@ -540,6 +593,7 @@ class BaseAgent:
|
|
|
540
593
|
content_chunk = next(gen)
|
|
541
594
|
yield content_chunk
|
|
542
595
|
current_turn_content_parts.append(content_chunk)
|
|
596
|
+
accumulated_response.append(content_chunk)
|
|
543
597
|
except StopIteration as e:
|
|
544
598
|
assembled_tool_calls, full_reasoning_content = e.value
|
|
545
599
|
else:
|
|
@@ -549,6 +603,7 @@ class BaseAgent:
|
|
|
549
603
|
if content:
|
|
550
604
|
yield content
|
|
551
605
|
current_turn_content_parts.append(content)
|
|
606
|
+
accumulated_response.append(content)
|
|
552
607
|
|
|
553
608
|
# Expand tool calls (fix for some models)
|
|
554
609
|
assembled_tool_calls = self._expand_tool_calls(assembled_tool_calls)
|
|
@@ -602,10 +657,17 @@ class BaseAgent:
|
|
|
602
657
|
for cb in self.callbacks:
|
|
603
658
|
cb.on_agent_finish(self.config.name, warn_msg)
|
|
604
659
|
yield warn_msg
|
|
660
|
+
accumulated_response.append(warn_msg)
|
|
661
|
+
|
|
662
|
+
full_response = "".join(accumulated_response)
|
|
663
|
+
for hook in self.post_hooks:
|
|
664
|
+
self._execute_hook(
|
|
665
|
+
hook, agent=self, user_input=user_input, response=full_response
|
|
666
|
+
)
|
|
605
667
|
|
|
606
668
|
# Public entrypoint
|
|
607
669
|
|
|
608
|
-
def
|
|
670
|
+
def run(
|
|
609
671
|
self,
|
|
610
672
|
user_input: str,
|
|
611
673
|
*,
|
|
@@ -613,7 +675,17 @@ class BaseAgent:
|
|
|
613
675
|
image_path: Optional[str] = None,
|
|
614
676
|
image_detail_override: Optional[str] = None,
|
|
615
677
|
) -> Union[str, Generator[str, None, None]]:
|
|
616
|
-
"""Main entrypoint to interact with the agent.
|
|
678
|
+
"""Main entrypoint to interact with the agent.
|
|
679
|
+
|
|
680
|
+
Args:
|
|
681
|
+
user_input: The text input from the user.
|
|
682
|
+
addr: The memory address for the conversation.
|
|
683
|
+
image_path: Optional path to an image file.
|
|
684
|
+
image_detail_override: Optional detail level for image processing.
|
|
685
|
+
|
|
686
|
+
Returns:
|
|
687
|
+
The agent's response as a string or a generator if streaming is enabled.
|
|
688
|
+
"""
|
|
617
689
|
a = self._addr_or_raise(addr)
|
|
618
690
|
response_generator = self._execute_agent_loop(
|
|
619
691
|
user_input,
|
|
@@ -628,6 +700,7 @@ class BaseAgent:
|
|
|
628
700
|
parts: List[str] = list(response_generator)
|
|
629
701
|
return "".join(parts).strip()
|
|
630
702
|
|
|
703
|
+
|
|
631
704
|
# Tool registry management
|
|
632
705
|
|
|
633
706
|
def tool_exists(self, name: str) -> bool:
|
|
@@ -21,7 +21,7 @@ class AgentConfig:
|
|
|
21
21
|
timeout: int = 60
|
|
22
22
|
stream: bool = False
|
|
23
23
|
max_retries: int = 3
|
|
24
|
-
max_tool_iter: int =
|
|
24
|
+
max_tool_iter: Optional[int] = 10
|
|
25
25
|
reasoning_effort: Optional[str] = None
|
|
26
26
|
model_kwargs: Optional[Dict[str, Any]] = None
|
|
27
27
|
client_config_override: Optional[Dict[str, Any]] = None
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from agentify.extensions.tools.time import TimeTool
|
|
2
|
+
from agentify.extensions.tools.calculator import CalculatorTool
|
|
3
|
+
from agentify.extensions.tools.weather import WeatherTool
|
|
4
|
+
from agentify.extensions.tools.planning import TodoTool
|
|
5
|
+
from agentify.extensions.tools.filesystem import ListDirTool, ReadFileTool, WriteFileTool
|
|
6
|
+
|
|
7
|
+
__all__ = [
|
|
8
|
+
"TimeTool",
|
|
9
|
+
"CalculatorTool",
|
|
10
|
+
"WeatherTool",
|
|
11
|
+
"TodoTool",
|
|
12
|
+
"ListDirTool",
|
|
13
|
+
"ReadFileTool",
|
|
14
|
+
"WriteFileTool",
|
|
15
|
+
]
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
from agentify.core.tool import Tool
|
|
2
|
+
import ast
|
|
3
|
+
import operator as op
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class CalculatorTool(Tool):
|
|
7
|
+
"""Tool for evaluating safe mathematical expressions."""
|
|
8
|
+
|
|
9
|
+
def __init__(self):
|
|
10
|
+
self._allowed_ops = {
|
|
11
|
+
ast.Add: op.add,
|
|
12
|
+
ast.Sub: op.sub,
|
|
13
|
+
ast.Mult: op.mul,
|
|
14
|
+
ast.Div: op.truediv,
|
|
15
|
+
ast.Pow: op.pow,
|
|
16
|
+
ast.Mod: op.mod,
|
|
17
|
+
ast.UAdd: op.pos,
|
|
18
|
+
ast.USub: op.neg,
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
schema = {
|
|
22
|
+
"name": "calculate_expression",
|
|
23
|
+
"description": "Evalúa una expresión matemática segura y devuelve el resultado.",
|
|
24
|
+
"parameters": {
|
|
25
|
+
"type": "object",
|
|
26
|
+
"properties": {
|
|
27
|
+
"expression": {
|
|
28
|
+
"type": "string",
|
|
29
|
+
"description": "Expresión matemática a calcular, por ejemplo '2 + 2 * (3 - 1)'.",
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
"required": ["expression"],
|
|
33
|
+
},
|
|
34
|
+
}
|
|
35
|
+
super().__init__(schema, self._calculate_expression)
|
|
36
|
+
|
|
37
|
+
def _eval_node(self, node):
|
|
38
|
+
if isinstance(node, ast.Num):
|
|
39
|
+
return node.n
|
|
40
|
+
if isinstance(node, ast.BinOp):
|
|
41
|
+
left = self._eval_node(node.left)
|
|
42
|
+
right = self._eval_node(node.right)
|
|
43
|
+
return self._allowed_ops[type(node.op)](left, right)
|
|
44
|
+
if isinstance(node, ast.UnaryOp):
|
|
45
|
+
operand = self._eval_node(node.operand)
|
|
46
|
+
return self._allowed_ops[type(node.op)](operand)
|
|
47
|
+
raise ValueError(f"Operador no permitido: {node}")
|
|
48
|
+
|
|
49
|
+
def _calculate_expression(self, expression: str):
|
|
50
|
+
try:
|
|
51
|
+
tree = ast.parse(expression, mode="eval").body
|
|
52
|
+
result = self._eval_node(tree)
|
|
53
|
+
return {"result": result}
|
|
54
|
+
except Exception as e:
|
|
55
|
+
return {"error": f"Expresión inválida: {e}"}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from typing import Any, Dict, List, Optional
|
|
3
|
+
from agentify.core.tool import Tool
|
|
4
|
+
|
|
5
|
+
class BaseFilesystemTool(Tool):
|
|
6
|
+
"""Base class for filesystem tools with sandbox security."""
|
|
7
|
+
|
|
8
|
+
def __init__(self, schema: Dict[str, Any], func: Any, sandbox_dir: Optional[str] = None):
|
|
9
|
+
super().__init__(schema, func)
|
|
10
|
+
# If no sandbox provided, default to current working directory or a safe temp dir could be better
|
|
11
|
+
# but for this agent library, let's default to CWD but allow override.
|
|
12
|
+
self.sandbox_dir = os.path.abspath(sandbox_dir or os.getcwd())
|
|
13
|
+
|
|
14
|
+
def _validate_path(self, file_path: str) -> str:
|
|
15
|
+
"""Ensure path is within sandbox."""
|
|
16
|
+
# Handle absolute paths by checking if they start with sandbox
|
|
17
|
+
abs_path = os.path.abspath(os.path.join(self.sandbox_dir, file_path))
|
|
18
|
+
|
|
19
|
+
if not abs_path.startswith(self.sandbox_dir):
|
|
20
|
+
raise ValueError(f"Access denied: Path '{file_path}' is outside sandbox directory '{self.sandbox_dir}'")
|
|
21
|
+
|
|
22
|
+
return abs_path
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class ListDirTool(BaseFilesystemTool):
|
|
26
|
+
def __init__(self, sandbox_dir: Optional[str] = None):
|
|
27
|
+
schema = {
|
|
28
|
+
"name": "list_files",
|
|
29
|
+
"description": "List files and directories in a given path.",
|
|
30
|
+
"parameters": {
|
|
31
|
+
"type": "object",
|
|
32
|
+
"properties": {
|
|
33
|
+
"directory_path": {
|
|
34
|
+
"type": "string",
|
|
35
|
+
"description": "Relative path to list contents of. Defaults to root of sandbox.",
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
}
|
|
40
|
+
super().__init__(schema, self._list_dir, sandbox_dir)
|
|
41
|
+
|
|
42
|
+
def _list_dir(self, directory_path: str = ".") -> str:
|
|
43
|
+
try:
|
|
44
|
+
target_path = self._validate_path(directory_path)
|
|
45
|
+
if not os.path.exists(target_path):
|
|
46
|
+
return f"Error: Directory '{directory_path}' does not exist."
|
|
47
|
+
|
|
48
|
+
items = os.listdir(target_path)
|
|
49
|
+
# Add indicators for directories
|
|
50
|
+
formatted_items = []
|
|
51
|
+
for item in items:
|
|
52
|
+
if os.path.isdir(os.path.join(target_path, item)):
|
|
53
|
+
formatted_items.append(f"{item}/")
|
|
54
|
+
else:
|
|
55
|
+
formatted_items.append(item)
|
|
56
|
+
|
|
57
|
+
return "\n".join(formatted_items) if formatted_items else "(empty directory)"
|
|
58
|
+
except Exception as e:
|
|
59
|
+
return f"Error listing directory: {str(e)}"
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class ReadFileTool(BaseFilesystemTool):
|
|
63
|
+
def __init__(self, sandbox_dir: Optional[str] = None):
|
|
64
|
+
schema = {
|
|
65
|
+
"name": "read_file",
|
|
66
|
+
"description": "Read the contents of a file.",
|
|
67
|
+
"parameters": {
|
|
68
|
+
"type": "object",
|
|
69
|
+
"properties": {
|
|
70
|
+
"file_path": {
|
|
71
|
+
"type": "string",
|
|
72
|
+
"description": "Path to the file to read.",
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
"required": ["file_path"],
|
|
76
|
+
},
|
|
77
|
+
}
|
|
78
|
+
super().__init__(schema, self._read_file, sandbox_dir)
|
|
79
|
+
|
|
80
|
+
def _read_file(self, file_path: str) -> str:
|
|
81
|
+
try:
|
|
82
|
+
target_path = self._validate_path(file_path)
|
|
83
|
+
if not os.path.exists(target_path):
|
|
84
|
+
return f"Error: File '{file_path}' does not exist."
|
|
85
|
+
|
|
86
|
+
with open(target_path, "r", encoding="utf-8") as f:
|
|
87
|
+
return f.read()
|
|
88
|
+
except Exception as e:
|
|
89
|
+
return f"Error reading file: {str(e)}"
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
class WriteFileTool(BaseFilesystemTool):
|
|
93
|
+
def __init__(self, sandbox_dir: Optional[str] = None):
|
|
94
|
+
schema = {
|
|
95
|
+
"name": "write_file",
|
|
96
|
+
"description": "Write content to a file. Overwrites if exists.",
|
|
97
|
+
"parameters": {
|
|
98
|
+
"type": "object",
|
|
99
|
+
"properties": {
|
|
100
|
+
"file_path": {
|
|
101
|
+
"type": "string",
|
|
102
|
+
"description": "Path to the file to write.",
|
|
103
|
+
},
|
|
104
|
+
"content": {
|
|
105
|
+
"type": "string",
|
|
106
|
+
"description": "Content to write to the file.",
|
|
107
|
+
}
|
|
108
|
+
},
|
|
109
|
+
"required": ["file_path", "content"],
|
|
110
|
+
},
|
|
111
|
+
}
|
|
112
|
+
super().__init__(schema, self._write_file, sandbox_dir)
|
|
113
|
+
|
|
114
|
+
def _write_file(self, file_path: str, content: str) -> str:
|
|
115
|
+
try:
|
|
116
|
+
target_path = self._validate_path(file_path)
|
|
117
|
+
|
|
118
|
+
# Ensure directory exists
|
|
119
|
+
os.makedirs(os.path.dirname(target_path), exist_ok=True)
|
|
120
|
+
|
|
121
|
+
with open(target_path, "w", encoding="utf-8") as f:
|
|
122
|
+
f.write(content)
|
|
123
|
+
|
|
124
|
+
return f"Successfully wrote to '{file_path}'."
|
|
125
|
+
except Exception as e:
|
|
126
|
+
return f"Error writing file: {str(e)}"
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
from typing import Any, Dict, List, Optional
|
|
2
|
+
from agentify.core.tool import Tool
|
|
3
|
+
|
|
4
|
+
class TodoTool(Tool):
|
|
5
|
+
"""A tool for agents to manage their own todo list / plan."""
|
|
6
|
+
|
|
7
|
+
def __init__(self):
|
|
8
|
+
self._todos: List[Dict[str, Any]] = []
|
|
9
|
+
schema = {
|
|
10
|
+
"name": "manage_plan",
|
|
11
|
+
"description": "Manage a todo list to plan and track progress.",
|
|
12
|
+
"parameters": {
|
|
13
|
+
"type": "object",
|
|
14
|
+
"properties": {
|
|
15
|
+
"action": {
|
|
16
|
+
"type": "string",
|
|
17
|
+
"enum": ["add", "complete", "list", "remove"],
|
|
18
|
+
"description": "Action to perform on the plan.",
|
|
19
|
+
},
|
|
20
|
+
"task": {
|
|
21
|
+
"type": "string",
|
|
22
|
+
"description": "Description of the task (required for 'add')",
|
|
23
|
+
},
|
|
24
|
+
"task_id": {
|
|
25
|
+
"type": "integer",
|
|
26
|
+
"description": "ID of the task (required for 'complete' or 'remove')",
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
"required": ["action"],
|
|
30
|
+
},
|
|
31
|
+
}
|
|
32
|
+
super().__init__(schema, self._manage_todos)
|
|
33
|
+
|
|
34
|
+
def _manage_todos(
|
|
35
|
+
self,
|
|
36
|
+
action: str,
|
|
37
|
+
task: Optional[str] = None,
|
|
38
|
+
task_id: Optional[int] = None
|
|
39
|
+
) -> str:
|
|
40
|
+
if action == "add":
|
|
41
|
+
if not task:
|
|
42
|
+
return "Error: 'task' description required for 'add' action."
|
|
43
|
+
new_id = len(self._todos)
|
|
44
|
+
self._todos.append({"id": new_id, "task": task, "status": "pending"})
|
|
45
|
+
return f"Task added: [{new_id}] {task}"
|
|
46
|
+
|
|
47
|
+
elif action == "complete":
|
|
48
|
+
if task_id is None:
|
|
49
|
+
return "Error: 'task_id' required for 'complete' action."
|
|
50
|
+
if 0 <= task_id < len(self._todos):
|
|
51
|
+
self._todos[task_id]["status"] = "completed"
|
|
52
|
+
return f"Task [{task_id}] marked as completed."
|
|
53
|
+
return f"Error: Invalid task_id {task_id}"
|
|
54
|
+
|
|
55
|
+
elif action == "remove":
|
|
56
|
+
if task_id is None:
|
|
57
|
+
return "Error: 'task_id' required for 'remove' action."
|
|
58
|
+
if 0 <= task_id < len(self._todos):
|
|
59
|
+
# Rebuild list without the specified task_id
|
|
60
|
+
self._todos = [t for i, t in enumerate(self._todos) if i != task_id]
|
|
61
|
+
# Re-assign IDs to keep them sequential
|
|
62
|
+
for i, t in enumerate(self._todos):
|
|
63
|
+
t["id"] = i
|
|
64
|
+
return f"Task removed. Remaining tasks re-indexed."
|
|
65
|
+
return f"Error: Invalid task_id {task_id}"
|
|
66
|
+
|
|
67
|
+
elif action == "list":
|
|
68
|
+
if not self._todos:
|
|
69
|
+
return "Plan is empty."
|
|
70
|
+
lines = []
|
|
71
|
+
for t in self._todos:
|
|
72
|
+
status = "[x]" if t["status"] == "completed" else "[ ]"
|
|
73
|
+
lines.append(f"{status} {t['id']}: {t['task']}")
|
|
74
|
+
return "\n".join(lines)
|
|
75
|
+
|
|
76
|
+
return f"Error: Unknown action '{action}'"
|