Skip to content

Commit 9b83ea5

Browse files
committed
RDBC-1005 Add Embeddings Generation Task operations
1 parent 1961532 commit 9b83ea5

12 files changed

Lines changed: 786 additions & 22 deletions

ravendb/__init__.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,15 @@
110110
AddOrUpdateAiAgentOperation,
111111
DeleteAiAgentOperation,
112112
)
113+
from ravendb.documents.operations.ai import (
114+
ChunkingOptions,
115+
ChunkingMethod,
116+
EmbeddingPathConfiguration,
117+
EmbeddingsTransformation,
118+
EmbeddingsGenerationConfiguration,
119+
AddEmbeddingsGenerationOperation,
120+
UpdateEmbeddingsGenerationOperation,
121+
)
113122

114123
from ravendb.documents.operations.etl.configuration import EtlConfiguration, RavenEtlConfiguration
115124
from ravendb.documents.operations.etl.olap.connection import OlapEtlConfiguration

ravendb/documents/ai/ai_conversation.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -145,9 +145,7 @@ def add_artificial_action_with_response(self, tool_id: str, action_response) ->
145145
else:
146146
content = json.dumps(action_response)
147147

148-
self._artificial_actions.append(
149-
AiAgentArtificialActionResponse(tool_id=tool_id, content=content)
150-
)
148+
self._artificial_actions.append(AiAgentArtificialActionResponse(tool_id=tool_id, content=content))
151149

