Files
ds4/ds4_prompt_prefix.c

250 lines
8.0 KiB
C

#include "ds4_prompt_prefix.h"
#include <errno.h>
#include <stdarg.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
static void prefix_error(char *error, size_t error_cap, const char *fmt, ...) {
if (!error || error_cap == 0) return;
va_list ap;
va_start(ap, fmt);
vsnprintf(error, error_cap, fmt, ap);
va_end(ap);
}
static bool prefix_marker_at(const char *data, size_t len, size_t pos,
ds4_prompt_prefix_role *role,
size_t *marker_len) {
static const char user[] = "USER:";
static const char assistant[] = "ASSISTANT:";
if (len - pos >= sizeof(user) - 1 &&
memcmp(data + pos, user, sizeof(user) - 1) == 0) {
*role = DS4_PROMPT_PREFIX_USER;
*marker_len = sizeof(user) - 1;
return true;
}
if (len - pos >= sizeof(assistant) - 1 &&
memcmp(data + pos, assistant, sizeof(assistant) - 1) == 0) {
*role = DS4_PROMPT_PREFIX_ASSISTANT;
*marker_len = sizeof(assistant) - 1;
return true;
}
return false;
}
static char *prefix_copy_content(const char *data, size_t start, size_t end) {
size_t len = end - start;
char *content = malloc(len + 1);
if (!content) return NULL;
memcpy(content, data + start, len);
content[len] = '\0';
return content;
}
void ds4_prompt_prefix_free(ds4_prompt_prefix *prefix) {
if (!prefix) return;
for (size_t i = 0; i < prefix->count; i++)
free(prefix->turns[i].content);
free(prefix->turns);
memset(prefix, 0, sizeof(*prefix));
}
int ds4_prompt_prefix_parse(ds4_prompt_prefix *out,
const char *data,
size_t len,
char *error,
size_t error_cap) {
if (!out) {
prefix_error(error, error_cap, "invalid prefix output");
return -1;
}
memset(out, 0, sizeof(*out));
if (!data || len == 0) {
prefix_error(error, error_cap, "prefix file is empty");
return -1;
}
if (memchr(data, '\0', len)) {
prefix_error(error, error_cap, "prefix file contains a NUL byte");
return -1;
}
size_t pos = 0;
if (len >= 3 && (unsigned char)data[0] == 0xef &&
(unsigned char)data[1] == 0xbb &&
(unsigned char)data[2] == 0xbf) {
pos = 3;
}
size_t line = 1;
while (pos < len) {
ds4_prompt_prefix_role role;
size_t marker_len = 0;
if (!prefix_marker_at(data, len, pos, &role, &marker_len)) {
prefix_error(error, error_cap,
"line %zu must start with USER: or ASSISTANT:", line);
ds4_prompt_prefix_free(out);
return -1;
}
const ds4_prompt_prefix_role expected =
(out->count % 2 == 0) ? DS4_PROMPT_PREFIX_USER
: DS4_PROMPT_PREFIX_ASSISTANT;
if (role != expected) {
prefix_error(error, error_cap, "line %zu: expected %s:", line,
expected == DS4_PROMPT_PREFIX_USER ? "USER"
: "ASSISTANT");
ds4_prompt_prefix_free(out);
return -1;
}
size_t content_start = pos + marker_len;
if (content_start < len &&
(data[content_start] == ' ' || data[content_start] == '\t')) {
content_start++;
}
size_t next = len;
size_t next_line = line;
size_t scan_line = line;
for (size_t i = content_start; i < len; i++) {
if (data[i] != '\n') continue;
scan_line++;
ds4_prompt_prefix_role ignored_role;
size_t ignored_len;
if (i + 1 < len &&
prefix_marker_at(data, len, i + 1,
&ignored_role, &ignored_len)) {
next = i + 1;
next_line = scan_line;
break;
}
}
size_t content_end = next;
if (content_end > content_start && data[content_end - 1] == '\n') {
content_end--;
if (content_end > content_start && data[content_end - 1] == '\r')
content_end--;
}
if (content_end == content_start) {
prefix_error(error, error_cap, "line %zu has an empty %s turn",
line, role == DS4_PROMPT_PREFIX_USER ? "USER"
: "ASSISTANT");
ds4_prompt_prefix_free(out);
return -1;
}
if (out->count == SIZE_MAX / sizeof(out->turns[0])) {
prefix_error(error, error_cap, "too many turns in prefix file");
ds4_prompt_prefix_free(out);
return -1;
}
ds4_prompt_prefix_turn *turns =
realloc(out->turns, (out->count + 1) * sizeof(out->turns[0]));
if (!turns) {
prefix_error(error, error_cap, "out of memory reading prefix file");
ds4_prompt_prefix_free(out);
return -1;
}
out->turns = turns;
char *content = prefix_copy_content(data, content_start, content_end);
if (!content) {
prefix_error(error, error_cap, "out of memory reading prefix file");
ds4_prompt_prefix_free(out);
return -1;
}
out->turns[out->count++] = (ds4_prompt_prefix_turn) {
.role = role,
.content = content,
};
if (next == len) {
pos = len;
} else {
pos = next;
line = next_line;
}
}
if (out->count == 0) {
prefix_error(error, error_cap, "prefix file contains no turns");
return -1;
}
if (out->turns[out->count - 1].role != DS4_PROMPT_PREFIX_ASSISTANT) {
prefix_error(error, error_cap,
"prefix file must end with an ASSISTANT turn");
ds4_prompt_prefix_free(out);
return -1;
}
return 0;
}
int ds4_prompt_prefix_load(ds4_prompt_prefix *out,
const char *path,
char *error,
size_t error_cap) {
if (!out || !path || !path[0]) {
prefix_error(error, error_cap, "invalid prefix file path");
return -1;
}
memset(out, 0, sizeof(*out));
FILE *fp = fopen(path, "rb");
if (!fp) {
prefix_error(error, error_cap, "cannot open prefix file %s: %s",
path, strerror(errno));
return -1;
}
if (fseek(fp, 0, SEEK_END) != 0) {
prefix_error(error, error_cap, "cannot seek prefix file %s: %s",
path, strerror(errno));
fclose(fp);
return -1;
}
long size = ftell(fp);
if (size < 0 || (uintmax_t)size > SIZE_MAX - 1) {
prefix_error(error, error_cap, "cannot size prefix file %s", path);
fclose(fp);
return -1;
}
if (fseek(fp, 0, SEEK_SET) != 0) {
prefix_error(error, error_cap, "cannot rewind prefix file %s: %s",
path, strerror(errno));
fclose(fp);
return -1;
}
size_t len = (size_t)size;
char *data = malloc(len + 1);
if (!data) {
prefix_error(error, error_cap, "out of memory reading prefix file");
fclose(fp);
return -1;
}
if (len && fread(data, 1, len, fp) != len) {
prefix_error(error, error_cap, "cannot read prefix file %s", path);
free(data);
fclose(fp);
return -1;
}
if (fclose(fp) != 0) {
prefix_error(error, error_cap, "cannot close prefix file %s: %s",
path, strerror(errno));
free(data);
return -1;
}
data[len] = '\0';
int rc = ds4_prompt_prefix_parse(out, data, len, error, error_cap);
free(data);
if (rc != 0 && error && error_cap && error[0]) {
char detail[256];
snprintf(detail, sizeof(detail), "%s", error);
prefix_error(error, error_cap, "%s: %s", path, detail);
}
return rc;
}