Skip to content

Commit aaddd5d

Browse files
feat: added support for continuous conversations in j33ves_api (#293)
1 parent d738385 commit aaddd5d

File tree

2 files changed

+263
-2
lines changed

2 files changed

+263
-2
lines changed

extensions/business/jeeves/jeeves_api.py

Lines changed: 254 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -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')

extensions/business/jeeves/partners/keysoft/keysoft_jeeves.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ def query(
4343
message: str,
4444
db_schema: str = None,
4545
domain: str = None,
46+
conversation_id: str = None,
4647
request_type: str = "query",
4748
**kwargs
4849
):
@@ -93,6 +94,14 @@ def query(
9394
user_token=user_token,
9495
pdf_base64=message,
9596
)
97+
elif request_type == "conversation":
98+
return super(KeysoftJeevesPlugin, self).conversation(
99+
user_token=user_token,
100+
message=message,
101+
domain=domain,
102+
conversation_id=conversation_id,
103+
**kwargs
104+
)
96105
# endif request_type is not query, nlsql_query or chat
97106
return {
98107
"error": f"Unknown request_type: {request_type}. Supported types are: query, nlsql_query, chat."

0 commit comments

Comments
 (0)