152150
def run(self) -> AiAnswer:
153151
"""

ravendb/documents/operations/ai/__init__.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,12 @@
1414
)
1515
from ravendb.documents.operations.ai.add_gen_ai_operation import AddGenAiOperation
1616
from ravendb.documents.operations.ai.update_gen_ai_operation import UpdateGenAiOperation
17+
from ravendb.documents.operations.ai.chunking_options import ChunkingOptions, ChunkingMethod
18+
from ravendb.documents.operations.ai.embedding_path_configuration import EmbeddingPathConfiguration
19+
from ravendb.documents.operations.ai.embeddings_transformation import EmbeddingsTransformation
20+
from ravendb.documents.operations.ai.embeddings_generation_configuration import EmbeddingsGenerationConfiguration
21+
from ravendb.documents.operations.ai.add_embeddings_generation_operation import AddEmbeddingsGenerationOperation
22+
from ravendb.documents.operations.ai.update_embeddings_generation_operation import UpdateEmbeddingsGenerationOperation
1723

1824
__all__ = [
1925
"AiConnectionString",
@@ -28,4 +34,11 @@
2834
"AddEmbeddingsGenerationOperationResult",
2935
"AddGenAiOperation",
3036
"UpdateGenAiOperation",
37+
"ChunkingOptions",
38+
"ChunkingMethod",
39+
"EmbeddingPathConfiguration",
40+
"EmbeddingsTransformation",
41+
"EmbeddingsGenerationConfiguration",
42+
"AddEmbeddingsGenerationOperation",
43+
"UpdateEmbeddingsGenerationOperation",
3144
]
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
from __future__ import annotations
2+
import json
3+
from typing import Optional, TYPE_CHECKING
4+
from urllib.parse import quote
5+
6+
from ravendb.documents.operations.definitions import MaintenanceOperation
7+
from ravendb.documents.conventions import DocumentConventions
8+
from ravendb.http.raven_command import RavenCommand
9+
from ravendb.http.server_node import ServerNode
10+
from ravendb.documents.starting_point_change_vector import StartingPointChangeVector
11+
from ravendb.documents.operations.ai.ai_task_operation_results import AddEmbeddingsGenerationOperationResult
12+
import requests
13+
14+
from ravendb.util.util import RaftIdGenerator
15+
16+
if TYPE_CHECKING:
17+
from ravendb.documents.operations.ai.embeddings_generation_configuration import EmbeddingsGenerationConfiguration
18+
19+
20+
class AddEmbeddingsGenerationOperation(MaintenanceOperation[AddEmbeddingsGenerationOperationResult]):
21+
"""
22+
Operation to add a new Embeddings Generation task to the database.
23+
"""
24+
25+
def __init__(
26+
self,
27+
configuration: EmbeddingsGenerationConfiguration,
28+
starting_point: Optional[StartingPointChangeVector] = StartingPointChangeVector.LAST_DOCUMENT,
29+
):
30+
if configuration is None:
31+
raise ValueError("configuration cannot be None")
32+
33+
self._configuration = configuration
34+
self._starting_point = starting_point
35+
36+
def get_command(self, conventions: DocumentConventions) -> RavenCommand[AddEmbeddingsGenerationOperationResult]:
37+
return AddEmbeddingsGenerationCommand(self._configuration, self._starting_point, conventions)
38+
39+
40+
class AddEmbeddingsGenerationCommand(RavenCommand[AddEmbeddingsGenerationOperationResult]):
41+
def __init__(
42+
self,
43+
configuration: EmbeddingsGenerationConfiguration,
44+
starting_point: StartingPointChangeVector,
45+
conventions: DocumentConventions,
46+
):
47+
super().__init__(AddEmbeddingsGenerationOperationResult)
48+
self._configuration = configuration
49+
self._starting_point = starting_point
50+
self._conventions = conventions
51+
52+
def is_read_request(self) -> bool:
53+
return False
54+
55+
def create_request(self, node: ServerNode) -> requests.Request:
56+
url = f"{node.url}/databases/{node.database}/admin/etl?changeVector={quote(self._starting_point.value)}"
57+
58+
body_json = self._configuration.to_json()
59+
body = json.dumps(body_json)
60+
61+
request = requests.Request("PUT", url)
62+
request.headers = {"Content-Type": "application/json"}
63+
request.data = body
64+
return request
65+
66+
def set_response(self, response: str, from_cache: bool) -> None:
67+
if response is None:
68+
self.result = AddEmbeddingsGenerationOperationResult()
69+
return
70+
71+
response_json = json.loads(response)
72+
self.result = AddEmbeddingsGenerationOperationResult.from_json(response_json)
73+
74+
def get_raft_unique_request_id(self) -> str:
75+
return RaftIdGenerator.new_id()

ravendb/documents/operations/ai/chunking_options.py

Lines changed: 43 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
from enum import Enum
2-
from typing import Dict, Any, Optional
2+
from typing import Dict, Any, Optional, List
33

44

5-
# todo: EmbeddingsGenerationConfiguration
65
class ChunkingMethod(Enum):
76
PLAIN_TEXT_SPLIT = "PlainTextSplit"
87
PLAIN_TEXT_SPLIT_LINES = "PlainTextSplitLines"
@@ -12,6 +11,14 @@ class ChunkingMethod(Enum):
1211
HTML_STRIP = "HtmlStrip"
1312

1413

14+
# Methods that support overlap tokens
15+
METHODS_SUPPORTING_OVERLAP_TOKENS = {
16+
ChunkingMethod.PLAIN_TEXT_SPLIT,
17+
ChunkingMethod.PLAIN_TEXT_SPLIT_LINES,
18+
ChunkingMethod.PLAIN_TEXT_SPLIT_PARAGRAPHS,
19+
}
20+
21+
1522
class ChunkingOptions:
1623
def __init__(
1724
self,
@@ -26,9 +33,9 @@ def __init__(
2633
@classmethod
2734
def from_json(cls, json_dict: Dict[str, Any]) -> "ChunkingOptions":
2835
return cls(
29-
chunking_method=ChunkingMethod(json_dict["ChunkingMethod"]) if json_dict.get("ChunkingMethod") else None,
30-
max_tokens_per_chunk=json_dict.get("MaxTokensPerChunk", 512),
31-
overlap_tokens=json_dict.get("OverlapTokens", 0),
36+
chunking_method=ChunkingMethod(json_dict["ChunkingMethod"]),
37+
max_tokens_per_chunk=json_dict.get("MaxTokensPerChunk", None),
38+
overlap_tokens=json_dict.get("OverlapTokens", None),
3239
)
3340

3441
def to_json(self) -> Dict[str, Any]:
@@ -38,6 +45,37 @@ def to_json(self) -> Dict[str, Any]:
3845
"OverlapTokens": self.overlap_tokens,
3946
}
4047

48+
def validate(self, source: str, errors: List[str]) -> None:
49+
"""Validates the chunking options.
50+
51+
Args:
52+
source: The source context for error messages (e.g., 'embeddings.generate').
53+
errors: List to append validation errors to.
54+
"""
55+
if self.max_tokens_per_chunk <= 0:
56+
errors.append(f"{source}: MaxTokensPerChunk must be greater than 0.")
57+
58+
if self.overlap_tokens < 0:
59+
errors.append(f"{source}: OverlapTokens cannot be negative.")
60+
61+
if self.overlap_tokens > self.max_tokens_per_chunk:
62+
errors.append(f"{source}: OverlapTokens cannot be greater than MaxTokensPerChunk.")
63+
64+
if self.overlap_tokens > 0 and self.chunking_method not in METHODS_SUPPORTING_OVERLAP_TOKENS:
65+
errors.append(
66+
f"{source}: OverlapTokens is only supported for PlainTextSplit, "
67+
f"PlainTextSplitLines, and PlainTextSplitParagraphs chunking methods."
68+
)
69+
70+
@staticmethod
71+
def are_equal(left: Optional["ChunkingOptions"], right: Optional["ChunkingOptions"]) -> bool:
72+
"""Check if two ChunkingOptions instances are equal, handling None values."""
73+
if left is None and right is None:
74+
return True
75+
if left is None or right is None:
76+
return False
77+
return left == right
78+
4179
def __eq__(self, other: object) -> bool:
4280
if other is None:
4381
return False

ravendb/documents/operations/ai/embedding_path_configuration.py

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,23 +3,42 @@
33
from ravendb.documents.operations.ai.chunking_options import ChunkingOptions
44

55

6-
# todo: EmbeddingsGenerationConfiguration
76
class EmbeddingPathConfiguration:
87
def __init__(self, path: Optional[str] = None, chunking_options: Optional[ChunkingOptions] = None):
98
self.path = path
109
self.chunking_options = chunking_options
1110

1211
@classmethod
1312
def from_json(cls, json_dict: Dict[str, Any]) -> "EmbeddingPathConfiguration":
13+
chunking_data = json_dict.get("ChunkingOptions")
1414
return cls(
15-
path=json_dict.get("Path"),
16-
chunking_options=(
17-
ChunkingOptions.from_json(json_dict["ChunkingOptions"]) if json_dict.get("ChunkingOptions") else None
18-
),
15+
path=json_dict["Path"],
16+
chunking_options=ChunkingOptions.from_json(chunking_data) if chunking_data else None,
1917
)
2018

2119
def to_json(self) -> Dict[str, Any]:
2220
return {
2321
"Path": self.path,
2422
"ChunkingOptions": self.chunking_options.to_json() if self.chunking_options else None,
2523
}
24+
25+
@staticmethod
26+
def are_equal(left: Optional["EmbeddingPathConfiguration"], right: Optional["EmbeddingPathConfiguration"]) -> bool:
27+
"""Check if two EmbeddingPathConfiguration instances are equal, handling None values."""
28+
if left is None and right is None:
29+
return True
30+
if left is None or right is None:
31+
return False
32+
return left == right
33+
34+
def __eq__(self, other: object) -> bool:
35+
if other is None:
36+
return False
37+
if self is other:
38+
return True
39+
if not isinstance(other, EmbeddingPathConfiguration):
40+
return False
41+
return self.path == other.path and ChunkingOptions.are_equal(self.chunking_options, other.chunking_options)
42+
43+
def __hash__(self) -> int:
44+
return hash((self.path, self.chunking_options))

0 commit comments

Comments
 (0)