naas-abi-core 1.4.1__py3-none-any.whl
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.
- assets/favicon.ico +0 -0
- assets/logo.png +0 -0
- naas_abi_core/__init__.py +1 -0
- naas_abi_core/apps/api/api.py +245 -0
- naas_abi_core/apps/api/api_test.py +281 -0
- naas_abi_core/apps/api/openapi_doc.py +144 -0
- naas_abi_core/apps/mcp/Dockerfile.mcp +35 -0
- naas_abi_core/apps/mcp/mcp_server.py +243 -0
- naas_abi_core/apps/mcp/mcp_server_test.py +163 -0
- naas_abi_core/apps/terminal_agent/main.py +555 -0
- naas_abi_core/apps/terminal_agent/terminal_style.py +175 -0
- naas_abi_core/engine/Engine.py +87 -0
- naas_abi_core/engine/EngineProxy.py +109 -0
- naas_abi_core/engine/Engine_test.py +6 -0
- naas_abi_core/engine/IEngine.py +91 -0
- naas_abi_core/engine/conftest.py +45 -0
- naas_abi_core/engine/engine_configuration/EngineConfiguration.py +216 -0
- naas_abi_core/engine/engine_configuration/EngineConfiguration_Deploy.py +7 -0
- naas_abi_core/engine/engine_configuration/EngineConfiguration_GenericLoader.py +49 -0
- naas_abi_core/engine/engine_configuration/EngineConfiguration_ObjectStorageService.py +159 -0
- naas_abi_core/engine/engine_configuration/EngineConfiguration_ObjectStorageService_test.py +26 -0
- naas_abi_core/engine/engine_configuration/EngineConfiguration_SecretService.py +138 -0
- naas_abi_core/engine/engine_configuration/EngineConfiguration_SecretService_test.py +74 -0
- naas_abi_core/engine/engine_configuration/EngineConfiguration_TripleStoreService.py +224 -0
- naas_abi_core/engine/engine_configuration/EngineConfiguration_TripleStoreService_test.py +109 -0
- naas_abi_core/engine/engine_configuration/EngineConfiguration_VectorStoreService.py +76 -0
- naas_abi_core/engine/engine_configuration/EngineConfiguration_VectorStoreService_test.py +33 -0
- naas_abi_core/engine/engine_configuration/EngineConfiguration_test.py +9 -0
- naas_abi_core/engine/engine_configuration/utils/PydanticModelValidator.py +15 -0
- naas_abi_core/engine/engine_loaders/EngineModuleLoader.py +302 -0
- naas_abi_core/engine/engine_loaders/EngineOntologyLoader.py +16 -0
- naas_abi_core/engine/engine_loaders/EngineServiceLoader.py +47 -0
- naas_abi_core/integration/__init__.py +7 -0
- naas_abi_core/integration/integration.py +28 -0
- naas_abi_core/models/Model.py +198 -0
- naas_abi_core/models/OpenRouter.py +18 -0
- naas_abi_core/models/OpenRouter_test.py +36 -0
- naas_abi_core/module/Module.py +252 -0
- naas_abi_core/module/ModuleAgentLoader.py +50 -0
- naas_abi_core/module/ModuleUtils.py +20 -0
- naas_abi_core/modules/templatablesparqlquery/README.md +196 -0
- naas_abi_core/modules/templatablesparqlquery/__init__.py +39 -0
- naas_abi_core/modules/templatablesparqlquery/ontologies/TemplatableSparqlQueryOntology.ttl +116 -0
- naas_abi_core/modules/templatablesparqlquery/workflows/GenericWorkflow.py +48 -0
- naas_abi_core/modules/templatablesparqlquery/workflows/TemplatableSparqlQueryLoader.py +192 -0
- naas_abi_core/pipeline/__init__.py +6 -0
- naas_abi_core/pipeline/pipeline.py +70 -0
- naas_abi_core/services/__init__.py +0 -0
- naas_abi_core/services/agent/Agent.py +1619 -0
- naas_abi_core/services/agent/AgentMemory_test.py +28 -0
- naas_abi_core/services/agent/Agent_test.py +214 -0
- naas_abi_core/services/agent/IntentAgent.py +1179 -0
- naas_abi_core/services/agent/IntentAgent_test.py +139 -0
- naas_abi_core/services/agent/beta/Embeddings.py +181 -0
- naas_abi_core/services/agent/beta/IntentMapper.py +120 -0
- naas_abi_core/services/agent/beta/LocalModel.py +88 -0
- naas_abi_core/services/agent/beta/VectorStore.py +89 -0
- naas_abi_core/services/agent/test_agent_memory.py +278 -0
- naas_abi_core/services/agent/test_postgres_integration.py +145 -0
- naas_abi_core/services/cache/CacheFactory.py +31 -0
- naas_abi_core/services/cache/CachePort.py +63 -0
- naas_abi_core/services/cache/CacheService.py +246 -0
- naas_abi_core/services/cache/CacheService_test.py +85 -0
- naas_abi_core/services/cache/adapters/secondary/CacheFSAdapter.py +39 -0
- naas_abi_core/services/object_storage/ObjectStorageFactory.py +57 -0
- naas_abi_core/services/object_storage/ObjectStoragePort.py +47 -0
- naas_abi_core/services/object_storage/ObjectStorageService.py +41 -0
- naas_abi_core/services/object_storage/adapters/secondary/ObjectStorageSecondaryAdapterFS.py +52 -0
- naas_abi_core/services/object_storage/adapters/secondary/ObjectStorageSecondaryAdapterNaas.py +131 -0
- naas_abi_core/services/object_storage/adapters/secondary/ObjectStorageSecondaryAdapterS3.py +171 -0
- naas_abi_core/services/ontology/OntologyPorts.py +36 -0
- naas_abi_core/services/ontology/OntologyService.py +17 -0
- naas_abi_core/services/ontology/adaptors/secondary/OntologyService_SecondaryAdaptor_NERPort.py +37 -0
- naas_abi_core/services/secret/Secret.py +138 -0
- naas_abi_core/services/secret/SecretPorts.py +45 -0
- naas_abi_core/services/secret/Secret_test.py +65 -0
- naas_abi_core/services/secret/adaptors/secondary/Base64Secret.py +57 -0
- naas_abi_core/services/secret/adaptors/secondary/Base64Secret_test.py +39 -0
- naas_abi_core/services/secret/adaptors/secondary/NaasSecret.py +88 -0
- naas_abi_core/services/secret/adaptors/secondary/NaasSecret_test.py +25 -0
- naas_abi_core/services/secret/adaptors/secondary/dotenv_secret_secondaryadaptor.py +29 -0
- naas_abi_core/services/triple_store/TripleStoreFactory.py +116 -0
- naas_abi_core/services/triple_store/TripleStorePorts.py +223 -0
- naas_abi_core/services/triple_store/TripleStoreService.py +419 -0
- naas_abi_core/services/triple_store/adaptors/secondary/AWSNeptune.py +1300 -0
- naas_abi_core/services/triple_store/adaptors/secondary/AWSNeptune_test.py +284 -0
- naas_abi_core/services/triple_store/adaptors/secondary/Oxigraph.py +597 -0
- naas_abi_core/services/triple_store/adaptors/secondary/Oxigraph_test.py +1474 -0
- naas_abi_core/services/triple_store/adaptors/secondary/TripleStoreService__SecondaryAdaptor__Filesystem.py +223 -0
- naas_abi_core/services/triple_store/adaptors/secondary/TripleStoreService__SecondaryAdaptor__ObjectStorage.py +234 -0
- naas_abi_core/services/triple_store/adaptors/secondary/base/TripleStoreService__SecondaryAdaptor__FileBase.py +18 -0
- naas_abi_core/services/vector_store/IVectorStorePort.py +101 -0
- naas_abi_core/services/vector_store/IVectorStorePort_test.py +189 -0
- naas_abi_core/services/vector_store/VectorStoreFactory.py +47 -0
- naas_abi_core/services/vector_store/VectorStoreService.py +171 -0
- naas_abi_core/services/vector_store/VectorStoreService_test.py +185 -0
- naas_abi_core/services/vector_store/__init__.py +13 -0
- naas_abi_core/services/vector_store/adapters/QdrantAdapter.py +251 -0
- naas_abi_core/services/vector_store/adapters/QdrantAdapter_test.py +57 -0
- naas_abi_core/tests/test_services_imports.py +69 -0
- naas_abi_core/utils/Expose.py +55 -0
- naas_abi_core/utils/Graph.py +182 -0
- naas_abi_core/utils/JSON.py +49 -0
- naas_abi_core/utils/LazyLoader.py +44 -0
- naas_abi_core/utils/Logger.py +12 -0
- naas_abi_core/utils/OntologyReasoner.py +141 -0
- naas_abi_core/utils/OntologyYaml.py +681 -0
- naas_abi_core/utils/SPARQL.py +256 -0
- naas_abi_core/utils/Storage.py +33 -0
- naas_abi_core/utils/StorageUtils.py +398 -0
- naas_abi_core/utils/String.py +52 -0
- naas_abi_core/utils/Workers.py +114 -0
- naas_abi_core/utils/__init__.py +0 -0
- naas_abi_core/utils/onto2py/README.md +0 -0
- naas_abi_core/utils/onto2py/__init__.py +10 -0
- naas_abi_core/utils/onto2py/__main__.py +29 -0
- naas_abi_core/utils/onto2py/onto2py.py +611 -0
- naas_abi_core/utils/onto2py/tests/ttl2py_test.py +271 -0
- naas_abi_core/workflow/__init__.py +5 -0
- naas_abi_core/workflow/workflow.py +48 -0
- naas_abi_core-1.4.1.dist-info/METADATA +630 -0
- naas_abi_core-1.4.1.dist-info/RECORD +124 -0
- naas_abi_core-1.4.1.dist-info/WHEEL +4 -0
- naas_abi_core-1.4.1.dist-info/entry_points.txt +2 -0
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
"""Tests for Agent memory configuration with PostgreSQL support."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from unittest.mock import ANY, MagicMock, patch
|
|
5
|
+
|
|
6
|
+
import pytest
|
|
7
|
+
from langgraph.checkpoint.base import BaseCheckpointSaver
|
|
8
|
+
from langgraph.checkpoint.memory import MemorySaver
|
|
9
|
+
from naas_abi_core.services.agent.Agent import (
|
|
10
|
+
Agent,
|
|
11
|
+
AgentSharedState,
|
|
12
|
+
create_checkpointer,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class TestCreateCheckpointer:
|
|
17
|
+
"""Test the create_checkpointer function that detects PostgreSQL configuration."""
|
|
18
|
+
|
|
19
|
+
def test_create_checkpointer_without_postgres_url(self):
|
|
20
|
+
"""Test that MemorySaver is returned when POSTGRES_URL is not set."""
|
|
21
|
+
with patch.dict(os.environ, {}, clear=True):
|
|
22
|
+
checkpointer = create_checkpointer()
|
|
23
|
+
assert isinstance(checkpointer, MemorySaver)
|
|
24
|
+
|
|
25
|
+
def test_create_checkpointer_with_postgres_url(self):
|
|
26
|
+
"""Test that PostgresSaver is returned when POSTGRES_URL is set."""
|
|
27
|
+
test_url = "postgresql://user:pass@localhost:5432/testdb"
|
|
28
|
+
|
|
29
|
+
# Mock the PostgresSaver class, psycopg connection, and instance
|
|
30
|
+
mock_postgres_saver = MagicMock()
|
|
31
|
+
mock_postgres_saver.setup = MagicMock()
|
|
32
|
+
mock_postgres_class = MagicMock(return_value=mock_postgres_saver)
|
|
33
|
+
mock_connection = MagicMock()
|
|
34
|
+
mock_connection_connect = MagicMock(return_value=mock_connection)
|
|
35
|
+
|
|
36
|
+
with patch.dict(os.environ, {"POSTGRES_URL": test_url}):
|
|
37
|
+
with patch(
|
|
38
|
+
"langgraph.checkpoint.postgres.PostgresSaver", mock_postgres_class
|
|
39
|
+
):
|
|
40
|
+
with patch("psycopg.Connection.connect", mock_connection_connect):
|
|
41
|
+
checkpointer = create_checkpointer()
|
|
42
|
+
|
|
43
|
+
# Verify Connection.connect was called with proper parameters
|
|
44
|
+
mock_connection_connect.assert_called_once_with(
|
|
45
|
+
test_url,
|
|
46
|
+
autocommit=True,
|
|
47
|
+
prepare_threshold=0,
|
|
48
|
+
row_factory=ANY, # dict_row import is mocked
|
|
49
|
+
)
|
|
50
|
+
# Verify PostgresSaver constructor was called with connection
|
|
51
|
+
mock_postgres_class.assert_called_once_with(mock_connection)
|
|
52
|
+
# Verify setup() was called
|
|
53
|
+
mock_postgres_saver.setup.assert_called_once()
|
|
54
|
+
assert checkpointer == mock_postgres_saver
|
|
55
|
+
|
|
56
|
+
def test_create_checkpointer_postgres_import_error(self):
|
|
57
|
+
"""Test fallback to MemorySaver when PostgresSaver import fails."""
|
|
58
|
+
test_url = "postgresql://user:pass@localhost:5432/testdb"
|
|
59
|
+
|
|
60
|
+
with patch.dict(os.environ, {"POSTGRES_URL": test_url}):
|
|
61
|
+
# Simulate ImportError when trying to import PostgresSaver
|
|
62
|
+
with patch(
|
|
63
|
+
"builtins.__import__",
|
|
64
|
+
side_effect=ImportError(
|
|
65
|
+
"No module named 'langgraph.checkpoint.postgres'"
|
|
66
|
+
),
|
|
67
|
+
):
|
|
68
|
+
checkpointer = create_checkpointer()
|
|
69
|
+
assert isinstance(checkpointer, MemorySaver)
|
|
70
|
+
|
|
71
|
+
def test_create_checkpointer_postgres_connection_error(self):
|
|
72
|
+
"""Test fallback to MemorySaver when PostgreSQL connection fails."""
|
|
73
|
+
test_url = "postgresql://user:pass@localhost:5432/testdb"
|
|
74
|
+
|
|
75
|
+
# Mock Connection.connect to raise an exception
|
|
76
|
+
mock_connection_connect = MagicMock(side_effect=Exception("Connection failed"))
|
|
77
|
+
|
|
78
|
+
with patch.dict(os.environ, {"POSTGRES_URL": test_url}):
|
|
79
|
+
with patch("psycopg.Connection.connect", mock_connection_connect):
|
|
80
|
+
checkpointer = create_checkpointer()
|
|
81
|
+
assert isinstance(checkpointer, MemorySaver)
|
|
82
|
+
|
|
83
|
+
def test_create_checkpointer_postgres_setup_error(self):
|
|
84
|
+
"""Test fallback to MemorySaver when PostgreSQL setup fails."""
|
|
85
|
+
test_url = "postgresql://user:pass@localhost:5432/testdb"
|
|
86
|
+
|
|
87
|
+
# Mock successful connection but setup failure
|
|
88
|
+
mock_postgres_saver = MagicMock()
|
|
89
|
+
mock_postgres_saver.setup.side_effect = Exception("Table creation failed")
|
|
90
|
+
mock_postgres_class = MagicMock(return_value=mock_postgres_saver)
|
|
91
|
+
mock_connection = MagicMock()
|
|
92
|
+
mock_connection_connect = MagicMock(return_value=mock_connection)
|
|
93
|
+
|
|
94
|
+
with patch.dict(os.environ, {"POSTGRES_URL": test_url}):
|
|
95
|
+
with patch(
|
|
96
|
+
"langgraph.checkpoint.postgres.PostgresSaver", mock_postgres_class
|
|
97
|
+
):
|
|
98
|
+
with patch("psycopg.Connection.connect", mock_connection_connect):
|
|
99
|
+
checkpointer = create_checkpointer()
|
|
100
|
+
assert isinstance(checkpointer, MemorySaver)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
class TestAgentMemoryConfiguration:
|
|
104
|
+
"""Test Agent class memory configuration."""
|
|
105
|
+
|
|
106
|
+
@pytest.fixture
|
|
107
|
+
def mock_chat_model(self):
|
|
108
|
+
"""Create a mock chat model."""
|
|
109
|
+
model = MagicMock()
|
|
110
|
+
model.bind_tools = MagicMock(return_value=model)
|
|
111
|
+
return model
|
|
112
|
+
|
|
113
|
+
def test_agent_uses_provided_memory(self, mock_chat_model):
|
|
114
|
+
"""Test that Agent uses explicitly provided memory."""
|
|
115
|
+
custom_memory = MagicMock(spec=BaseCheckpointSaver)
|
|
116
|
+
|
|
117
|
+
agent = Agent(
|
|
118
|
+
name="test_agent",
|
|
119
|
+
description="Test agent",
|
|
120
|
+
chat_model=mock_chat_model,
|
|
121
|
+
memory=custom_memory,
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
assert agent._checkpointer == custom_memory
|
|
125
|
+
|
|
126
|
+
def test_agent_creates_memory_when_none_provided(self, mock_chat_model):
|
|
127
|
+
"""Test that Agent creates memory based on environment when None is provided."""
|
|
128
|
+
with patch("abi.services.agent.Agent.create_checkpointer") as mock_create:
|
|
129
|
+
mock_checkpointer = MagicMock(spec=BaseCheckpointSaver)
|
|
130
|
+
mock_create.return_value = mock_checkpointer
|
|
131
|
+
|
|
132
|
+
agent = Agent(
|
|
133
|
+
name="test_agent",
|
|
134
|
+
description="Test agent",
|
|
135
|
+
chat_model=mock_chat_model,
|
|
136
|
+
memory=None,
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
mock_create.assert_called_once()
|
|
140
|
+
assert agent._checkpointer == mock_checkpointer
|
|
141
|
+
|
|
142
|
+
def test_agent_default_memory_creation(self, mock_chat_model):
|
|
143
|
+
"""Test that Agent creates memory automatically when not provided."""
|
|
144
|
+
with patch("abi.services.agent.Agent.create_checkpointer") as mock_create:
|
|
145
|
+
mock_checkpointer = MagicMock(spec=BaseCheckpointSaver)
|
|
146
|
+
mock_create.return_value = mock_checkpointer
|
|
147
|
+
|
|
148
|
+
# Don't provide memory parameter at all
|
|
149
|
+
agent = Agent(
|
|
150
|
+
name="test_agent", description="Test agent", chat_model=mock_chat_model
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
mock_create.assert_called_once()
|
|
154
|
+
assert agent._checkpointer == mock_checkpointer
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
class TestAgentPostgresIntegration:
|
|
158
|
+
"""Integration tests for Agent with PostgreSQL checkpointer."""
|
|
159
|
+
|
|
160
|
+
@pytest.fixture
|
|
161
|
+
def mock_chat_model(self):
|
|
162
|
+
"""Create a mock chat model that simulates real behavior."""
|
|
163
|
+
model = MagicMock()
|
|
164
|
+
model.bind_tools = MagicMock(return_value=model)
|
|
165
|
+
|
|
166
|
+
# Mock invoke to return a proper message
|
|
167
|
+
from langchain_core.messages import AIMessage
|
|
168
|
+
|
|
169
|
+
model.invoke = MagicMock(return_value=AIMessage(content="Test response"))
|
|
170
|
+
|
|
171
|
+
return model
|
|
172
|
+
|
|
173
|
+
@pytest.mark.integration
|
|
174
|
+
def test_agent_with_postgres_preserves_state(self, mock_chat_model):
|
|
175
|
+
"""Test that Agent with PostgreSQL preserves conversation state."""
|
|
176
|
+
# This test would require a running PostgreSQL instance
|
|
177
|
+
# It's marked as integration test and can be skipped in CI
|
|
178
|
+
|
|
179
|
+
test_url = "postgresql://abi_user:abi_password@localhost:5432/abi_memory"
|
|
180
|
+
|
|
181
|
+
with patch.dict(os.environ, {"POSTGRES_URL": test_url}):
|
|
182
|
+
# First agent instance
|
|
183
|
+
agent1 = Agent(
|
|
184
|
+
name="test_agent",
|
|
185
|
+
description="Test agent",
|
|
186
|
+
chat_model=mock_chat_model,
|
|
187
|
+
state=AgentSharedState(thread_id="123"),
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
# Simulate some conversation
|
|
191
|
+
# In a real test, we would invoke the agent and check state persistence
|
|
192
|
+
|
|
193
|
+
# Second agent instance with same thread_id should have access to same state
|
|
194
|
+
agent2 = Agent(
|
|
195
|
+
name="test_agent",
|
|
196
|
+
description="Test agent",
|
|
197
|
+
chat_model=mock_chat_model,
|
|
198
|
+
state=AgentSharedState(thread_id="123"),
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
# Both agents should share the same checkpointer type
|
|
202
|
+
assert type(agent1._checkpointer) is type(agent2._checkpointer)
|
|
203
|
+
|
|
204
|
+
def test_agent_duplicate_preserves_memory_config(self, mock_chat_model):
|
|
205
|
+
"""Test that duplicating an agent preserves memory configuration."""
|
|
206
|
+
custom_memory = MagicMock(spec=BaseCheckpointSaver)
|
|
207
|
+
|
|
208
|
+
original_agent = Agent(
|
|
209
|
+
name="test_agent",
|
|
210
|
+
description="Test agent",
|
|
211
|
+
chat_model=mock_chat_model,
|
|
212
|
+
memory=custom_memory,
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
duplicated_agent = original_agent.duplicate()
|
|
216
|
+
|
|
217
|
+
# The duplicated agent should use the same memory type
|
|
218
|
+
assert duplicated_agent._checkpointer == original_agent._checkpointer
|
|
219
|
+
|
|
220
|
+
def test_agent_thread_id_passed_as_string(self, mock_chat_model):
|
|
221
|
+
"""Test that thread_id is passed as string to graph config."""
|
|
222
|
+
# Mock the graph.stream method to capture the config
|
|
223
|
+
mock_graph = MagicMock()
|
|
224
|
+
mock_graph.stream.return_value = iter([("source", {"messages": []})])
|
|
225
|
+
|
|
226
|
+
agent = Agent(
|
|
227
|
+
name="test_agent", description="Test agent", chat_model=mock_chat_model
|
|
228
|
+
)
|
|
229
|
+
agent.graph = mock_graph
|
|
230
|
+
|
|
231
|
+
# Set a specific thread_id
|
|
232
|
+
agent._state.set_thread_id("123")
|
|
233
|
+
|
|
234
|
+
# Call stream which should pass thread_id as string directly
|
|
235
|
+
list(agent.stream("test prompt"))
|
|
236
|
+
|
|
237
|
+
# Verify that graph.stream was called with thread_id as string
|
|
238
|
+
mock_graph.stream.assert_called_once()
|
|
239
|
+
call_args = mock_graph.stream.call_args
|
|
240
|
+
config = call_args[1]["config"] # kwargs
|
|
241
|
+
|
|
242
|
+
assert config["configurable"]["thread_id"] == "123"
|
|
243
|
+
assert isinstance(config["configurable"]["thread_id"], str)
|
|
244
|
+
|
|
245
|
+
def test_agent_thread_id_increment(self, mock_chat_model):
|
|
246
|
+
"""Test that thread_id increments correctly with string type."""
|
|
247
|
+
# Create agent with explicit thread_id to avoid test interference
|
|
248
|
+
from naas_abi_core.services.agent.Agent import AgentSharedState
|
|
249
|
+
|
|
250
|
+
state = AgentSharedState(thread_id="1")
|
|
251
|
+
|
|
252
|
+
agent = Agent(
|
|
253
|
+
name="test_agent",
|
|
254
|
+
description="Test agent",
|
|
255
|
+
chat_model=mock_chat_model,
|
|
256
|
+
state=state,
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
# Default thread_id should be "1"
|
|
260
|
+
assert agent._state.thread_id == "1"
|
|
261
|
+
|
|
262
|
+
# Test increment
|
|
263
|
+
agent.reset()
|
|
264
|
+
assert agent._state.thread_id == "2"
|
|
265
|
+
|
|
266
|
+
# Test multiple increments
|
|
267
|
+
agent.reset()
|
|
268
|
+
agent.reset()
|
|
269
|
+
assert agent._state.thread_id == "4"
|
|
270
|
+
|
|
271
|
+
# Test setting a custom string thread_id and incrementing
|
|
272
|
+
agent._state.set_thread_id("100")
|
|
273
|
+
agent.reset()
|
|
274
|
+
assert agent._state.thread_id == "101"
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
if __name__ == "__main__":
|
|
278
|
+
pytest.main([__file__, "-v"])
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Integration test to verify PostgreSQL memory works with Agent.
|
|
3
|
+
|
|
4
|
+
This test requires PostgreSQL to be running. It can be started with:
|
|
5
|
+
make dev-up
|
|
6
|
+
|
|
7
|
+
Run this test with:
|
|
8
|
+
POSTGRES_URL=postgresql://abi_user:abi_password@localhost:5432/abi_memory python test_postgres_integration.py
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import os
|
|
12
|
+
import sys
|
|
13
|
+
from unittest.mock import MagicMock
|
|
14
|
+
|
|
15
|
+
from langchain_core.messages import AIMessage
|
|
16
|
+
|
|
17
|
+
# Add parent directory to path to import Agent
|
|
18
|
+
sys.path.insert(
|
|
19
|
+
0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(__file__))))
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
from naas_abi_core.services.agent.Agent import (
|
|
23
|
+
Agent,
|
|
24
|
+
AgentSharedState,
|
|
25
|
+
create_checkpointer,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def test_postgres_checkpointer():
|
|
30
|
+
"""Test that PostgreSQL checkpointer is created when POSTGRES_URL is set."""
|
|
31
|
+
postgres_url = os.getenv("POSTGRES_URL")
|
|
32
|
+
|
|
33
|
+
if not postgres_url:
|
|
34
|
+
print("⚠️ POSTGRES_URL not set. Skipping PostgreSQL integration test.")
|
|
35
|
+
print(" To run this test, start PostgreSQL with: make dev-up")
|
|
36
|
+
print(
|
|
37
|
+
" Then set POSTGRES_URL=postgresql://abi_user:abi_password@localhost:5432/abi_memory"
|
|
38
|
+
)
|
|
39
|
+
return False
|
|
40
|
+
|
|
41
|
+
print(f"✓ POSTGRES_URL found: {postgres_url}")
|
|
42
|
+
|
|
43
|
+
try:
|
|
44
|
+
checkpointer = create_checkpointer()
|
|
45
|
+
print(f"✓ Created checkpointer: {type(checkpointer).__name__}")
|
|
46
|
+
|
|
47
|
+
# Check if it's a PostgreSQL checkpointer
|
|
48
|
+
if "PostgresSaver" in type(checkpointer).__name__:
|
|
49
|
+
print("✓ PostgreSQL checkpointer successfully created!")
|
|
50
|
+
print(
|
|
51
|
+
"✓ Connection configured with autocommit=True, prepare_threshold=0, row_factory=dict_row"
|
|
52
|
+
)
|
|
53
|
+
print("✓ Database tables initialized with setup()")
|
|
54
|
+
return True
|
|
55
|
+
else:
|
|
56
|
+
print(f"⚠️ Expected PostgresSaver but got {type(checkpointer).__name__}")
|
|
57
|
+
return False
|
|
58
|
+
|
|
59
|
+
except Exception as e:
|
|
60
|
+
print(f"✗ Error creating checkpointer: {e}")
|
|
61
|
+
return False
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def test_agent_with_postgres():
|
|
65
|
+
"""Test that Agent can use PostgreSQL for memory persistence."""
|
|
66
|
+
postgres_url = os.getenv("POSTGRES_URL")
|
|
67
|
+
|
|
68
|
+
if not postgres_url:
|
|
69
|
+
print("⚠️ Skipping Agent PostgreSQL test (POSTGRES_URL not set)")
|
|
70
|
+
return False
|
|
71
|
+
|
|
72
|
+
try:
|
|
73
|
+
# Create a mock chat model
|
|
74
|
+
mock_model = MagicMock()
|
|
75
|
+
mock_model.bind_tools = MagicMock(return_value=mock_model)
|
|
76
|
+
mock_model.invoke = MagicMock(return_value=AIMessage(content="Test response"))
|
|
77
|
+
|
|
78
|
+
# Create agent with a specific thread ID
|
|
79
|
+
thread_id = 12345
|
|
80
|
+
agent = Agent(
|
|
81
|
+
name="test_agent",
|
|
82
|
+
description="Test agent for PostgreSQL integration",
|
|
83
|
+
chat_model=mock_model,
|
|
84
|
+
state=AgentSharedState(thread_id=thread_id),
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
print(f"✓ Created agent with thread_id={thread_id}")
|
|
88
|
+
print(f"✓ Agent using checkpointer: {type(agent._checkpointer).__name__}")
|
|
89
|
+
|
|
90
|
+
# Test that the agent's checkpointer is PostgreSQL-based
|
|
91
|
+
if "PostgresSaver" in type(agent._checkpointer).__name__:
|
|
92
|
+
print("✓ Agent successfully using PostgreSQL for memory!")
|
|
93
|
+
return True
|
|
94
|
+
else:
|
|
95
|
+
print(
|
|
96
|
+
f"⚠️ Agent using {type(agent._checkpointer).__name__} instead of PostgresSaver"
|
|
97
|
+
)
|
|
98
|
+
return False
|
|
99
|
+
|
|
100
|
+
except Exception as e:
|
|
101
|
+
print(f"✗ Error creating agent with PostgreSQL: {e}")
|
|
102
|
+
import traceback
|
|
103
|
+
|
|
104
|
+
traceback.print_exc()
|
|
105
|
+
return False
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def main():
|
|
109
|
+
"""Run all integration tests."""
|
|
110
|
+
print("=" * 60)
|
|
111
|
+
print("PostgreSQL Integration Tests for Agent Memory")
|
|
112
|
+
print("=" * 60)
|
|
113
|
+
print()
|
|
114
|
+
|
|
115
|
+
tests_passed = 0
|
|
116
|
+
tests_total = 2
|
|
117
|
+
|
|
118
|
+
# Test 1: Checkpointer creation
|
|
119
|
+
print("Test 1: PostgreSQL Checkpointer Creation")
|
|
120
|
+
print("-" * 40)
|
|
121
|
+
if test_postgres_checkpointer():
|
|
122
|
+
tests_passed += 1
|
|
123
|
+
print()
|
|
124
|
+
|
|
125
|
+
# Test 2: Agent with PostgreSQL
|
|
126
|
+
print("Test 2: Agent with PostgreSQL Memory")
|
|
127
|
+
print("-" * 40)
|
|
128
|
+
if test_agent_with_postgres():
|
|
129
|
+
tests_passed += 1
|
|
130
|
+
print()
|
|
131
|
+
|
|
132
|
+
# Summary
|
|
133
|
+
print("=" * 60)
|
|
134
|
+
if tests_passed == tests_total:
|
|
135
|
+
print(f"✅ All tests passed! ({tests_passed}/{tests_total})")
|
|
136
|
+
else:
|
|
137
|
+
print(f"⚠️ {tests_passed}/{tests_total} tests passed")
|
|
138
|
+
print("=" * 60)
|
|
139
|
+
|
|
140
|
+
return tests_passed == tests_total
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
if __name__ == "__main__":
|
|
144
|
+
success = main()
|
|
145
|
+
sys.exit(0 if success else 1)
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import os
|
|
2
|
+
|
|
3
|
+
from naas_abi_core.services.cache.adapters.secondary.CacheFSAdapter import (
|
|
4
|
+
CacheFSAdapter,
|
|
5
|
+
)
|
|
6
|
+
from naas_abi_core.services.cache.CacheService import CacheService
|
|
7
|
+
from naas_abi_core.utils.Storage import NoStorageFolderFound, find_storage_folder
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class CacheFactory:
|
|
11
|
+
@staticmethod
|
|
12
|
+
def CacheFS_find_storage(
|
|
13
|
+
subpath: str = "cache", needle: str = "storage"
|
|
14
|
+
) -> CacheService:
|
|
15
|
+
if not subpath.startswith("cache"):
|
|
16
|
+
subpath = os.path.join("cache", subpath)
|
|
17
|
+
|
|
18
|
+
try:
|
|
19
|
+
return CacheService(
|
|
20
|
+
CacheFSAdapter(
|
|
21
|
+
os.path.join(find_storage_folder(os.getcwd(), needle), subpath)
|
|
22
|
+
)
|
|
23
|
+
)
|
|
24
|
+
except NoStorageFolderFound as _:
|
|
25
|
+
# Create a "storage" folder for the cache
|
|
26
|
+
os.makedirs(os.path.join(os.getcwd(), "storage"), exist_ok=True)
|
|
27
|
+
return CacheService(
|
|
28
|
+
CacheFSAdapter(
|
|
29
|
+
os.path.join(find_storage_folder(os.getcwd(), needle), subpath)
|
|
30
|
+
)
|
|
31
|
+
)
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
from pydantic import BaseModel, Field
|
|
2
|
+
import datetime
|
|
3
|
+
from enum import Enum
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
class CacheNotFoundError(Exception):
|
|
7
|
+
pass
|
|
8
|
+
|
|
9
|
+
class CacheExpiredError(Exception):
|
|
10
|
+
pass
|
|
11
|
+
|
|
12
|
+
class DataType(str, Enum):
|
|
13
|
+
TEXT = "text"
|
|
14
|
+
JSON = "json"
|
|
15
|
+
BINARY = "binary"
|
|
16
|
+
PICKLE = "pickle"
|
|
17
|
+
|
|
18
|
+
class CachedData(BaseModel):
|
|
19
|
+
key: str
|
|
20
|
+
data: Any
|
|
21
|
+
data_type: DataType
|
|
22
|
+
created_at: str = Field(default_factory=lambda: datetime.datetime.now().isoformat())
|
|
23
|
+
|
|
24
|
+
class ICacheAdapter:
|
|
25
|
+
|
|
26
|
+
def get(self, key: str) -> CachedData:
|
|
27
|
+
raise NotImplementedError("Not implemented")
|
|
28
|
+
|
|
29
|
+
def set(self, key: str, value: CachedData) -> None:
|
|
30
|
+
raise NotImplementedError("Not implemented")
|
|
31
|
+
|
|
32
|
+
def delete(self, key: str) -> None:
|
|
33
|
+
raise NotImplementedError("Not implemented")
|
|
34
|
+
|
|
35
|
+
def exists(self, key: str) -> bool:
|
|
36
|
+
raise NotImplementedError("Not implemented")
|
|
37
|
+
|
|
38
|
+
class ICacheService:
|
|
39
|
+
|
|
40
|
+
adapter: ICacheAdapter
|
|
41
|
+
|
|
42
|
+
def __init__(self, adapter: ICacheAdapter):
|
|
43
|
+
self.adapter = adapter
|
|
44
|
+
|
|
45
|
+
def exists(self, key: str) -> bool:
|
|
46
|
+
raise NotImplementedError("Not implemented")
|
|
47
|
+
|
|
48
|
+
def get(self, key: str, ttl: datetime.timedelta | None = None) -> Any:
|
|
49
|
+
raise NotImplementedError("Not implemented")
|
|
50
|
+
|
|
51
|
+
def set_text(self, key: str, value: str) -> None:
|
|
52
|
+
raise NotImplementedError("Not implemented")
|
|
53
|
+
|
|
54
|
+
def set_json(self, key: str, value: dict) -> None:
|
|
55
|
+
raise NotImplementedError("Not implemented")
|
|
56
|
+
|
|
57
|
+
def set_binary(self, key: str, value: bytes) -> None:
|
|
58
|
+
raise NotImplementedError("Not implemented")
|
|
59
|
+
|
|
60
|
+
def set_pickle(self, key: str, value: Any) -> None:
|
|
61
|
+
raise NotImplementedError("Not implemented")
|
|
62
|
+
|
|
63
|
+
|