Skip to content

Commit 70d7c62

Browse files
authored
Merge pull request #266 from poissoncorp/RDBC-998
RDBC-998 Write tests for Python AI Agents
2 parents 4cd66c4 + bb6e76f commit 70d7c62

3 files changed

Lines changed: 788 additions & 0 deletions

File tree

ravendb/documents/operations/ai/agents/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
AiAgentParameter,
44
AiAgentToolAction,
55
AiAgentToolQuery,
6+
AiAgentToolQueryOptions,
67
AiAgentPersistenceConfiguration,
78
AiAgentChatTrimmingConfiguration,
89
AiAgentSummarizationByTokens,
@@ -38,6 +39,7 @@
3839
"AiAgentParameter",
3940
"AiAgentToolAction",
4041
"AiAgentToolQuery",
42+
"AiAgentToolQueryOptions",
4143
"AiAgentPersistenceConfiguration",
4244
"AiAgentChatTrimmingConfiguration",
4345
"AiAgentSummarizationByTokens",
Lines changed: 343 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,343 @@
1+
"""Integration tests for AI agent CRUD operations."""
2+
3+
import os
4+
import unittest
5+
6+
from ravendb import (
7+
AiAgentConfiguration,
8+
AiAgentParameter,
9+
AiAgentSummarizationByTokens,
10+
AiAgentChatTrimmingConfiguration,
11+
AiAgentToolQuery,
12+
AiAgentToolAction,
13+
)
14+
from ravendb.documents.operations.ai import AiConnectionString, AiModelType
15+
from ravendb.documents.operations.ai.agents import (
16+
AddOrUpdateAiAgentOperation,
17+
GetAiAgentOperation,
18+
DeleteAiAgentOperation,
19+
AiAgentToolQueryOptions,
20+
)
21+
from ravendb.documents.operations.ai.open_ai_settings import OpenAiSettings
22+
from ravendb.documents.operations.connection_string.put_connection_string_operation import PutConnectionStringOperation
23+
from ravendb.tests.test_base import TestBase
24+
25+
26+
@unittest.skipIf(os.environ.get("RAVENDB_LICENSE") is None, "Insufficient license permissions. Skipping on CI/CD.")
27+
class TestAiAgentCrudOperations(TestBase):
28+
"""Integration tests for AI agent CRUD operations (require server connection)."""
29+
30+
CONNECTION_STRING_NAME = "test-ai-agent-cs"
31+
32+
def setUp(self):
33+
super().setUp()
34+
ai_connection_string = AiConnectionString(
35+
name=self.CONNECTION_STRING_NAME,
36+
identifier="test-ai-agent-cs",
37+
model_type=AiModelType.CHAT,
38+
openai_settings=OpenAiSettings(
39+
api_key="dummy-api-key",
40+
endpoint="https://api.openai.com/v1",
41+
model="gpt-4",
42+
),
43+
)
44+
self.store.maintenance.send(PutConnectionStringOperation(ai_connection_string))
45+
self._created_agent_ids = []
46+
47+
def tearDown(self):
48+
for agent_id in self._created_agent_ids:
49+
try:
50+
self.store.maintenance.send(DeleteAiAgentOperation(agent_id))
51+
except Exception:
52+
pass
53+
super().tearDown()
54+
55+
def _create_basic_agent(self, name: str, identifier: str) -> AiAgentConfiguration:
56+
"""Helper to create a minimal valid AiAgentConfiguration."""
57+
agent = AiAgentConfiguration(
58+
name=name,
59+
identifier=identifier,
60+
connection_string_name=self.CONNECTION_STRING_NAME,
61+
system_prompt="You are a helpful assistant.",
62+
sample_object='{"answer": "embed your answer here"}',
63+
)
64+
return agent
65+
66+
def _create_full_agent(self, name: str, identifier: str) -> AiAgentConfiguration:
67+
"""Helper to create a fully configured AiAgentConfiguration."""
68+
agent = AiAgentConfiguration(
69+
name=name,
70+
identifier=identifier,
71+
connection_string_name=self.CONNECTION_STRING_NAME,
72+
system_prompt="You are a helpful assistant that queries the database.",
73+
sample_object='{"answer": "embed your answer here"}',
74+
parameters=[
75+
AiAgentParameter("country", "The country to filter by."),
76+
],
77+
queries=[
78+
AiAgentToolQuery(
79+
name="get-orders",
80+
description="Retrieve all orders.",
81+
query="from Orders",
82+
parameters_sample_object="{}",
83+
),
84+
],
85+
actions=[
86+
AiAgentToolAction(
87+
name="store-result",
88+
description="Store the result in the database.",
89+
parameters_sample_object='{"result": "embed result here"}',
90+
),
91+
],
92+
chat_trimming=AiAgentChatTrimmingConfiguration(
93+
tokens_config=AiAgentSummarizationByTokens(
94+
max_tokens_before_summarization=32768,
95+
max_tokens_after_summarization=1024,
96+
)
97+
),
98+
max_model_iterations_per_call=3,
99+
)
100+
return agent
101+
102+
# ---- Create ----
103+
104+
def test_create_agent_returns_identifier(self):
105+
agent = self._create_basic_agent("TestCreate", "test-create")
106+
result = self.store.maintenance.send(AddOrUpdateAiAgentOperation(agent))
107+
self._created_agent_ids.append(result.identifier)
108+
109+
self.assertIsNotNone(result)
110+
self.assertEqual("test-create", result.identifier)
111+
112+
def test_create_agent_via_store_ai(self):
113+
agent = self._create_basic_agent("TestCreateViaAi", "test-create-via-ai")
114+
result = self.store.ai.add_or_update_agent(agent)
115+
self._created_agent_ids.append(result.identifier)
116+
117+
self.assertIsNotNone(result)
118+
self.assertEqual("test-create-via-ai", result.identifier)
119+
120+
def test_create_full_agent(self):
121+
agent = self._create_full_agent("TestCreateFull", "test-create-full")
122+
result = self.store.ai.add_or_update_agent(agent)
123+
self._created_agent_ids.append(result.identifier)
124+
125+
self.assertIsNotNone(result)
126+
self.assertEqual("test-create-full", result.identifier)
127+
128+
# ---- Get ----
129+
130+
def test_get_all_agents_returns_list(self):
131+
agent1 = self._create_basic_agent("TestGetAll1", "test-get-all-1")
132+
agent2 = self._create_basic_agent("TestGetAll2", "test-get-all-2")
133+
r1 = self.store.ai.add_or_update_agent(agent1)
134+
r2 = self.store.ai.add_or_update_agent(agent2)
135+
self._created_agent_ids.extend([r1.identifier, r2.identifier])
136+
137+
response = self.store.ai.get_agents()
138+
139+
self.assertIsNotNone(response)
140+
ids = [a.identifier for a in response.ai_agents]
141+
self.assertIn("test-get-all-1", ids)
142+
self.assertIn("test-get-all-2", ids)
143+
144+
def test_get_agent_by_id(self):
145+
agent = self._create_basic_agent("TestGetById", "test-get-by-id")
146+
result = self.store.ai.add_or_update_agent(agent)
147+
self._created_agent_ids.append(result.identifier)
148+
149+
response = self.store.ai.get_agents("test-get-by-id")
150+
151+
self.assertIsNotNone(response)
152+
self.assertEqual(1, len(response.ai_agents))
153+
self.assertEqual("test-get-by-id", response.ai_agents[0].identifier)
154+
155+
def test_get_agent_by_id_returns_correct_config(self):
156+
agent = self._create_full_agent("TestGetConfig", "test-get-config")
157+
result = self.store.ai.add_or_update_agent(agent)
158+
self._created_agent_ids.append(result.identifier)
159+
160+
response = self.store.ai.get_agents("test-get-config")
161+
fetched = response.ai_agents[0]
162+
163+
self.assertEqual("TestGetConfig", fetched.name)
164+
self.assertEqual(self.CONNECTION_STRING_NAME, fetched.connection_string_name)
165+
self.assertEqual(1, len(fetched.queries))
166+
self.assertEqual("get-orders", fetched.queries[0].name)
167+
self.assertEqual(1, len(fetched.actions))
168+
self.assertEqual("store-result", fetched.actions[0].name)
169+
self.assertEqual(1, len(fetched.parameters))
170+
self.assertEqual("country", fetched.parameters[0].name)
171+
self.assertEqual(3, fetched.max_model_iterations_per_call)
172+
173+
def test_get_agent_via_operation(self):
174+
agent = self._create_basic_agent("TestGetOp", "test-get-op")
175+
result = self.store.maintenance.send(AddOrUpdateAiAgentOperation(agent))
176+
self._created_agent_ids.append(result.identifier)
177+
178+
response = self.store.maintenance.send(GetAiAgentOperation("test-get-op"))
179+
180+
self.assertIsNotNone(response)
181+
self.assertEqual(1, len(response.ai_agents))
182+
self.assertEqual("test-get-op", response.ai_agents[0].identifier)
183+
184+
# ---- Update ----
185+
186+
def test_update_agent_system_prompt(self):
187+
agent = self._create_basic_agent("TestUpdate", "test-update")
188+
result = self.store.ai.add_or_update_agent(agent)
189+
self._created_agent_ids.append(result.identifier)
190+
191+
agent.system_prompt = "Updated system prompt."
192+
self.store.ai.add_or_update_agent(agent)
193+
194+
response = self.store.ai.get_agents("test-update")
195+
self.assertEqual("Updated system prompt.", response.ai_agents[0].system_prompt)
196+
197+
def test_update_agent_adds_query_tool(self):
198+
agent = self._create_basic_agent("TestUpdateQuery", "test-update-query")
199+
result = self.store.ai.add_or_update_agent(agent)
200+
self._created_agent_ids.append(result.identifier)
201+
202+
agent.queries.append(
203+
AiAgentToolQuery(
204+
name="new-query",
205+
description="A newly added query tool.",
206+
query="from Employees",
207+
parameters_sample_object="{}",
208+
)
209+
)
210+
self.store.ai.add_or_update_agent(agent)
211+
212+
response = self.store.ai.get_agents("test-update-query")
213+
query_names = [q.name for q in response.ai_agents[0].queries]
214+
self.assertIn("new-query", query_names)
215+
216+
def test_update_agent_max_iterations(self):
217+
agent = self._create_basic_agent("TestUpdateIter", "test-update-iter")
218+
result = self.store.ai.add_or_update_agent(agent)
219+
self._created_agent_ids.append(result.identifier)
220+
221+
agent.max_model_iterations_per_call = 10
222+
self.store.ai.add_or_update_agent(agent)
223+
224+
response = self.store.ai.get_agents("test-update-iter")
225+
self.assertEqual(10, response.ai_agents[0].max_model_iterations_per_call)
226+
227+
# ---- Delete ----
228+
229+
def test_delete_agent(self):
230+
agent = self._create_basic_agent("TestDelete", "test-delete")
231+
result = self.store.ai.add_or_update_agent(agent)
232+
233+
self.store.ai.delete_agent(result.identifier)
234+
235+
with self.assertRaises(RuntimeError):
236+
self.store.ai.get_agents("test-delete")
237+
238+
def test_delete_agent_via_operation(self):
239+
agent = self._create_basic_agent("TestDeleteOp", "test-delete-op")
240+
result = self.store.maintenance.send(AddOrUpdateAiAgentOperation(agent))
241+
242+
self.store.maintenance.send(DeleteAiAgentOperation(result.identifier))
243+
244+
with self.assertRaises(RuntimeError):
245+
self.store.maintenance.send(GetAiAgentOperation("test-delete-op"))
246+
247+
# ---- Full lifecycle ----
248+
249+
def test_full_lifecycle(self):
250+
# 1. Create
251+
agent = self._create_full_agent("TestLifecycle", "test-lifecycle")
252+
create_result = self.store.ai.add_or_update_agent(agent)
253+
self.assertEqual("test-lifecycle", create_result.identifier)
254+
255+
# 2. Get
256+
response = self.store.ai.get_agents("test-lifecycle")
257+
self.assertEqual(1, len(response.ai_agents))
258+
fetched = response.ai_agents[0]
259+
self.assertEqual("TestLifecycle", fetched.name)
260+
261+
# 3. Update
262+
agent.system_prompt = "Updated lifecycle prompt."
263+
agent.max_model_iterations_per_call = 5
264+
self.store.ai.add_or_update_agent(agent)
265+
266+
# 4. Verify update
267+
updated_response = self.store.ai.get_agents("test-lifecycle")
268+
updated = updated_response.ai_agents[0]
269+
self.assertEqual("Updated lifecycle prompt.", updated.system_prompt)
270+
self.assertEqual(5, updated.max_model_iterations_per_call)
271+
272+
# 5. Delete
273+
self.store.ai.delete_agent("test-lifecycle")
274+
275+
# 6. Verify deletion - server raises error when agent doesn't exist
276+
with self.assertRaises(RuntimeError):
277+
self.store.ai.get_agents("test-lifecycle")
278+
279+
# ---- Query tool options ----
280+
281+
def test_query_tool_options_persisted(self):
282+
agent = AiAgentConfiguration(
283+
identifier="test-query-options",
284+
name="TestQueryOptions",
285+
connection_string_name=self.CONNECTION_STRING_NAME,
286+
system_prompt="You help customers with their orders.",
287+
sample_object='{"answer": "embed your answer here"}',
288+
queries=[
289+
AiAgentToolQuery(
290+
name="GetRecentOrders",
291+
description="Retrieves recent orders for a customer",
292+
query="from Orders where CustomerId = $customerId order by OrderDate desc limit 5",
293+
parameters_sample_object='{"customerId": "embed customer id here"}',
294+
options=AiAgentToolQueryOptions(
295+
add_to_initial_context=True,
296+
allow_model_queries=False,
297+
),
298+
),
299+
],
300+
)
301+
self._created_agent_ids.append("test-query-options")
302+
303+
self.store.ai.add_or_update_agent(agent)
304+
305+
response = self.store.ai.get_agents("test-query-options")
306+
self.assertEqual(1, len(response.ai_agents))
307+
308+
fetched = response.ai_agents[0]
309+
self.assertEqual(1, len(fetched.queries))
310+
311+
opts = fetched.queries[0].options
312+
self.assertIsNotNone(opts)
313+
self.assertTrue(opts.add_to_initial_context)
314+
self.assertFalse(opts.allow_model_queries)
315+
316+
def test_query_tool_options_defaults_when_not_set(self):
317+
agent = AiAgentConfiguration(
318+
identifier="test-query-no-options",
319+
name="TestQueryNoOptions",
320+
connection_string_name=self.CONNECTION_STRING_NAME,
321+
system_prompt="Agent without query options.",
322+
sample_object='{"answer": "embed your answer here"}',
323+
queries=[
324+
AiAgentToolQuery(
325+
name="GetOrders",
326+
description="Retrieves orders",
327+
query="from Orders",
328+
parameters_sample_object="{}",
329+
),
330+
],
331+
)
332+
self._created_agent_ids.append("test-query-no-options")
333+
334+
self.store.ai.add_or_update_agent(agent)
335+
336+
response = self.store.ai.get_agents("test-query-no-options")
337+
fetched = response.ai_agents[0]
338+
self.assertEqual(1, len(fetched.queries))
339+
# No options set — options should be None or have default values
340+
opts = fetched.queries[0].options
341+
if opts is not None:
342+
self.assertIsNone(opts.add_to_initial_context)
343+
self.assertIsNone(opts.allow_model_queries)

0 commit comments

Comments
 (0)