Skip to content

Commit 90fb5f4

Browse files
authored
Merge pull request #295 from poissoncorp/RDBC-1068-sync-7.2.3
RDBC-1068 Sync Python SDK to 7.2.3
2 parents 9ea3601 + c3ca81d commit 90fb5f4

16 files changed

Lines changed: 516 additions & 78 deletions

File tree

.github/pull_request_template.md

Lines changed: 6 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,49 +1,12 @@
1-
### Issue link
1+
### Issue
22

33
https://issues.hibernatingrhinos.com/issue/RDBC-...
44

5-
### Additional description
5+
### What changed
66

7-
...Include details of the change made in this Pull Request or additional notes for the solution. Anything that can be useful for reviewers of this PR...
7+
...
88

9-
### Type of change
9+
### Checklist
1010

11-
- [ ] Bug fix
12-
- [ ] Regression bug fix
13-
- [ ] Optimization
14-
- [ ] New feature
15-
16-
### How risky is the change?
17-
18-
- [ ] Low
19-
- [ ] Moderate
20-
- [ ] High
21-
- [ ] Not relevant
22-
23-
### Backward compatibility
24-
25-
- [ ] Non breaking change
26-
- [ ] Ensured. Please explain how has it been implemented?
27-
- [ ] Breaking change
28-
- [ ] Not relevant
29-
30-
### Is it platform specific issue?
31-
32-
- [ ] Yes. Please list the affected platforms.
33-
- [ ] No
34-
35-
### Documentation update
36-
37-
- [ ] This change requires a documentation update. Please mark the issue on YouTrack using `Python Documentation Required` tag.
38-
- [ ] No documentation update is needed
39-
40-
### Testing by Contributor
41-
42-
- [ ] Tests have been added that prove the fix is effective or that the feature works
43-
- [ ] It has been verified by manual testing
44-
- [ ] Existing tests verify the correct behavior
45-
46-
### Is there any existing behavior change of other features due to this change?
47-
48-
- [ ] Yes. Please list the affected features/subsystems and provide appropriate explanation
49-
- [ ] No
11+
- [ ] Tests added or existing tests cover the change
12+
- [ ] Breaking change (explain above if checked)

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,3 +18,4 @@
1818
*.raven-cluster-topology
1919
/build/lib/ravendb
2020
/venv/
21+
/.venv/

ravendb/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -247,6 +247,7 @@
247247
MethodCall,
248248
OptimisticConcurrencyMode,
249249
OrderingType,
250+
NullsOrdering,
250251
JavaScriptMap,
251252
DocumentQueryCustomization,
252253
ResponseTimeInformation,

ravendb/documents/ai/ai_conversation.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,18 +42,21 @@ def __init__(
4242
options: AiConversationCreationOptions = None,
4343
conversation_id: str = None,
4444
change_vector: str = None,
45+
debug: Optional[bool] = None,
4546
):
4647
self._store = store
4748
self._agent_id = agent_id
4849
self._options = options or AiConversationCreationOptions()
4950
self._conversation_id = conversation_id
5051
self._change_vector = change_vector
52+
self._debug = debug
5153

5254
self._prompt_parts: List[ContentPart] = []
5355
self._action_responses: Dict[str, AiAgentActionResponse] = {}
5456
self._artificial_actions: List[AiAgentArtificialActionResponse] = []
5557
self._action_requests: Optional[List[AiAgentActionRequest]] = None
5658
self._attachments_commands: List = []
59+
self._dispatched_tool_ids: set = set()
5760

5861
self._invocations: Dict[str, Callable[[AiAgentActionRequest], None]] = {}
5962
self.on_unhandled_action: Optional[Callable[[UnhandledActionEventArgs], None]] = None
@@ -135,6 +138,8 @@ def add_artificial_action_with_response(self, tool_id: str, action_response) ->
135138
self._artificial_actions.append(AiAgentArtificialActionResponse(tool_id=tool_id, content=content))
136139

