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.
Files changed (51) hide show
  1. {agentify_core-0.1.2/agentify_core.egg-info → agentify_core-0.1.4}/PKG-INFO +14 -17
  2. {agentify_core-0.1.2 → agentify_core-0.1.4}/README.md +13 -3
  3. agentify_core-0.1.4/README_PYPI.md +104 -0
  4. {agentify_core-0.1.2 → agentify_core-0.1.4}/agentify/__init__.py +1 -1
  5. {agentify_core-0.1.2 → agentify_core-0.1.4}/agentify/core/agent.py +78 -5
  6. {agentify_core-0.1.2 → agentify_core-0.1.4}/agentify/core/config.py +1 -1
  7. agentify_core-0.1.4/agentify/extensions/tools/__init__.py +15 -0
  8. agentify_core-0.1.4/agentify/extensions/tools/calculator.py +55 -0
  9. agentify_core-0.1.4/agentify/extensions/tools/filesystem.py +126 -0
  10. agentify_core-0.1.4/agentify/extensions/tools/planning.py +76 -0
  11. agentify_core-0.1.4/agentify/extensions/tools/time.py +22 -0
  12. agentify_core-0.1.4/agentify/extensions/tools/weather.py +52 -0
  13. {agentify_core-0.1.2 → agentify_core-0.1.4}/agentify/memory/interfaces.py +10 -0
  14. agentify_core-0.1.4/agentify/memory/stores/__init__.py +5 -0
  15. {agentify_core-0.1.2 → agentify_core-0.1.4}/agentify/multi_agent/hierarchical.py +1 -1
  16. {agentify_core-0.1.2 → agentify_core-0.1.4}/agentify/multi_agent/pipeline.py +2 -1
  17. {agentify_core-0.1.2 → agentify_core-0.1.4}/agentify/multi_agent/team.py +1 -1
  18. {agentify_core-0.1.2 → agentify_core-0.1.4}/agentify/multi_agent/tool_wrapper.py +86 -1
  19. {agentify_core-0.1.2 → agentify_core-0.1.4/agentify_core.egg-info}/PKG-INFO +14 -17
  20. {agentify_core-0.1.2 → agentify_core-0.1.4}/agentify_core.egg-info/SOURCES.txt +6 -1
  21. {agentify_core-0.1.2 → agentify_core-0.1.4}/pyproject.toml +5 -5
  22. agentify_core-0.1.4/tests/test_filesystem_tools.py +62 -0
  23. agentify_core-0.1.4/tests/test_planning_tool.py +55 -0
  24. agentify_core-0.1.2/agentify/extensions/tools/__init__.py +0 -9
  25. agentify_core-0.1.2/agentify/extensions/tools/calculator.py +0 -56
  26. agentify_core-0.1.2/agentify/extensions/tools/time.py +0 -21
  27. agentify_core-0.1.2/agentify/extensions/tools/weather.py +0 -51
  28. agentify_core-0.1.2/agentify/memory/stores/__init__.py +0 -6
  29. {agentify_core-0.1.2 → agentify_core-0.1.4}/LICENSE +0 -0
  30. {agentify_core-0.1.2 → agentify_core-0.1.4}/MANIFEST.in +0 -0
  31. {agentify_core-0.1.2 → agentify_core-0.1.4}/agentify/core/__init__.py +0 -0
  32. {agentify_core-0.1.2 → agentify_core-0.1.4}/agentify/core/callbacks.py +0 -0
  33. {agentify_core-0.1.2 → agentify_core-0.1.4}/agentify/core/tool.py +0 -0
  34. {agentify_core-0.1.2 → agentify_core-0.1.4}/agentify/extensions/__init__.py +0 -0
  35. {agentify_core-0.1.2 → agentify_core-0.1.4}/agentify/extensions/prompts/__init__.py +0 -0
  36. {agentify_core-0.1.2 → agentify_core-0.1.4}/agentify/extensions/prompts/assistant.py +0 -0
  37. {agentify_core-0.1.2 → agentify_core-0.1.4}/agentify/llm/__init__.py +0 -0
  38. {agentify_core-0.1.2 → agentify_core-0.1.4}/agentify/llm/client.py +0 -0
  39. {agentify_core-0.1.2 → agentify_core-0.1.4}/agentify/memory/__init__.py +0 -0
  40. {agentify_core-0.1.2 → agentify_core-0.1.4}/agentify/memory/policies.py +0 -0
  41. {agentify_core-0.1.2 → agentify_core-0.1.4}/agentify/memory/service.py +0 -0
  42. {agentify_core-0.1.2 → agentify_core-0.1.4}/agentify/memory/stores/in_memory_store.py +0 -0
  43. {agentify_core-0.1.2 → agentify_core-0.1.4}/agentify/memory/stores/redis_store.py +0 -0
  44. {agentify_core-0.1.2 → agentify_core-0.1.4}/agentify/multi_agent/__init__.py +0 -0
  45. {agentify_core-0.1.2 → agentify_core-0.1.4}/agentify/utils/__init__.py +0 -0
  46. {agentify_core-0.1.2 → agentify_core-0.1.4}/agentify/utils/style.py +0 -0
  47. {agentify_core-0.1.2 → agentify_core-0.1.4}/agentify_core.egg-info/dependency_links.txt +0 -0
  48. {agentify_core-0.1.2 → agentify_core-0.1.4}/agentify_core.egg-info/requires.txt +0 -0
  49. {agentify_core-0.1.2 → agentify_core-0.1.4}/agentify_core.egg-info/top_level.txt +0 -0
  50. {agentify_core-0.1.2 → agentify_core-0.1.4}/requirements.txt +0 -0
  51. {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.2
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.9
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
- **Framework-agnostic AI agent library for building single and multi-agent systems**
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.respond(user_input="Hello! How can you help me?")
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
- ### More Examples
137
-
138
- Check out the [examples](examples/) directory for detailed implementations:
137
+ ## Learn More
139
138
 
140
- * [Single Agent Chatbot](examples/chatbot/)
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
- ## Author
143
+ Contributions are welcome! Please visit the [repository](https://github.com/fa8i/Agentify) to report issues or submit pull requests.
147
144
 
148
- - **Fabian Melchor** [fabianmp_98@hotmail.com](mailto:fabianmp_98@hotmail.com)
145
+ ## License
149
146
 
147
+ MIT License - see the repository for details.
150
148
 
151
- ## Links
149
+ ## Author
152
150
 
153
- - **Repository**: https://github.com/fa8i/Agentify
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
- **Framework-agnostic AI agent library for building single and multi-agent systems**
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.respond(user_input="Hello! How can you help me?")
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)
@@ -16,7 +16,7 @@ from agentify.memory.service import MemoryService
16
16
  from agentify.memory.interfaces import MemoryAddress
17
17
  from agentify.memory.policies import MemoryPolicy
18
18
 
19
- __version__ = "0.1.0"
19
+ __version__ = "0.1.3"
20
20
 
21
21
  __all__ = [
22
22
  "BaseAgent",
@@ -5,7 +5,8 @@ import time
5
5
  import uuid
6
6
  import base64
7
7
  from io import BytesIO
8
- from typing import Any, Dict, Generator, List, Optional, Union, Iterator
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
- """Framework-agnostic AI Agent core class."""
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
- for _ in range(self.config.max_tool_iter):
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 respond(
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 = 5
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}'"