agentify-core 0.2.0__tar.gz → 0.3.0__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 (55) hide show
  1. {agentify_core-0.2.0/agentify_core.egg-info → agentify_core-0.3.0}/PKG-INFO +13 -7
  2. {agentify_core-0.2.0 → agentify_core-0.3.0}/README.md +14 -29
  3. {agentify_core-0.2.0 → agentify_core-0.3.0}/README_PYPI.md +2 -1
  4. {agentify_core-0.2.0 → agentify_core-0.3.0}/agentify/__init__.py +1 -1
  5. agentify_core-0.3.0/agentify/mcp/__init__.py +3 -0
  6. agentify_core-0.3.0/agentify/mcp/adapter.py +46 -0
  7. agentify_core-0.3.0/agentify/mcp/client.py +141 -0
  8. {agentify_core-0.2.0 → agentify_core-0.3.0}/agentify/memory/interfaces.py +2 -2
  9. {agentify_core-0.2.0 → agentify_core-0.3.0}/agentify/memory/service.py +5 -1
  10. agentify_core-0.3.0/agentify/memory/stores/__init__.py +10 -0
  11. agentify_core-0.3.0/agentify/memory/stores/elastic_store.py +244 -0
  12. {agentify_core-0.2.0 → agentify_core-0.3.0}/agentify/memory/stores/in_memory_store.py +6 -0
  13. {agentify_core-0.2.0 → agentify_core-0.3.0}/agentify/memory/stores/redis_store.py +45 -0
  14. agentify_core-0.3.0/agentify/memory/stores/sqlite_store.py +196 -0
  15. {agentify_core-0.2.0 → agentify_core-0.3.0/agentify_core.egg-info}/PKG-INFO +13 -7
  16. {agentify_core-0.2.0 → agentify_core-0.3.0}/agentify_core.egg-info/SOURCES.txt +8 -1
  17. {agentify_core-0.2.0 → agentify_core-0.3.0}/agentify_core.egg-info/requires.txt +11 -3
  18. {agentify_core-0.2.0 → agentify_core-0.3.0}/pyproject.toml +16 -7
  19. agentify_core-0.3.0/requirements.txt +26 -0
  20. agentify_core-0.3.0/tests/test_mcp.py +124 -0
  21. agentify_core-0.3.0/tests/test_verify_hooks.py +69 -0
  22. agentify_core-0.2.0/agentify/memory/stores/__init__.py +0 -5
  23. agentify_core-0.2.0/requirements.txt +0 -18
  24. {agentify_core-0.2.0 → agentify_core-0.3.0}/LICENSE +0 -0
  25. {agentify_core-0.2.0 → agentify_core-0.3.0}/MANIFEST.in +0 -0
  26. {agentify_core-0.2.0 → agentify_core-0.3.0}/agentify/core/__init__.py +0 -0
  27. {agentify_core-0.2.0 → agentify_core-0.3.0}/agentify/core/agent.py +0 -0
  28. {agentify_core-0.2.0 → agentify_core-0.3.0}/agentify/core/callbacks.py +0 -0
  29. {agentify_core-0.2.0 → agentify_core-0.3.0}/agentify/core/config.py +0 -0
  30. {agentify_core-0.2.0 → agentify_core-0.3.0}/agentify/core/tool.py +0 -0
  31. {agentify_core-0.2.0 → agentify_core-0.3.0}/agentify/extensions/__init__.py +0 -0
  32. {agentify_core-0.2.0 → agentify_core-0.3.0}/agentify/extensions/prompts/__init__.py +0 -0
  33. {agentify_core-0.2.0 → agentify_core-0.3.0}/agentify/extensions/prompts/assistant.py +0 -0
  34. {agentify_core-0.2.0 → agentify_core-0.3.0}/agentify/extensions/tools/__init__.py +0 -0
  35. {agentify_core-0.2.0 → agentify_core-0.3.0}/agentify/extensions/tools/calculator.py +0 -0
  36. {agentify_core-0.2.0 → agentify_core-0.3.0}/agentify/extensions/tools/filesystem.py +0 -0
  37. {agentify_core-0.2.0 → agentify_core-0.3.0}/agentify/extensions/tools/planning.py +0 -0
  38. {agentify_core-0.2.0 → agentify_core-0.3.0}/agentify/extensions/tools/time.py +0 -0
  39. {agentify_core-0.2.0 → agentify_core-0.3.0}/agentify/extensions/tools/weather.py +0 -0
  40. {agentify_core-0.2.0 → agentify_core-0.3.0}/agentify/llm/__init__.py +0 -0
  41. {agentify_core-0.2.0 → agentify_core-0.3.0}/agentify/llm/client.py +0 -0
  42. {agentify_core-0.2.0 → agentify_core-0.3.0}/agentify/memory/__init__.py +0 -0
  43. {agentify_core-0.2.0 → agentify_core-0.3.0}/agentify/memory/policies.py +0 -0
  44. {agentify_core-0.2.0 → agentify_core-0.3.0}/agentify/multi_agent/__init__.py +0 -0
  45. {agentify_core-0.2.0 → agentify_core-0.3.0}/agentify/multi_agent/hierarchical.py +0 -0
  46. {agentify_core-0.2.0 → agentify_core-0.3.0}/agentify/multi_agent/pipeline.py +0 -0
  47. {agentify_core-0.2.0 → agentify_core-0.3.0}/agentify/multi_agent/team.py +0 -0
  48. {agentify_core-0.2.0 → agentify_core-0.3.0}/agentify/multi_agent/tool_wrapper.py +0 -0
  49. {agentify_core-0.2.0 → agentify_core-0.3.0}/agentify/utils/__init__.py +0 -0
  50. {agentify_core-0.2.0 → agentify_core-0.3.0}/agentify/utils/style.py +0 -0
  51. {agentify_core-0.2.0 → agentify_core-0.3.0}/agentify_core.egg-info/dependency_links.txt +0 -0
  52. {agentify_core-0.2.0 → agentify_core-0.3.0}/agentify_core.egg-info/top_level.txt +0 -0
  53. {agentify_core-0.2.0 → agentify_core-0.3.0}/setup.cfg +0 -0
  54. {agentify_core-0.2.0 → agentify_core-0.3.0}/tests/test_filesystem_tools.py +0 -0
  55. {agentify_core-0.2.0 → agentify_core-0.3.0}/tests/test_planning_tool.py +0 -0