137140
def run(self) -> AiAnswer:
141+
self._dispatched_tool_ids.clear()
142+
138143
while True:
139144
r = self._run_internal()
140145
if self._handle_server_reply(r):
@@ -154,9 +159,10 @@ def _run_internal(
154159
from ravendb.documents.operations.ai.agents import RunConversationOperation
155160
import time
156161

157-
# Already round-tripped and nothing new to send.
162+
# Already round-tripped and nothing new to send (no pending actions either).
158163
if (
159164
self._action_requests is not None
165+
and len(self._action_requests) == 0
160166
and len(self._prompt_parts) == 0
161167
and len(self._action_responses) == 0
162168
and len(self._artificial_actions) == 0
@@ -187,6 +193,7 @@ def _run_internal(
187193
stream_property_path=stream_property_path,
188194
streamed_chunks_callback=streamed_chunks_callback,
189195
attachments_commands=self._attachments_commands,
196+
debug=self._debug,
190197
)
191198

192199
try:
@@ -225,6 +232,11 @@ def _handle_server_reply(self, answer: AiAnswer) -> bool:
225232
)
226233

227234
for action in self._action_requests:
235+
# Skip actions we've already dispatched in a previous turn of this run
236+
if action.tool_id in self._dispatched_tool_ids:
237+
continue
238+
self._dispatched_tool_ids.add(action.tool_id)
239+
228240
if action.name in self._invocations:
229241
self._invocations[action.name](action)
230242
elif self.on_unhandled_action is not None:

ravendb/documents/ai/ai_operations.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
from __future__ import annotations
2-
from typing import TYPE_CHECKING, Dict, Any, Type
2+
from typing import TYPE_CHECKING, Dict, Any, Optional, Type
33

44
import warnings
55

@@ -77,6 +77,7 @@ def conversation(
7777
conversation_id: str,
7878
creation_options: "AiConversationCreationOptions" = None,
7979
change_vector: str = None,
80+
debug: Optional[bool] = None,
8081
) -> AiConversation:
8182
"""
8283
Creates a new conversation with the specified AI agent.
@@ -86,12 +87,13 @@ def conversation(
8687
conversation_id: The unique identifier for the conversation. You can also use e.g. chats/ for automatic id.
8788
creation_options: Optional creation options for the conversation
8889
change_vector: Optional change vector for concurrency control
90+
debug: Optional flag enabling server-side conversation debugging
8991
9092
Returns:
9193
Conversation operations interface for managing the conversation
9294
"""
9395

94-
return AiConversation(self._store, agent_id, creation_options, conversation_id, change_vector)
96+
return AiConversation(self._store, agent_id, creation_options, conversation_id, change_vector, debug)
9597

9698
def conversation_with_id(self, conversation_id: str, change_vector: str = None) -> AiConversation:
9799
"""

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

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -294,6 +294,7 @@ def __init__(
294294
stream_property_path: Optional[str] = None,
295295
streamed_chunks_callback: Optional[Callable[[str], None]] = None,
296296
attachments_commands: Optional[List[Any]] = None,
297+
debug: Optional[bool] = None,
297298
):
298299
if not agent_id or (isinstance(agent_id, str) and agent_id.isspace()):
299300
raise ValueError("agent_id cannot be None or empty")
@@ -312,6 +313,7 @@ def __init__(
312313
self._stream_property_path = stream_property_path
313314
self._streamed_chunks_callback = streamed_chunks_callback
314315
self._attachments_commands = attachments_commands or []
316+
self._debug = debug
315317

316318
def get_command(self, conventions: DocumentConventions) -> RavenCommand[ConversationResult[TSchema]]:
317319
return RunConversationCommand(
@@ -326,6 +328,7 @@ def get_command(self, conventions: DocumentConventions) -> RavenCommand[Conversa
326328
streamed_chunks_callback=self._streamed_chunks_callback,
327329
conventions=conventions,
328330
attachments_commands=self._attachments_commands,
331+
debug=self._debug,
329332
)
330333

331334

@@ -343,6 +346,7 @@ def __init__(
343346
streamed_chunks_callback: Optional[Callable[[str], None]] = None,
344347
conventions: Optional[DocumentConventions] = None,
345348
attachments_commands: Optional[List[Any]] = None,
349+
debug: Optional[bool] = None,
346350
):
347351
from ravendb.util.util import RaftIdGenerator
348352
from ravendb.documents.commands.batches import PutAttachmentCommandData
@@ -358,6 +362,7 @@ def __init__(
358362
self._stream_property_path = stream_property_path
359363
self._streamed_chunks_callback = streamed_chunks_callback
360364
self._conventions = conventions
365+
self._debug = debug
361366
self._attachments_commands = attachments_commands or []
362367

363368
# Raft id pinned at construction so retries keep the same id.
@@ -400,6 +405,10 @@ def create_request(self, node: ServerNode) -> requests.Request:
400405
if self._stream_property_path:
401406
url += f"&streaming=true&streamPropertyPath={quote(self._stream_property_path)}"
402407

408+
# Add debug flag if requested
409+
if self._debug is not None:
410+
url += f"&debug={self._debug}"
411+
403412
request_body = ConversationRequestBody(
404413
action_responses=self._action_responses,
405414
artificial_actions=self._artificial_actions,

ravendb/documents/operations/ai/chunking_options.py

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,24 +25,36 @@ def __init__(
2525
chunking_method: Optional[ChunkingMethod] = None,
2626
max_tokens_per_chunk: int = 512,
2727
overlap_tokens: int = 0,
28+
context_prefix: Optional[str] = None,
2829
):
2930
self.chunking_method = chunking_method
3031
self.max_tokens_per_chunk = max_tokens_per_chunk
3132
self.overlap_tokens = overlap_tokens
3233

34+
# Optional constant text prepended to every produced chunk before it is sent to the embedding model.
35+
# Useful for adding broader document context (e.g. title) to isolated chunks. The prefix's tokens count
36+
# against max_tokens_per_chunk - the effective chunking budget is reduced accordingly.
37+
self.context_prefix = context_prefix
38+
39+
# Internal-only marker: when set, the value is emitted unchunked with context_prefix prepended, and
40+
# max_tokens_per_chunk / overlap_tokens are ignored. Never set by user-constructed config; not serialized.
41+
self.no_chunking = False
42+
3343
@classmethod
3444
def from_json(cls, json_dict: Dict[str, Any]) -> "ChunkingOptions":
3545
return cls(
3646
chunking_method=ChunkingMethod(json_dict["ChunkingMethod"]),
3747
max_tokens_per_chunk=json_dict.get("MaxTokensPerChunk", None),
3848
overlap_tokens=json_dict.get("OverlapTokens", None),
49+
context_prefix=json_dict.get("ContextPrefix", None),
3950
)
4051

4152
def to_json(self) -> Dict[str, Any]:
4253
return {
4354
"ChunkingMethod": self.chunking_method.value if self.chunking_method else None,
4455
"MaxTokensPerChunk": self.max_tokens_per_chunk,
4556
"OverlapTokens": self.overlap_tokens,
57+
"ContextPrefix": self.context_prefix,
4658
}
4759

4860
def validate(self, source: str, errors: List[str]) -> None:
@@ -52,6 +64,16 @@ def validate(self, source: str, errors: List[str]) -> None:
5264
source: The source context for error messages (e.g., 'embeddings.generate').
5365
errors: List to append validation errors to.
5466
"""
67+
if self.context_prefix is not None and not self.context_prefix.strip():
68+
errors.append(
69+
f"{source}: ContextPrefix cannot be empty or whitespace-only. "
70+
f"Either provide a non-empty value or omit it."
71+
)
72+
73+
# no_chunking is set only by the with_context_prefix handler on raw strings/arrays and bypasses budget rules.
74+
if self.no_chunking:
75+
return
76+
5577
if self.max_tokens_per_chunk <= 0:
5678
errors.append(f"{source}: MaxTokensPerChunk must be greater than 0.")
5779

@@ -87,7 +109,17 @@ def __eq__(self, other: object) -> bool:
87109
self.chunking_method == other.chunking_method
88110
and self.max_tokens_per_chunk == other.max_tokens_per_chunk
89111
and self.overlap_tokens == other.overlap_tokens
112+
and self.context_prefix == other.context_prefix
113+
and self.no_chunking == other.no_chunking
90114
)
91115

