33"""A chat chain
44"""
55import json
6+ import hashlib
67import pprint
78import re
8- from random import randint
99from typing import Any
1010
1111from langchain_community .chat_models import ChatLlamaCpp
12- from langchain_core .messages import SystemMessage , HumanMessage , ToolMessage
12+ from langchain_core .messages import SystemMessage , HumanMessage
1313from langchain_core .messages .ai import AIMessage
1414
15+ from streaming import StreamContext , run_runnable_with_streaming
16+
1517def generate_tool_call (tool_call : dict ):
1618 content = '<tool_call>'
1719 content += json .dumps ({"name" : tool_call ['name' ], "arguments" : tool_call ['args' ]})
1820 content += '</tool_call>'
1921 return content
2022
23+
24+ def generate_tool_call_id (tool_call : dict ) -> str :
25+ stable_payload = json .dumps (
26+ {
27+ "name" : tool_call .get ("name" ),
28+ "args" : tool_call .get ("args" , tool_call .get ("arguments" , {})),
29+ },
30+ sort_keys = True ,
31+ )
32+ return hashlib .sha1 (stable_payload .encode ("utf-8" )).hexdigest ()[:16 ]
33+
2134def try_parse_tool_calls (content : str ):
2235 """Try parse the tool calls."""
2336 tool_calls = []
@@ -40,7 +53,9 @@ def try_parse_tool_calls(content: str):
4053 func ['args' ] = func ['arguments' ]
4154 del func ['arguments' ]
4255 if not 'id' in func :
43- func ['id' ] = str (randint (1 , 10000000000 ))
56+ func ['id' ] = generate_tool_call_id (func )
57+ if 'type' not in func :
58+ func ['type' ] = 'tool_call'
4459 found = True
4560 except json .JSONDecodeError as e :
4661 print (f"Failed to parse tool calls: the content is { m .group (1 )} and { e } " )
@@ -66,7 +81,9 @@ def try_parse_tool_calls(content: str):
6681 func ['args' ] = func ['arguments' ]
6782 del func ['arguments' ]
6883 if not 'id' in func :
69- func ['id' ] = str (randint (1 , 10000000000 ))
84+ func ['id' ] = generate_tool_call_id (func )
85+ if 'type' not in func :
86+ func ['type' ] = 'tool_call'
7087 except json .JSONDecodeError as e :
7188 print (f"Failed to parse tool calls: the content is { m .group (1 )} and { e } " )
7289 pass
@@ -79,6 +96,31 @@ def try_parse_tool_calls(content: str):
7996 return {"role" : "assistant" , "content" : c , "tool_calls" : tool_calls }
8097 return {"role" : "assistant" , "content" : re .sub (r"<\|im_end\|>$" , "" , content )}
8198
99+
100+ def strip_tool_calls_for_streaming (content : str ) -> str :
101+ sanitized = re .sub (r"<tool_call>.*?</tool_call>" , "" , content , flags = re .DOTALL )
102+ sanitized = re .sub (r"```tool_call\n.*?\n```" , "" , sanitized , flags = re .DOTALL )
103+
104+ partial_markers = [index for index in (sanitized .find ("<tool" ), sanitized .find ("```tool" )) if index != - 1 ]
105+ if partial_markers :
106+ sanitized = sanitized [:min (partial_markers )]
107+
108+ return re .sub (r"<\|im_end\|>$" , "" , sanitized )
109+
110+
111+ def build_streaming_payload (content : str ) -> dict [str , Any ] | None :
112+ payload : dict [str , Any ] = {}
113+ cleaned_output = strip_tool_calls_for_streaming (content )
114+ parsed_response = try_parse_tool_calls (content )
115+ tool_calls = parsed_response .get ('tool_calls' )
116+
117+ if cleaned_output or tool_calls :
118+ payload ['output' ] = cleaned_output
119+ if tool_calls :
120+ payload ['tool_calls' ] = json .dumps (tool_calls )
121+
122+ return payload or None
123+
82124class ChatWithToolsProcessor :
83125 """
84126 A chat with tools processor that supports batch processing
@@ -89,7 +131,7 @@ class ChatWithToolsProcessor:
89131 def __init__ (self , runner : ChatLlamaCpp ):
90132 self .model = runner
91133
92- def _process_single_input (self , input_data : dict [str , Any ]) -> dict [str , Any ]:
134+ def _process_single_input (self , input_data : dict [str , Any ], context : StreamContext | None = None ) -> dict [str , Any ]:
93135 system_prompt = """
94136{downstream_system_prompt}
95137
@@ -150,15 +192,20 @@ def _process_single_input(self, input_data: dict[str, Any]) -> dict[str, Any]:
150192 messages .append (HumanMessage (content = '' ))
151193
152194 pprint .pprint (messages )
153- response = self .model .invoke (messages )
195+ response_content = run_runnable_with_streaming (
196+ self .model ,
197+ messages ,
198+ context ,
199+ stream_payload_transform = build_streaming_payload ,
200+ suppress_empty_stream_updates = True ,
201+ )
154202
155- #if not response.tool_calls or len(response.tool_calls) == 0:
156- response = AIMessage (** try_parse_tool_calls (response .content ))
203+ response = AIMessage (** try_parse_tool_calls (response_content ))
157204
158205 return {
159206 'output' : response .content ,
160207 'tool_calls' : json .dumps (response .tool_calls )
161208 }
162209
163- def __call__ (self , inputs : dict [str , Any ]) -> dict [str , Any ]:
164- return self ._process_single_input (inputs )
210+ def __call__ (self , inputs : dict [str , Any ], context : StreamContext | None = None ) -> dict [str , Any ]:
211+ return self ._process_single_input (inputs , context )
0 commit comments