543 lines
16 KiB
C
543 lines
16 KiB
C
#define _POSIX_C_SOURCE 200809L
|
|
|
|
#include "prompt_template.h"
|
|
|
|
#include <ctype.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
|
|
static char* dup_range(const char* s, size_t n) {
|
|
char* out = (char*)malloc(n + 1U);
|
|
if (!out) return NULL;
|
|
if (n > 0) {
|
|
memcpy(out, s, n);
|
|
}
|
|
out[n] = '\0';
|
|
return out;
|
|
}
|
|
|
|
static char* ltrim_inplace(char* s) {
|
|
if (!s) return s;
|
|
while (*s && isspace((unsigned char)*s)) s++;
|
|
return s;
|
|
}
|
|
|
|
static void rtrim_inplace(char* s) {
|
|
if (!s) return;
|
|
size_t n = strlen(s);
|
|
while (n > 0 && isspace((unsigned char)s[n - 1])) {
|
|
s[n - 1] = '\0';
|
|
n--;
|
|
}
|
|
}
|
|
|
|
static int starts_with(const char* s, const char* prefix) {
|
|
if (!s || !prefix) return 0;
|
|
size_t n = strlen(prefix);
|
|
return strncmp(s, prefix, n) == 0;
|
|
}
|
|
|
|
static int add_line(char*** lines, int* count, int* cap, char* line) {
|
|
if (!lines || !count || !cap) return -1;
|
|
if (*count >= *cap) {
|
|
int next = (*cap == 0) ? 64 : (*cap * 2);
|
|
char** grown = (char**)realloc(*lines, (size_t)next * sizeof(char*));
|
|
if (!grown) return -1;
|
|
*lines = grown;
|
|
*cap = next;
|
|
}
|
|
(*lines)[*count] = line;
|
|
(*count)++;
|
|
return 0;
|
|
}
|
|
|
|
static int split_lines_inplace(char* s, char*** out_lines, int* out_count) {
|
|
if (!s || !out_lines || !out_count) return -1;
|
|
|
|
char** lines = NULL;
|
|
int count = 0;
|
|
int cap = 0;
|
|
|
|
char* p = s;
|
|
while (*p) {
|
|
char* start = p;
|
|
while (*p && *p != '\n') p++;
|
|
if (*p == '\n') {
|
|
*p = '\0';
|
|
p++;
|
|
}
|
|
if (add_line(&lines, &count, &cap, start) != 0) {
|
|
free(lines);
|
|
return -1;
|
|
}
|
|
}
|
|
|
|
*out_lines = lines;
|
|
*out_count = count;
|
|
return 0;
|
|
}
|
|
|
|
static int append_text(char** buf, size_t* cap, size_t* used, const char* s) {
|
|
if (!buf || !cap || !used || !s) return -1;
|
|
size_t n = strlen(s);
|
|
if (*used + n + 1U > *cap) {
|
|
size_t next = *cap;
|
|
while (*used + n + 1U > next) {
|
|
next = (next == 0U) ? 256U : (next * 2U);
|
|
}
|
|
char* grown = (char*)realloc(*buf, next);
|
|
if (!grown) return -1;
|
|
*buf = grown;
|
|
*cap = next;
|
|
}
|
|
memcpy(*buf + *used, s, n);
|
|
*used += n;
|
|
(*buf)[*used] = '\0';
|
|
return 0;
|
|
}
|
|
|
|
static char* resolve_placeholders(const char* tpl,
|
|
prompt_var_resolver_fn resolver_fn,
|
|
void* resolver_user_data) {
|
|
if (!tpl) return strdup("");
|
|
|
|
size_t cap = strlen(tpl) + 64U;
|
|
char* out = (char*)malloc(cap ? cap : 128U);
|
|
if (!out) return NULL;
|
|
out[0] = '\0';
|
|
size_t used = 0;
|
|
|
|
const char* p = tpl;
|
|
while (*p) {
|
|
const char* open = strstr(p, "{{");
|
|
if (!open) {
|
|
if (append_text(&out, &cap, &used, p) != 0) {
|
|
free(out);
|
|
return NULL;
|
|
}
|
|
break;
|
|
}
|
|
|
|
if (open > p) {
|
|
char* literal = dup_range(p, (size_t)(open - p));
|
|
if (!literal) {
|
|
free(out);
|
|
return NULL;
|
|
}
|
|
int rc = append_text(&out, &cap, &used, literal);
|
|
free(literal);
|
|
if (rc != 0) {
|
|
free(out);
|
|
return NULL;
|
|
}
|
|
}
|
|
|
|
const char* close = strstr(open + 2, "}}");
|
|
if (!close) {
|
|
if (append_text(&out, &cap, &used, open) != 0) {
|
|
free(out);
|
|
return NULL;
|
|
}
|
|
break;
|
|
}
|
|
|
|
char* name = dup_range(open + 2, (size_t)(close - (open + 2)));
|
|
if (!name) {
|
|
free(out);
|
|
return NULL;
|
|
}
|
|
char* name_trim = ltrim_inplace(name);
|
|
rtrim_inplace(name_trim);
|
|
|
|
char* val = resolver_fn ? resolver_fn(name_trim, resolver_user_data) : NULL;
|
|
if (!val) {
|
|
val = strdup("");
|
|
}
|
|
|
|
int rc = 0;
|
|
if (val) {
|
|
rc = append_text(&out, &cap, &used, val);
|
|
}
|
|
free(val);
|
|
free(name);
|
|
if (rc != 0) {
|
|
free(out);
|
|
return NULL;
|
|
}
|
|
|
|
p = close + 2;
|
|
}
|
|
|
|
return out;
|
|
}
|
|
|
|
static void init_section_defaults(prompt_template_section_t* sec) {
|
|
if (!sec) return;
|
|
memset(sec->name, 0, sizeof(sec->name));
|
|
memset(sec->role, 0, sizeof(sec->role));
|
|
snprintf(sec->role, sizeof(sec->role), "system");
|
|
sec->content_template = NULL;
|
|
sec->limit = 0;
|
|
sec->skip_if_empty = 0;
|
|
sec->provider_name = NULL;
|
|
sec->provider_content_template = NULL;
|
|
}
|
|
|
|
static int parse_int_or_zero(const char* s) {
|
|
if (!s) return 0;
|
|
while (*s && isspace((unsigned char)*s)) s++;
|
|
return atoi(s);
|
|
}
|
|
|
|
int prompt_template_parse(const char* soul_content, prompt_template_t* out_template) {
|
|
if (!soul_content || !out_template) {
|
|
return -1;
|
|
}
|
|
|
|
memset(out_template, 0, sizeof(*out_template));
|
|
|
|
const char* marker = strstr(soul_content, PROMPT_TEMPLATE_MARKER);
|
|
if (!marker) {
|
|
return -1;
|
|
}
|
|
|
|
size_t personality_len = (size_t)(marker - soul_content);
|
|
out_template->personality = dup_range(soul_content, personality_len);
|
|
if (!out_template->personality) {
|
|
return -1;
|
|
}
|
|
rtrim_inplace(out_template->personality);
|
|
|
|
const char* after = marker + strlen(PROMPT_TEMPLATE_MARKER);
|
|
while (*after == '\r' || *after == '\n') after++;
|
|
|
|
char* tpl = strdup(after);
|
|
if (!tpl) {
|
|
prompt_template_free(out_template);
|
|
return -1;
|
|
}
|
|
|
|
char** lines = NULL;
|
|
int line_count = 0;
|
|
if (split_lines_inplace(tpl, &lines, &line_count) != 0) {
|
|
free(tpl);
|
|
prompt_template_free(out_template);
|
|
return -1;
|
|
}
|
|
|
|
int current = -1;
|
|
int i = 0;
|
|
while (i < line_count) {
|
|
char* raw = lines[i];
|
|
rtrim_inplace(raw);
|
|
char* line = ltrim_inplace(raw);
|
|
|
|
if (*line == '\0') {
|
|
i++;
|
|
continue;
|
|
}
|
|
|
|
if (starts_with(line, "- section:")) {
|
|
if (out_template->section_count >= PROMPT_TEMPLATE_MAX_SECTIONS) {
|
|
break;
|
|
}
|
|
|
|
current = out_template->section_count;
|
|
init_section_defaults(&out_template->sections[current]);
|
|
out_template->section_count++;
|
|
|
|
char* name = line + strlen("- section:");
|
|
name = ltrim_inplace(name);
|
|
rtrim_inplace(name);
|
|
snprintf(out_template->sections[current].name,
|
|
sizeof(out_template->sections[current].name),
|
|
"%s",
|
|
name);
|
|
i++;
|
|
continue;
|
|
}
|
|
|
|
if (current < 0) {
|
|
i++;
|
|
continue;
|
|
}
|
|
|
|
if (starts_with(line, "role:")) {
|
|
char* role = line + strlen("role:");
|
|
role = ltrim_inplace(role);
|
|
rtrim_inplace(role);
|
|
snprintf(out_template->sections[current].role,
|
|
sizeof(out_template->sections[current].role),
|
|
"%s",
|
|
(*role) ? role : "system");
|
|
i++;
|
|
continue;
|
|
}
|
|
|
|
if (starts_with(line, "limit:")) {
|
|
char* lim = line + strlen("limit:");
|
|
out_template->sections[current].limit = parse_int_or_zero(lim);
|
|
i++;
|
|
continue;
|
|
}
|
|
|
|
if (starts_with(line, "skip_if_empty:")) {
|
|
char* flag = line + strlen("skip_if_empty:");
|
|
flag = ltrim_inplace(flag);
|
|
rtrim_inplace(flag);
|
|
out_template->sections[current].skip_if_empty =
|
|
(strcmp(flag, "true") == 0 || strcmp(flag, "1") == 0) ? 1 : 0;
|
|
i++;
|
|
continue;
|
|
}
|
|
|
|
if (starts_with(line, "content:")) {
|
|
char* val = line + strlen("content:");
|
|
val = ltrim_inplace(val);
|
|
|
|
free(out_template->sections[current].content_template);
|
|
out_template->sections[current].content_template = NULL;
|
|
|
|
if (strcmp(val, "|") == 0) {
|
|
char* acc = strdup("");
|
|
size_t cap = acc ? 1U : 0U;
|
|
size_t used = 0U;
|
|
if (!acc) {
|
|
free(lines);
|
|
free(tpl);
|
|
prompt_template_free(out_template);
|
|
return -1;
|
|
}
|
|
|
|
i++;
|
|
while (i < line_count) {
|
|
char* next_raw = lines[i];
|
|
char* next_ltrim = ltrim_inplace(next_raw);
|
|
|
|
if (starts_with(next_ltrim, "- section:") ||
|
|
starts_with(next_ltrim, "role:") ||
|
|
starts_with(next_ltrim, "limit:") ||
|
|
starts_with(next_ltrim, "skip_if_empty:") ||
|
|
starts_with(next_ltrim, "content:") ||
|
|
starts_with(next_ltrim, "provider:")) {
|
|
break;
|
|
}
|
|
|
|
char* piece = next_raw;
|
|
if (strncmp(piece, " ", 4) == 0) piece += 4;
|
|
else if (strncmp(piece, " ", 2) == 0) piece += 2;
|
|
rtrim_inplace(piece);
|
|
|
|
if (append_text(&acc, &cap, &used, piece) != 0 ||
|
|
append_text(&acc, &cap, &used, "\n") != 0) {
|
|
free(acc);
|
|
free(lines);
|
|
free(tpl);
|
|
prompt_template_free(out_template);
|
|
return -1;
|
|
}
|
|
i++;
|
|
}
|
|
|
|
rtrim_inplace(acc);
|
|
out_template->sections[current].content_template = acc;
|
|
continue;
|
|
}
|
|
|
|
rtrim_inplace(val);
|
|
out_template->sections[current].content_template = strdup(val);
|
|
i++;
|
|
continue;
|
|
}
|
|
|
|
if (starts_with(line, "provider:")) {
|
|
i++;
|
|
if (i >= line_count) continue;
|
|
|
|
char* provider_line = ltrim_inplace(lines[i]);
|
|
rtrim_inplace(provider_line);
|
|
char* colon = strchr(provider_line, ':');
|
|
if (!colon) {
|
|
continue;
|
|
}
|
|
|
|
*colon = '\0';
|
|
char* provider_name = ltrim_inplace(provider_line);
|
|
rtrim_inplace(provider_name);
|
|
char* provider_val = ltrim_inplace(colon + 1);
|
|
rtrim_inplace(provider_val);
|
|
|
|
free(out_template->sections[current].provider_name);
|
|
out_template->sections[current].provider_name = strdup(provider_name);
|
|
free(out_template->sections[current].provider_content_template);
|
|
out_template->sections[current].provider_content_template = NULL;
|
|
|
|
if (strcmp(provider_val, "|") == 0) {
|
|
char* acc = strdup("");
|
|
size_t cap = acc ? 1U : 0U;
|
|
size_t used = 0U;
|
|
if (!acc) {
|
|
free(lines);
|
|
free(tpl);
|
|
prompt_template_free(out_template);
|
|
return -1;
|
|
}
|
|
|
|
i++;
|
|
while (i < line_count) {
|
|
char* next = ltrim_inplace(lines[i]);
|
|
if (starts_with(next, "- section:") ||
|
|
starts_with(next, "role:") ||
|
|
starts_with(next, "limit:") ||
|
|
starts_with(next, "skip_if_empty:") ||
|
|
starts_with(next, "content:") ||
|
|
starts_with(next, "provider:")) {
|
|
break;
|
|
}
|
|
char* piece = lines[i];
|
|
if (strncmp(piece, " ", 4) == 0) piece += 4;
|
|
else if (strncmp(piece, " ", 2) == 0) piece += 2;
|
|
rtrim_inplace(piece);
|
|
if (append_text(&acc, &cap, &used, piece) != 0 ||
|
|
append_text(&acc, &cap, &used, "\n") != 0) {
|
|
free(acc);
|
|
free(lines);
|
|
free(tpl);
|
|
prompt_template_free(out_template);
|
|
return -1;
|
|
}
|
|
i++;
|
|
}
|
|
rtrim_inplace(acc);
|
|
out_template->sections[current].provider_content_template = acc;
|
|
continue;
|
|
}
|
|
|
|
out_template->sections[current].provider_content_template = strdup(provider_val);
|
|
i++;
|
|
continue;
|
|
}
|
|
|
|
i++;
|
|
}
|
|
|
|
free(lines);
|
|
free(tpl);
|
|
return 0;
|
|
}
|
|
|
|
static int append_message_object(cJSON* messages, const char* role, const char* content) {
|
|
if (!messages || !role) return -1;
|
|
cJSON* msg = cJSON_CreateObject();
|
|
if (!msg) return -1;
|
|
cJSON_AddStringToObject(msg, "role", role);
|
|
cJSON_AddStringToObject(msg, "content", content ? content : "");
|
|
cJSON_AddItemToArray(messages, msg);
|
|
return 0;
|
|
}
|
|
|
|
cJSON* prompt_template_build_messages(const prompt_template_t* tmpl,
|
|
const char* provider_name,
|
|
prompt_var_resolver_fn resolver_fn,
|
|
void* resolver_user_data,
|
|
cJSON* dm_history_messages,
|
|
int dm_history_default_limit,
|
|
prompt_template_emit_hook_fn emit_hook,
|
|
void* emit_hook_user_data) {
|
|
if (!tmpl) return NULL;
|
|
|
|
cJSON* out = cJSON_CreateArray();
|
|
if (!out) return NULL;
|
|
|
|
int out_idx = 0;
|
|
if (tmpl->personality && tmpl->personality[0] != '\0') {
|
|
if (append_message_object(out, "system", tmpl->personality) != 0) {
|
|
cJSON_Delete(out);
|
|
return NULL;
|
|
}
|
|
if (emit_hook) emit_hook("system_prompt", out_idx, emit_hook_user_data);
|
|
out_idx++;
|
|
}
|
|
|
|
for (int i = 0; i < tmpl->section_count; i++) {
|
|
const prompt_template_section_t* sec = &tmpl->sections[i];
|
|
const char* role = sec->role[0] ? sec->role : "system";
|
|
|
|
if (strcmp(role, "expand") == 0) {
|
|
if (!dm_history_messages || !cJSON_IsArray(dm_history_messages)) {
|
|
continue;
|
|
}
|
|
int total = cJSON_GetArraySize(dm_history_messages);
|
|
int lim = sec->limit > 0 ? sec->limit : dm_history_default_limit;
|
|
int start = (lim > 0 && total > lim) ? (total - lim) : 0;
|
|
|
|
for (int j = start; j < total; j++) {
|
|
cJSON* item = cJSON_GetArrayItem(dm_history_messages, j);
|
|
if (!item || !cJSON_IsObject(item)) continue;
|
|
cJSON* dup = cJSON_Duplicate(item, 1);
|
|
if (!dup) {
|
|
cJSON_Delete(out);
|
|
return NULL;
|
|
}
|
|
cJSON_AddItemToArray(out, dup);
|
|
if (emit_hook) emit_hook(sec->name[0] ? sec->name : "context_part", out_idx, emit_hook_user_data);
|
|
out_idx++;
|
|
}
|
|
continue;
|
|
}
|
|
|
|
const char* tpl = sec->content_template;
|
|
if (provider_name && sec->provider_name && sec->provider_content_template &&
|
|
strcmp(provider_name, sec->provider_name) == 0) {
|
|
tpl = sec->provider_content_template;
|
|
}
|
|
|
|
char* resolved = resolve_placeholders(tpl ? tpl : "", resolver_fn, resolver_user_data);
|
|
if (!resolved) {
|
|
cJSON_Delete(out);
|
|
return NULL;
|
|
}
|
|
|
|
if (sec->skip_if_empty) {
|
|
char* chk = ltrim_inplace(resolved);
|
|
if (chk && *chk == '\0') {
|
|
free(resolved);
|
|
continue;
|
|
}
|
|
}
|
|
|
|
if (append_message_object(out, role, resolved) != 0) {
|
|
free(resolved);
|
|
cJSON_Delete(out);
|
|
return NULL;
|
|
}
|
|
if (emit_hook) emit_hook(sec->name[0] ? sec->name : "context_part", out_idx, emit_hook_user_data);
|
|
out_idx++;
|
|
free(resolved);
|
|
}
|
|
|
|
return out;
|
|
}
|
|
|
|
void prompt_template_free(prompt_template_t* tmpl) {
|
|
if (!tmpl) return;
|
|
|
|
free(tmpl->personality);
|
|
tmpl->personality = NULL;
|
|
|
|
for (int i = 0; i < tmpl->section_count; i++) {
|
|
free(tmpl->sections[i].content_template);
|
|
tmpl->sections[i].content_template = NULL;
|
|
free(tmpl->sections[i].provider_name);
|
|
tmpl->sections[i].provider_name = NULL;
|
|
free(tmpl->sections[i].provider_content_template);
|
|
tmpl->sections[i].provider_content_template = NULL;
|
|
tmpl->sections[i].name[0] = '\0';
|
|
tmpl->sections[i].role[0] = '\0';
|
|
tmpl->sections[i].limit = 0;
|
|
tmpl->sections[i].skip_if_empty = 0;
|
|
}
|
|
|
|
tmpl->section_count = 0;
|
|
} |