Skip to content

Commit cdf954b

Browse files
committed
RDBC-934 Add Handle & Receive, clean-up
1 parent 310a3b9 commit cdf954b

File tree

7 files changed

+128
-102
lines changed

7 files changed

+128
-102
lines changed

ravendb/__init__.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,6 @@
8080
# AI Operations
8181
from ravendb.documents.ai import (
8282
AiOperations,
83-
IAiConversationOperations,
8483
AiConversation,
8584
AiConversationResult,
8685
AiAgentParametersBuilder,

ravendb/documents/ai/__init__.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,10 @@
11
from .ai_operations import AiOperations
2-
from .ai_conversation_operations import IAiConversationOperations
32
from .ai_conversation import AiConversation
43
from .ai_conversation_result import AiConversationResult
54
from .ai_agent_parameters_builder import AiAgentParametersBuilder, IAiAgentParametersBuilder
65

76
__all__ = [
87
"AiOperations",
9-
"IAiConversationOperations",
108
"AiConversation",
119
"AiConversationResult",
1210
"AiAgentParametersBuilder",

ravendb/documents/ai/ai_conversation.py

Lines changed: 114 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
from __future__ import annotations
2+
23
import json
3-
from typing import List, Dict, Any, Optional, Union, TypeVar, TYPE_CHECKING
4+
import traceback
5+
from typing import List, Dict, Any, Optional, TypeVar, TYPE_CHECKING, Callable
46

5-
from ravendb.documents.ai.ai_conversation_operations import IAiConversationOperations
67
from ravendb.documents.ai.ai_conversation_result import AiConversationResult
78

89
if TYPE_CHECKING:
@@ -16,7 +17,12 @@
1617
TResponse = TypeVar("TResponse")
1718

1819

19-
class AiConversation(IAiConversationOperations[TResponse]):
20+
class AiHandleErrorStrategy:
21+
SEND_ERRORS_TO_MODEL = "SendErrorsToModel"
22+
RAISE_IMMEDIATELY = "RaiseImmediately"
23+
24+
25+
class AiConversation:
2026
"""
2127
Implementation of AI conversation operations for managing conversations with AI agents.
2228
@@ -26,6 +32,8 @@ class AiConversation(IAiConversationOperations[TResponse]):
2632
result = conversation.run()
2733
"""
2834

35+
_invocations: Dict[str, Callable[[AiAgentActionRequest], None]] = {}
36+
2937
def __init__(
3038
self,
3139
store: DocumentStore,
@@ -41,12 +49,20 @@ def __init__(
4149
self._change_vector = change_vector
4250
self._user_prompt: Optional[str] = None
4351
self._action_responses: List[AiAgentActionResponse] = []
44-
self._last_result: Optional[ConversationResult[TResponse]] = None
52+
self._last_result: Optional[ConversationResult] = None
53+
54+
def __enter__(self) -> AiConversation:
55+
"""Context manager entry."""
56+
return self
57+
58+
def __exit__(self, exc_type, exc_val, exc_tb) -> None:
59+
"""Context manager exit - cleanup resources."""
60+
pass
4561

4662
@classmethod
4763
def with_conversation_id(
4864
cls, store: DocumentStore, conversation_id: str, change_vector: str = None
49-
) -> AiConversation[TResponse]:
65+
) -> AiConversation:
5066
"""
5167
Creates a conversation instance for continuing an existing conversation.
5268
@@ -74,35 +90,27 @@ def required_actions(self) -> List[AiAgentActionRequest]:
7490
return self._last_result.action_requests
7591
return []
7692

77-
def add_action_response(self, action_id: str, action_response: Union[str, TResponse]) -> None:
93+
def add_action_response(self, action_id: str, action_response: str) -> None:
7894
"""
7995
Adds a response for a given action request.
8096
8197
Args:
8298
action_id: The ID of the action to respond to
83-
action_response: The response content (string or typed response object)
99+
action_response: The response content
84100
"""
85101
from ravendb.documents.operations.ai.agents import AiAgentActionResponse
86102

87103
response = AiAgentActionResponse(tool_id=action_id)
88104

89105
if isinstance(action_response, str):
90106
response.content = action_response
91-
else:
92-
# More robust JSON serialization
93-
try:
94-
response.content = json.dumps(
95-
action_response.__dict__ if hasattr(action_response, "__dict__") else action_response, default=str
96-
)
97-
except (TypeError, ValueError) as e:
98-
response.content = str(action_response)
99107

100108
self._action_responses.append(response)
101109

102-
def run(self) -> AiConversationResult[TResponse]:
110+
def run(self) -> AiConversationResult:
103111
"""
104112
Executes one "turn" of the conversation:
105-
sends the current prompt, processes any required actions,
113+
sends the current prompt or replies to any required actions,
106114
and awaits the agent's reply.
107115
"""
108116
from ravendb.documents.operations.ai.agents import RunConversationOperation
@@ -150,7 +158,7 @@ def run(self) -> AiConversationResult[TResponse]:
150158
self._action_responses.clear()
151159

152160
# Convert to AiConversationResult
153-
conversation_result = AiConversationResult[TResponse]()
161+
conversation_result = AiConversationResult()
154162
conversation_result.conversation_id = result.conversation_id
155163
conversation_result.change_vector = result.change_vector
156164
conversation_result.response = result.response
@@ -173,12 +181,92 @@ def set_user_prompt(self, user_prompt: str) -> None:
173181
raise ValueError("User prompt cannot be empty or whitespace-only")
174182
self._user_prompt = user_prompt
175183

176-
def __enter__(self) -> AiConversation[TResponse]:
177-
"""Context manager entry."""
178-
return self
184+
def handle(
185+
self,
186+
action_name: str,
187+
action: Callable[[dict], Any],
188+
ai_handle_error: AiHandleErrorStrategy,
189+
) -> None:
190+
self.handle_ai_agent_action_request(action_name, lambda _, args: action(args), ai_handle_error)
179191

180-
def __exit__(self, exc_type, exc_val, exc_tb) -> None:
181-
"""Context manager exit - cleanup resources."""
182-
# Clear any pending data
183-
self._user_prompt = None
184-
self._action_responses.clear()
192+
def handle_ai_agent_action_request(
193+
self,
194+
action_name: str,
195+
action: Callable[[AiAgentActionRequest, dict], Any],
196+
ai_handle_error: AiHandleErrorStrategy = AiHandleErrorStrategy.SEND_ERRORS_TO_MODEL,
197+
) -> None:
198+
def wrapped_no_return(request: AiAgentActionRequest, args: dict) -> Any:
199+
result = action(request, args)
200+
self.add_action_response(request.tool_id, result)
201+
202+
self.receive(action_name, wrapped_no_return, ai_handle_error)
203+
204+
def receive(
205+
self,
206+
action_name: str,
207+
action: Callable[[AiAgentActionRequest, dict], None],
208+
ai_handle_error: AiHandleErrorStrategy = AiHandleErrorStrategy.SEND_ERRORS_TO_MODEL,
209+
):
210+
t = self.AiActionContext(self, lambda request, args: action(request, args), ai_handle_error)
211+
self._add_action(action_name, t.execute)
212+
213+
def _add_action(self, action_name: str, action: Callable[[AiAgentActionRequest], Any]):
214+
if action_name in self._invocations:
215+
raise ValueError(f"Action '{action_name}' already exists")
216+
217+
self._invocations[action_name] = action
218+
219+
class AiActionContext:
220+
def __init__(
221+
self,
222+
conversation: AiConversation,
223+
action: Callable[[AiAgentActionRequest, dict], Any],
224+
ai_handle_error: AiHandleErrorStrategy,
225+
):
226+
self._conversation = conversation
227+
self._action = action
228+
self._ai_handle_error = ai_handle_error
229+
230+
def execute(self, action_request: AiAgentActionRequest):
231+
args = json.loads(action_request.arguments)
232+
self.invoke(action_request, args)
233+
234+
def invoke(self, action_request: AiAgentActionRequest, args: dict):
235+
try:
236+
self._action(action_request, args)
237+
except Exception as e:
238+
if self._ai_handle_error == AiHandleErrorStrategy.SEND_ERRORS_TO_MODEL:
239+
self._conversation.add_action_response(action_request.tool_id, self.create_error_message_for_llm(e))
240+
else:
241+
raise e
242+
243+
@staticmethod
244+
def create_error_message_for_llm(exc: Exception) -> str:
245+
parts = []
246+
247+
current = exc
248+
indent = 0
249+
250+
while current is not None:
251+
prefix = " " * indent
252+
header = f"{prefix}{current.__class__.__name__}: {current}"
253+
parts.append(header)
254+
255+
tb = "".join(traceback.format_exception(type(current), current, current.__traceback__))
256+
tb_lines = tb.strip().splitlines()
257+
258+
# indent the traceback block
259+
indented_tb = "\n".join(prefix + " " + line for line in tb_lines)
260+
parts.append(indented_tb)
261+
262+
# Move to next exception in the chain
263+
if current.__cause__:
264+
current = current.__cause__
265+
elif current.__context__ and not current.__suppress_context__:
266+
current = current.__context__
267+
else:
268+
current = None
269+
270+
indent += 1
271+
272+
return "\n".join(parts)

ravendb/documents/ai/ai_conversation_operations.py

Lines changed: 0 additions & 62 deletions
This file was deleted.

ravendb/documents/ai/ai_operations.py

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from __future__ import annotations
2-
from typing import TYPE_CHECKING, Optional, Dict, Any, Type
2+
from typing import TYPE_CHECKING, Dict, Any, Type
3+
from ravendb.documents.ai.ai_conversation import AiConversation
34

45
if TYPE_CHECKING:
56
from ravendb.documents.store.definition import DocumentStore
@@ -8,7 +9,6 @@
89
AiAgentConfigurationResult,
910
GetAiAgentsResponse,
1011
)
11-
from ravendb.documents.ai.ai_conversation_operations import IAiConversationOperations
1212

1313

1414
class AiOperations:
@@ -67,7 +67,7 @@ def get_agents(self, agent_id: str = None) -> GetAiAgentsResponse:
6767
operation = GetAiAgentOperation(agent_id)
6868
return self._store.maintenance.send(operation)
6969

70-
def conversation(self, agent_id: str, parameters: Dict[str, Any] = None) -> IAiConversationOperations:
70+
def conversation(self, agent_id: str, parameters: Dict[str, Any] = None) -> AiConversation:
7171
"""
7272
Creates a new conversation with the specified AI agent.
7373
@@ -78,11 +78,10 @@ def conversation(self, agent_id: str, parameters: Dict[str, Any] = None) -> IAiC
7878
Returns:
7979
Conversation operations interface for managing the conversation
8080
"""
81-
from ravendb.documents.ai.ai_conversation import AiConversation
8281

8382
return AiConversation(self._store, agent_id, parameters)
8483

85-
def conversation_with_id(self, conversation_id: str, change_vector: str = None) -> IAiConversationOperations:
84+
def conversation_with_id(self, conversation_id: str, change_vector: str = None) -> AiConversation:
8685
"""
8786
Continues an existing conversation by its ID.
8887

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ def __init__(self, configuration: AiAgentConfiguration, schema_type: Type = None
3434
"The 'schema_type' parameter is deprecated and will be removed in 8.0 version of Python client."
3535
" Use 'sample_object' or 'output_schema' inside AiAgentConfiguration instead.",
3636
DeprecationWarning,
37-
stacklevel=2
37+
stacklevel=2,
3838
)
3939
if configuration is None:
4040
raise ValueError("configuration cannot be None")

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

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,17 +12,21 @@
1212
TSchema = TypeVar("TSchema")
1313

1414

15-
@dataclass
1615
class AiAgentActionRequest:
1716
"""Represents an action request from an AI agent."""
1817

19-
name: Optional[str] = None
20-
tool_id: Optional[str] = None
21-
arguments: Optional[str] = None
18+
def __init__(self, name: str = None, tool_id: str = None, arguments: str = None):
19+
self.name = name
20+
self.tool_id = tool_id
21+
self.arguments = arguments
2222

2323
@classmethod
2424
def from_json(cls, json_dict: Dict[str, Any]) -> AiAgentActionRequest:
25-
return cls(name=json_dict.get("Name"), tool_id=json_dict.get("ToolId"), arguments=json.loads(json_dict.get("Arguments")))
25+
return cls(
26+
name=json_dict.get("Name"),
27+
tool_id=json_dict.get("ToolId"),
28+
arguments=json_dict.get("Arguments"),
29+
)
2630

2731
def to_json(self) -> Dict[str, Any]:
2832
return {

0 commit comments

Comments
 (0)