92116
def __hash__(self) -> int:
93-
return hash((self.chunking_method, self.max_tokens_per_chunk, self.overlap_tokens))
117+
return hash(
118+
(
119+
self.chunking_method,
120+
self.max_tokens_per_chunk,
121+
self.overlap_tokens,
122+
self.context_prefix,
123+
self.no_chunking,
124+
)
125+
)

ravendb/documents/session/misc.py

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from __future__ import annotations
22
import datetime
33
import hashlib
4+
import json
45
import threading
56
from abc import ABC
67
from enum import Enum
@@ -338,15 +339,26 @@ def _get_next_argument_name(self) -> str:
338339
self._arg_counter += 1
339340
return f"val_{self._arg_counter - 1}_{self._suffix}"
340341

342+
@staticmethod
343+
def _format_key_for_javascript(key: _T_Key) -> str:
344+
# Emit the key as a JS string literal (with surrounding quotes and proper escaping) so that keys
345+
# containing dots, spaces, quotes, or numeric/special characters are handled correctly.
346+
if key is None:
347+
raise ValueError("Dictionary key cannot be None")
348+
return json.dumps(str(key))
349+
341350
def put(self, key: _T_Key, value: _T_Value) -> JavaScriptMap[_T_Key, _T_Value]:
342351
argument_name = self._get_next_argument_name()
343352

344-
self._script_lines.append(f"this.{self._path_to_map}.{key} = args.{argument_name};")
353+
formatted_key = self._format_key_for_javascript(key)
354+
self._script_lines.append(f"this.{self._path_to_map}[{formatted_key}] = args.{argument_name};")
345355
self.parameters[argument_name] = value
346356
return self
347357

348358
def remove(self, key: _T_Key) -> JavaScriptMap[_T_Key, _T_Value]:
349-
self._script_lines.append(f"delete this.{self._path_to_map}.{key};")
359+
formatted_key = self._format_key_for_javascript(key)
360+
self._script_lines.append(f"delete this.{self._path_to_map}[{formatted_key}];")
361+
return self
350362

351363

352364
class MethodCall(ABC):
@@ -379,3 +391,22 @@ class OrderingType(Enum):
379391

380392
def __str__(self):
381393
return self.value
394+
395+
396+
class NullsOrdering(Enum):
397+
"""
398+
Controls where ``null`` values are placed in the result of an ``ORDER BY`` clause.
399+
400+
Per-query null placement (``FIRST`` / ``LAST``) is supported only by the Corax indexing engine.
401+
Queries that specify ``FIRST`` or ``LAST`` against a Lucene index are rejected.
402+
"""
403+
404+
# No per-query placement is specified; the index/server configuration decides where nulls go.
405+
DEFAULT = "Default"
406+
# Null values appear first in the result, regardless of sort direction. Corax only.
407+
FIRST = "First"
408+
# Null values appear last in the result, regardless of sort direction. Corax only.
409+
LAST = "Last"
410+
411+
def __str__(self):
412+
return self.value

0 commit comments

Comments
 (0)