char-fork/llama.cpp

git clone https://git.t4t.associates/char-fork/llama.cpp

Xuan-Son Nguyencommon: add json.h abstraction (#27511)d9f918d2d

char/aliceai-t5-moe
12.6 KiB363 linesraw
1#include "chat-auto-parser-helpers.h"
2
3#include "chat-auto-parser.h"
4#include "chat-peg-parser.h"
5#include "chat.h"
6#include "log.h"
7#include "peg-parser.h"
8
9#include <cctype>
10#include <numeric>
11
12std::string trim_whitespace(const std::string & str) {
13    size_t start = 0;
14    while (start < str.length() && std::isspace(static_cast<unsigned char>(str[start]))) {
15        start++;
16    }
17
18    if (start == str.length()) {
19        return "";
20    }
21
22    size_t end = str.length() - 1;
23    while (end > start && std::isspace(static_cast<unsigned char>(str[end]))) {
24        end--;
25    }
26
27    return str.substr(start, end - start + 1);
28}
29
30std::string trim_leading_whitespace(const std::string & str) {
31    size_t start = 0;
32    while (start < str.length() && std::isspace(static_cast<unsigned char>(str[start]))) {
33        start++;
34    }
35
36    return str.substr(start);
37}
38
39std::string trim_trailing_whitespace(const std::string & str) {
40    if (str.empty()) {
41        return "";
42    }
43
44    size_t end = str.length() - 1;
45    while (end > 0 && std::isspace(static_cast<unsigned char>(str[end]))) {
46        end--;
47    }
48
49    // If first char is also whitespace, return empty string
50    if (end == 0 && std::isspace(static_cast<unsigned char>(str[0]))) {
51        return "";
52    }
53
54    return str.substr(0, end + 1);
55}
56
57std::string trim_trailing_newlines(const std::string & str) {
58    size_t end = str.length();
59    while (end > 0 && str[end - 1] == '\n') {
60        end--;
61    }
62
63    return str.substr(0, end);
64}
65
66static size_t common_prefix_len(const std::string & left, const std::string & right) {
67    size_t prefix_len = 0;
68    size_t min_len    = std::min(left.length(), right.length());
69    while (prefix_len < min_len && left[prefix_len] == right[prefix_len]) {
70        prefix_len++;
71    }
72    return prefix_len;
73}
74
75static size_t common_suffix_len(const std::string & left, const std::string & right) {
76    size_t suffix_len = 0;
77    size_t min_len    = std::min(left.length(), right.length());
78    while (suffix_len < min_len && left[left.length() - 1 - suffix_len] == right[right.length() - 1 - suffix_len]) {
79        suffix_len++;
80    }
81    return suffix_len;
82}
83
84diff_split calculate_diff_split(const std::string & left, const std::string & right) {
85    diff_split result;
86
87    auto left_seg = segmentize_markers(left);
88    auto right_seg = segmentize_markers(right);
89
90    if (left_seg.empty()) {
91        result.right = right;
92        return result;
93    }
94    if (right_seg.empty()) {
95        result.left = left;
96        return result;
97    }
98
99    auto left_start = left_seg.begin();
100    auto left_end = --left_seg.end();
101    auto right_start = right_seg.begin();
102    auto right_end = --right_seg.end();
103
104    auto test = [&] () {
105        return left_start != left_end && right_start != right_end;
106    };
107
108    bool left_fully_consumed = false;
109    bool right_fully_consumed = false;
110
111    while (test()) {
112        bool advanced = false;
113        if (*left_start == *right_start) {
114            result.prefix.append(left_start->value);
115            left_start++;
116            right_start++;
117            advanced = true;
118        }
119        if (*left_end == *right_end) {
120            result.suffix = left_end->value + result.suffix;
121            if (left_start != left_end) {
122                left_end--;
123            } else {
124                left_fully_consumed = true;
125            }
126            if (right_start != right_end) {
127                right_end--;
128            } else {
129                right_fully_consumed = true;
130            }
131            advanced = true;
132        }
133        if (!advanced) {
134            break;
135        }
136    }
137
138    if (left_start == left_end && right_start != right_end) {
139        if (*left_start == *right_end) {
140            result.suffix = right_end->value + result.suffix;
141            right_end--;
142            left_fully_consumed = true;
143        } else if (*left_start == *right_start) {
144            result.prefix.append(right_start->value);
145            right_start++;
146            left_fully_consumed = true;
147        }
148    } else if (right_start == right_end && left_start != left_end) {
149        if (*left_end == *right_start) {
150            result.suffix = left_end->value + result.suffix;
151            left_end--;
152            right_fully_consumed = true;
153        } else if (*left_start == *right_start) {
154            result.prefix.append(left_start->value);
155            left_start++;
156            right_fully_consumed = true;
157        }
158    } else if (left_start == left_end && right_start == right_end && *left_start == *right_start && left_start->type == segment_type::MARKER) {
159        result.prefix.append(right_start->value);
160        left_fully_consumed = true;
161        right_fully_consumed = true;
162    }
163
164    auto eat_segment = [](std::string str, const segment & seg) -> std::string { return std::move(str) + seg.value; };
165
166    bool can_have_text_suffix = left_end->type == segment_type::TEXT && right_end->type == segment_type::TEXT;
167    bool can_have_text_prefix = right_start->type == segment_type::TEXT && left_start->type == segment_type::TEXT;
168
169    std::string remainder_left = std::accumulate(left_start, left_fully_consumed ? left_end : ++left_end, std::string(), eat_segment);
170    std::string remainder_right = std::accumulate(right_start, right_fully_consumed ? right_end : ++right_end, std::string(), eat_segment);
171
172    size_t suffix_len = can_have_text_suffix ? common_suffix_len(remainder_left, remainder_right) : 0;
173    // avoid overlaps between prefix and suffix
174    size_t prefix_len = can_have_text_prefix ? common_prefix_len(remainder_left.substr(0, remainder_left.size() - suffix_len),
175        remainder_right.substr(0, remainder_right.size() - suffix_len)) : 0;
176
177    result.prefix.append(remainder_left.substr(0, prefix_len));
178    result.suffix = remainder_left.substr(remainder_left.length() - suffix_len, suffix_len) + result.suffix;
179    result.left = remainder_left.substr(prefix_len, remainder_left.length() - prefix_len - suffix_len);
180    result.right = remainder_right.substr(prefix_len, remainder_right.length() - prefix_len - suffix_len);
181
182    if (result.left == "" && result.right == "") {
183        // degenerate case, no diff
184        result.prefix = left;
185        result.suffix = "";
186        // pick prefix = all as representation
187    }
188
189    // When left has no unique content (result.left is empty), left is entirely
190    // shared with right. The simultaneous prefix/suffix segment matching can
191    // incorrectly consume trailing segments of left as suffix when those same
192    // segments also appear at the end of right (e.g. "\n" at the end of both
193    // the shared content and the generation prompt). This rotates the diff.
194    // Fix: if left is a prefix of right, enforce that directly.
195    if (result.left.empty() && !result.right.empty() &&
196            left.size() <= right.size() &&
197            right.substr(0, left.size()) == left) {
198        result.prefix = left;
199        result.suffix = "";
200        result.right  = right.substr(left.size());
201    }
202
203    return result;
204}
205
206// Returns the prefix of `full` up until the first occurrence of the common prefix of `left` and `right`
207std::string until_common_prefix(const std::string & full, const std::string & left, const std::string & right) {
208    // Find the common prefix of left and right
209    size_t common_prefix_len = 0;
210    size_t min_len           = std::min(left.length(), right.length());
211    while (common_prefix_len < min_len && left[common_prefix_len] == right[common_prefix_len]) {
212        common_prefix_len++;
213    }
214
215    // If there's no common prefix, return empty string
216    if (common_prefix_len == 0) {
217        return "";
218    }
219
220    // Find the common prefix in the full string
221    std::string common_prefix = left.substr(0, common_prefix_len);
222    size_t      pos           = full.find(common_prefix);
223
224    // If not found, return empty string
225    if (pos == std::string::npos) {
226        return "";
227    }
228
229    // Return everything before the common prefix
230    return full.substr(0, pos);
231}
232
233// Returns the suffix of `full` after the last occurrence of the common suffix of `left` and `right`
234std::string after_common_suffix(const std::string & full, const std::string & left, const std::string & right) {
235    // Find the common suffix of left and right (compare from the end)
236    size_t common_suffix_len = 0;
237    size_t min_len           = std::min(left.length(), right.length());
238    while (common_suffix_len < min_len &&
239           left[left.length() - 1 - common_suffix_len] == right[right.length() - 1 - common_suffix_len]) {
240        common_suffix_len++;
241    }
242
243    // If there's no common suffix, return empty string
244    if (common_suffix_len == 0) {
245        return "";
246    }
247
248    // Extract the common suffix
249    std::string common_suffix = left.substr(left.length() - common_suffix_len);
250
251    // Find the last occurrence of the common suffix in the full string
252    size_t pos = full.rfind(common_suffix);
253
254    // If not found, return empty string
255    if (pos == std::string::npos) {
256        return "";
257    }
258
259    // Return everything after the common suffix
260    return full.substr(pos + common_suffix_len);
261}
262
263// TODO: segmentize will treat a JSON array inside tags as a tag: <calls>[{ "fun": { ... } }]</calls> will be three markers
264// not too worried about that because it hasn't turned out as a problem anywhere, but noting here in case it will
265// Might have to put some restrictions on tag contents as well (like "no { }")
266std::vector<segment> segmentize_markers(const std::string & text) {
267    std::vector<segment> retval;
268    bool in_marker = false;
269    char marker_opener = '\0';
270
271    auto is_marker_opener = [](char c) -> bool { return c == '<' || c == '['; };
272    auto is_marker_closer = [](char op, char c) -> bool { return (op == '<' && c == '>') || (op == '[' && c == ']'); };
273
274    size_t last_border = 0;
275
276    for (size_t cur_pos = 0; cur_pos < text.length(); cur_pos++) {
277        if (!in_marker && is_marker_opener(text[cur_pos])) {
278            if (last_border < cur_pos) {
279                retval.push_back(segment(segment_type::TEXT, text.substr(last_border, cur_pos - last_border)));
280            }
281            last_border = cur_pos;
282            in_marker = true;
283            marker_opener = text[cur_pos];
284        } else if (in_marker && is_marker_closer(marker_opener, text[cur_pos])) {
285            // no need to check because last_border will always be smaller
286                retval.push_back(segment(segment_type::MARKER, text.substr(last_border, cur_pos - last_border + 1)));
287            last_border = cur_pos + 1;
288            in_marker = false;
289            marker_opener = '\0';
290        }
291    }
292    if (last_border < text.length()) {
293            retval.push_back(segment(segment_type::TEXT, text.substr(last_border)));
294    }
295    return retval;
296}
297
298std::vector<segment> prune_whitespace_segments(const std::vector<segment> & segments) {
299    std::vector<segment> result;
300    for (const auto & seg : segments) {
301        if (!trim_whitespace(seg.value).empty()) {
302            result.push_back(seg);
303        }
304    }
305    return result;
306}
307
308namespace autoparser {
309
310static const std::string ERR_TMPL = "#**ERROR**#";
311
312std::string apply_template(const common_chat_template & tmpl, const template_params & params) {
313    generation_params tmpl_params;
314    tmpl_params.messages              = params.messages;
315    tmpl_params.tools                 = params.tools;
316    tmpl_params.add_generation_prompt = params.add_generation_prompt;
317    tmpl_params.enable_thinking       = params.enable_thinking;
318
319    if (params.extra_context) {
320        tmpl_params.extra_context = *params.extra_context;
321    }
322    tmpl_params.extra_context["enable_thinking"] = params.enable_thinking;
323
324    try {
325        return common_chat_template_direct_apply(tmpl, tmpl_params);
326    } catch (const std::exception & e) {
327        LOG_DBG("Template application failed: %s\n", e.what());
328        return ERR_TMPL;
329    }
330}
331
332std::optional<compare_variants_result> compare_variants(
333    const common_chat_template &                   tmpl,
334    const template_params &                        params_A,
335    const std::function<void(template_params &)> & params_modifier) {
336    // Create variant B by copying A
337    template_params params_B = params_A;
338
339    // Apply modifier to create variant B
340    if (params_modifier) {
341        params_modifier(params_B);
342    }
343
344    // Apply template to both variants
345    std::string output_A = apply_template(tmpl, params_A);
346    std::string output_B = apply_template(tmpl, params_B);
347
348    // Check for template application failures
349    if (output_A == ERR_TMPL || output_B == ERR_TMPL) {
350        return std::nullopt;
351    }
352
353    // Calculate diff and return result with both outputs
354    compare_variants_result result;
355    result.diff     = calculate_diff_split(output_A, output_B);
356    result.output_A = output_A;
357    result.output_B = output_B;
358
359    return result;
360}
361
362}  // namespace autoparser
363