Skip to content

Commit 9ea3601

Browse files
authored
Merge pull request #294 from poissoncorp/RDBC-1059
RDBC-1059 Python SDK 7.2.1 -> 7.2.2
2 parents b1919f5 + d4b939c commit 9ea3601

46 files changed

Lines changed: 2679 additions & 432 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

ravendb/__init__.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,8 +91,11 @@
9191
AiAgentConfiguration,
9292
AiAgentConfigurationResult,
9393
AiAgentParameter,
94+
AiAgentParameterPolicy,
95+
AiAgentParameterValueType,
9496
AiAgentToolAction,
9597
AiAgentToolQuery,
98+
AiAgentToolSubAgent,
9699
AiAgentPersistenceConfiguration,
97100
AiAgentChatTrimmingConfiguration,
98101
AiAgentSummarizationByTokens,
@@ -101,10 +104,13 @@
101104
RunConversationOperation,
102105
ConversationResult,
103106
AiAgentActionRequest,
107+
AiAgentActionRequestType,
104108
AiAgentActionResponse,
105109
AiAgentArtificialActionResponse,
106110
AiUsage,
107111
AiConversationCreationOptions,
112+
AiConversationParameter,
113+
AiConversationParameterOptions,
108114
GetAiAgentOperation,
109115
GetAiAgentsResponse,
110116
AddOrUpdateAiAgentOperation,
@@ -195,6 +201,7 @@
195201
from ravendb.documents.queries.highlighting import HighlightingOptions, QueryHighlightings
196202
from ravendb.documents.queries.index_query import IndexQuery
197203
from ravendb.documents.queries.misc import SearchOperator
204+
from ravendb.documents.queries.raven_document_query import RavenDocumentQuery
198205
from ravendb.documents.queries.more_like_this import (
199206
MoreLikeThisOperations,
200207
MoreLikeThisBase,
@@ -238,6 +245,7 @@
238245
DocumentsChanges,
239246
ForceRevisionStrategy,
240247
MethodCall,
248+
OptimisticConcurrencyMode,
241249
OrderingType,
242250
JavaScriptMap,
243251
DocumentQueryCustomization,

ravendb/documents/ai/ai_conversation.py

Lines changed: 37 additions & 116 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
import json
44
import traceback
5-
from typing import List, Dict, Any, Optional, TypeVar, TYPE_CHECKING, Callable
5+
from typing import List, Dict, Any, IO, Optional, TypeVar, TYPE_CHECKING, Callable, Union
66
from datetime import timedelta
77

88
from ravendb.documents.ai.ai_answer import AiAnswer, AiConversationStatus
@@ -33,14 +33,7 @@ def __init__(self, sender: AiConversation, action: AiAgentActionRequest):
3333

3434

3535
class AiConversation:
36-
"""
37-
Implementation of AI conversation operations for managing conversations with AI agents.
38-
39-
Can be used as a context manager for automatic cleanup:
40-
with store.ai.conversation(agent_id) as conversation:
41-
conversation.set_user_prompt("Hello!")
42-
result = conversation.run()
43-
"""
36+
# Usable as a context manager: `with store.ai.conversation(agent_id) as c:`.
4437

4538
def __init__(
4639
self,
@@ -60,35 +53,43 @@ def __init__(
6053
self._action_responses: Dict[str, AiAgentActionResponse] = {}
6154
self._artificial_actions: List[AiAgentArtificialActionResponse] = []
6255
self._action_requests: Optional[List[AiAgentActionRequest]] = None
56+
self._attachments_commands: List = []
6357

64-
# Action handlers
6558
self._invocations: Dict[str, Callable[[AiAgentActionRequest], None]] = {}
66-
6759
self.on_unhandled_action: Optional[Callable[[UnhandledActionEventArgs], None]] = None
6860

61+
def add_attachment(self, name: str, stream: Union[bytes, IO[bytes]], content_type: str) -> None:
62+
# `stream` is raw bytes or any binary file-like; each stream may only
63+
# be used once per turn (SingleNodeBatchCommand enforces uniqueness).
64+
if stream is None:
65+
raise ValueError("stream cannot be None")
66+
from ravendb.documents.commands.batches import PutAttachmentCommandData
67+
68+
self._attachments_commands.append(
69+
PutAttachmentCommandData("__this__", name, stream, content_type, change_vector=None)
70+
)
71+
72+
def copy_attachment_from(self, source_document_id: str, file_name: str) -> None:
73+
if not source_document_id or (isinstance(source_document_id, str) and source_document_id.isspace()):
74+
raise ValueError("source_document_id cannot be None or empty")
75+
if not file_name or (isinstance(file_name, str) and file_name.isspace()):
76+
raise ValueError("file_name cannot be None or empty")
77+
from ravendb.documents.commands.batches import CopyAttachmentCommandData
78+
79+
self._attachments_commands.append(
80+
CopyAttachmentCommandData(source_document_id, file_name, "__this__", file_name, change_vector=None)
81+
)
82+
6983
def __enter__(self) -> AiConversation:
70-
"""Context manager entry."""
7184
return self
7285

7386
def __exit__(self, exc_type, exc_val, exc_tb) -> None:
74-
"""Context manager exit - cleanup resources."""
7587
pass
7688

7789
@classmethod
7890
def with_conversation_id(
7991
cls, store: DocumentStore, conversation_id: str, change_vector: str = None
8092
) -> AiConversation:
81-
"""
82-
Creates a conversation instance for continuing an existing conversation.
83-
84-
Args:
85-
store: The document store
86-
conversation_id: The ID of the existing conversation
87-
change_vector: Optional change vector for optimistic concurrency
88-
89-
Returns:
90-
A new conversation instance
91-
"""
9293
return cls(
9394
store=store,
9495
conversation_id=conversation_id,
@@ -97,28 +98,11 @@ def with_conversation_id(
9798

9899
@property
99100
def required_actions(self) -> List[AiAgentActionRequest]:
100-
"""
101-
Gets the list of action requests that need to be fulfilled before
102-
the conversation can continue.
103-
104-
Raises:
105-
RuntimeError: If run() hasn't been called yet
106-
"""
107101
if self._action_requests is None:
108102
raise RuntimeError("You have to call run() first")
109103
return self._action_requests
110104

111105
def add_action_response(self, action_id: str, action_response: str) -> None:
112-
"""
113-
Adds a response for a given action request.
114-
115-
Args:
116-
action_id: The ID of the action to respond to
117-
action_response: The response content
118-
119-
Raises:
120-
InvalidOperationException: If a response for the given tool-id was already added
121-
"""
122106
from ravendb.documents.operations.ai.agents import AiAgentActionResponse
123107

124108
if action_id in self._action_responses:
@@ -136,16 +120,8 @@ def add_action_response(self, action_id: str, action_response: str) -> None:
136120
self._action_responses[action_id] = response
137121

138122
def add_artificial_action_with_response(self, tool_id: str, action_response) -> None:
139-
"""
140-
Injects an artificial action (tool call) and a response into the model's conversation context.
141-
This is an advanced mechanism to programmatically prompt the agent, causing it to "believe"
142-
it successfully executed a tool and received the specified action_response.
143-
144-
Args:
145-
tool_id: The name of the tool to simulate the agent called.
146-
action_response: The response to supply to the agent as the result of the simulated action.
147-
Can be a string or any object that will be serialized to JSON.
148-
"""
123+
# Injects a synthetic tool-call + response so the agent "believes" it
124+
# already executed `tool_id` and got `action_response` back.
149125
if not tool_id or (isinstance(tool_id, str) and tool_id.isspace()):
150126
raise ValueError("tool_id cannot be None or empty")
151127
if action_response is None:
@@ -159,22 +135,12 @@ def add_artificial_action_with_response(self, tool_id: str, action_response) ->
159135
self._artificial_actions.append(AiAgentArtificialActionResponse(tool_id=tool_id, content=content))
160136

161137
def run(self) -> AiAnswer:
162-
"""
163-
Executes the conversation loop, automatically handling action requests
164-
until the conversation is complete or no handlers are available.
165-
166-
Returns:
167-
AiAnswer with the final response, status, usage, and elapsed time
168-
"""
169138
while True:
170139
r = self._run_internal()
171140
if self._handle_server_reply(r):
172141
return r
173142

174143
def stream(self, stream_property_path: str = None, on_chunk: Optional[Callable[[str], None]] = None) -> AiAnswer:
175-
"""
176-
Stream the LLM response for the given property and return the final AiAnswer when done.
177-
"""
178144
while True:
179145
r = self._run_internal(stream_property_path=stream_property_path, streamed_chunks_callback=on_chunk)
180146
if self._handle_server_reply(r):
@@ -185,21 +151,16 @@ def _run_internal(
185151
stream_property_path: Optional[str] = None,
186152
streamed_chunks_callback: Optional[Callable[[str], None]] = None,
187153
) -> AiAnswer:
188-
"""
189-
Internal method that executes a single server call.
190-
191-
Returns:
192-
AiAnswer from this single turn
193-
"""
194154
from ravendb.documents.operations.ai.agents import RunConversationOperation
195155
import time
196156

197-
# If we already went to the server and have nothing new to tell it, we're done
157+
# Already round-tripped and nothing new to send.
198158
if (
199159
self._action_requests is not None
200160
and len(self._prompt_parts) == 0
201161
and len(self._action_responses) == 0
202162
and len(self._artificial_actions) == 0
163+
and len(self._attachments_commands) == 0
203164
):
204165
return AiAnswer(
205166
answer=None,
@@ -208,40 +169,35 @@ def _run_internal(
208169
elapsed=None,
209170
)
210171

211-
# Build the operation
212172
if not self._agent_id:
213173
raise ValueError("Agent ID is required")
214174

215-
# If we don't have a conversation ID yet, generate one with the prefix
216-
# The server will complete it with a unique ID
175+
# Trailing "/" tells the server to assign a unique id.
217176
if not self._conversation_id:
218177
self._conversation_id = "conversations/"
219178

220-
# Create operation with all required parameters
221179
operation = RunConversationOperation(
222180
agent_id=self._agent_id,
223181
conversation_id=self._conversation_id,
224-
prompt_parts=self._prompt_parts, # Always send list, even if empty
225-
action_responses=list(self._action_responses.values()), # Always send list, even if empty
226-
artificial_actions=self._artificial_actions, # Always send list, even if empty
182+
prompt_parts=self._prompt_parts,
183+
action_responses=list(self._action_responses.values()),
184+
artificial_actions=self._artificial_actions,
227185
options=self._options,
228186
change_vector=self._change_vector,
229187
stream_property_path=stream_property_path,
230188
streamed_chunks_callback=streamed_chunks_callback,
189+
attachments_commands=self._attachments_commands,
231190
)
232191

233192
try:
234-
# Track elapsed time
235193
start_time = time.time()
236194
result = self._store.maintenance.send(operation)
237195
elapsed = timedelta(seconds=time.time() - start_time)
238196

239-
# Update conversation state
240197
self._change_vector = result.change_vector
241198
self._conversation_id = result.conversation_id
242199
self._action_requests = result.action_requests or []
243200

244-
# Build AiAnswer
245201
return AiAnswer(
246202
answer=result.response,
247203
status=(
@@ -252,25 +208,14 @@ def _run_internal(
252208
usage=result.usage,
253209
elapsed=elapsed,
254210
)
255-
# except ConcurrencyException as e:
256-
# self._change_vector = e.actual_change_vector
257-
# raise
258211
finally:
259-
# Clear the user prompt and tool responses after running the conversation
260212
self._prompt_parts.clear()
261213
self._action_responses.clear()
262214
self._artificial_actions.clear()
215+
self._attachments_commands.clear()
263216

264217
def _handle_server_reply(self, answer: AiAnswer) -> bool:
265-
"""
266-
Handles the server reply by invoking registered action handlers.
267-
268-
Args:
269-
answer: The answer from the server
270-
271-
Returns:
272-
True if the conversation is done, False if it should continue
273-
"""
218+
# Returns True when the conversation is done.
274219
if answer.status == AiConversationStatus.DONE:
275220
return True
276221

@@ -279,52 +224,28 @@ def _handle_server_reply(self, answer: AiAnswer) -> bool:
279224
f"There are no action requests to process, but Status was {answer.status}, should not be possible."
280225
)
281226

282-
# Process each action request
283227
for action in self._action_requests:
284228
if action.name in self._invocations:
285-
# Invoke the registered handler
286-
# Error handling is done by the invocation based on the error strategy
287229
self._invocations[action.name](action)
288230
elif self.on_unhandled_action is not None:
289231
self.on_unhandled_action(UnhandledActionEventArgs(self, action))
290232
else:
291-
# No handler registered for this action
292233
raise RuntimeError(
293234
f"There is no action defined for action '{action.name}' on agent '{self._agent_id}' "
294235
f"({self._conversation_id}), but it was invoked by the model with: {action.arguments}. "
295236
f"Did you forget to call {self.receive.__name__}() or {self.handle.__name__}()? You can also handle unexpected action invocations using the 'on_unhandled_action' event."
296237
)
297238

298-
# If we have nothing to tell the server (no action responses), we're done
299-
# Otherwise, continue the loop to send the responses
239+
# No responses to deliver => nothing more to tell the server.
300240
return len(self._action_responses) == 0
301241

302242
def set_user_prompt(self, user_prompt: str) -> None:
303-
"""
304-
Sets the user prompt to send to the AI agent.
305-
Clears any existing prompt parts and adds the new prompt.
306-
307-
Args:
308-
user_prompt: The prompt text to send to the agent
309-
310-
Raises:
311-
ValueError: If user_prompt is empty or whitespace-only
312-
"""
313243
if not user_prompt or user_prompt.isspace():
314244
raise ValueError("User prompt cannot be empty or whitespace-only")
315245
self._prompt_parts.clear()
316246
self.add_user_prompt(user_prompt)
317247

318248
def add_user_prompt(self, *prompts: str) -> None:
319-
"""
320-
Adds one or more user prompts to the conversation.
321-
322-
Args:
323-
*prompts: One or more prompt strings to add
324-
325-
Raises:
326-
ValueError: If any prompt is empty or whitespace-only
327-
"""
328249
for prompt in prompts:
329250
if not prompt or prompt.isspace():
330251
raise ValueError("User prompt cannot be empty or whitespace-only")

ravendb/documents/ai/content_part.py

Lines changed: 1 addition & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -3,24 +3,16 @@
33

44

55
class AiMessagePromptFields:
6-
"""Constants for AI message prompt field names."""
7-
86
TEXT = "text"
97
TYPE = "type"
8+
IMAGE = "image"
109

1110

1211
class AiMessagePromptTypes:
13-
"""Constants for AI message prompt types."""
14-
1512
TEXT = "text"
1613

1714

1815
class ContentPart:
19-
"""
20-
Base class for content parts in AI prompts.
21-
Content parts allow structured prompt content with different types (text, etc.).
22-
"""
23-
2416
def __init__(self, content_type: str):
2517
self._type = content_type
2618

@@ -29,18 +21,10 @@ def type(self) -> str:
2921
return self._type
3022

3123
def to_json(self) -> Dict[str, Any]:
32-
"""
33-
Converts the content part to a JSON-serializable dictionary.
34-
Subclasses should override this method to include their specific fields.
35-
"""
3624
return {AiMessagePromptFields.TYPE: self._type}
3725

3826

3927
class TextPart(ContentPart):
40-
"""
41-
Represents a text content part in AI prompts.
42-
"""
43-
4428
def __init__(self, text: str):
4529
super().__init__(AiMessagePromptTypes.TEXT)
4630
self._text = text

0 commit comments

Comments
 (0)