agentify-core 0.2.0__tar.gz → 0.3.1__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.2.0/agentify_core.egg-info → agentify_core-0.3.1}/PKG-INFO +15 -7
- {agentify_core-0.2.0 → agentify_core-0.3.1}/README.md +20 -32
- {agentify_core-0.2.0 → agentify_core-0.3.1}/README_PYPI.md +2 -1
- {agentify_core-0.2.0 → agentify_core-0.3.1}/agentify/__init__.py +1 -1
- {agentify_core-0.2.0 → agentify_core-0.3.1}/agentify/core/agent.py +66 -7
- {agentify_core-0.2.0 → agentify_core-0.3.1}/agentify/core/callbacks.py +32 -4
- {agentify_core-0.2.0 → agentify_core-0.3.1}/agentify/core/config.py +1 -0
- agentify_core-0.3.1/agentify/core/runnable.py +20 -0
- {agentify_core-0.2.0 → agentify_core-0.3.1}/agentify/core/tool.py +2 -2
- {agentify_core-0.2.0 → agentify_core-0.3.1}/agentify/extensions/tools/filesystem.py +37 -12
- {agentify_core-0.2.0 → agentify_core-0.3.1}/agentify/llm/client.py +1 -1
- agentify_core-0.3.1/agentify/mcp/__init__.py +3 -0
- agentify_core-0.3.1/agentify/mcp/adapter.py +46 -0
- agentify_core-0.3.1/agentify/mcp/client.py +141 -0
- {agentify_core-0.2.0 → agentify_core-0.3.1}/agentify/memory/interfaces.py +23 -9
- {agentify_core-0.2.0 → agentify_core-0.3.1}/agentify/memory/service.py +31 -12
- agentify_core-0.3.1/agentify/memory/stores/__init__.py +10 -0
- agentify_core-0.3.1/agentify/memory/stores/elastic_store.py +246 -0
- {agentify_core-0.2.0 → agentify_core-0.3.1}/agentify/memory/stores/in_memory_store.py +6 -0
- {agentify_core-0.2.0 → agentify_core-0.3.1}/agentify/memory/stores/redis_store.py +48 -0
- agentify_core-0.3.1/agentify/memory/stores/sqlite_store.py +199 -0
- {agentify_core-0.2.0 → agentify_core-0.3.1}/agentify/multi_agent/hierarchical.py +7 -2
- {agentify_core-0.2.0 → agentify_core-0.3.1}/agentify/multi_agent/pipeline.py +32 -35
- {agentify_core-0.2.0 → agentify_core-0.3.1}/agentify/multi_agent/team.py +20 -11
- {agentify_core-0.2.0 → agentify_core-0.3.1}/agentify/multi_agent/tool_wrapper.py +6 -3
- {agentify_core-0.2.0 → agentify_core-0.3.1/agentify_core.egg-info}/PKG-INFO +15 -7
- {agentify_core-0.2.0 → agentify_core-0.3.1}/agentify_core.egg-info/SOURCES.txt +11 -1
- {agentify_core-0.2.0 → agentify_core-0.3.1}/agentify_core.egg-info/requires.txt +12 -3
- {agentify_core-0.2.0 → agentify_core-0.3.1}/pyproject.toml +18 -7
- agentify_core-0.3.1/requirements.txt +27 -0
- {agentify_core-0.2.0 → agentify_core-0.3.1}/tests/test_filesystem_tools.py +15 -1
- agentify_core-0.3.1/tests/test_mcp.py +124 -0
- agentify_core-0.3.1/tests/test_memory_address.py +26 -0
- agentify_core-0.3.1/tests/test_memory_logging.py +58 -0
- {agentify_core-0.2.0 → agentify_core-0.3.1}/tests/test_planning_tool.py +1 -1
- agentify_core-0.3.1/tests/test_verify_hooks.py +69 -0
- agentify_core-0.2.0/agentify/memory/stores/__init__.py +0 -5
- agentify_core-0.2.0/requirements.txt +0 -18
- {agentify_core-0.2.0 → agentify_core-0.3.1}/LICENSE +0 -0
- {agentify_core-0.2.0 → agentify_core-0.3.1}/MANIFEST.in +0 -0
- {agentify_core-0.2.0 → agentify_core-0.3.1}/agentify/core/__init__.py +0 -0
- {agentify_core-0.2.0 → agentify_core-0.3.1}/agentify/extensions/__init__.py +0 -0
- {agentify_core-0.2.0 → agentify_core-0.3.1}/agentify/extensions/prompts/__init__.py +0 -0
- {agentify_core-0.2.0 → agentify_core-0.3.1}/agentify/extensions/prompts/assistant.py +0 -0
- {agentify_core-0.2.0 → agentify_core-0.3.1}/agentify/extensions/tools/__init__.py +0 -0
- {agentify_core-0.2.0 → agentify_core-0.3.1}/agentify/extensions/tools/calculator.py +0 -0
- {agentify_core-0.2.0 → agentify_core-0.3.1}/agentify/extensions/tools/planning.py +0 -0
- {agentify_core-0.2.0 → agentify_core-0.3.1}/agentify/extensions/tools/time.py +0 -0
- {agentify_core-0.2.0 → agentify_core-0.3.1}/agentify/extensions/tools/weather.py +0 -0
- {agentify_core-0.2.0 → agentify_core-0.3.1}/agentify/llm/__init__.py +0 -0
- {agentify_core-0.2.0 → agentify_core-0.3.1}/agentify/memory/__init__.py +0 -0
- {agentify_core-0.2.0 → agentify_core-0.3.1}/agentify/memory/policies.py +0 -0
- {agentify_core-0.2.0 → agentify_core-0.3.1}/agentify/multi_agent/__init__.py +0 -0
- {agentify_core-0.2.0 → agentify_core-0.3.1}/agentify/utils/__init__.py +0 -0
- {agentify_core-0.2.0 → agentify_core-0.3.1}/agentify/utils/style.py +0 -0
- {agentify_core-0.2.0 → agentify_core-0.3.1}/agentify_core.egg-info/dependency_links.txt +0 -0
- {agentify_core-0.2.0 → agentify_core-0.3.1}/agentify_core.egg-info/top_level.txt +0 -0
- {agentify_core-0.2.0 → agentify_core-0.3.1}/setup.cfg +0 -0
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: agentify-core
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.1
|
|
4
4
|
Summary: Framework-agnostic AI agent library for building single and multi-agent systems
|
|
5
|
-
Author
|
|
5
|
+
Author: Fabian M
|
|
6
|
+
Author-email: fabianmp_98@hotmail.com
|
|
6
7
|
License: MIT
|
|
7
8
|
Project-URL: Homepage, https://github.com/fa8i/Agentify
|
|
8
9
|
Project-URL: Repository, https://github.com/fa8i/Agentify
|
|
9
10
|
Project-URL: Bug Tracker, https://github.com/fa8i/Agentify/issues
|
|
10
|
-
Keywords: agent,multi-agent,ai,llm,openai,framework
|
|
11
|
+
Keywords: agentify,agentify-core,agent,multi-agent,ai,llm,openai,framework
|
|
11
12
|
Classifier: Development Status :: 3 - Alpha
|
|
12
13
|
Classifier: Intended Audience :: Developers
|
|
13
14
|
Classifier: License :: OSI Approved :: MIT License
|
|
14
15
|
Classifier: Operating System :: OS Independent
|
|
15
16
|
Classifier: Programming Language :: Python :: 3
|
|
16
|
-
Classifier: Programming Language :: Python :: 3.9
|
|
17
17
|
Classifier: Programming Language :: Python :: 3.10
|
|
18
18
|
Classifier: Programming Language :: Python :: 3.11
|
|
19
19
|
Classifier: Programming Language :: Python :: 3.12
|
|
@@ -22,19 +22,26 @@ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
|
22
22
|
Requires-Python: >=3.10
|
|
23
23
|
Description-Content-Type: text/markdown
|
|
24
24
|
License-File: LICENSE
|
|
25
|
-
Requires-Dist: openai
|
|
26
|
-
Requires-Dist: python-dotenv
|
|
27
|
-
Requires-Dist: Pillow
|
|
25
|
+
Requires-Dist: openai
|
|
26
|
+
Requires-Dist: python-dotenv
|
|
27
|
+
Requires-Dist: Pillow
|
|
28
|
+
Requires-Dist: jsonschema>=4.0.0
|
|
28
29
|
Provides-Extra: redis
|
|
29
30
|
Requires-Dist: redis>=4.0.0; extra == "redis"
|
|
31
|
+
Provides-Extra: elastic
|
|
32
|
+
Requires-Dist: elasticsearch>=8.0.0; extra == "elastic"
|
|
30
33
|
Provides-Extra: tools
|
|
31
34
|
Requires-Dist: requests>=2.25.0; extra == "tools"
|
|
35
|
+
Provides-Extra: mcp
|
|
36
|
+
Requires-Dist: mcp; extra == "mcp"
|
|
32
37
|
Provides-Extra: ui
|
|
33
38
|
Requires-Dist: gradio==5.49.1; extra == "ui"
|
|
34
39
|
Provides-Extra: all
|
|
35
40
|
Requires-Dist: redis>=4.0.0; extra == "all"
|
|
41
|
+
Requires-Dist: elasticsearch>=8.0.0; extra == "all"
|
|
36
42
|
Requires-Dist: requests>=2.25.0; extra == "all"
|
|
37
43
|
Requires-Dist: gradio==5.49.1; extra == "all"
|
|
44
|
+
Requires-Dist: mcp; extra == "all"
|
|
38
45
|
Provides-Extra: dev
|
|
39
46
|
Requires-Dist: pytest>=7.0.0; extra == "dev"
|
|
40
47
|
Requires-Dist: pytest-cov>=4.0.0; extra == "dev"
|
|
@@ -47,6 +54,7 @@ Dynamic: license-file
|
|
|
47
54
|
|
|
48
55
|
# Agentify
|
|
49
56
|
|
|
57
|
+
|
|
50
58
|
**Independent AI agent library based on the OpenAI SDK**
|
|
51
59
|
|
|
52
60
|
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.
|
|
@@ -1,40 +1,25 @@
|
|
|
1
1
|
# Agentify
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
3
|
+
[](https://pypi.org/project/agentify-core/)
|
|
4
|
+
[](https://pepy.tech/project/agentify-core)
|
|
5
|
+
[](https://opensource.org/licenses/MIT)
|
|
6
|
+
[](https://pypi.org/project/agentify-core/)
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
**Independent AI agent library based on the OpenAI SDK**
|
|
9
9
|
|
|
10
|
-
-
|
|
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.
|
|
10
|
+
Agentify is a Python library for building AI agents and multi-agent systems. Built on the OpenAI-compatible Chat Completions interface, it supports multiple providers (OpenAI, Azure, DeepSeek, Gemini, Claude) with clear abstractions for memory, tools, and orchestration—no heavy framework lock-in.
|
|
13
11
|
|
|
14
12
|
|
|
15
13
|
## Key Features
|
|
16
14
|
|
|
17
|
-
- **
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
- **
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
- **
|
|
24
|
-
|
|
25
|
-
"Chain of Thought" in conversation history, and log reasoning steps in real-time for visibility.
|
|
26
|
-
|
|
27
|
-
- **Tools and actions**
|
|
28
|
-
Simple `@tool` decorator for creating tools from functions with automatic JSON Schema generation, or type-annotated tool interface for custom implementations.
|
|
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
|
-
- **Async & Parallel Execution**
|
|
37
|
-
Built-in `async/await` support (`arun()`) for all agents and flows. Automatically executes independent tool calls in parallel (e.g., fetching data from 3 APIs simultaneously), significantly reducing latency.
|
|
15
|
+
- **Multi-agent orchestration**: Teams, pipelines, hierarchies, and dynamic sub-agent spawning
|
|
16
|
+
- **Memory service**: Pluggable backends (in-memory, SQLite, Redis, Elasticsearch) with policies (TTL, limits, token budgets)
|
|
17
|
+
- **Tools**: `@tool` decorator for auto-schema generation, or custom tool classes. Built-in file I/O, planning, weather, and more
|
|
18
|
+
- **MCP Integration**: Easy connection to MCP servers via StdIO (local) or SSE/HTTP (remote) to use external tools
|
|
19
|
+
- **Reasoning models**: Configure thinking depth, store chain-of-thought, real-time reasoning logs
|
|
20
|
+
- **Async & parallel**: `arun()` support with automatic parallel tool and agent execution
|
|
21
|
+
- **Observability**: Callback system for monitoring and debugging
|
|
22
|
+
- **Advanced capabilities**: Dynamic workflows, file/directory operations, complex state management
|
|
38
23
|
|
|
39
24
|
|
|
40
25
|
## Installation
|
|
@@ -48,9 +33,11 @@ For optional features:
|
|
|
48
33
|
pip install agentify-core[all] # Installs all optional dependencies
|
|
49
34
|
```
|
|
50
35
|
|
|
51
|
-
|
|
52
|
-
|
|
36
|
+
### Quick Start
|
|
53
37
|
```python
|
|
38
|
+
# Note: Agentify does not auto-load .env. Load it manually if needed.
|
|
39
|
+
# from dotenv import load_dotenv; load_dotenv()
|
|
40
|
+
|
|
54
41
|
from agentify import BaseAgent, AgentConfig, MemoryService, MemoryAddress, tool
|
|
55
42
|
from agentify.memory.stores import InMemoryStore
|
|
56
43
|
|
|
@@ -73,7 +60,8 @@ agent = BaseAgent(
|
|
|
73
60
|
provider="provider",
|
|
74
61
|
model_name="model",
|
|
75
62
|
reasoning_effort="high", # optional param:"low", "medium", "high"
|
|
76
|
-
model_kwargs={"max_completion_tokens": 5000} # Pass model-specific params
|
|
63
|
+
model_kwargs={"max_completion_tokens": 5000}, # Pass model-specific params
|
|
64
|
+
verbose=True, # Controls logging (True by default)
|
|
77
65
|
),
|
|
78
66
|
memory=memory,
|
|
79
67
|
memory_address=addr,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
# Agentify
|
|
2
2
|
|
|
3
|
+
|
|
3
4
|
**Independent AI agent library based on the OpenAI SDK**
|
|
4
5
|
|
|
5
6
|
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.
|
|
@@ -112,4 +113,4 @@ MIT License - see the repository for details.
|
|
|
112
113
|
|
|
113
114
|
## Author
|
|
114
115
|
|
|
115
|
-
**Fabian Melchor** - [fabianmp_98@hotmail.com](mailto:fabianmp_98@hotmail.com)
|
|
116
|
+
**Fabian Melchor** - [fabianmp_98@hotmail.com](mailto:fabianmp_98@hotmail.com)
|
|
@@ -9,8 +9,11 @@ from io import BytesIO
|
|
|
9
9
|
import inspect
|
|
10
10
|
from typing import Any, Dict, Generator, List, Optional, Union, Iterator, Callable, AsyncGenerator
|
|
11
11
|
|
|
12
|
+
from agentify.core.runnable import Runnable
|
|
13
|
+
|
|
12
14
|
from PIL import Image
|
|
13
15
|
from openai import RateLimitError
|
|
16
|
+
from jsonschema import validate, ValidationError
|
|
14
17
|
|
|
15
18
|
from agentify.core.tool import Tool
|
|
16
19
|
from agentify.llm.client import LLMClientFactory, LLMClientType, AsyncLLMClientType
|
|
@@ -22,12 +25,15 @@ from agentify.core.callbacks import LoggingCallbackHandler
|
|
|
22
25
|
logger = logging.getLogger(__name__)
|
|
23
26
|
|
|
24
27
|
|
|
25
|
-
class BaseAgent:
|
|
28
|
+
class BaseAgent(Runnable):
|
|
26
29
|
"""Core AI Agent class based on chat completions interface.
|
|
27
30
|
|
|
28
31
|
BaseAgent is the primary abstraction for building AI agents in Agentify. It provides
|
|
29
32
|
a unified interface for interacting with various LLM providers (OpenAI, Azure, DeepSeek,
|
|
30
33
|
Gemini, etc.) that implement the OpenAI SDK-compatible chat completions format.
|
|
34
|
+
|
|
35
|
+
It implements the Runnable protocol, making it composable within pipelines and teams.
|
|
36
|
+
|
|
31
37
|
|
|
32
38
|
The agent orchestrates the interaction between users, LLMs, and registered tools, managing
|
|
33
39
|
conversation history, tool execution, and model responses. It supports both synchronous
|
|
@@ -100,7 +106,7 @@ class BaseAgent:
|
|
|
100
106
|
|
|
101
107
|
# Decouple callbacks from config to avoid mutation of shared config
|
|
102
108
|
self.callbacks = list(self.config.callbacks) if self.config.callbacks else []
|
|
103
|
-
if not self.callbacks:
|
|
109
|
+
if not self.callbacks and self.config.verbose:
|
|
104
110
|
self.callbacks.append(LoggingCallbackHandler(logger))
|
|
105
111
|
|
|
106
112
|
self._tools: Dict[str, Tool] = {t.name: t for t in tools or []}
|
|
@@ -411,6 +417,38 @@ class BaseAgent:
|
|
|
411
417
|
)
|
|
412
418
|
raise ValueError(f"Invalid JSON arguments: {exc}")
|
|
413
419
|
|
|
420
|
+
def _validate_tool_arguments(self, tool: Tool, arguments: Dict[str, Any]) -> None:
|
|
421
|
+
"""Validate tool arguments against the tool's JSON schema."""
|
|
422
|
+
if not isinstance(arguments, dict):
|
|
423
|
+
raise ValueError(f"Tool '{tool.name}' arguments must be a JSON object.")
|
|
424
|
+
|
|
425
|
+
params_schema = tool.schema.get("parameters") or {"type": "object"}
|
|
426
|
+
if "type" not in params_schema:
|
|
427
|
+
params_schema = {"type": "object", **params_schema}
|
|
428
|
+
|
|
429
|
+
try:
|
|
430
|
+
validate(instance=arguments, schema=params_schema)
|
|
431
|
+
except ValidationError as exc:
|
|
432
|
+
raise ValueError(
|
|
433
|
+
f"Tool '{tool.name}' arguments failed schema validation: {exc.message}"
|
|
434
|
+
) from exc
|
|
435
|
+
|
|
436
|
+
def _serialize_tool_result(self, result: Any) -> str:
|
|
437
|
+
"""Normalize tool results to a JSON string when possible."""
|
|
438
|
+
if isinstance(result, bytes):
|
|
439
|
+
try:
|
|
440
|
+
return result.decode("utf-8")
|
|
441
|
+
except UnicodeDecodeError:
|
|
442
|
+
return base64.b64encode(result).decode("utf-8")
|
|
443
|
+
|
|
444
|
+
if isinstance(result, (dict, list)):
|
|
445
|
+
try:
|
|
446
|
+
return json.dumps(result, ensure_ascii=False)
|
|
447
|
+
except TypeError:
|
|
448
|
+
return json.dumps({"result": str(result)}, ensure_ascii=False)
|
|
449
|
+
|
|
450
|
+
return str(result)
|
|
451
|
+
|
|
414
452
|
def _execute_tool(self, tool_name: str, arguments: Dict[str, Any]) -> str:
|
|
415
453
|
"""Execute a single tool and return its output as a string."""
|
|
416
454
|
tool = self._tools.get(tool_name)
|
|
@@ -425,8 +463,9 @@ class BaseAgent:
|
|
|
425
463
|
return err_msg
|
|
426
464
|
|
|
427
465
|
try:
|
|
466
|
+
self._validate_tool_arguments(tool, arguments)
|
|
428
467
|
result = tool(**arguments)
|
|
429
|
-
result_str =
|
|
468
|
+
result_str = self._serialize_tool_result(result)
|
|
430
469
|
for cb in self.callbacks:
|
|
431
470
|
cb.on_tool_finish(tool_name, result_str)
|
|
432
471
|
return result_str
|
|
@@ -656,7 +695,7 @@ class BaseAgent:
|
|
|
656
695
|
assembled_tool_calls = self._expand_tool_calls(assembled_tool_calls)
|
|
657
696
|
full_turn_content = "".join(current_turn_content_parts)
|
|
658
697
|
|
|
659
|
-
#
|
|
698
|
+
# Exit if no tool calls are present
|
|
660
699
|
if not assembled_tool_calls:
|
|
661
700
|
# Add reasoning to metadata if present
|
|
662
701
|
msg_kwargs = {}
|
|
@@ -721,6 +760,7 @@ class BaseAgent:
|
|
|
721
760
|
addr: Optional[MemoryAddress] = None,
|
|
722
761
|
image_path: Optional[str] = None,
|
|
723
762
|
image_detail_override: Optional[str] = None,
|
|
763
|
+
**kwargs: Any,
|
|
724
764
|
) -> Union[str, Generator[str, None, None]]:
|
|
725
765
|
"""Main entrypoint to interact with the agent.
|
|
726
766
|
|
|
@@ -729,10 +769,15 @@ class BaseAgent:
|
|
|
729
769
|
addr: The memory address for the conversation.
|
|
730
770
|
image_path: Optional path to an image file.
|
|
731
771
|
image_detail_override: Optional detail level for image processing.
|
|
772
|
+
**kwargs: Additional arguments for compatibility.
|
|
732
773
|
|
|
733
774
|
Returns:
|
|
734
775
|
The agent's response as a string or a generator if streaming is enabled.
|
|
735
776
|
"""
|
|
777
|
+
# If addr is not provided, try to get it from kwargs (Protocol compatibility)
|
|
778
|
+
if addr is None and "memory_address" in kwargs:
|
|
779
|
+
addr = kwargs["memory_address"]
|
|
780
|
+
|
|
736
781
|
a = self._addr_or_raise(addr)
|
|
737
782
|
response_generator = self._execute_agent_loop(
|
|
738
783
|
user_input,
|
|
@@ -852,6 +897,7 @@ class BaseAgent:
|
|
|
852
897
|
return err_msg
|
|
853
898
|
|
|
854
899
|
try:
|
|
900
|
+
self._validate_tool_arguments(tool, arguments)
|
|
855
901
|
# Check for async_func attribute (used by AgentTool, FlowTool, SpawnAgentTool)
|
|
856
902
|
if hasattr(tool, "async_func") and asyncio.iscoroutinefunction(tool.async_func):
|
|
857
903
|
result = await tool.async_func(**arguments)
|
|
@@ -863,7 +909,7 @@ class BaseAgent:
|
|
|
863
909
|
result = await asyncio.get_event_loop().run_in_executor(
|
|
864
910
|
None, lambda: tool(**arguments)
|
|
865
911
|
)
|
|
866
|
-
result_str =
|
|
912
|
+
result_str = self._serialize_tool_result(result)
|
|
867
913
|
for cb in self.callbacks:
|
|
868
914
|
cb.on_tool_finish(tool_name, result_str)
|
|
869
915
|
return result_str
|
|
@@ -1004,7 +1050,7 @@ class BaseAgent:
|
|
|
1004
1050
|
assembled_tool_calls = self._expand_tool_calls(assembled_tool_calls)
|
|
1005
1051
|
full_turn_content = "".join(current_turn_content_parts)
|
|
1006
1052
|
|
|
1007
|
-
#
|
|
1053
|
+
# Exit if no tool calls are present
|
|
1008
1054
|
if not assembled_tool_calls:
|
|
1009
1055
|
msg_kwargs = {}
|
|
1010
1056
|
if full_reasoning_content:
|
|
@@ -1032,7 +1078,13 @@ class BaseAgent:
|
|
|
1032
1078
|
args_str = tc["function"]["arguments"]
|
|
1033
1079
|
try:
|
|
1034
1080
|
args = self._parse_tool_arguments(tool_name, args_str)
|
|
1035
|
-
|
|
1081
|
+
# Add timeout to prevent indefinite hangs
|
|
1082
|
+
result_content = await asyncio.wait_for(
|
|
1083
|
+
self._aexecute_tool(tool_name, args),
|
|
1084
|
+
timeout=60.0 # Default 60s timeout for tools
|
|
1085
|
+
)
|
|
1086
|
+
except asyncio.TimeoutError:
|
|
1087
|
+
result_content = json.dumps({"error": f"Tool '{tool_name}' execution timed out after 60 seconds."})
|
|
1036
1088
|
except ValueError as e:
|
|
1037
1089
|
result_content = json.dumps({"error": str(e)})
|
|
1038
1090
|
return tool_call_id, tool_name, result_content
|
|
@@ -1071,6 +1123,7 @@ class BaseAgent:
|
|
|
1071
1123
|
addr: Optional[MemoryAddress] = None,
|
|
1072
1124
|
image_path: Optional[str] = None,
|
|
1073
1125
|
image_detail_override: Optional[str] = None,
|
|
1126
|
+
**kwargs: Any,
|
|
1074
1127
|
) -> Union[str, AsyncGenerator[str, None]]:
|
|
1075
1128
|
"""Async entrypoint to interact with the agent.
|
|
1076
1129
|
|
|
@@ -1083,11 +1136,17 @@ class BaseAgent:
|
|
|
1083
1136
|
addr: The memory address for the conversation.
|
|
1084
1137
|
image_path: Optional path to an image file.
|
|
1085
1138
|
image_detail_override: Optional detail level for image processing.
|
|
1139
|
+
**kwargs: Additional arguments for compatibility.
|
|
1086
1140
|
|
|
1087
1141
|
Returns:
|
|
1088
1142
|
The agent's response as a string or an async generator if streaming is enabled.
|
|
1089
1143
|
"""
|
|
1144
|
+
# If addr is not provided, try to get it from kwargs (Protocol compatibility)
|
|
1145
|
+
if addr is None and "memory_address" in kwargs:
|
|
1146
|
+
addr = kwargs["memory_address"]
|
|
1147
|
+
|
|
1090
1148
|
a = self._addr_or_raise(addr)
|
|
1149
|
+
|
|
1091
1150
|
response_generator = self._aexecute_agent_loop(
|
|
1092
1151
|
user_input,
|
|
1093
1152
|
addr=a,
|
|
@@ -50,20 +50,48 @@ class LoggingCallbackHandler(AgentCallbackHandler):
|
|
|
50
50
|
|
|
51
51
|
def __init__(self, logger_instance: Optional[logging.Logger] = None):
|
|
52
52
|
self.logger = logger_instance or logger
|
|
53
|
+
self.redact_keys = {"password", "api_key", "token", "secret", "key", "authorization"}
|
|
54
|
+
|
|
55
|
+
# Auto-configure handler if none exists
|
|
56
|
+
if not self.logger.handlers:
|
|
57
|
+
handler = logging.StreamHandler()
|
|
58
|
+
handler.setLevel(logging.INFO)
|
|
59
|
+
self.logger.addHandler(handler)
|
|
60
|
+
self.logger.setLevel(logging.INFO)
|
|
61
|
+
|
|
62
|
+
def _mask_secrets(self, data: Dict[str, Any]) -> Dict[str, Any]:
|
|
63
|
+
"""Recursively mask sensitive keys in a dictionary."""
|
|
64
|
+
masked = {}
|
|
65
|
+
for k, v in data.items():
|
|
66
|
+
if k.lower() in self.redact_keys:
|
|
67
|
+
masked[k] = "******"
|
|
68
|
+
elif isinstance(v, dict):
|
|
69
|
+
masked[k] = self._mask_secrets(v)
|
|
70
|
+
else:
|
|
71
|
+
masked[k] = v
|
|
72
|
+
return masked
|
|
53
73
|
|
|
54
74
|
def on_agent_start(self, agent_name: str, user_input: str) -> None:
|
|
55
|
-
self.logger.info(
|
|
75
|
+
self.logger.info(
|
|
76
|
+
f"{Colors.BLUE}[VERBOSE] Agent '{agent_name}' started.{Colors.RESET} Input: {user_input[:100]}..."
|
|
77
|
+
)
|
|
56
78
|
|
|
57
79
|
def on_agent_finish(self, agent_name: str, response: str) -> None:
|
|
58
80
|
self.logger.info(
|
|
59
|
-
f"Agent '{agent_name}' finished. Response: {response[:100]}..."
|
|
81
|
+
f"{Colors.BLUE}[VERBOSE] Agent '{agent_name}' finished.{Colors.RESET} Response: {response[:100]}..."
|
|
60
82
|
)
|
|
61
83
|
|
|
62
84
|
def on_tool_start(self, tool_name: str, args: Dict[str, Any]) -> None:
|
|
63
|
-
|
|
85
|
+
# Redact sensitive values
|
|
86
|
+
safe_args = self._mask_secrets(args)
|
|
87
|
+
self.logger.info(
|
|
88
|
+
f"{Colors.CYAN}[VERBOSE] Tool '{tool_name}' started.{Colors.RESET} Args: {safe_args}"
|
|
89
|
+
)
|
|
64
90
|
|
|
65
91
|
def on_tool_finish(self, tool_name: str, output: str) -> None:
|
|
66
|
-
self.logger.info(
|
|
92
|
+
self.logger.info(
|
|
93
|
+
f"{Colors.CYAN}[VERBOSE] Tool '{tool_name}' finished.{Colors.RESET} Output: {output[:100]}..."
|
|
94
|
+
)
|
|
67
95
|
|
|
68
96
|
def on_llm_start(self, model_name: str, messages: List[Dict[str, Any]]) -> None:
|
|
69
97
|
self.logger.debug(f"LLM '{model_name}' started. Messages: {len(messages)}")
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
from typing import Protocol, Any, Dict, Optional, Generator, AsyncGenerator, Union
|
|
2
|
+
|
|
3
|
+
class Runnable(Protocol):
|
|
4
|
+
"""Standard interface for any chainable unit/agent in Agentify."""
|
|
5
|
+
|
|
6
|
+
def run(
|
|
7
|
+
self,
|
|
8
|
+
user_input: str,
|
|
9
|
+
**kwargs: Any
|
|
10
|
+
) -> Union[str, Generator[str, None, None]]:
|
|
11
|
+
"""Synchronous execution."""
|
|
12
|
+
...
|
|
13
|
+
|
|
14
|
+
async def arun(
|
|
15
|
+
self,
|
|
16
|
+
user_input: str,
|
|
17
|
+
**kwargs: Any
|
|
18
|
+
) -> Union[str, AsyncGenerator[str, None]]:
|
|
19
|
+
"""Asynchronous execution."""
|
|
20
|
+
...
|
|
@@ -19,10 +19,10 @@ class Tool:
|
|
|
19
19
|
def name(self) -> str:
|
|
20
20
|
return self.schema["name"]
|
|
21
21
|
|
|
22
|
-
def __call__(self, **kwargs: Any) -> str:
|
|
22
|
+
def __call__(self, *args: Any, **kwargs: Any) -> str:
|
|
23
23
|
"""Executes the function and returns JSON or string; captures generic errors."""
|
|
24
24
|
try:
|
|
25
|
-
result = self.func(**kwargs)
|
|
25
|
+
result = self.func(*args, **kwargs)
|
|
26
26
|
except Exception as exc: # noqa: BLE001
|
|
27
27
|
return json.dumps({"error": str(exc)}, ensure_ascii=False)
|
|
28
28
|
|
|
@@ -2,24 +2,32 @@ import os
|
|
|
2
2
|
from typing import Any, Dict, List, Optional
|
|
3
3
|
from agentify.core.tool import Tool
|
|
4
4
|
|
|
5
|
+
DEFAULT_MAX_READ_BYTES = 1024 * 1024
|
|
6
|
+
HARD_MAX_READ_BYTES = 5 * 1024 * 1024
|
|
7
|
+
|
|
5
8
|
class BaseFilesystemTool(Tool):
|
|
6
9
|
"""Base class for filesystem tools with sandbox security."""
|
|
7
10
|
|
|
8
11
|
def __init__(self, schema: Dict[str, Any], func: Any, sandbox_dir: Optional[str] = None):
|
|
9
12
|
super().__init__(schema, func)
|
|
10
|
-
#
|
|
11
|
-
# but for this agent library, let's default to CWD but allow override.
|
|
13
|
+
# Default to current working directory if no sandbox is provided.
|
|
12
14
|
self.sandbox_dir = os.path.abspath(sandbox_dir or os.getcwd())
|
|
13
15
|
|
|
14
16
|
def _validate_path(self, file_path: str) -> str:
|
|
15
17
|
"""Ensure path is within sandbox."""
|
|
16
|
-
#
|
|
17
|
-
|
|
18
|
+
# Resolve user path relative to sandbox.
|
|
19
|
+
# Note: os.path.join discards sandbox_dir if file_path is absolute.
|
|
20
|
+
full_path = os.path.join(self.sandbox_dir, file_path)
|
|
18
21
|
|
|
19
|
-
|
|
20
|
-
|
|
22
|
+
# Resolve symlinks and .. components
|
|
23
|
+
real_path = os.path.realpath(full_path)
|
|
24
|
+
real_sandbox = os.path.realpath(self.sandbox_dir)
|
|
25
|
+
|
|
26
|
+
# Check if the resolved path starts with the resolved sandbox path
|
|
27
|
+
if os.path.commonpath([real_sandbox, real_path]) != real_sandbox:
|
|
28
|
+
raise ValueError(f"Access denied: Path '{file_path}' resolves to '{real_path}', which is outside sandbox '{real_sandbox}'")
|
|
21
29
|
|
|
22
|
-
return
|
|
30
|
+
return real_path
|
|
23
31
|
|
|
24
32
|
|
|
25
33
|
class ListDirTool(BaseFilesystemTool):
|
|
@@ -70,21 +78,38 @@ class ReadFileTool(BaseFilesystemTool):
|
|
|
70
78
|
"file_path": {
|
|
71
79
|
"type": "string",
|
|
72
80
|
"description": "Path to the file to read.",
|
|
73
|
-
}
|
|
81
|
+
},
|
|
82
|
+
"max_bytes": {
|
|
83
|
+
"type": "integer",
|
|
84
|
+
"description": "Maximum bytes to read from the file (hard-capped).",
|
|
85
|
+
},
|
|
74
86
|
},
|
|
75
87
|
"required": ["file_path"],
|
|
76
88
|
},
|
|
77
89
|
}
|
|
78
90
|
super().__init__(schema, self._read_file, sandbox_dir)
|
|
79
91
|
|
|
80
|
-
def _read_file(self, file_path: str) -> str:
|
|
92
|
+
def _read_file(self, file_path: str, max_bytes: Optional[int] = None) -> str:
|
|
81
93
|
try:
|
|
82
94
|
target_path = self._validate_path(file_path)
|
|
83
95
|
if not os.path.exists(target_path):
|
|
84
96
|
return f"Error: File '{file_path}' does not exist."
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
return
|
|
97
|
+
|
|
98
|
+
if max_bytes is not None and max_bytes <= 0:
|
|
99
|
+
return "Error: 'max_bytes' must be a positive integer."
|
|
100
|
+
|
|
101
|
+
read_limit = min(max_bytes or DEFAULT_MAX_READ_BYTES, HARD_MAX_READ_BYTES)
|
|
102
|
+
|
|
103
|
+
with open(target_path, "rb") as f:
|
|
104
|
+
content = f.read(read_limit + 1)
|
|
105
|
+
|
|
106
|
+
truncated = len(content) > read_limit
|
|
107
|
+
text = content[:read_limit].decode("utf-8", errors="replace")
|
|
108
|
+
|
|
109
|
+
if truncated:
|
|
110
|
+
return f"{text}\n[Truncated to {read_limit} bytes]"
|
|
111
|
+
|
|
112
|
+
return text
|
|
88
113
|
except Exception as e:
|
|
89
114
|
return f"Error reading file: {str(e)}"
|
|
90
115
|
|
|
@@ -3,7 +3,7 @@ from dotenv import load_dotenv
|
|
|
3
3
|
from typing import Union, Dict, Any, Optional, Callable
|
|
4
4
|
from openai import OpenAI, AzureOpenAI, AsyncOpenAI, AsyncAzureOpenAI
|
|
5
5
|
|
|
6
|
-
load_dotenv()
|
|
6
|
+
# load_dotenv() # Removed to avoid side effects on import
|
|
7
7
|
|
|
8
8
|
LLMClientType = Union[OpenAI, AzureOpenAI]
|
|
9
9
|
AsyncLLMClientType = Union[AsyncOpenAI, AsyncAzureOpenAI]
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""Adapter to convert MCP tools into Agentify Tool objects."""
|
|
2
|
+
from typing import Any, Callable, List
|
|
3
|
+
from mcp import ClientSession
|
|
4
|
+
from agentify.core.tool import Tool
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
async def convert_mcp_tools_to_agentify(
|
|
8
|
+
session: ClientSession,
|
|
9
|
+
mcp_tools: List[Any],
|
|
10
|
+
) -> List[Tool]:
|
|
11
|
+
"""Transforms MCP tools into Agentify-compatible Tool objects."""
|
|
12
|
+
agentify_tools: List[Tool] = []
|
|
13
|
+
|
|
14
|
+
for m_tool in mcp_tools:
|
|
15
|
+
schema = {
|
|
16
|
+
"name": m_tool.name,
|
|
17
|
+
"description": m_tool.description or "",
|
|
18
|
+
"parameters": m_tool.inputSchema,
|
|
19
|
+
}
|
|
20
|
+
wrapper = _create_tool_wrapper(session, m_tool.name)
|
|
21
|
+
agentify_tools.append(Tool(schema=schema, func=wrapper))
|
|
22
|
+
|
|
23
|
+
return agentify_tools
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _create_tool_wrapper(session: ClientSession, tool_name: str) -> Callable[..., Any]:
|
|
27
|
+
"""Creates an async wrapper function that calls the MCP server."""
|
|
28
|
+
|
|
29
|
+
async def _mcp_tool_wrapper(**kwargs: Any) -> Any:
|
|
30
|
+
result = await session.call_tool(tool_name, arguments=kwargs)
|
|
31
|
+
|
|
32
|
+
output_parts = []
|
|
33
|
+
if result.content:
|
|
34
|
+
for item in result.content:
|
|
35
|
+
if item.type == "text":
|
|
36
|
+
output_parts.append(item.text)
|
|
37
|
+
elif item.type == "image":
|
|
38
|
+
output_parts.append(f"[Image: {item.mimeType}]")
|
|
39
|
+
elif item.type == "resource":
|
|
40
|
+
output_parts.append(f"[Resource: {item.resource.uri}]")
|
|
41
|
+
|
|
42
|
+
return "\n".join(output_parts)
|
|
43
|
+
|
|
44
|
+
_mcp_tool_wrapper.__name__ = tool_name
|
|
45
|
+
_mcp_tool_wrapper.__doc__ = f"MCP Tool: {tool_name}"
|
|
46
|
+
return _mcp_tool_wrapper
|