@@ -1,19 +1,18 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agentify-core
3
- Version: 0.2.0
3
+ Version: 0.3.0
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-email: Fabian M <fabianmp_98@hotmail.com>
6
6
  License: MIT
7
7
  Project-URL: Homepage, https://github.com/fa8i/Agentify
8
8
  Project-URL: Repository, https://github.com/fa8i/Agentify
9
9
  Project-URL: Bug Tracker, https://github.com/fa8i/Agentify/issues
10
- Keywords: agent,multi-agent,ai,llm,openai,framework
10
+ Keywords: agentify,agentify-core,agent,multi-agent,ai,llm,openai,framework
11
11
  Classifier: Development Status :: 3 - Alpha
12
12
  Classifier: Intended Audience :: Developers
13
13
  Classifier: License :: OSI Approved :: MIT License
14
14
  Classifier: Operating System :: OS Independent
15
15
  Classifier: Programming Language :: Python :: 3
16
- Classifier: Programming Language :: Python :: 3.9
17
16
  Classifier: Programming Language :: Python :: 3.10
18
17
  Classifier: Programming Language :: Python :: 3.11
19
18
  Classifier: Programming Language :: Python :: 3.12
@@ -22,19 +21,25 @@ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
22
21
  Requires-Python: >=3.10
23
22
  Description-Content-Type: text/markdown
24
23
  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
