Skip to content

Commit 29b9bf2

Browse files
committed
RDBC-1059 Add AiAgentParameter Policy and ValueType
1 parent 0756cd7 commit 29b9bf2

4 files changed

Lines changed: 178 additions & 56 deletions

File tree

ravendb/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,8 @@
9191
AiAgentConfiguration,
9292
AiAgentConfigurationResult,
9393
AiAgentParameter,
94+
AiAgentParameterPolicy,
95+
AiAgentParameterValueType,
9496
AiAgentToolAction,
9597
AiAgentToolQuery,
9698
AiAgentPersistenceConfiguration,

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
from .ai_agent_configuration import (
22
AiAgentConfiguration,
33
AiAgentParameter,
4+
AiAgentParameterPolicy,
5+
AiAgentParameterValueType,
46
AiAgentToolAction,
57
AiAgentToolQuery,
68
AiAgentToolQueryOptions,
@@ -38,6 +40,8 @@
3840
"AiAgentConfiguration",
3941
"AiAgentConfigurationResult",
4042
"AiAgentParameter",
43+
"AiAgentParameterPolicy",
44+
"AiAgentParameterValueType",
4145
"AiAgentToolAction",
4246
"AiAgentToolQuery",
4347
"AiAgentToolQueryOptions",

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

Lines changed: 46 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -1,58 +1,79 @@
11
from __future__ import annotations
2+
import enum
23
from typing import List, Optional, Dict, Any, Union
34

45

5-
class AiAgentParameter:
6-
"""
7-
Represents a parameter for an AI agent configuration.
8-
Parameters can be used to pass values to the agent's system prompt.
9-
"""
6+
class AiAgentParameterPolicy(enum.IntFlag):
7+
# FORBID_MODEL_GENERATION blocks a parent agent from generating values for
8+
# this parameter when invoking a sub-agent that declares it.
9+
DEFAULT = 0
10+
FORBID_MODEL_GENERATION = 1
11+
12+
13+
class AiAgentParameterValueType(enum.Enum):
14+
DEFAULT = "Default"
15+
STRING = "String"
16+
NUMBER = "Number"
17+
BOOLEAN = "Boolean"
18+
ARRAY_OF_STRING = "ArrayOfString"
19+
ARRAY_OF_NUMBER = "ArrayOfNumber"
20+
ARRAY_OF_BOOLEAN = "ArrayOfBoolean"
21+
NULL = "Null"
22+
23+
def __str__(self) -> str:
24+
return self.value
1025

26+
27+
class AiAgentParameter:
1128
def __init__(
1229
self,
1330
name: str = None,
1431
description: str = None,
1532
send_to_model: bool = None,
33+
policy: AiAgentParameterPolicy = AiAgentParameterPolicy.DEFAULT,
34+
type: AiAgentParameterValueType = AiAgentParameterValueType.DEFAULT,
1635
):
17-
"""
18-
Initialize an agent parameter.
19-
20-
Args:
21-
name: The parameter name. Cannot be null or empty.
22-
description: A human-readable description. May be null or empty.
23-
send_to_model: When False, the parameter is hidden from the model
24-
(it will not be included in prompts/echo messages).
25-
When True, the parameter is exposed to the model.
26-
If None (default), treated as exposed.
27-
"""
2836
self.name = name
2937
self.description: Optional[str] = description
3038
self.send_to_model: Optional[bool] = send_to_model
39+
self.policy: AiAgentParameterPolicy = policy
40+
self.type: AiAgentParameterValueType = type
3141

3242
def to_json(self) -> Dict[str, Any]:
3343
return {
3444
"Name": self.name,
3545
"Description": self.description,
3646
"SendToModel": self.send_to_model,
47+
"Policy": int(self.policy),
48+
"Type": self.type.value,
3749
}
3850

3951
@classmethod
4052
def from_json(cls, json_dict: Dict[str, Any]) -> AiAgentParameter:
53+
# Server emits Policy as either int (1) or PascalCase ("ForbidModelGeneration").
54+
policy_raw = json_dict.get("policy") if "policy" in json_dict else json_dict.get("Policy")
55+
if policy_raw is None or policy_raw == 0 or policy_raw == "":
56+
policy = AiAgentParameterPolicy.DEFAULT
57+
elif isinstance(policy_raw, str):
58+
snake = "".join("_" + c if i > 0 and c.isupper() else c for i, c in enumerate(policy_raw)).upper()
59+
policy = AiAgentParameterPolicy[snake]
60+
else:
61+
policy = AiAgentParameterPolicy(policy_raw)
62+
63+
type_raw = json_dict.get("type") if "type" in json_dict else json_dict.get("Type")
64+
type_ = AiAgentParameterValueType(type_raw) if type_raw else AiAgentParameterValueType.DEFAULT
65+
4166
return cls(
4267
name=json_dict.get("name") or json_dict.get("Name"),
4368
description=json_dict.get("description") or json_dict.get("Description"),
4469
send_to_model=json_dict.get("sendToModel") if "sendToModel" in json_dict else json_dict.get("SendToModel"),
70+
policy=policy,
71+
type=type_,
4572
)
4673

4774

4875
class AiAgentToolQuery:
49-
"""
50-
Represents a query tool that can be invoked by an AI agent.
51-
The tool includes a name, description, query string, and parameter schema or sample object.
52-
When invoked by the AI model, the query is expected to be executed by the server (database),
53-
and its results provided back to the model.
54-
"""
55-
76+
# Database-side RQL the model can call. Results are sent back to the model.
5677
def __init__(
5778
self,
5879
name: str = None,
@@ -98,12 +119,8 @@ def from_json(cls, json_dict: Dict[str, Any]) -> AiAgentToolQuery:
98119

99120

100121
class AiAgentToolAction:
101-
"""
102-
Represents a tool action that can be invoked by an AI agent.
103-
Includes metadata such as name, description, and optional parameters schema or sample.
104-
Tool actions represent external functions whose results are provided by the user
105-
"""
106-
122+
# External function the model can call. Its result is supplied by the user
123+
# (vs AiAgentToolQuery whose result comes from the database).
107124
def __init__(
108125
self,
109126
name: str = None,
@@ -137,11 +154,6 @@ def from_json(cls, json_dict: Dict[str, Any]) -> AiAgentToolAction:
137154

138155

139156
class AiAgentPersistenceConfiguration:
140-
"""
141-
Configuration for persisting chat history in RavenDB.
142-
Defines where chat sessions should be stored and optionally how long they should be retained (expiration).
143-
"""
144-
145157
def __init__(self, conversation_id_prefix: str = None, expires: int = None):
146158
self.conversation_id_prefix = conversation_id_prefix
147159
self.conversation_expiration_in_sec: Optional[int] = expires
@@ -163,10 +175,6 @@ def from_json(cls, json_dict: Dict[str, Any]) -> AiAgentPersistenceConfiguration
163175

164176

165177
class AiAgentSummarizationByTokens:
166-
"""
167-
Configuration settings for AI agent conversation summarization.
168-
"""
169-
170178
DEFAULT_MAX_TOKENS_BEFORE_SUMMARIZATION = 32 * 1024
171179

172180
def __init__(
@@ -208,10 +216,6 @@ def from_json(cls, json_dict: Dict[str, Any]) -> AiAgentSummarizationByTokens:
208216

209217

210218
class AiAgentTruncateChat:
211-
"""
212-
Configuration for truncating the AI chat history based on message count.
213-
"""
214-
215219
DEFAULT_MESSAGES_LENGTH_BEFORE_TRUNCATE = 500
216220

217221
def __init__(self, messages_length_before_truncate: int = None, messages_length_after_truncate: int = None):
@@ -241,10 +245,6 @@ def from_json(cls, json_dict: Dict[str, Any]) -> AiAgentTruncateChat:
241245

242246

243247
class AiAgentHistoryConfiguration:
244-
"""
245-
Defines the configuration for retention and expiration of AI agent chat history documents.
246-
"""
247-
248248
def __init__(self, history_expiration_in_sec: int = None):
249249
self.history_expiration_in_sec: Optional[int] = history_expiration_in_sec
250250

@@ -261,10 +261,6 @@ def from_json(cls, json_dict: Dict[str, Any]) -> AiAgentHistoryConfiguration:
261261

262262

263263
class AiAgentChatTrimmingConfiguration:
264-
"""
265-
Defines configuration options for reducing the size of the AI agent's chat history.
266-
"""
267-
268264
def __init__(
269265
self,
270266
tokens_config: AiAgentSummarizationByTokens = None,
@@ -295,11 +291,6 @@ def from_json(cls, json_dict: Dict[str, Any]) -> AiAgentChatTrimmingConfiguratio
295291

296292

297293
class AiAgentConfiguration:
298-
"""
299-
Defines the configuration for an AI agent in RavenDB, including the system prompt,
300-
tools (queries/actions), output schema, persistence settings, and connection string.
301-
"""
302-
303294
def __init__(
304295
self,
305296
name: str = None,
@@ -363,7 +354,6 @@ def to_json(self) -> Dict[str, Any]:
363354
@classmethod
364355
def from_json(cls, json_dict: Dict[str, Any]) -> AiAgentConfiguration:
365356
instance = cls()
366-
# Handle both camelCase and PascalCase for compatibility
367357
instance.identifier = json_dict.get("identifier") or json_dict.get("Identifier")
368358
instance.name = json_dict.get("name") or json_dict.get("Name")
369359
instance.connection_string_name = json_dict.get("connectionStringName") or json_dict.get("ConnectionStringName")
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
"""
2+
Unit tests for the 7.2.3 AI agent configuration additions:
3+
* AiAgentParameter.policy / .type enums
4+
* AiAgentToolSubAgent
5+
* AiAgentConfiguration.sub_agents
6+
* MissingAiAgentParameterException + dispatcher mapping
7+
* AiMessagePromptFields.IMAGE
8+
* AzureOpenAiSettings endpoint validation
9+
"""
10+
11+
import unittest
12+
13+
from ravendb import (
14+
AiAgentConfiguration,
15+
AiAgentParameter,
16+
AiAgentParameterPolicy,
17+
AiAgentParameterValueType,
18+
AiAgentToolSubAgent,
19+
)
20+
from ravendb.documents.ai.content_part import AiMessagePromptFields
21+
from ravendb.documents.operations.ai.azure_open_ai_settings import AzureOpenAiSettings
22+
from ravendb.exceptions.exception_dispatcher import _EXCEPTION_MAP
23+
from ravendb.exceptions.raven_exceptions import MissingAiAgentParameterException
24+
25+
26+
class TestAiAgentParameterPolicyAndType(unittest.TestCase):
27+
def test_defaults(self):
28+
p = AiAgentParameter(name="userId")
29+
self.assertEqual(AiAgentParameterPolicy.DEFAULT, p.policy)
30+
self.assertEqual(AiAgentParameterValueType.DEFAULT, p.type)
31+
32+
def test_to_json_emits_policy_and_type(self):
33+
p = AiAgentParameter(
34+
name="userId",
35+
description="Hidden",
36+
send_to_model=False,
37+
policy=AiAgentParameterPolicy.FORBID_MODEL_GENERATION,
38+
type=AiAgentParameterValueType.STRING,
39+
)
40+
out = p.to_json()
41+
self.assertEqual(1, out["Policy"])
42+
self.assertEqual("String", out["Type"])
43+
44+
def test_from_json_round_trip(self):
45+
out = {
46+
"Name": "u",
47+
"Description": "d",
48+
"SendToModel": True,
49+
"Policy": 1,
50+
"Type": "ArrayOfNumber",
51+
}
52+
p = AiAgentParameter.from_json(out)
53+
self.assertEqual(AiAgentParameterPolicy.FORBID_MODEL_GENERATION, p.policy)
54+
self.assertEqual(AiAgentParameterValueType.ARRAY_OF_NUMBER, p.type)
55+
56+
def test_from_json_accepts_policy_as_string(self):
57+
# The server returns Policy as the enum *name* string in GET responses
58+
# (e.g. "Default", "ForbidModelGeneration"), even though Python emits
59+
# the int on the way out. from_json must handle both forms.
60+
p = AiAgentParameter.from_json({"Name": "u", "Policy": "ForbidModelGeneration"})
61+
self.assertEqual(AiAgentParameterPolicy.FORBID_MODEL_GENERATION, p.policy)
62+
63+
def test_from_json_default_policy_as_string(self):
64+
p = AiAgentParameter.from_json({"Name": "u", "Policy": "Default"})
65+
self.assertEqual(AiAgentParameterPolicy.DEFAULT, p.policy)
66+
67+
68+
class TestAiAgentToolSubAgent(unittest.TestCase):
69+
def test_round_trip(self):
70+
sa = AiAgentToolSubAgent(identifier="benefits-agent", description="Handles benefits questions")
71+
out = sa.to_json()
72+
self.assertEqual("benefits-agent", out["Identifier"])
73+
self.assertEqual("Handles benefits questions", out["Description"])
74+
back = AiAgentToolSubAgent.from_json(out)
75+
self.assertEqual(sa.identifier, back.identifier)
76+
self.assertEqual(sa.description, back.description)
77+
78+
79+
class TestAiAgentConfigurationSubAgents(unittest.TestCase):
80+
def test_to_json_includes_sub_agents(self):
81+
cfg = AiAgentConfiguration(
82+
name="main",
83+
connection_string_name="cs",
84+
system_prompt="be helpful",
85+
sub_agents=[
86+
AiAgentToolSubAgent("benefits", "benefits-related"),
87+
AiAgentToolSubAgent("attendance", "PTO tracking"),
88+
],
89+
)
90+
out = cfg.to_json()
91+
self.assertEqual(2, len(out["SubAgents"]))
92+
self.assertEqual("benefits", out["SubAgents"][0]["Identifier"])
93+
94+
95+
class TestMissingAiAgentParameterException(unittest.TestCase):
96+
def test_dispatcher_registers_short_name(self):
97+
self.assertIn("MissingAiAgentParameterException", _EXCEPTION_MAP)
98+
self.assertIs(MissingAiAgentParameterException, _EXCEPTION_MAP["MissingAiAgentParameterException"])
99+
100+
def test_is_raven_exception(self):
101+
from ravendb.exceptions.raven_exceptions import RavenException
102+
103+
self.assertTrue(issubclass(MissingAiAgentParameterException, RavenException))
104+
105+
106+
class TestAiMessagePromptFieldsImage(unittest.TestCase):
107+
def test_image_constant(self):
108+
self.assertEqual("image", AiMessagePromptFields.IMAGE)
109+
110+
111+
class TestAzureOpenAiSettingsEndpointValidation(unittest.TestCase):
112+
def test_none_endpoint_rejected(self):
113+
with self.assertRaises(ValueError):
114+
AzureOpenAiSettings(api_key="k", endpoint=None, model="m", deployment_name="d")
115+
116+
def test_blank_endpoint_rejected(self):
117+
with self.assertRaises(ValueError):
118+
AzureOpenAiSettings(api_key="k", endpoint=" ", model="m", deployment_name="d")
119+
120+
def test_valid_endpoint_accepted(self):
121+
s = AzureOpenAiSettings(api_key="k", endpoint="https://x.openai.azure.com", model="m", deployment_name="d")
122+
self.assertEqual("https://x.openai.azure.com", s.endpoint)
123+
124+
125+
if __name__ == "__main__":
126+
unittest.main()

0 commit comments

Comments
 (0)