@@ -180,7 +180,8 @@ def _create_user_token(
180180 'last_access_time' : self .time (),
181181 'n_requests' : 0 ,
182182 'messages' : [],
183- 'long_term_memory_is_empty' : True
183+ 'long_term_memory_is_empty' : True ,
184+ 'conversations' : {},
184185 }
185186 self .__user_data [user_token ] = new_user_data
186187 self .maybe_persistence_save (force = True )
@@ -222,12 +223,23 @@ def choose_sum(a, b):
222223 return a
223224 return a + b
224225
226+ def choose_merge (a , b ):
227+ if a is None :
228+ return b
229+ if b is None :
230+ return a
231+ return {
232+ ** a ,
233+ ** b ,
234+ }
235+
225236 merging_methods = {
226237 'creation_time' : choose_min ,
227238 'last_access_time' : choose_max ,
228239 'messages' : choose_sum ,
229240 'n_requests' : choose_sum ,
230241 'long_term_memory_is_empty' : choose_min ,
242+ 'conversations' : choose_merge ,
231243 }
232244 in_memory_user_data = in_memory_user_data or {}
233245 return {
@@ -910,6 +922,7 @@ def get_description_of_chat_step(
910922 preprocess_request_method : callable = None ,
911923 compute_request_result_method : callable = None ,
912924 extracted_param_names : list = None ,
925+ conversation_id : str = None ,
913926 ** kwargs
914927 ):
915928 extracted_param_names = extracted_param_names or []
@@ -932,6 +945,7 @@ def get_description_of_chat_step(
932945 ('messages' , messages or []),
933946 ('keep_conversation_history' , keep_conversation_history ),
934947 ('use_long_term_memory' , use_long_term_memory ),
948+ ('conversation_id' , conversation_id ),
935949 * domain_additional_tuples ,
936950 * [
937951 (k , v ) for k , v in kwargs .items ()
@@ -1241,6 +1255,7 @@ def pre_process_chat_request(
12411255 domain : str = None ,
12421256 short_term_memory_only : bool = False ,
12431257 is_chat_request : bool = False ,
1258+ conversation_id : str = None ,
12441259 ** kwargs ,
12451260 ):
12461261 """
@@ -1297,6 +1312,20 @@ def pre_process_chat_request(
12971312 return result
12981313 # endif message is None
12991314
1315+ if isinstance (conversation_id , str ) and self .__user_data [user_token ].get (conversation_id ) is not None :
1316+ # Detected existing conversation.
1317+ conversation_kwargs = self .__user_data [user_token ][conversation_id ].get ('conversation_kwargs' , {})
1318+ domain = domain or conversation_kwargs .get ('domain' )
1319+ remaining_kwargs = {
1320+ k : v for k , v in conversation_kwargs .items ()
1321+ if k != 'domain'
1322+ }
1323+ kwargs = {
1324+ ** remaining_kwargs ,
1325+ ** kwargs ,
1326+ }
1327+ # endif existing conversation detected
1328+
13001329 domain_prompt , additional_kwargs = self .get_domain_prompt (
13011330 user_token = user_token ,
13021331 domain = domain ,
@@ -1577,7 +1606,32 @@ def compute_request_result_chat(
15771606 color = "green"
15781607 )
15791608
1580- if keep_conversation_history :
1609+ conversation_id = request_data .get ('conversation_id' )
1610+ self .P (f"Extracted conversation ID: `{ conversation_id } `" )
1611+ message_saved = False
1612+ if isinstance (conversation_id , str ):
1613+ conversation_data = self .__user_data [user_token ].get ("conversations" , {}).get (conversation_id )
1614+ if conversation_data :
1615+ self .Pd (f"Adding messages to conversation '{ conversation_id } ' for user '{ user_token } '" )
1616+ current_messages = conversation_data .get ('messages' , [])
1617+ last_user_message = self .get_last_user_message (user_messages )
1618+ if last_user_message is not None :
1619+ current_messages .append (last_user_message )
1620+ # endif last user message
1621+ current_messages .append ({
1622+ 'role' : 'assistant' ,
1623+ 'content' : reply_text ,
1624+ })
1625+ conversation_data ['messages' ] = current_messages
1626+ conversation_data ['last_access_time' ] = self .time ()
1627+ conversation_data ['n_requests' ] += 1
1628+ message_saved = True
1629+ self .__user_data [user_token ][conversation_id ] = conversation_data
1630+ result ['conversation_id' ] = conversation_id
1631+ # endif conversation_data exists
1632+ # endif
1633+
1634+ if not message_saved and keep_conversation_history :
15811635 self .P (f"User messages: { user_messages } " )
15821636 last_user_message = self .get_last_user_message (user_messages )
15831637 if last_user_message is not None :
@@ -1686,6 +1740,204 @@ def chat(
16861740 request_steps = request_steps ,
16871741 )
16881742 return postponed_request
1743+
1744+ def create_conversation_data (self , conversation_kwargs : dict = None ):
1745+ conversation_kwargs = conversation_kwargs or {}
1746+ return {
1747+ 'creation_time' : self .time (),
1748+ 'last_access_time' : self .time (),
1749+ 'messages' : [],
1750+ 'n_requests' : 0 ,
1751+ "conversation_kwargs" : conversation_kwargs ,
1752+ }
1753+
1754+ def process_conversation_messages (
1755+ self , conversation_messages : list [dict ],
1756+ ** kwargs
1757+ ):
1758+ """
1759+ Process the conversation messages if needed.
1760+ By default, this is the identity function.
1761+ Parameters
1762+ ----------
1763+ conversation_messages: list[dict]
1764+
1765+ kwargs
1766+
1767+ Returns
1768+ -------
1769+
1770+ """
1771+ user_replies = []
1772+ last_assistant_message = None
1773+ for msg in conversation_messages :
1774+ msg_content = msg .get ("content" )
1775+ if not msg_content :
1776+ continue
1777+ if msg .get ("role" ) == "user" :
1778+ user_replies .append (msg_content )
1779+ elif msg .get ("role" ) == "assistant" :
1780+ # Here, the entire dictionary is used, since it will be wrapped in the same way.
1781+ last_assistant_message = msg
1782+ # endfor conversation messages
1783+ res = []
1784+ if user_replies :
1785+ agg_label = "User messages:"
1786+ res .append ({
1787+ "role" : "user" ,
1788+ "content" : f"{ agg_label } \n \n " + "\n \n ---\n \n " .join (user_replies )
1789+ })
1790+ # endif existing user replies
1791+ if last_assistant_message :
1792+ res .append (last_assistant_message )
1793+ # endif existing assistant message
1794+ return res
1795+
1796+ def maybe_add_conversation_messages (
1797+ self ,
1798+ conversation_data : dict ,
1799+ messages : list [dict ],
1800+ ** kwargs
1801+ ):
1802+ """
1803+ Handle the conversation history for the Jeeves API.
1804+ This will merge the registered messages from conversation_data,
1805+ the message(s) from the user, and the system prompt if present.
1806+ Parameters
1807+ ----------
1808+ conversation_data : dict
1809+ The conversation data from a specific conversation of a specific user.
1810+ messages : list[dict]
1811+ The messages to send to the API. This will contain the current user message
1812+ and optionally the system prompt as the last message.
1813+
1814+
1815+ Returns
1816+ -------
1817+ res : list[dict]
1818+ The messages to send to the API.
1819+ """
1820+ last_message = messages [- 1 ]
1821+ if last_message .get ('role' ) == 'system' :
1822+ current_messages = messages [- 2 :]
1823+ else :
1824+ current_messages = messages [- 1 :]
1825+ # endif last message is system
1826+ # Here, the conversation messages are already stored in a raw manner.
1827+ conversation_messages = self .deepcopy (conversation_data .get ('messages' , []))
1828+ self .Pd (f"Extracted conversation messages: { conversation_messages } " )
1829+ conversation_messages = self .process_conversation_messages (conversation_messages , ** kwargs )
1830+ self .Pd (f"Processed conversation messages: { conversation_messages } " )
1831+ conversation_messages += current_messages
1832+
1833+ return conversation_messages
1834+
1835+ @BasePlugin .endpoint (method = "post" )
1836+ def conversation (
1837+ self ,
1838+ user_token : str = None ,
1839+ conversation_id : str = None ,
1840+ message : str = None ,
1841+ domain : str = None ,
1842+ ** kwargs
1843+ ):
1844+ """
1845+ Start or continue a conversation with the Jeeves API.
1846+ In case this is a new conversation, the kwargs will be stored for future reference.
1847+ In case this is a continuation of a conversation, if any kwargs are provided,
1848+ they will be used instead of the stored ones, but they will not be stored for future reference.
1849+ Parameters
1850+ ----------
1851+ user_token : str
1852+ The user token to use for the API. Default is None.
1853+ conversation_id : str
1854+ The conversation ID to use for the API. Default is None.
1855+ If None, a new conversation will be started.
1856+ message : str
1857+ The message to send to the API. Default is None.
1858+ domain : str
1859+ The domain to use for the API. Default is None.
1860+ kwargs : dict
1861+ Additional parameters to send to the API. Default is None.
1862+
1863+ Returns
1864+ -------
1865+
1866+ """
1867+ processed_request = self .pre_process_chat_request (
1868+ user_token = user_token ,
1869+ message = message ,
1870+ domain = domain ,
1871+ conversation_id = conversation_id ,
1872+ ** kwargs ,
1873+ )
1874+ if processed_request ['err_response' ] is not None :
1875+ return processed_request ['err_response' ]
1876+ # endif error in processing request
1877+
1878+ additional_kwargs = processed_request ['additional_kwargs' ]
1879+ messages = processed_request ['messages' ]
1880+ # Handling conversation history
1881+ current_user_conversations_data = self .__user_data [user_token ].get ('conversations' , {})
1882+ if conversation_id is None :
1883+ conversation_id = self .uuid ()
1884+ while conversation_id in current_user_conversations_data :
1885+ conversation_id = self .uuid ()
1886+ # endwhile conversation_id already existent
1887+ # endif conversation_id not provided
1888+ conversation_data = self .__user_data [user_token ].get ('conversations' , {}).get (conversation_id , {})
1889+ if not conversation_data :
1890+ self .Pd (f"Creating new conversation '{ conversation_id } ' for user '{ user_token } '" )
1891+ conversation_kwargs = {
1892+ 'domain' : domain ,
1893+ ** additional_kwargs
1894+ }
1895+ self .__user_data [user_token ]['conversations' ][conversation_id ] = self .create_conversation_data (
1896+ conversation_kwargs = conversation_kwargs
1897+ )
1898+ conversation_data = self .__user_data [user_token ]['conversations' ][conversation_id ]
1899+ # endif new conversation
1900+ messages = self .maybe_add_conversation_messages (
1901+ conversation_data = conversation_data ,
1902+ messages = messages
1903+ )
1904+
1905+ domain_additional_data_step_description = self .get_description_of_retrieval_step (
1906+ domain = domain ,
1907+ query = message ,
1908+ user_token = user_token ,
1909+ short_term_memory_only = False ,
1910+ # optional, since no preprocessing is needed
1911+ preprocess_request_method = None ,
1912+ compute_request_result_method = self .compute_request_result_retrieval_domain_additional_data ,
1913+ )
1914+ chat_step_description = self .get_description_of_chat_step (
1915+ domain = domain ,
1916+ user_token = user_token ,
1917+ messages = messages ,
1918+ keep_conversation_history = False ,
1919+ use_long_term_memory = False ,
1920+ preprocess_request_method = self .preprocess_request_method_query ,
1921+ compute_request_result_method = self .compute_request_result_chat ,
1922+ extracted_param_names = [
1923+ self .ct .JeevesCt .CONTEXT
1924+ ],
1925+ conversation_id = conversation_id ,
1926+ ** additional_kwargs
1927+ )
1928+
1929+ request_steps = [
1930+ domain_additional_data_step_description ,
1931+ chat_step_description
1932+ ]
1933+ request_steps = [
1934+ step for step in request_steps
1935+ if step is not None
1936+ ]
1937+ postponed_request = self .start_request_steps (
1938+ request_steps = request_steps
1939+ )
1940+ return postponed_request
16891941 """END LLM SECTION"""
16901942
16911943 @BasePlugin .endpoint (method = 'post' )
0 commit comments