24
+ Requires-Dist: openai
25
+ Requires-Dist: python-dotenv
26
+ Requires-Dist: Pillow
28
27
  Provides-Extra: redis
29
28
  Requires-Dist: redis>=4.0.0; extra == "redis"
29
+ Provides-Extra: elastic
30
+ Requires-Dist: elasticsearch>=8.0.0; extra == "elastic"
30
31
  Provides-Extra: tools
31
32
  Requires-Dist: requests>=2.25.0; extra == "tools"
33
+ Provides-Extra: mcp
34
+ Requires-Dist: mcp; extra == "mcp"
32
35
  Provides-Extra: ui
33
36
  Requires-Dist: gradio==5.49.1; extra == "ui"
34
37
  Provides-Extra: all
35
38
  Requires-Dist: redis>=4.0.0; extra == "all"
39
+ Requires-Dist: elasticsearch>=8.0.0; extra == "all"
36
40
  Requires-Dist: requests>=2.25.0; extra == "all"
37
41
  Requires-Dist: gradio==5.49.1; extra == "all"
42
+ Requires-Dist: mcp; extra == "all"
38
43
  Provides-Extra: dev
39
44
  Requires-Dist: pytest>=7.0.0; extra == "dev"
40
45
  Requires-Dist: pytest-cov>=4.0.0; extra == "dev"
@@ -47,6 +52,7 @@ Dynamic: license-file
47
52
 
48
53
  # Agentify
49
54
 
55
+
50
56
  **Independent AI agent library based on the OpenAI SDK**
51
57
 
52
58
  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
@@ -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.0"
15
15
 
