617 lines
16 KiB
C
617 lines
16 KiB
C
/* SPDX-License-Identifier: AGPL-3.0-or-later */
|
|
/* Copyright (C) 2026 David (Corey) Herrera */
|
|
#include "data.h"
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <time.h>
|
|
|
|
/* ============================================================= */
|
|
/* Random state */
|
|
/* ============================================================= */
|
|
|
|
static uint64_t fd_rng_state = 0;
|
|
|
|
void fd_seed(uint64_t seed) {
|
|
fd_rng_state = seed;
|
|
}
|
|
|
|
static uint64_t fd_rand(void) {
|
|
if (fd_rng_state == 0) {
|
|
fd_rng_state = (uint64_t)time(NULL);
|
|
}
|
|
/* xorshift64 */
|
|
fd_rng_state ^= fd_rng_state << 13;
|
|
fd_rng_state ^= fd_rng_state >> 7;
|
|
fd_rng_state ^= fd_rng_state << 17;
|
|
return fd_rng_state;
|
|
}
|
|
|
|
/* ============================================================= */
|
|
/* Sample */
|
|
/* ============================================================= */
|
|
|
|
fd_sample* fd_sample_new(void) {
|
|
fd_sample* s = calloc(1, sizeof(fd_sample));
|
|
return s;
|
|
}
|
|
|
|
void fd_sample_free(fd_sample* s) {
|
|
if (!s) return;
|
|
|
|
for (int i = 0; i < s->num_fields; i++) {
|
|
free(s->keys[i]);
|
|
free(s->values[i]);
|
|
}
|
|
free(s->keys);
|
|
free(s->values);
|
|
free(s);
|
|
}
|
|
|
|
void fd_sample_set(fd_sample* s, const char* key, const char* value) {
|
|
if (!s || !key || !value) return;
|
|
|
|
/* Check if key already exists */
|
|
for (int i = 0; i < s->num_fields; i++) {
|
|
if (strcmp(s->keys[i], key) == 0) {
|
|
free(s->values[i]);
|
|
s->values[i] = strdup(value);
|
|
return;
|
|
}
|
|
}
|
|
|
|
/* Add new field */
|
|
s->num_fields++;
|
|
s->keys = realloc(s->keys, s->num_fields * sizeof(char*));
|
|
s->values = realloc(s->values, s->num_fields * sizeof(char*));
|
|
s->keys[s->num_fields - 1] = strdup(key);
|
|
s->values[s->num_fields - 1] = strdup(value);
|
|
}
|
|
|
|
const char* fd_sample_get(fd_sample* s, const char* key) {
|
|
if (!s || !key) return NULL;
|
|
|
|
for (int i = 0; i < s->num_fields; i++) {
|
|
if (strcmp(s->keys[i], key) == 0) {
|
|
return s->values[i];
|
|
}
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
bool fd_sample_has(fd_sample* s, const char* key) {
|
|
return fd_sample_get(s, key) != NULL;
|
|
}
|
|
|
|
fd_sample* fd_sample_clone(fd_sample* s) {
|
|
if (!s) return NULL;
|
|
|
|
fd_sample* clone = fd_sample_new();
|
|
for (int i = 0; i < s->num_fields; i++) {
|
|
fd_sample_set(clone, s->keys[i], s->values[i]);
|
|
}
|
|
return clone;
|
|
}
|
|
|
|
void fd_sample_print(fd_sample* s) {
|
|
if (!s) {
|
|
printf("(null sample)\n");
|
|
return;
|
|
}
|
|
|
|
printf("{");
|
|
for (int i = 0; i < s->num_fields; i++) {
|
|
if (i > 0) printf(", ");
|
|
printf("\"%s\": \"%s\"", s->keys[i], s->values[i]);
|
|
}
|
|
printf("}\n");
|
|
}
|
|
|
|
/* ============================================================= */
|
|
/* Dataset */
|
|
/* ============================================================= */
|
|
|
|
struct fd_dataset {
|
|
fd_sample** samples;
|
|
int num_samples;
|
|
int capacity;
|
|
};
|
|
|
|
fd_dataset* fd_new(void) {
|
|
fd_dataset* ds = calloc(1, sizeof(fd_dataset));
|
|
if (!ds) return NULL;
|
|
|
|
ds->capacity = 16;
|
|
ds->samples = calloc(ds->capacity, sizeof(fd_sample*));
|
|
return ds;
|
|
}
|
|
|
|
void fd_free(fd_dataset* ds) {
|
|
if (!ds) return;
|
|
|
|
for (int i = 0; i < ds->num_samples; i++) {
|
|
fd_sample_free(ds->samples[i]);
|
|
}
|
|
free(ds->samples);
|
|
free(ds);
|
|
}
|
|
|
|
void fd_add_sample(fd_dataset* ds, fd_sample* sample) {
|
|
if (!ds || !sample) return;
|
|
|
|
if (ds->num_samples >= ds->capacity) {
|
|
ds->capacity *= 2;
|
|
ds->samples = realloc(ds->samples, ds->capacity * sizeof(fd_sample*));
|
|
}
|
|
|
|
ds->samples[ds->num_samples++] = sample;
|
|
}
|
|
|
|
int fd_len(fd_dataset* ds) {
|
|
return ds ? ds->num_samples : 0;
|
|
}
|
|
|
|
fd_sample* fd_get(fd_dataset* ds, int idx) {
|
|
if (!ds || idx < 0 || idx >= ds->num_samples) return NULL;
|
|
return ds->samples[idx];
|
|
}
|
|
|
|
void fd_shuffle(fd_dataset* ds) {
|
|
if (!ds || ds->num_samples <= 1) return;
|
|
|
|
/* Fisher-Yates shuffle */
|
|
for (int i = ds->num_samples - 1; i > 0; i--) {
|
|
int j = fd_rand() % (i + 1);
|
|
fd_sample* tmp = ds->samples[i];
|
|
ds->samples[i] = ds->samples[j];
|
|
ds->samples[j] = tmp;
|
|
}
|
|
}
|
|
|
|
void fd_split(fd_dataset* ds, float ratio, fd_dataset** train, fd_dataset** test) {
|
|
if (!ds || !train || !test || ratio <= 0.0f || ratio >= 1.0f) return;
|
|
|
|
*train = fd_new();
|
|
*test = fd_new();
|
|
|
|
int split_idx = (int)(ds->num_samples * ratio);
|
|
|
|
for (int i = 0; i < ds->num_samples; i++) {
|
|
fd_sample* clone = fd_sample_clone(ds->samples[i]);
|
|
if (i < split_idx) {
|
|
fd_add_sample(*train, clone);
|
|
} else {
|
|
fd_add_sample(*test, clone);
|
|
}
|
|
}
|
|
}
|
|
|
|
void fd_print(fd_dataset* ds) {
|
|
if (!ds) {
|
|
printf("(null dataset)\n");
|
|
return;
|
|
}
|
|
|
|
printf("Dataset: %d samples\n", ds->num_samples);
|
|
|
|
if (ds->num_samples > 0) {
|
|
printf("Fields: ");
|
|
fd_sample* s = ds->samples[0];
|
|
for (int i = 0; i < s->num_fields; i++) {
|
|
if (i > 0) printf(", ");
|
|
printf("%s", s->keys[i]);
|
|
}
|
|
printf("\n");
|
|
}
|
|
}
|
|
|
|
/* ============================================================= */
|
|
/* JSON Parsing (minimal) */
|
|
/* ============================================================= */
|
|
|
|
static const char* skip_ws(const char* p) {
|
|
while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r') p++;
|
|
return p;
|
|
}
|
|
|
|
static char* parse_json_string(const char** pp) {
|
|
const char* p = *pp;
|
|
if (*p != '"') return NULL;
|
|
p++;
|
|
|
|
/* Find end of string */
|
|
const char* start = p;
|
|
while (*p && *p != '"') {
|
|
if (*p == '\\' && *(p+1)) p++;
|
|
p++;
|
|
}
|
|
|
|
size_t len = p - start;
|
|
char* s = malloc(len + 1);
|
|
if (!s) return NULL;
|
|
|
|
/* Copy with escape handling */
|
|
size_t j = 0;
|
|
for (size_t i = 0; i < len; i++) {
|
|
if (start[i] == '\\' && i + 1 < len) {
|
|
i++;
|
|
switch (start[i]) {
|
|
case 'n': s[j++] = '\n'; break;
|
|
case 't': s[j++] = '\t'; break;
|
|
case 'r': s[j++] = '\r'; break;
|
|
case '"': s[j++] = '"'; break;
|
|
case '\\': s[j++] = '\\'; break;
|
|
default: s[j++] = start[i]; break;
|
|
}
|
|
} else {
|
|
s[j++] = start[i];
|
|
}
|
|
}
|
|
s[j] = '\0';
|
|
|
|
if (*p == '"') p++;
|
|
*pp = p;
|
|
return s;
|
|
}
|
|
|
|
/* Skip a JSON value */
|
|
static void skip_json_value(const char** pp) {
|
|
const char* p = *pp;
|
|
p = skip_ws(p);
|
|
|
|
if (*p == '"') {
|
|
p++;
|
|
while (*p && *p != '"') {
|
|
if (*p == '\\') p++;
|
|
if (*p) p++;
|
|
}
|
|
if (*p == '"') p++;
|
|
} else if (*p == '[') {
|
|
int depth = 1;
|
|
p++;
|
|
while (*p && depth > 0) {
|
|
if (*p == '[') depth++;
|
|
else if (*p == ']') depth--;
|
|
else if (*p == '"') {
|
|
p++;
|
|
while (*p && *p != '"') {
|
|
if (*p == '\\') p++;
|
|
if (*p) p++;
|
|
}
|
|
}
|
|
if (*p) p++;
|
|
}
|
|
} else if (*p == '{') {
|
|
int depth = 1;
|
|
p++;
|
|
while (*p && depth > 0) {
|
|
if (*p == '{') depth++;
|
|
else if (*p == '}') depth--;
|
|
else if (*p == '"') {
|
|
p++;
|
|
while (*p && *p != '"') {
|
|
if (*p == '\\') p++;
|
|
if (*p) p++;
|
|
}
|
|
}
|
|
if (*p) p++;
|
|
}
|
|
} else {
|
|
/* Number, bool, null */
|
|
while (*p && *p != ',' && *p != '}' && *p != ']' &&
|
|
*p != ' ' && *p != '\t' && *p != '\n' && *p != '\r') {
|
|
p++;
|
|
}
|
|
}
|
|
*pp = p;
|
|
}
|
|
|
|
/* Parse a JSON value as a string (for non-string values) */
|
|
static char* parse_json_value_as_string(const char** pp) {
|
|
const char* p = *pp;
|
|
p = skip_ws(p);
|
|
|
|
if (*p == '"') {
|
|
return parse_json_string(pp);
|
|
}
|
|
|
|
/* For non-string values, capture the raw text */
|
|
const char* start = p;
|
|
|
|
if (*p == '[' || *p == '{') {
|
|
/* Capture entire object/array as string */
|
|
skip_json_value(pp);
|
|
size_t len = *pp - start;
|
|
char* s = malloc(len + 1);
|
|
memcpy(s, start, len);
|
|
s[len] = '\0';
|
|
return s;
|
|
}
|
|
|
|
/* Number, bool, null */
|
|
while (*p && *p != ',' && *p != '}' && *p != ']' &&
|
|
*p != ' ' && *p != '\t' && *p != '\n' && *p != '\r') {
|
|
p++;
|
|
}
|
|
|
|
size_t len = p - start;
|
|
char* s = malloc(len + 1);
|
|
memcpy(s, start, len);
|
|
s[len] = '\0';
|
|
*pp = p;
|
|
return s;
|
|
}
|
|
|
|
static fd_sample* parse_json_object(const char* line) {
|
|
const char* p = line;
|
|
p = skip_ws(p);
|
|
|
|
if (*p != '{') return NULL;
|
|
p++;
|
|
|
|
fd_sample* s = fd_sample_new();
|
|
|
|
while (*p && *p != '}') {
|
|
p = skip_ws(p);
|
|
if (*p != '"') break;
|
|
|
|
char* key = parse_json_string(&p);
|
|
if (!key) break;
|
|
|
|
p = skip_ws(p);
|
|
if (*p == ':') p++;
|
|
p = skip_ws(p);
|
|
|
|
char* value = parse_json_value_as_string(&p);
|
|
|
|
if (key && value) {
|
|
fd_sample_set(s, key, value);
|
|
}
|
|
|
|
free(key);
|
|
free(value);
|
|
|
|
p = skip_ws(p);
|
|
if (*p == ',') p++;
|
|
}
|
|
|
|
return s;
|
|
}
|
|
|
|
fd_dataset* fd_load_jsonl(const char* path) {
|
|
if (!path) return NULL;
|
|
|
|
FILE* f = fopen(path, "r");
|
|
if (!f) return NULL;
|
|
|
|
fd_dataset* ds = fd_new();
|
|
if (!ds) {
|
|
fclose(f);
|
|
return NULL;
|
|
}
|
|
|
|
char* line = NULL;
|
|
size_t len = 0;
|
|
|
|
while (getline(&line, &len, f) != -1) {
|
|
/* Skip empty lines */
|
|
const char* p = line;
|
|
p = skip_ws(p);
|
|
if (*p == '\0' || *p == '\n') continue;
|
|
|
|
fd_sample* s = parse_json_object(line);
|
|
if (s && s->num_fields > 0) {
|
|
fd_add_sample(ds, s);
|
|
} else {
|
|
fd_sample_free(s);
|
|
}
|
|
}
|
|
|
|
free(line);
|
|
fclose(f);
|
|
return ds;
|
|
}
|
|
|
|
/* ============================================================= */
|
|
/* Data Loader */
|
|
/* ============================================================= */
|
|
|
|
struct fd_loader {
|
|
fd_dataset* ds;
|
|
int batch_size;
|
|
bool shuffle;
|
|
int current_idx;
|
|
int* indices; /* Shuffled indices */
|
|
};
|
|
|
|
fd_loader* fd_loader_new(fd_dataset* ds, int batch_size, bool shuffle) {
|
|
if (!ds || batch_size <= 0) return NULL;
|
|
|
|
fd_loader* loader = calloc(1, sizeof(fd_loader));
|
|
if (!loader) return NULL;
|
|
|
|
loader->ds = ds;
|
|
loader->batch_size = batch_size;
|
|
loader->shuffle = shuffle;
|
|
loader->current_idx = 0;
|
|
|
|
/* Create index array */
|
|
loader->indices = malloc(ds->num_samples * sizeof(int));
|
|
for (int i = 0; i < ds->num_samples; i++) {
|
|
loader->indices[i] = i;
|
|
}
|
|
|
|
/* Shuffle if requested */
|
|
if (shuffle && ds->num_samples > 1) {
|
|
for (int i = ds->num_samples - 1; i > 0; i--) {
|
|
int j = fd_rand() % (i + 1);
|
|
int tmp = loader->indices[i];
|
|
loader->indices[i] = loader->indices[j];
|
|
loader->indices[j] = tmp;
|
|
}
|
|
}
|
|
|
|
return loader;
|
|
}
|
|
|
|
void fd_loader_free(fd_loader* loader) {
|
|
if (!loader) return;
|
|
free(loader->indices);
|
|
free(loader);
|
|
}
|
|
|
|
void fd_loader_reset(fd_loader* loader) {
|
|
if (!loader) return;
|
|
|
|
loader->current_idx = 0;
|
|
|
|
/* Re-shuffle if shuffle is enabled */
|
|
if (loader->shuffle && loader->ds->num_samples > 1) {
|
|
for (int i = loader->ds->num_samples - 1; i > 0; i--) {
|
|
int j = fd_rand() % (i + 1);
|
|
int tmp = loader->indices[i];
|
|
loader->indices[i] = loader->indices[j];
|
|
loader->indices[j] = tmp;
|
|
}
|
|
}
|
|
}
|
|
|
|
fd_batch* fd_loader_next(fd_loader* loader) {
|
|
if (!loader) return NULL;
|
|
if (loader->current_idx >= loader->ds->num_samples) return NULL;
|
|
|
|
int remaining = loader->ds->num_samples - loader->current_idx;
|
|
int batch_size = (remaining < loader->batch_size) ? remaining : loader->batch_size;
|
|
|
|
fd_batch* batch = calloc(1, sizeof(fd_batch));
|
|
batch->samples = calloc(batch_size, sizeof(fd_sample*));
|
|
batch->size = batch_size;
|
|
|
|
for (int i = 0; i < batch_size; i++) {
|
|
int idx = loader->indices[loader->current_idx + i];
|
|
batch->samples[i] = loader->ds->samples[idx];
|
|
}
|
|
|
|
loader->current_idx += batch_size;
|
|
return batch;
|
|
}
|
|
|
|
void fd_batch_free(fd_batch* batch) {
|
|
if (!batch) return;
|
|
/* Note: we don't free the samples, they belong to the dataset */
|
|
free(batch->samples);
|
|
free(batch);
|
|
}
|
|
|
|
int fd_loader_num_batches(fd_loader* loader) {
|
|
if (!loader || loader->batch_size <= 0) return 0;
|
|
return (loader->ds->num_samples + loader->batch_size - 1) / loader->batch_size;
|
|
}
|
|
|
|
/* ============================================================= */
|
|
/* SQLite Chat History Loading */
|
|
/* ============================================================= */
|
|
|
|
#ifdef HAS_SQLITE
|
|
#include <sqlite3.h>
|
|
|
|
fd_dataset* fd_load_chat_sqlite(const char *db_path, int max_conversations) {
|
|
if (!db_path) return NULL;
|
|
|
|
sqlite3 *db = NULL;
|
|
if (sqlite3_open_v2(db_path, &db, SQLITE_OPEN_READONLY, NULL) != SQLITE_OK) {
|
|
if (db) sqlite3_close(db);
|
|
return NULL;
|
|
}
|
|
|
|
fd_dataset *ds = fd_new();
|
|
if (!ds) { sqlite3_close(db); return NULL; }
|
|
|
|
/* Query all conversations with their messages ordered by timestamp */
|
|
const char *sql =
|
|
"SELECT c.id, m.role, m.content "
|
|
"FROM messages m "
|
|
"JOIN conversations c ON m.conversation_id = c.id "
|
|
"WHERE m.role IN ('user', 'assistant') "
|
|
" AND m.content IS NOT NULL "
|
|
" AND length(m.content) > 0 "
|
|
"ORDER BY c.started_at, c.id, m.timestamp";
|
|
|
|
sqlite3_stmt *stmt = NULL;
|
|
if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) != SQLITE_OK) {
|
|
sqlite3_close(db);
|
|
fd_free(ds);
|
|
return NULL;
|
|
}
|
|
|
|
/* Accumulate messages per conversation, format as chat template */
|
|
char *current_conv_id = NULL;
|
|
char *text_buf = NULL;
|
|
size_t text_len = 0;
|
|
size_t text_cap = 0;
|
|
int msg_count = 0;
|
|
int conv_count = 0;
|
|
|
|
while (sqlite3_step(stmt) == SQLITE_ROW) {
|
|
const char *conv_id = (const char*)sqlite3_column_text(stmt, 0);
|
|
const char *role = (const char*)sqlite3_column_text(stmt, 1);
|
|
const char *content = (const char*)sqlite3_column_text(stmt, 2);
|
|
if (!conv_id || !role || !content) continue;
|
|
|
|
/* New conversation? Flush the previous one. */
|
|
if (!current_conv_id || strcmp(current_conv_id, conv_id) != 0) {
|
|
if (text_buf && msg_count >= 2) {
|
|
fd_sample *s = fd_sample_new();
|
|
fd_sample_set(s, "text", text_buf);
|
|
fd_add_sample(ds, s);
|
|
conv_count++;
|
|
if (max_conversations > 0 && conv_count >= max_conversations)
|
|
break;
|
|
}
|
|
free(current_conv_id);
|
|
current_conv_id = strdup(conv_id);
|
|
text_len = 0;
|
|
msg_count = 0;
|
|
if (!text_buf) { text_cap = 4096; text_buf = (char*)malloc(text_cap); }
|
|
text_buf[0] = '\0';
|
|
}
|
|
|
|
/* Format: <|role|>\ncontent\n */
|
|
size_t role_len = strlen(role);
|
|
size_t content_len = strlen(content);
|
|
size_t needed = text_len + 4 + role_len + 2 + content_len + 1 + 1;
|
|
if (needed > text_cap) {
|
|
text_cap = needed * 2;
|
|
text_buf = (char*)realloc(text_buf, text_cap);
|
|
}
|
|
text_len += snprintf(text_buf + text_len, text_cap - text_len,
|
|
"<|%s|>\n%s\n", role, content);
|
|
msg_count++;
|
|
}
|
|
|
|
/* Flush last conversation */
|
|
if (text_buf && msg_count >= 2 &&
|
|
(max_conversations <= 0 || conv_count < max_conversations)) {
|
|
fd_sample *s = fd_sample_new();
|
|
fd_sample_set(s, "text", text_buf);
|
|
fd_add_sample(ds, s);
|
|
}
|
|
|
|
free(current_conv_id);
|
|
free(text_buf);
|
|
sqlite3_finalize(stmt);
|
|
sqlite3_close(db);
|
|
return ds;
|
|
}
|
|
|
|
#else /* !HAS_SQLITE */
|
|
|
|
fd_dataset* fd_load_chat_sqlite(const char *db_path, int max_conversations) {
|
|
(void)db_path;
|
|
(void)max_conversations;
|
|
fprintf(stderr, "fd_load_chat_sqlite: compiled without sqlite3 support\n");
|
|
return NULL;
|
|
}
|
|
|
|
#endif /* HAS_SQLITE */
|