agentify-core 0.1.2__tar.gz → 0.1.3__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.3}/PKG-INFO +15 -5
- {agentify_core-0.1.2 → agentify_core-0.1.3}/README.md +13 -3
- {agentify_core-0.1.2 → agentify_core-0.1.3}/agentify/__init__.py +1 -1
- {agentify_core-0.1.2 → agentify_core-0.1.3}/agentify/core/agent.py +78 -5
- {agentify_core-0.1.2 → agentify_core-0.1.3}/agentify/core/config.py +1 -1
- agentify_core-0.1.3/agentify/extensions/tools/__init__.py +15 -0
- agentify_core-0.1.3/agentify/extensions/tools/calculator.py +55 -0
- agentify_core-0.1.3/agentify/extensions/tools/filesystem.py +126 -0
- agentify_core-0.1.3/agentify/extensions/tools/planning.py +76 -0
- agentify_core-0.1.3/agentify/extensions/tools/time.py +22 -0
- agentify_core-0.1.3/agentify/extensions/tools/weather.py +52 -0
- {agentify_core-0.1.2 → agentify_core-0.1.3}/agentify/memory/interfaces.py +10 -0
- agentify_core-0.1.3/agentify/memory/stores/__init__.py +5 -0
- {agentify_core-0.1.2 → agentify_core-0.1.3}/agentify/multi_agent/hierarchical.py +1 -1
- {agentify_core-0.1.2 → agentify_core-0.1.3}/agentify/multi_agent/pipeline.py +2 -1
- {agentify_core-0.1.2 → agentify_core-0.1.3}/agentify/multi_agent/team.py +1 -1
- {agentify_core-0.1.2 → agentify_core-0.1.3}/agentify/multi_agent/tool_wrapper.py +86 -1
- {agentify_core-0.1.2 → agentify_core-0.1.3/agentify_core.egg-info}/PKG-INFO +15 -5
- {agentify_core-0.1.2 → agentify_core-0.1.3}/agentify_core.egg-info/SOURCES.txt +5 -1
- {agentify_core-0.1.2 → agentify_core-0.1.3}/pyproject.toml +4 -4
- agentify_core-0.1.3/tests/test_filesystem_tools.py +62 -0
- agentify_core-0.1.3/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.3}/LICENSE +0 -0
- {agentify_core-0.1.2 → agentify_core-0.1.3}/MANIFEST.in +0 -0
- {agentify_core-0.1.2 → agentify_core-0.1.3}/agentify/core/__init__.py +0 -0
- {agentify_core-0.1.2 → agentify_core-0.1.3}/agentify/core/callbacks.py +0 -0
- {agentify_core-0.1.2 → agentify_core-0.1.3}/agentify/core/tool.py +0 -0
- {agentify_core-0.1.2 → agentify_core-0.1.3}/agentify/extensions/__init__.py +0 -0
- {agentify_core-0.1.2 → agentify_core-0.1.3}/agentify/extensions/prompts/__init__.py +0 -0
- {agentify_core-0.1.2 → agentify_core-0.1.3}/agentify/extensions/prompts/assistant.py +0 -0
- {agentify_core-0.1.2 → agentify_core-0.1.3}/agentify/llm/__init__.py +0 -0
- {agentify_core-0.1.2 → agentify_core-0.1.3}/agentify/llm/client.py +0 -0
- {agentify_core-0.1.2 → agentify_core-0.1.3}/agentify/memory/__init__.py +0 -0
- {agentify_core-0.1.2 → agentify_core-0.1.3}/agentify/memory/policies.py +0 -0
- {agentify_core-0.1.2 → agentify_core-0.1.3}/agentify/memory/service.py +0 -0
- {agentify_core-0.1.2 → agentify_core-0.1.3}/agentify/memory/stores/in_memory_store.py +0 -0
- {agentify_core-0.1.2 → agentify_core-0.1.3}/agentify/memory/stores/redis_store.py +0 -0
- {agentify_core-0.1.2 → agentify_core-0.1.3}/agentify/multi_agent/__init__.py +0 -0
- {agentify_core-0.1.2 → agentify_core-0.1.3}/agentify/utils/__init__.py +0 -0
- {agentify_core-0.1.2 → agentify_core-0.1.3}/agentify/utils/style.py +0 -0
- {agentify_core-0.1.2 → agentify_core-0.1.3}/agentify_core.egg-info/dependency_links.txt +0 -0
- {agentify_core-0.1.2 → agentify_core-0.1.3}/agentify_core.egg-info/requires.txt +0 -0
- {agentify_core-0.1.2 → agentify_core-0.1.3}/agentify_core.egg-info/top_level.txt +0 -0
- {agentify_core-0.1.2 → agentify_core-0.1.3}/requirements.txt +0 -0
- {agentify_core-0.1.2 → agentify_core-0.1.3}/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.3
|
|
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,6 +134,15 @@ 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
|
|
|
137
|
+
## Documentation
|
|
138
|
+
|
|
139
|
+
- [Getting Started](docs/getting_started.md) - Installation and first steps
|
|
140
|
+
- [Core Concepts](docs/core_concepts.md) - Agents, memory, and tools
|
|
141
|
+
- [Multi-Agent Systems](docs/multi_agent.md) - Teams, pipelines, and hierarchies
|
|
142
|
+
- [Advanced Features](docs/advanced.md) - Vision, streaming, hooks, and more
|
|
143
|
+
- [API Reference](docs/api_reference.md) - Complete API documentation
|
|
144
|
+
|
|
145
|
+
|
|
136
146
|
### More Examples
|
|
137
147
|
|
|
138
148
|
Check out the [examples](examples/) directory for detailed implementations:
|
|
@@ -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:
|
|
@@ -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}'"
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
from agentify.core.tool import Tool
|
|
2
|
+
import datetime
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class TimeTool(Tool):
|
|
6
|
+
"""Tool for getting current date and time."""
|
|
7
|
+
|
|
8
|
+
def __init__(self):
|
|
9
|
+
schema = {
|
|
10
|
+
"name": "get_current_time",
|
|
11
|
+
"description": "Devuelve la hora y fecha actual en formato ISO 8601.",
|
|
12
|
+
"parameters": {
|
|
13
|
+
"type": "object",
|
|
14
|
+
"properties": {},
|
|
15
|
+
"required": [],
|
|
16
|
+
},
|
|
17
|
+
}
|
|
18
|
+
super().__init__(schema, self._get_current_time)
|
|
19
|
+
|
|
20
|
+
def _get_current_time(self):
|
|
21
|
+
now = datetime.datetime.now().astimezone().isoformat()
|
|
22
|
+
return {"current_time": now}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
from agentify.core.tool import Tool
|
|
2
|
+
import os
|
|
3
|
+
import requests
|
|
4
|
+
from dotenv import load_dotenv
|
|
5
|
+
|
|
6
|
+
load_dotenv()
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class WeatherTool(Tool):
|
|
10
|
+
"""Tool for getting current weather information."""
|
|
11
|
+
|
|
12
|
+
def __init__(self):
|
|
13
|
+
schema = {
|
|
14
|
+
"name": "get_weather",
|
|
15
|
+
"description": "Obtiene el estado del tiempo o clima actual para una ciudad o zona especificada.",
|
|
16
|
+
"parameters": {
|
|
17
|
+
"type": "object",
|
|
18
|
+
"properties": {
|
|
19
|
+
"location": {
|
|
20
|
+
"type": "string",
|
|
21
|
+
"description": "Nombre de la ciudad o zona para consultar el clima.",
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
"required": ["location"],
|
|
25
|
+
},
|
|
26
|
+
}
|
|
27
|
+
super().__init__(schema, self._get_weather)
|
|
28
|
+
|
|
29
|
+
def _get_weather(self, location: str):
|
|
30
|
+
api_key = os.getenv("OPENWEATHER_API_KEY")
|
|
31
|
+
if not api_key:
|
|
32
|
+
return {"error": "Variable de entorno OPENWEATHER_API_KEY no configurada."}
|
|
33
|
+
try:
|
|
34
|
+
response = requests.get(
|
|
35
|
+
"https://api.openweathermap.org/data/2.5/weather",
|
|
36
|
+
params={"q": location, "appid": api_key, "units": "metric"},
|
|
37
|
+
)
|
|
38
|
+
data = response.json()
|
|
39
|
+
if response.status_code != 200:
|
|
40
|
+
return {
|
|
41
|
+
"error": data.get("message", "Error desconocido al obtener el clima.")
|
|
42
|
+
}
|
|
43
|
+
weather = {
|
|
44
|
+
"location": data["name"],
|
|
45
|
+
"description": data["weather"][0]["description"],
|
|
46
|
+
"temperature": data["main"]["temp"],
|
|
47
|
+
"humidity": data["main"]["humidity"],
|
|
48
|
+
"wind_speed": data["wind"]["speed"],
|
|
49
|
+
}
|
|
50
|
+
return {"weather": weather}
|
|
51
|
+
except Exception as e:
|
|
52
|
+
return {"error": f"Error al conectar con el servicio de clima: {e}"}
|
|
@@ -99,3 +99,13 @@ class ConversationStore(Protocol):
|
|
|
99
99
|
class TokenCounter(Protocol):
|
|
100
100
|
"""Callable that returns token count for OpenAI-formatted messages."""
|
|
101
101
|
def __call__(self, openai_messages: List[Dict[str, Any]]) -> int: ...
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
class KeyValueStore(Protocol):
|
|
105
|
+
"""
|
|
106
|
+
Interface for long-term semantic memory or global state.
|
|
107
|
+
"""
|
|
108
|
+
def get(self, key: str) -> Optional[Any]: ...
|
|
109
|
+
def set(self, key: str, value: Any) -> None: ...
|
|
110
|
+
def delete(self, key: str) -> None: ...
|
|
111
|
+
def search(self, query: str, limit: int = 5) -> List[Any]: ...
|
|
@@ -44,7 +44,7 @@ class HierarchicalTeam:
|
|
|
44
44
|
self._register_hierarchy_tools(session_id, user_id)
|
|
45
45
|
|
|
46
46
|
# 3. Run Root
|
|
47
|
-
return self.root.
|
|
47
|
+
return self.root.run(user_input=user_input, addr=root_addr)
|
|
48
48
|
|
|
49
49
|
def _register_hierarchy_tools(self, session_id: str, user_id: str) -> None:
|
|
50
50
|
"""Registers children as tools for their parents based on the current session."""
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
from typing import List, Union, Generator, Any
|
|
2
|
+
|
|
2
3
|
from agentify.core.agent import BaseAgent
|
|
3
4
|
from agentify.memory.interfaces import MemoryAddress
|
|
4
5
|
from agentify.multi_agent.team import Team
|
|
@@ -43,7 +44,7 @@ class SequentialPipeline:
|
|
|
43
44
|
step_addr = MemoryAddress(
|
|
44
45
|
user_id=user_id, conversation_id=session_id, agent_id=step_name
|
|
45
46
|
)
|
|
46
|
-
response = step.
|
|
47
|
+
response = step.run(user_input=current_input, addr=step_addr)
|
|
47
48
|
|
|
48
49
|
elif hasattr(step, "run"):
|
|
49
50
|
# Team, SequentialPipeline, HierarchicalTeam
|
|
@@ -57,7 +57,7 @@ class AgentTool(Tool):
|
|
|
57
57
|
)
|
|
58
58
|
|
|
59
59
|
# Run the agent
|
|
60
|
-
response = self.agent.
|
|
60
|
+
response = self.agent.run(user_input=instructions, addr=child_addr)
|
|
61
61
|
|
|
62
62
|
# Consume generator if needed
|
|
63
63
|
if hasattr(response, "__iter__") and not isinstance(response, str):
|
|
@@ -122,3 +122,88 @@ class FlowTool(Tool):
|
|
|
122
122
|
response = "".join(list(response))
|
|
123
123
|
|
|
124
124
|
return {"response": response}
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
class SpawnAgentTool(Tool):
|
|
128
|
+
"""Tool to dynamically spawn a transient sub-agent for a specific task."""
|
|
129
|
+
|
|
130
|
+
def __init__(
|
|
131
|
+
self,
|
|
132
|
+
base_config: Any, # AgentConfig type ideally, but Any to avoid circular imports context
|
|
133
|
+
memory_service: Any, # MemoryService
|
|
134
|
+
parent_addr: MemoryAddress,
|
|
135
|
+
client_factory: Optional[Any] = None,
|
|
136
|
+
):
|
|
137
|
+
self.base_config = base_config
|
|
138
|
+
self.memory_service = memory_service
|
|
139
|
+
self.parent_addr = parent_addr
|
|
140
|
+
self.client_factory = client_factory
|
|
141
|
+
|
|
142
|
+
schema = {
|
|
143
|
+
"name": "spawn_subagent",
|
|
144
|
+
"description": "Spawn a temporary specialized sub-agent to handle a complex sub-task.",
|
|
145
|
+
"parameters": {
|
|
146
|
+
"type": "object",
|
|
147
|
+
"properties": {
|
|
148
|
+
"role_name": {
|
|
149
|
+
"type": "string",
|
|
150
|
+
"description": "Name of the sub-agent (e.g., 'ResearchAssistant').",
|
|
151
|
+
},
|
|
152
|
+
"instructions": {
|
|
153
|
+
"type": "string",
|
|
154
|
+
"description": "Specific task instructions for the sub-agent.",
|
|
155
|
+
},
|
|
156
|
+
"system_prompt": {
|
|
157
|
+
"type": "string",
|
|
158
|
+
"description": "System prompt defining the sub-agent's persona and constraints.",
|
|
159
|
+
}
|
|
160
|
+
},
|
|
161
|
+
"required": ["role_name", "instructions"],
|
|
162
|
+
},
|
|
163
|
+
}
|
|
164
|
+
super().__init__(schema, self._spawn_and_run)
|
|
165
|
+
|
|
166
|
+
def _spawn_and_run(
|
|
167
|
+
self,
|
|
168
|
+
role_name: str,
|
|
169
|
+
instructions: str,
|
|
170
|
+
system_prompt: Optional[str] = None
|
|
171
|
+
) -> Dict[str, Any]:
|
|
172
|
+
"""Creates and runs a new agent instance."""
|
|
173
|
+
from agentify.core.agent import BaseAgent
|
|
174
|
+
from agentify.core.config import AgentConfig
|
|
175
|
+
import copy
|
|
176
|
+
|
|
177
|
+
# Clone config but override name and system prompt
|
|
178
|
+
# Assuming base_config is an AgentConfig object or similar dataclass
|
|
179
|
+
new_config = copy.deepcopy(self.base_config)
|
|
180
|
+
new_config.name = f"{self.base_config.name}.{role_name}"
|
|
181
|
+
if system_prompt:
|
|
182
|
+
new_config.system_prompt = system_prompt
|
|
183
|
+
|
|
184
|
+
# Create a unique address for this interaction
|
|
185
|
+
child_addr = MemoryAddress(
|
|
186
|
+
user_id=self.parent_addr.user_id,
|
|
187
|
+
conversation_id=f"{self.parent_addr.conversation_id}_{role_name}_{instructions[:10]}", # Unique-ish
|
|
188
|
+
agent_id=new_config.name,
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
# Create the agent
|
|
192
|
+
sub_agent = BaseAgent(
|
|
193
|
+
config=new_config,
|
|
194
|
+
memory=self.memory_service,
|
|
195
|
+
memory_address=child_addr,
|
|
196
|
+
client_factory=self.client_factory
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
response = sub_agent.run(user_input=instructions)
|
|
200
|
+
|
|
201
|
+
# Consume generator if needed
|
|
202
|
+
if hasattr(response, "__iter__") and not isinstance(response, str):
|
|
203
|
+
response = "".join(list(response))
|
|
204
|
+
|
|
205
|
+
return {
|
|
206
|
+
"subagent": role_name,
|
|
207
|
+
"status": "finished",
|
|
208
|
+
"response": response
|
|
209
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: agentify-core
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.3
|
|
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,6 +134,15 @@ 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
|
|
|
137
|
+
## Documentation
|
|
138
|
+
|
|
139
|
+
- [Getting Started](docs/getting_started.md) - Installation and first steps
|
|
140
|
+
- [Core Concepts](docs/core_concepts.md) - Agents, memory, and tools
|
|
141
|
+
- [Multi-Agent Systems](docs/multi_agent.md) - Teams, pipelines, and hierarchies
|
|
142
|
+
- [Advanced Features](docs/advanced.md) - Vision, streaming, hooks, and more
|
|
143
|
+
- [API Reference](docs/api_reference.md) - Complete API documentation
|
|
144
|
+
|
|
145
|
+
|
|
136
146
|
### More Examples
|
|
137
147
|
|
|
138
148
|
Check out the [examples](examples/) directory for detailed implementations:
|
|
@@ -14,6 +14,8 @@ agentify/extensions/prompts/__init__.py
|
|
|
14
14
|
agentify/extensions/prompts/assistant.py
|
|
15
15
|
agentify/extensions/tools/__init__.py
|
|
16
16
|
agentify/extensions/tools/calculator.py
|
|
17
|
+
agentify/extensions/tools/filesystem.py
|
|
18
|
+
agentify/extensions/tools/planning.py
|
|
17
19
|
agentify/extensions/tools/time.py
|
|
18
20
|
agentify/extensions/tools/weather.py
|
|
19
21
|
agentify/llm/__init__.py
|
|
@@ -36,4 +38,6 @@ agentify_core.egg-info/PKG-INFO
|
|
|
36
38
|
agentify_core.egg-info/SOURCES.txt
|
|
37
39
|
agentify_core.egg-info/dependency_links.txt
|
|
38
40
|
agentify_core.egg-info/requires.txt
|
|
39
|
-
agentify_core.egg-info/top_level.txt
|
|
41
|
+
agentify_core.egg-info/top_level.txt
|
|
42
|
+
tests/test_filesystem_tools.py
|
|
43
|
+
tests/test_planning_tool.py
|
|
@@ -4,10 +4,10 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "agentify-core"
|
|
7
|
-
version = "0.1.
|
|
7
|
+
version = "0.1.3"
|
|
8
8
|
description = "Framework-agnostic AI agent library for building single and multi-agent systems"
|
|
9
9
|
readme = "README.md"
|
|
10
|
-
requires-python = ">=3.
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
11
|
license = {text = "MIT"}
|
|
12
12
|
authors = [
|
|
13
13
|
{name = "Fabian M", email = "fabian@example.com"}
|
|
@@ -83,10 +83,10 @@ agentify = ["py.typed"]
|
|
|
83
83
|
|
|
84
84
|
[tool.black]
|
|
85
85
|
line-length = 100
|
|
86
|
-
target-version = ['
|
|
86
|
+
target-version = ['py310', 'py311']
|
|
87
87
|
|
|
88
88
|
[tool.mypy]
|
|
89
|
-
python_version = "3.
|
|
89
|
+
python_version = "3.10"
|
|
90
90
|
warn_return_any = true
|
|
91
91
|
warn_unused_configs = true
|
|
92
92
|
disallow_untyped_defs = false
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import tempfile
|
|
3
|
+
import pytest
|
|
4
|
+
from agentify.tools.filesystem import ListDirTool, ReadFileTool, WriteFileTool
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class TestFilesystemTools:
|
|
8
|
+
"""Test suite for filesystem tools."""
|
|
9
|
+
|
|
10
|
+
def test_list_dir_tool(self):
|
|
11
|
+
"""Test ListDirTool enumerates directory contents."""
|
|
12
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
13
|
+
# Create test files
|
|
14
|
+
open(os.path.join(tmpdir, "file1.txt"), "w").close()
|
|
15
|
+
open(os.path.join(tmpdir, "file2.txt"), "w").close()
|
|
16
|
+
os.makedirs(os.path.join(tmpdir, "subdir"))
|
|
17
|
+
|
|
18
|
+
tool = ListDirTool(sandbox_dir=tmpdir)
|
|
19
|
+
result = tool._list_dir(".")
|
|
20
|
+
|
|
21
|
+
assert "file1.txt" in result
|
|
22
|
+
assert "file2.txt" in result
|
|
23
|
+
assert "subdir/" in result
|
|
24
|
+
|
|
25
|
+
def test_read_file_tool(self):
|
|
26
|
+
"""Test ReadFileTool reads file contents."""
|
|
27
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
28
|
+
test_file = os.path.join(tmpdir, "test.txt")
|
|
29
|
+
test_content = "Hello, Deep Agent!"
|
|
30
|
+
|
|
31
|
+
with open(test_file, "w") as f:
|
|
32
|
+
f.write(test_content)
|
|
33
|
+
|
|
34
|
+
tool = ReadFileTool(sandbox_dir=tmpdir)
|
|
35
|
+
result = tool._read_file("test.txt")
|
|
36
|
+
|
|
37
|
+
assert result == test_content
|
|
38
|
+
|
|
39
|
+
def test_write_file_tool(self):
|
|
40
|
+
"""Test WriteFileTool creates and writes files."""
|
|
41
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
42
|
+
tool = WriteFileTool(sandbox_dir=tmpdir)
|
|
43
|
+
result = tool._write_file("output.txt", "Test content")
|
|
44
|
+
|
|
45
|
+
assert "Successfully wrote" in result
|
|
46
|
+
|
|
47
|
+
# Verify file exists and has correct content
|
|
48
|
+
with open(os.path.join(tmpdir, "output.txt"), "r") as f:
|
|
49
|
+
assert f.read() == "Test content"
|
|
50
|
+
|
|
51
|
+
def test_sandbox_security(self):
|
|
52
|
+
"""Test that sandbox prevents access outside its boundaries."""
|
|
53
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
54
|
+
tool = ReadFileTool(sandbox_dir=tmpdir)
|
|
55
|
+
|
|
56
|
+
# Try to read outside sandbox
|
|
57
|
+
result = tool._read_file("../../etc/passwd")
|
|
58
|
+
assert "Access denied" in result or "Error" in result
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
if __name__ == "__main__":
|
|
62
|
+
pytest.main([__file__, "-v"])
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
from agentify.tools.planning import TodoTool
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class TestPlanningTool:
|
|
6
|
+
"""Test suite for planning tool."""
|
|
7
|
+
|
|
8
|
+
def test_add_task(self):
|
|
9
|
+
"""Test adding tasks to the plan."""
|
|
10
|
+
tool = TodoTool()
|
|
11
|
+
result = tool._manage_todos(action="add", task="Complete the demo")
|
|
12
|
+
|
|
13
|
+
assert "Task added" in result
|
|
14
|
+
assert "[0]" in result
|
|
15
|
+
assert "Complete the demo" in result
|
|
16
|
+
|
|
17
|
+
def test_list_tasks(self):
|
|
18
|
+
"""Test listing all tasks."""
|
|
19
|
+
tool = TodoTool()
|
|
20
|
+
tool._manage_todos(action="add", task="Task 1")
|
|
21
|
+
tool._manage_todos(action="add", task="Task 2")
|
|
22
|
+
|
|
23
|
+
result = tool._manage_todos(action="list")
|
|
24
|
+
|
|
25
|
+
assert "Task 1" in result
|
|
26
|
+
assert "Task 2" in result
|
|
27
|
+
assert "[ ]" in result # Pending status
|
|
28
|
+
|
|
29
|
+
def test_complete_task(self):
|
|
30
|
+
"""Test marking a task as complete."""
|
|
31
|
+
tool = TodoTool()
|
|
32
|
+
tool._manage_todos(action="add", task="Do something")
|
|
33
|
+
|
|
34
|
+
result = tool._manage_todos(action="complete", task_id=0)
|
|
35
|
+
assert "marked as completed" in result
|
|
36
|
+
|
|
37
|
+
list_result = tool._manage_todos(action="list")
|
|
38
|
+
assert "[x]" in list_result # Completed status
|
|
39
|
+
|
|
40
|
+
def test_remove_task(self):
|
|
41
|
+
"""Test removing a task."""
|
|
42
|
+
tool = TodoTool()
|
|
43
|
+
tool._manage_todos(action="add", task="Task to remove")
|
|
44
|
+
tool._manage_todos(action="add", task="Task to keep")
|
|
45
|
+
|
|
46
|
+
result = tool._manage_todos(action="remove", task_id=0)
|
|
47
|
+
assert "removed" in result
|
|
48
|
+
|
|
49
|
+
list_result = tool._manage_todos(action="list")
|
|
50
|
+
assert "Task to remove" not in list_result
|
|
51
|
+
assert "Task to keep" in list_result
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
if __name__ == "__main__":
|
|
55
|
+
pytest.main([__file__, "-v"])
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
from agentify.extensions.tools.time import get_current_time_tool
|
|
2
|
-
from agentify.extensions.tools.calculator import calculate_expression_tool
|
|
3
|
-
from agentify.extensions.tools.weather import get_weather_tool
|
|
4
|
-
|
|
5
|
-
__all__ = [
|
|
6
|
-
"get_current_time_tool",
|
|
7
|
-
"calculate_expression_tool",
|
|
8
|
-
"get_weather_tool",
|
|
9
|
-
]
|
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
from agentify.core.tool import Tool
|
|
2
|
-
import ast
|
|
3
|
-
import operator as op
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
calculate_expression_schema = {
|
|
7
|
-
"name": "calculate_expression",
|
|
8
|
-
"description": "Evalúa una expresión matemática segura y devuelve el resultado.",
|
|
9
|
-
"parameters": {
|
|
10
|
-
"type": "object",
|
|
11
|
-
"properties": {
|
|
12
|
-
"expression": {
|
|
13
|
-
"type": "string",
|
|
14
|
-
"description": "Expresión matemática a calcular, por ejemplo '2 + 2 * (3 - 1)'.",
|
|
15
|
-
}
|
|
16
|
-
},
|
|
17
|
-
"required": ["expression"],
|
|
18
|
-
},
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
_allowed_ops = {
|
|
23
|
-
ast.Add: op.add,
|
|
24
|
-
ast.Sub: op.sub,
|
|
25
|
-
ast.Mult: op.mul,
|
|
26
|
-
ast.Div: op.truediv,
|
|
27
|
-
ast.Pow: op.pow,
|
|
28
|
-
ast.Mod: op.mod,
|
|
29
|
-
ast.UAdd: op.pos,
|
|
30
|
-
ast.USub: op.neg,
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
def _eval_node(node):
|
|
35
|
-
if isinstance(node, ast.Num):
|
|
36
|
-
return node.n
|
|
37
|
-
if isinstance(node, ast.BinOp):
|
|
38
|
-
left = _eval_node(node.left)
|
|
39
|
-
right = _eval_node(node.right)
|
|
40
|
-
return _allowed_ops[type(node.op)](left, right)
|
|
41
|
-
if isinstance(node, ast.UnaryOp):
|
|
42
|
-
operand = _eval_node(node.operand)
|
|
43
|
-
return _allowed_ops[type(node.op)](operand)
|
|
44
|
-
raise ValueError(f"Operador no permitido: {node}")
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
def calculate_expression(expression: str):
|
|
48
|
-
try:
|
|
49
|
-
tree = ast.parse(expression, mode="eval").body
|
|
50
|
-
result = _eval_node(tree)
|
|
51
|
-
return {"result": result}
|
|
52
|
-
except Exception as e:
|
|
53
|
-
return {"error": f"Expresión inválida: {e}"}
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
calculate_expression_tool = Tool(calculate_expression_schema, calculate_expression)
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
from agentify.core.tool import Tool
|
|
2
|
-
import datetime
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
get_current_time_schema = {
|
|
6
|
-
"name": "get_current_time",
|
|
7
|
-
"description": "Devuelve la hora y fecha actual en formato ISO 8601.",
|
|
8
|
-
"parameters": {
|
|
9
|
-
"type": "object",
|
|
10
|
-
"properties": {},
|
|
11
|
-
"required": [],
|
|
12
|
-
},
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
def get_current_time():
|
|
17
|
-
now = datetime.datetime.now().astimezone().isoformat()
|
|
18
|
-
return {"current_time": now}
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
get_current_time_tool = Tool(get_current_time_schema, get_current_time)
|
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
from agentify.core.tool import Tool
|
|
2
|
-
import os
|
|
3
|
-
import requests
|
|
4
|
-
from dotenv import load_dotenv
|
|
5
|
-
|
|
6
|
-
load_dotenv()
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
get_weather_schema = {
|
|
10
|
-
"name": "get_weather",
|
|
11
|
-
"description": "Obtiene el estado del tiempo o clima actual para una ciudad o zona especificada.",
|
|
12
|
-
"parameters": {
|
|
13
|
-
"type": "object",
|
|
14
|
-
"properties": {
|
|
15
|
-
"location": {
|
|
16
|
-
"type": "string",
|
|
17
|
-
"description": "Nombre de la ciudad o zona para consultar el clima.",
|
|
18
|
-
}
|
|
19
|
-
},
|
|
20
|
-
"required": ["location"],
|
|
21
|
-
},
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
def get_weather(location: str):
|
|
26
|
-
api_key = os.getenv("OPENWEATHER_API_KEY")
|
|
27
|
-
if not api_key:
|
|
28
|
-
return {"error": "Variable de entorno OPENWEATHER_API_KEY no configurada."}
|
|
29
|
-
try:
|
|
30
|
-
response = requests.get(
|
|
31
|
-
"https://api.openweathermap.org/data/2.5/weather",
|
|
32
|
-
params={"q": location, "appid": api_key, "units": "metric"},
|
|
33
|
-
)
|
|
34
|
-
data = response.json()
|
|
35
|
-
if response.status_code != 200:
|
|
36
|
-
return {
|
|
37
|
-
"error": data.get("message", "Error desconocido al obtener el clima.")
|
|
38
|
-
}
|
|
39
|
-
weather = {
|
|
40
|
-
"location": data["name"],
|
|
41
|
-
"description": data["weather"][0]["description"],
|
|
42
|
-
"temperature": data["main"]["temp"],
|
|
43
|
-
"humidity": data["main"]["humidity"],
|
|
44
|
-
"wind_speed": data["wind"]["speed"],
|
|
45
|
-
}
|
|
46
|
-
return {"weather": weather}
|
|
47
|
-
except Exception as e:
|
|
48
|
-
return {"error": f"Error al conectar con el servicio de clima: {e}"}
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
get_weather_tool = Tool(get_weather_schema, get_weather)
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|