11from __future__ import annotations
2+
23import 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
67from ravendb .documents .ai .ai_conversation_result import AiConversationResult
78
89if TYPE_CHECKING :
1617TResponse = 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 )
0 commit comments