From 2336202a04d3af1dfde1fb3da0af82df1f8f1d90 Mon Sep 17 00:00:00 2001 From: Toshiaki Takeuchi Date: Mon, 26 Feb 2024 18:05:46 -0500 Subject: [PATCH 1/4] Support function calls in streaming --- +llms/+internal/callOpenAIChatAPI.m | 16 +++++++++++-- +llms/+stream/responseStreamer.m | 36 +++++++++++++++++++++++++---- 2 files changed, 45 insertions(+), 7 deletions(-) diff --git a/+llms/+internal/callOpenAIChatAPI.m b/+llms/+internal/callOpenAIChatAPI.m index 975de52..37f250e 100644 --- a/+llms/+internal/callOpenAIChatAPI.m +++ b/+llms/+internal/callOpenAIChatAPI.m @@ -84,8 +84,20 @@ if isempty(nvp.StreamFun) message = response.Body.Data.choices(1).message; else - message = struct("role", "assistant", ... - "content", streamedText); + pat = '{"' + wildcardPattern + '":'; + if contains(streamedText,'{"id":"call_') + s = jsondecode(streamedText); + if contains(s.function.arguments,pat) + prompt = jsondecode(s.function.arguments); + s.function.arguments = prompt; + end + message = struct("role", "assistant", ... + "content",[], ... + "tool_calls",jsondecode(streamedText)); + else + message = struct("role", "assistant", ... + "content", streamedText); + end end if isfield(message, "tool_choice") text = ""; diff --git a/+llms/+stream/responseStreamer.m b/+llms/+stream/responseStreamer.m index 58925a6..a2d87cf 100644 --- a/+llms/+stream/responseStreamer.m +++ b/+llms/+stream/responseStreamer.m @@ -36,14 +36,40 @@ str = erase(str,"data: "); for i = 1:length(str) - json = jsondecode(str{i}); - if strcmp(json.choices.finish_reason,'stop') + if strcmp(str{i},'[DONE]') stop = true; return else - txt = json.choices.delta.content; - this.StreamFun(txt); - this.ResponseText = [this.ResponseText txt]; + try + json = jsondecode(str{i}); + catch ME + error("API returned il-formed json:" + str{i}) + end + if ischar(json.choices.finish_reason) && ismember(json.choices.finish_reason,["stop","tool_calls"]) + stop = true; + return + else + if isfield(json.choices.delta,"tool_calls") + if isfield(json.choices.delta.tool_calls,"id") + id = json.choices.delta.tool_calls.id; + type = json.choices.delta.tool_calls.type; + fcn = json.choices.delta.tool_calls.function; + s = struct('id',id,'type',type,'function',fcn); + txt = jsonencode(s); + else + s = jsondecode(this.ResponseText); + args = json.choices.delta.tool_calls.function.arguments; + s.function.arguments = [s.function.arguments args]; + txt = jsonencode(s); + end + this.StreamFun(''); + this.ResponseText = txt; + else + txt = json.choices.delta.content; + this.StreamFun(txt); + this.ResponseText = [this.ResponseText txt]; + end + end end end end From 5cf9c0ed6212b32d070340ae504e870529cd03ed Mon Sep 17 00:00:00 2001 From: Toshiaki Takeuchi Date: Mon, 26 Feb 2024 18:50:52 -0500 Subject: [PATCH 2/4] minor fixes --- +llms/+internal/callOpenAIChatAPI.m | 2 +- +llms/+stream/responseStreamer.m | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/+llms/+internal/callOpenAIChatAPI.m b/+llms/+internal/callOpenAIChatAPI.m index 37f250e..3cd485c 100644 --- a/+llms/+internal/callOpenAIChatAPI.m +++ b/+llms/+internal/callOpenAIChatAPI.m @@ -85,7 +85,7 @@ message = response.Body.Data.choices(1).message; else pat = '{"' + wildcardPattern + '":'; - if contains(streamedText,'{"id":"call_') + if contains(streamedText,pat) s = jsondecode(streamedText); if contains(s.function.arguments,pat) prompt = jsondecode(s.function.arguments); diff --git a/+llms/+stream/responseStreamer.m b/+llms/+stream/responseStreamer.m index a2d87cf..e35188f 100644 --- a/+llms/+stream/responseStreamer.m +++ b/+llms/+stream/responseStreamer.m @@ -43,7 +43,7 @@ try json = jsondecode(str{i}); catch ME - error("API returned il-formed json:" + str{i}) + error("API returned il-formed json: " + str{i}) end if ischar(json.choices.finish_reason) && ismember(json.choices.finish_reason,["stop","tool_calls"]) stop = true; From 2731296b0b66d17676113199d7593d177f70b1c8 Mon Sep 17 00:00:00 2001 From: Toshiaki Takeuchi Date: Tue, 27 Feb 2024 09:18:44 -0500 Subject: [PATCH 3/4] Imrpoved error message handling --- +llms/+stream/responseStreamer.m | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/+llms/+stream/responseStreamer.m b/+llms/+stream/responseStreamer.m index e35188f..83f4ac9 100644 --- a/+llms/+stream/responseStreamer.m +++ b/+llms/+stream/responseStreamer.m @@ -43,7 +43,11 @@ try json = jsondecode(str{i}); catch ME - error("API returned il-formed json: " + str{i}) + errID = 'llms:stream:responseStreamer:InvalidInput'; + msg = "Input does not have the expected json format. " + str{i}; + causeException = MException(errID,msg); + ME = addCause(ME,causeException); + rethrow(ME) end if ischar(json.choices.finish_reason) && ismember(json.choices.finish_reason,["stop","tool_calls"]) stop = true; From 0b4b546ee9d79a8377e26dabc61ce82ca5177401 Mon Sep 17 00:00:00 2001 From: Toshiaki Takeuchi Date: Tue, 27 Feb 2024 15:40:42 -0500 Subject: [PATCH 4/4] updated an example --- .gitignore | 2 ++ examples/ExampleParallelFunctionCalls.mlx | Bin 7004 -> 6807 bytes 2 files changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index e853794..15e5229 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,5 @@ *.asv *.mat startup.m +papers_to_read.csv +data/* diff --git a/examples/ExampleParallelFunctionCalls.mlx b/examples/ExampleParallelFunctionCalls.mlx index 4ea3a465deaed1f33e101c2aa98b371b8b8355e8..11ab88cf52c9e078db796f3ab5f2c2f339e2d899 100644 GIT binary patch delta 4696 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 alkgfU11%Q-lMfgvlSUg521ytI0000mGw$>N delta 4917 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