16
16
  __all__ = [
17
17
  "BaseAgent",
@@ -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
@@ -0,0 +1,141 @@
1
+ """Async Context Manager for MCP Client connections."""
2
+ import os
3
+ from enum import Enum, auto
4
+ from typing import Any, Dict, List, Optional
5
+ from contextlib import AbstractAsyncContextManager, AsyncExitStack
6
+
7
+ from mcp import ClientSession, StdioServerParameters
8
+ from mcp.client.stdio import stdio_client
9
+ from mcp.client.sse import sse_client
10
+
11
+ from agentify.mcp.adapter import convert_mcp_tools_to_agentify
12
+ from agentify.core.tool import Tool
13
+
14
+
15
+ class _Transport(Enum):
16
+ """Internal enum for transport type."""
17
+ STDIO = auto()
18
+ SSE = auto()
19
+
20
+
21
+ class MCPConnection(AbstractAsyncContextManager):
22
+ """Manages MCP server connections via StdIO or SSE transport.
23
+
24
+ Use the factory methods to create connections:
25
+ - `MCPConnection.stdio(...)` for local process servers
26
+ - `MCPConnection.sse(...)` for remote HTTP/SSE servers
27
+
28
+ Example (StdIO):
29
+ async with MCPConnection.stdio(command="uvx", args=["mcp-server-fetch"]) as mcp:
30
+ tools = await mcp.get_tools()
31
+
32
+ Example (SSE):
33
+ async with MCPConnection.sse(url="http://localhost:8080/sse") as mcp:
34
+ tools = await mcp.get_tools()
35
+ """
36
+
37
+ def __init__(self) -> None:
38
+ """Private constructor. Use factory methods instead."""
39
+ self._transport: Optional[_Transport] = None
40
+ self._session: Optional[ClientSession] = None
41
+ self._exit_stack: Optional[AsyncExitStack] = None
42
+ # StdIO params
43
+ self._stdio_params: Optional[StdioServerParameters] = None
44
+ # SSE params
45
+ self._sse_url: Optional[str] = None
46
+ self._sse_headers: Optional[Dict[str, Any]] = None
47
+ self._sse_timeout: float = 5.0
48
+ self._sse_read_timeout: float = 300.0
49
+
50
+ @classmethod
51
+ def stdio(
52
+ cls,
53
+ command: str,
54
+ args: List[str],
55
+ env: Optional[Dict[str, str]] = None
56
+ ) -> "MCPConnection":
57
+ """Create a connection to a local MCP server via StdIO.
58
+
59
+ Args:
60
+ command: The command to run (e.g., "python", "uvx").
61
+ args: Arguments to pass to the command.
62
+ env: Optional environment variables for the process.
63
+ """
64
+ instance = cls()
65
+ instance._transport = _Transport.STDIO
66
+ instance._stdio_params = StdioServerParameters(
67
+ command=command,
68
+ args=args,
69
+ env=env or os.environ.copy()
70
+ )
71
+ return instance
72
+
73
+ @classmethod
74
+ def sse(
75
+ cls,
76
+ url: str,
77
+ headers: Optional[Dict[str, Any]] = None,
78
+ timeout: float = 5.0,
79
+ sse_read_timeout: float = 300.0,
80
+ ) -> "MCPConnection":
81
+ """Create a connection to a remote MCP server via SSE/HTTP.
82
+
83
+ Args:
84
+ url: The SSE endpoint URL.
85
+ headers: Optional HTTP headers (e.g., for authentication).
86
+ timeout: HTTP timeout for regular operations.
87
+ sse_read_timeout: Timeout for SSE read operations.
88
+ """
89
+ instance = cls()
90
+ instance._transport = _Transport.SSE
91
+ instance._sse_url = url
92
+ instance._sse_headers = headers
93
+ instance._sse_timeout = timeout
94
+ instance._sse_read_timeout = sse_read_timeout
95
+ return instance
96
+
97
+ async def __aenter__(self) -> "MCPConnection":
98
+ self._exit_stack = AsyncExitStack()
99
+ try:
100
+ if self._transport == _Transport.STDIO:
101
+ read, write = await self._exit_stack.enter_async_context(
102
+ stdio_client(self._stdio_params)
103
+ )
104
+ elif self._transport == _Transport.SSE:
105
+ read, write = await self._exit_stack.enter_async_context(
106
+ sse_client(
107
+ url=self._sse_url,
108
+ headers=self._sse_headers,
109
+ timeout=self._sse_timeout,
110
+ sse_read_timeout=self._sse_read_timeout,
111
+ )
112
+ )
113
+ else:
114
+ raise ValueError("Transport not configured. Use MCPConnection.stdio() or MCPConnection.sse().")
115
+
116
+ self._session = await self._exit_stack.enter_async_context(
117
+ ClientSession(read, write)
118
+ )
119
+ await self._session.initialize()
120
+ return self
121
+ except Exception:
122
+ await self.aclose()
123
+ raise
124
+
125
+ async def __aexit__(self, exc_type, exc_value, traceback) -> None:
126
+ await self.aclose()
127
+
128
+ async def aclose(self) -> None:
129
+ """Gracefully closes the MCP connection."""
130
+ if self._exit_stack:
131
+ await self._exit_stack.aclose()
132
+ self._exit_stack = None
133
+ self._session = None
134
+
135
+ async def get_tools(self) -> List[Tool]:
136
+ """Fetches tools from the MCP server and converts them to Agentify format."""
137
+ if not self._session:
138
+ raise RuntimeError("MCPConnection is not active. Use 'async with ...'")
139
+
140
+ result = await self._session.list_tools()
141
+ return await convert_mcp_tools_to_agentify(self._session, result.tools)
@@ -17,8 +17,7 @@ class MemoryAddress:
17
17
  user_id: Optional[str] = None
18
18
  conversation_id: Optional[str] = None
19
19
  agent_id: Optional[str] = None
20
- # extras lets you pass stable routing dimensions if needed.
21
- extras: Tuple[Tuple[str, str], ...] = ()
20
+ extras: Tuple[Tuple[str, str], ...] = () # extras lets pass stable routing dimensions if needed.
22
21
 
23
22
  def as_tuple(self) -> Tuple:
24
23
  """Stable, hashable representation for keys and indexing."""
@@ -94,6 +93,7 @@ class ConversationStore(Protocol):
94
93
  def replace_messages(self, addr: MemoryAddress, messages: List[Message]) -> None: ...
95
94
  def delete_conversation(self, addr: MemoryAddress) -> None: ...
96
95
  def set_ttl(self, addr: MemoryAddress, seconds: int) -> None: ...
96
+ def list_conversations(self, limit: int = 100, offset: int = 0) -> List[MemoryAddress]: ...
97
97
 
98
98
 
99
99
  class TokenCounter(Protocol):
@@ -69,7 +69,7 @@ class MemoryService:
69
69
 
70
70
  color = role_colors.get(msg.role, Colors.RESET)
71
71
 
72
- # Extract agent_id if available to show WHO is speaking
72
+ # Extract agent_id if available to show who is speaking
73
73
  agent_id = addr.agent_id if addr and addr.agent_id else "unknown"
74
74
  agent_tag = f"[{agent_id}]" if agent_id else ""
75
75
 
@@ -124,3 +124,7 @@ class MemoryService:
124
124
  def delete_history(self, addr: MemoryAddress) -> None:
125
125
  """Remove all messages for the given address."""
126
126
  self.store.delete_conversation(addr)
127
+
128
+ def list_conversations(self, limit: int = 100, offset: int = 0) -> List[MemoryAddress]:
129
+ """List active conversations from the underlying store."""
130
+ return self.store.list_conversations(limit=limit, offset=offset)
@@ -0,0 +1,10 @@
1
+ """Memory storage backends."""
2
+ from .in_memory_store import InMemoryStore
3
+ from .redis_store import RedisStore
4
+ from .elastic_store import ElasticsearchStore
5
+ from .sqlite_store import SQLiteStore
6
+
7
+ __all__ = ["InMemoryStore", "RedisStore", "ElasticsearchStore", "SQLiteStore"]
8
+
9
+
10
+
@@ -0,0 +1,244 @@
1
+ from __future__ import annotations
2
+ import json
3
+ import logging
4
+ from typing import List, Optional, Any, Dict
5
+ from datetime import datetime
6
+
7
+ try:
8
+ from elasticsearch import Elasticsearch
9
+ from elasticsearch.helpers import bulk
10
+ except ImportError:
11
+ Elasticsearch = None # type: ignore
12
+ bulk = None # type: ignore
13
+
14
+ from ..interfaces import ConversationStore, MemoryAddress, Message
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+
19
+ class ElasticsearchStore(ConversationStore):
20
+ """
21
+ Elasticsearch-backed store.
22
+ Stores messages as individual documents to enable advanced search.
23
+ """
24
+
25
+ def __init__(
26
+ self,
27
+ url: str = "http://localhost:9200",
28
+ api_key: Optional[str] = None,
29
+ index_name: str = "agentify-memory",
30
+ verify_certs: bool = True,
31
+ ) -> None:
32
+ if Elasticsearch is None:
33
+ raise ImportError(
34
+ "Elasticsearch is not installed. Please install agentify[elastic] or 'pip install elasticsearch'."
35
+ )
36
+
37
+ # Connection setup
38
+ options = {}
39
+ if api_key:
40
+ options["api_key"] = api_key
41
+
42
+ self.client = Elasticsearch(
43
+ url,
44
+ verify_certs=verify_certs,
45
+ **options
46
+ )
47
+ self.index_name = index_name
48
+ self._ensure_index()
49
+
50
+ def _ensure_index(self) -> None:
51
+ """Create index with optimal mappings if it doesn't exist."""
52
+ if self.client.indices.exists(index=self.index_name):
53
+ return
54
+
55
+ mapping = {
56
+ "mappings": {
57
+ "properties": {
58
+ # Address fields (Exact match)
59
+ "api_version": {"type": "keyword"},
60
+ "tenant_id": {"type": "keyword"},
61
+ "user_id": {"type": "keyword"},
62
+ "conversation_id": {"type": "keyword"},
63
+ "agent_id": {"type": "keyword"},
64
+
65
+ # Message fields
66
+ "role": {"type": "keyword"},
67
+ "content": {"type": "text"}, # Full-text searchable
68
+ "name": {"type": "keyword"},
69
+ "id": {"type": "keyword"},
70
+ "ts": {"type": "date", "format": "epoch_second"},
71
+
72
+ # Structured Objects
73
+ "metadata": {"type": "object", "enabled": True},
74
+ "address_key": {"type": "keyword"}
75
+ }
76
+ }
77
+ }
78
+ try:
79
+ self.client.indices.create(index=self.index_name, body=mapping)
80
+ logger.info(f"Created Elasticsearch index: {self.index_name}")
81
+ except Exception as e:
82
+ logger.error(f"Failed to create index {self.index_name}: {e}")
83
+ raise
84
+
85
+ def _addr_to_doc(self, addr: MemoryAddress, msg: Message) -> Dict[str, Any]:
86
+ """Flatten address + message into a single document."""
87
+ base = msg.to_dict()
88
+ # Add filtering fields
89
+ base.update({
90
+ "api_version": addr.api_version,
91
+ "tenant_id": addr.tenant_id,
92
+ "user_id": addr.user_id,
93
+ "conversation_id": addr.conversation_id,
94
+ "agent_id": addr.agent_id,
95
+ "address_key": addr.key_str(),
96
+ })
97
+
98
+ return base
99
+
100
+ def _build_filter_query(self, addr: MemoryAddress) -> List[Dict[str, Any]]:
101
+ """Construct bool/filter clauses for the exact address."""
102
+ must = []
103
+ if addr.api_version:
104
+ must.append({"term": {"api_version": addr.api_version}})
105
+ if addr.tenant_id:
106
+ must.append({"term": {"tenant_id": addr.tenant_id}})
107
+ if addr.user_id:
108
+ must.append({"term": {"user_id": addr.user_id}})
109
+ if addr.conversation_id:
110
+ must.append({"term": {"conversation_id": addr.conversation_id}})
111
+ if addr.agent_id:
112
+ must.append({"term": {"agent_id": addr.agent_id}})
113
+ return must
114
+
115
+ def append_message(self, addr: MemoryAddress, msg: Message) -> None:
116
+ doc = self._addr_to_doc(addr, msg)
117
+ self.client.index(index=self.index_name, id=msg.id, document=doc, refresh=True)
118
+ # refresh='true' makes it visible immediately (good for chat consistency, possibly slower for bulk)
119
+
120
+ def read_messages(self, addr: MemoryAddress, start: int = 0, end: int = -1) -> List[Message]:
121
+ must = self._build_filter_query(addr)
122
+
123
+ # We need to fetch enough messages to support the slice.
124
+ # Since 'end' can be -1 (all), we might need a large size or scroll.
125
+ # For chat history, usually 100-1000 is enough.
126
+ size = 1000 if end == -1 else (end + 20)
127
+
128
+ query = {
129
+ "query": {"bool": {"filter": must}},
130
+ "sort": [{"ts": {"order": "asc"}}],
131
+ "size": size,
132
+ "from": start
133
+ }
134
+
135
+ resp = self.client.search(index=self.index_name, body=query)
136
+ hits = resp["hits"]["hits"]
137
+
138
+ msgs = []
139
+ for h in hits:
140
+ src = h["_source"]
141
+ # Extract only message fields to reconstruct
142
+ m_kwargs = {
143
+ "role": src.get("role"),
144
+ "content": src.get("content"),
145
+ "name": src.get("name"),
146
+ "tool_call_id": src.get("tool_call_id"),
147
+ "metadata": src.get("metadata", {}),
148
+ "id": src.get("id"),
149
+ "ts": int(src.get("ts", 0))
150
+ }
151
+ msgs.append(Message(**m_kwargs))
152
+
153
+ # Slice locally if specific end requested
154
+ if end != -1:
155
+ limit = end - start + 1
156
+ msgs = msgs[:limit]
157
+
158
+ return msgs
159
+
160
+ def replace_messages(self, addr: MemoryAddress, messages: List[Message]) -> None:
161
+ # 1. Delete existing for this address
162
+ self.delete_conversation(addr)
163
+
164
+ if not messages:
165
+ return
166
+
167
+ # 2. Bulk insert new
168
+ actions = []
169
+ for msg in messages:
170
+ doc = self._addr_to_doc(addr, msg)
171
+ actions.append({
172
+ "_index": self.index_name,
173
+ "_id": msg.id,
174
+ "_source": doc
175
+ })
176
+
177
+ if actions:
178
+ bulk(self.client, actions, refresh=True)
179
+
180
+ def delete_conversation(self, addr: MemoryAddress) -> None:
181
+ must = self._build_filter_query(addr)
182
+ query = {"query": {"bool": {"filter": must}}}
183
+ self.client.delete_by_query(index=self.index_name, body=query, refresh=True)
184
+
185
+ def set_ttl(self, addr: MemoryAddress, seconds: int) -> None:
186
+ # Implementing TTL in ES usually requires Index Lifecycle Management (ILM)
187
+ # or a separate cleanup job, as TTL is per-index or per-document requires setup.
188
+ # For simplicity, we log strictly: This is not natively supported per-conversation efficiently.
189
+ logger.warning("set_ttl is not fully supported in simple ElasticsearchStore yet.")
190
+
191
+ def list_conversations(self, limit: int = 100, offset: int = 0) -> List[MemoryAddress]:
192
+ """
193
+ List unique conversation addresses using aggregations.
194
+ Uses a terms aggregation on 'address_key'.
195
+ """
196
+ size = offset + limit
197
+ query = {
198
+ "size": 0,
199
+ "aggs": {
200
+ "unique_conversations": {
201
+ "terms": {
202
+ "field": "address_key",
203
+ "size": size
204
+ }
205
+ }
206
+ }
207
+ }
208
+
209
+ resp = self.client.search(index=self.index_name, body=query)
210
+ buckets = resp["aggregations"]["unique_conversations"]["buckets"]
211
+
212
+ # Apply offset/limit locally
213
+ buckets_slice = buckets[offset : offset + limit]
214
+
215
+ results = []
216
+ for b in buckets_slice:
217
+ k = b["key"]
218
+ core = k
219
+ prefix_check = "mem:"
220
+ if core.startswith(prefix_check):
221
+ core = core[len(prefix_check):]
222
+
223
+ parts = core.split(":")
224
+ kwargs = {}
225
+ extras = []
226
+
227
+ for part in parts:
228
+ if "=" not in part:
229
+ continue
230
+ key, val = part.split("=", 1)
231
+ if key == "v": kwargs["api_version"] = val
232
+ elif key == "t": kwargs["tenant_id"] = val
233
+ elif key == "u": kwargs["user_id"] = val
234
+ elif key == "c": kwargs["conversation_id"] = val
235
+ elif key == "a": kwargs["agent_id"] = val
236
+ else:
237
+ extras.append((key, val))
238
+
239
+ if extras:
240
+ kwargs["extras"] = tuple(extras)
241
+
242
+ results.append(MemoryAddress(**kwargs))
243
+
244
+ return results
@@ -27,3 +27,9 @@ class InMemoryStore(ConversationStore):
27
27
 
28
28
  def set_ttl(self, addr: MemoryAddress, seconds: int) -> None:
29
29
  self._ttl[addr] = seconds # not enforced in-memory
30
+
31
+ def list_conversations(self, limit: int = 100, offset: int = 0) -> List[MemoryAddress]:
32
+ keys = list(self._history.keys())
33
+ # Sort for stability
34
+ keys.sort(key=lambda k: k.key_str())
35
+ return keys[offset : offset + limit]