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.
Files changed (58) hide show
  1. {agentify_core-0.2.0/agentify_core.egg-info → agentify_core-0.3.1}/PKG-INFO +15 -7
  2. {agentify_core-0.2.0 → agentify_core-0.3.1}/README.md +20 -32
  3. {agentify_core-0.2.0 → agentify_core-0.3.1}/README_PYPI.md +2 -1
  4. {agentify_core-0.2.0 → agentify_core-0.3.1}/agentify/__init__.py +1 -1
  5. {agentify_core-0.2.0 → agentify_core-0.3.1}/agentify/core/agent.py +66 -7
  6. {agentify_core-0.2.0 → agentify_core-0.3.1}/agentify/core/callbacks.py +32 -4
  7. {agentify_core-0.2.0 → agentify_core-0.3.1}/agentify/core/config.py +1 -0
  8. agentify_core-0.3.1/agentify/core/runnable.py +20 -0
  9. {agentify_core-0.2.0 → agentify_core-0.3.1}/agentify/core/tool.py +2 -2
  10. {agentify_core-0.2.0 → agentify_core-0.3.1}/agentify/extensions/tools/filesystem.py +37 -12
  11. {agentify_core-0.2.0 → agentify_core-0.3.1}/agentify/llm/client.py +1 -1
  12. agentify_core-0.3.1/agentify/mcp/__init__.py +3 -0
  13. agentify_core-0.3.1/agentify/mcp/adapter.py +46 -0
  14. agentify_core-0.3.1/agentify/mcp/client.py +141 -0
  15. {agentify_core-0.2.0 → agentify_core-0.3.1}/agentify/memory/interfaces.py +23 -9
  16. {agentify_core-0.2.0 → agentify_core-0.3.1}/agentify/memory/service.py +31 -12
  17. agentify_core-0.3.1/agentify/memory/stores/__init__.py +10 -0
  18. agentify_core-0.3.1/agentify/memory/stores/elastic_store.py +246 -0
  19. {agentify_core-0.2.0 → agentify_core-0.3.1}/agentify/memory/stores/in_memory_store.py +6 -0
  20. {agentify_core-0.2.0 → agentify_core-0.3.1}/agentify/memory/stores/redis_store.py +48 -0
  21. agentify_core-0.3.1/agentify/memory/stores/sqlite_store.py +199 -0
  22. {agentify_core-0.2.0 → agentify_core-0.3.1}/agentify/multi_agent/hierarchical.py +7 -2
  23. {agentify_core-0.2.0 → agentify_core-0.3.1}/agentify/multi_agent/pipeline.py +32 -35
  24. {agentify_core-0.2.0 → agentify_core-0.3.1}/agentify/multi_agent/team.py +20 -11
  25. {agentify_core-0.2.0 → agentify_core-0.3.1}/agentify/multi_agent/tool_wrapper.py +6 -3
  26. {agentify_core-0.2.0 → agentify_core-0.3.1/agentify_core.egg-info}/PKG-INFO +15 -7
  27. {agentify_core-0.2.0 → agentify_core-0.3.1}/agentify_core.egg-info/SOURCES.txt +11 -1
  28. {agentify_core-0.2.0 → agentify_core-0.3.1}/agentify_core.egg-info/requires.txt +12 -3
  29. {agentify_core-0.2.0 → agentify_core-0.3.1}/pyproject.toml +18 -7
  30. agentify_core-0.3.1/requirements.txt +27 -0
  31. {agentify_core-0.2.0 → agentify_core-0.3.1}/tests/test_filesystem_tools.py +15 -1
  32. agentify_core-0.3.1/tests/test_mcp.py +124 -0
  33. agentify_core-0.3.1/tests/test_memory_address.py +26 -0
  34. agentify_core-0.3.1/tests/test_memory_logging.py +58 -0
  35. {agentify_core-0.2.0 → agentify_core-0.3.1}/tests/test_planning_tool.py +1 -1
  36. agentify_core-0.3.1/tests/test_verify_hooks.py +69 -0
  37. agentify_core-0.2.0/agentify/memory/stores/__init__.py +0 -5
  38. agentify_core-0.2.0/requirements.txt +0 -18
  39. {agentify_core-0.2.0 → agentify_core-0.3.1}/LICENSE +0 -0
  40. {agentify_core-0.2.0 → agentify_core-0.3.1}/MANIFEST.in +0 -0
  41. {agentify_core-0.2.0 → agentify_core-0.3.1}/agentify/core/__init__.py +0 -0
  42. {agentify_core-0.2.0 → agentify_core-0.3.1}/agentify/extensions/__init__.py +0 -0
  43. {agentify_core-0.2.0 → agentify_core-0.3.1}/agentify/extensions/prompts/__init__.py +0 -0
  44. {agentify_core-0.2.0 → agentify_core-0.3.1}/agentify/extensions/prompts/assistant.py +0 -0
  45. {agentify_core-0.2.0 → agentify_core-0.3.1}/agentify/extensions/tools/__init__.py +0 -0
  46. {agentify_core-0.2.0 → agentify_core-0.3.1}/agentify/extensions/tools/calculator.py +0 -0
  47. {agentify_core-0.2.0 → agentify_core-0.3.1}/agentify/extensions/tools/planning.py +0 -0
  48. {agentify_core-0.2.0 → agentify_core-0.3.1}/agentify/extensions/tools/time.py +0 -0
  49. {agentify_core-0.2.0 → agentify_core-0.3.1}/agentify/extensions/tools/weather.py +0 -0
  50. {agentify_core-0.2.0 → agentify_core-0.3.1}/agentify/llm/__init__.py +0 -0
  51. {agentify_core-0.2.0 → agentify_core-0.3.1}/agentify/memory/__init__.py +0 -0
  52. {agentify_core-0.2.0 → agentify_core-0.3.1}/agentify/memory/policies.py +0 -0
  53. {agentify_core-0.2.0 → agentify_core-0.3.1}/agentify/multi_agent/__init__.py +0 -0
  54. {agentify_core-0.2.0 → agentify_core-0.3.1}/agentify/utils/__init__.py +0 -0
  55. {agentify_core-0.2.0 → agentify_core-0.3.1}/agentify/utils/style.py +0 -0
  56. {agentify_core-0.2.0 → agentify_core-0.3.1}/agentify_core.egg-info/dependency_links.txt +0 -0
  57. {agentify_core-0.2.0 → agentify_core-0.3.1}/agentify_core.egg-info/top_level.txt +0 -0
  58. {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.2.0
3
+ Version: 0.3.1
4
4
  Summary: Framework-agnostic AI agent library for building single and multi-agent systems
5
- Author-email: Fabian M <fabian@example.com>
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>=1.0.0
26
- Requires-Dist: python-dotenv>=0.19.0
27
- Requires-Dist: Pillow>=9.0.0
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
- **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
-
3
+ [![PyPI version](https://img.shields.io/pypi/v/agentify-core?color=orange)](https://pypi.org/project/agentify-core/)
4
+ [![Downloads](https://img.shields.io/pepy/dt/agentify-core)](https://pepy.tech/project/agentify-core)
5
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
6
+ [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/agentify-core)](https://pypi.org/project/agentify-core/)
7
7
 
8
- ## Why Agentify?
8
+ **Independent AI agent library based on the OpenAI SDK**
9
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.
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
- - **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
- 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
- ## Quick Start
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)
@@ -11,7 +11,7 @@ from agentify.memory.service import MemoryService
11
11
  from agentify.memory.interfaces import MemoryAddress
12
12
  from agentify.memory.policies import MemoryPolicy
13
13
 
14
- __version__ = "0.2.0"
14
+ __version__ = "0.3.1"
15
15
 
16
16
  __all__ = [
17
17
  "BaseAgent",
@@ -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 = str(result)
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
- # If no tool calls, we are done
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 = str(result)
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
- # If no tool calls, we are done
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
- result_content = await self._aexecute_tool(tool_name, args)
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(f"Agent '{agent_name}' started. Input: {user_input[:100]}...")
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
- self.logger.info(f"Tool '{tool_name}' started. Args: {args}")
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(f"Tool '{tool_name}' finished. Output: {output[:100]}...")
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)}")
@@ -21,6 +21,7 @@ class AgentConfig:
21
21
  timeout: int = 60
22
22
  stream: bool = False
23
23
  max_retries: int = 3
24
+ verbose: bool = True
24
25
  max_tool_iter: Optional[int] = 10
25
26
  reasoning_effort: Optional[str] = None
26
27
  model_kwargs: Optional[Dict[str, Any]] = None
@@ -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
- # 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.
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
- # 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
+ # 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
- if not abs_path.startswith(self.sandbox_dir):
20
- raise ValueError(f"Access denied: Path '{file_path}' is outside sandbox directory '{self.sandbox_dir}'")
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 abs_path
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
- with open(target_path, "r", encoding="utf-8") as f:
87
- return f.read()
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,3 @@
1
+ from agentify.mcp.client import MCPConnection
2
+
3
+ __all__ = ["MCPConnection"]
@@ -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