Skip to content

Commit fbba5da

Browse files
author
ochafik
committed
support partial content streaming in Generic mode
1 parent 64b4039 commit fbba5da

File tree

4 files changed

+59
-15
lines changed

4 files changed

+59
-15
lines changed

common/chat-parser.cpp

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -242,16 +242,18 @@ common_json common_chat_msg_parser::consume_json() {
242242
}
243243

244244
common_chat_msg_parser::consume_json_result common_chat_msg_parser::consume_json_with_dumped_args(
245-
const std::vector<std::vector<std::string>> & args_paths
245+
const std::vector<std::vector<std::string>> & args_paths,
246+
const std::vector<std::vector<std::string>> & content_paths
246247
) {
247-
if (auto result = try_consume_json_with_dumped_args(args_paths)) {
248+
if (auto result = try_consume_json_with_dumped_args(args_paths, content_paths)) {
248249
return *result;
249250
}
250251
throw common_chat_msg_partial_exception("JSON");
251252
}
252253

253254
std::optional<common_chat_msg_parser::consume_json_result> common_chat_msg_parser::try_consume_json_with_dumped_args(
254-
const std::vector<std::vector<std::string>> & args_paths
255+
const std::vector<std::vector<std::string>> & args_paths,
256+
const std::vector<std::vector<std::string>> & content_paths
255257
) {
256258
auto partial = try_consume_json();
257259
if (!partial) {
@@ -260,6 +262,9 @@ std::optional<common_chat_msg_parser::consume_json_result> common_chat_msg_parse
260262
auto is_arguments_path = [&](const std::vector<std::string> & path) {
261263
return std::find(args_paths.begin(), args_paths.end(), path) != args_paths.end();
262264
};
265+
auto is_content_path = [&](const std::vector<std::string> & path) {
266+
return std::find(content_paths.begin(), content_paths.end(), path) != content_paths.end();
267+
};
263268

264269
if (partial->healing_marker.marker.empty()) {
265270
if (args_paths.empty()) {
@@ -298,6 +303,18 @@ std::optional<common_chat_msg_parser::consume_json_result> common_chat_msg_parse
298303
}
299304
return arguments;
300305
}
306+
if (is_content_path(path)) {
307+
if (!j.is_string()) {
308+
throw std::runtime_error("Content path must be a string");
309+
}
310+
std::string str = j;
311+
auto idx = str.find(partial->healing_marker.marker); // not using json_dump_marker as we're inside a string
312+
if (idx != std::string::npos) {
313+
str.resize(idx);
314+
found_healing_marker = true;
315+
}
316+
return str;
317+
}
301318
if (j.is_object()) {
302319
auto obj = json::object();
303320
for (const auto & p : j.items()) {
@@ -314,6 +331,12 @@ std::optional<common_chat_msg_parser::consume_json_result> common_chat_msg_parse
314331
const std::string value_str = value;
315332
if (value_str.find(healing_marker_) != std::string::npos) {
316333
found_healing_marker = true;
334+
if (is_content_path(path)) {
335+
if (partial->healing_marker.marker == partial->healing_marker.json_dump_marker) {
336+
// The healing occurred inside the string: good. Otherwise we just ditch the entire key/value pair.
337+
obj[key] = remove_unsupported_healings_and_dump_args(value);
338+
}
339+
}
317340
break;
318341
}
319342
obj[key] = value;

common/chat-parser.h

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -95,10 +95,22 @@ class common_chat_msg_parser {
9595
bool is_partial;
9696
};
9797

98+
/*
99+
Consume (possibly partial) json and converts specific subtrees to (possibly truncated) JSON strings.
100+
101+
By default, object keys can't be truncated, nor can string values (their corresponding key is removed,
102+
e.g. `{"foo": "bar", "baz": "b` -> `{"foo": "bar"}`
103+
104+
But one can allow subpaths to be kept truncated, and possibly json-dumped to truncated json strings
105+
- with `content_paths={{"foo"}}` -> `{"foo": "b` -> {"foo": "b"}`
106+
- with `args_paths={{"foo"}}` -> `{"foo": {"b` -> `{"foo": "{b"}`
107+
*/
98108
consume_json_result consume_json_with_dumped_args(
99-
const std::vector<std::vector<std::string>> & args_paths = {}
109+
const std::vector<std::vector<std::string>> & args_paths = {},
110+
const std::vector<std::vector<std::string>> & content_paths = {}
100111
);
101112
std::optional<consume_json_result> try_consume_json_with_dumped_args(
102-
const std::vector<std::vector<std::string>> & args_paths = {}
113+
const std::vector<std::vector<std::string>> & args_paths = {},
114+
const std::vector<std::vector<std::string>> & content_paths = {}
103115
);
104116
};

common/chat.cpp

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -810,11 +810,14 @@ static common_chat_params common_chat_params_init_generic(const common_chat_temp
810810
return data;
811811
}
812812
static void common_chat_parse_generic(common_chat_msg_parser & builder) {
813+
static const std::vector<std::vector<std::string>> content_paths = {
814+
{"response"},
815+
};
813816
static const std::vector<std::vector<std::string>> args_paths = {
814817
{"tool_call", "arguments"},
815818
{"tool_calls", "arguments"},
816819
};
817-
auto data = builder.consume_json_with_dumped_args(args_paths);
820+
auto data = builder.consume_json_with_dumped_args(args_paths, content_paths);
818821
if (data.value.contains("tool_calls")) {
819822
if (!builder.add_tool_calls(data.value.at("tool_calls")) || data.is_partial) {
820823
throw common_chat_msg_partial_exception("incomplete tool calls");

tests/test-chat-parser.cpp

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -165,40 +165,46 @@ const std::vector<std::string> barely_healable_jsons = {
165165
"{\"name\":\"python",
166166
};
167167

168-
static void test(const std::string & input, bool is_partial, const std::vector<std::vector<std::string>> & args_paths, const std::string & expected) {
168+
static void test(const std::string & input, bool is_partial, const std::vector<std::vector<std::string>> & args_paths, const std::vector<std::vector<std::string>> & content_paths, const std::string & expected) {
169169
common_chat_msg_parser builder(input, is_partial, {});
170-
auto js = builder.try_consume_json_with_dumped_args(args_paths);
170+
auto js = builder.try_consume_json_with_dumped_args(args_paths, content_paths);
171171
assert_equals(true, js.has_value());
172172
assert_equals(is_partial, js->is_partial);
173173
assert_equals(expected, args_paths.size() == 1 && args_paths[0].empty() ? js->value.get<std::string>() : js->value.dump());
174174
}
175175
static void test_with_args(const std::string & input, const std::string & expected, bool parse_as_partial = true, bool is_partial = true) {
176176
common_chat_msg_parser builder(input, parse_as_partial, {});
177-
auto js = builder.try_consume_json_with_dumped_args({{"args"}});
177+
auto js = builder.try_consume_json_with_dumped_args({{"args"}}, {});
178178
assert_equals(true, js.has_value());
179179
assert_equals(is_partial, js->is_partial);
180180
assert_equals(expected, js->value.dump());
181181
}
182182

183183
static void test_json_with_dumped_args_no_args() {
184184
// Normal JSON, nothing to heal, nothing to dump
185-
test("{\"name\": \"python\"}", false, {}, "{\"name\":\"python\"}");
185+
test("{\"name\": \"python\"}", false, {}, {}, "{\"name\":\"python\"}");
186186
// Full json is args
187-
test("{\"name\": \"python\"}", false, {{}}, "{\"name\":\"python\"}");
187+
test("{\"name\": \"python\"}", false, {{}}, {}, "{\"name\":\"python\"}");
188188

189189
// If the arguments are further down, don't heal partial content.
190190
for (const auto & src : barely_healable_jsons) {
191-
test(src, true, {{"arguments"}}, "{}");
191+
test(src, true, {{"arguments"}}, {}, "{}");
192192
}
193193
// But heal content that isn't partial.
194-
test("{\"name\": \"python\"", true, {{"arguments"}}, "{\"name\":\"python\"}");
194+
test("{\"name\": \"python\"", true, {{"arguments"}}, {}, "{\"name\":\"python\"}");
195195
}
196196

197197
static void test_json_with_dumped_args() {
198+
199+
// Partial content.
200+
test("{\"content\": \"t", true, {}, {{"content"}}, "{\"content\":\"t\"}");
201+
test("{\"content\": \"", true, {}, {{"content"}}, "{\"content\":\"\"}");
202+
test("{\"content\": ", true, {}, {{"content"}}, "{}");
203+
198204
// If the entire JSON is the arguments, healing it them dumping it produces the same output as the input (just reformatted).
199-
test("{\"name\": \"python", true, {{}}, "{\"name\":\"python");
205+
test("{\"name\": \"python", true, {{}}, {}, "{\"name\":\"python");
200206
for (const auto & src : barely_healable_jsons) {
201-
test(src, true, {{}}, src);
207+
test(src, true, {{}}, {}, src);
202208
}
203209

204210
// Full JSON w/ args

0 commit comments

Comments
 (0)