v0.8.6: general mode, clipboard text, PDF text extraction
Working mode: --mode coding|general (CST_MODE, settings.json, /mode, WebUI run settings). general swaps the default framing for a general assistant one -- workflow, safety notes, tool wording (run_shell, the subagents) -- and /status shows it; minimal (the paper profile) stays byte-frozen. The prompt state carries the mode so a switch leaves a system note in history. Clipboard: clipboard_read / clipboard_write tools (16 MiB cap read, default 64 KiB window; writing is a WRITE permission with a preview) on pbcopy / wl-copy / xclip / pbpaste / wl-paste / xclip and the Win32 clipboard. In the TUI, /attach paste falls back to clipboard text when there is no image on it. PDF: a dependency-free text extractor (src/pdf_text.c): inflate, Flate/ASCIIHex/ASCII85, PNG/TIFF predictors, layout-independent object scan, object streams, page tree walk with inherited resources, content lexer (Tj/TJ/'/" and the Td/TD/T*/Tm spacing heuristics), per-font ToUnicode CMaps and inline images skipped. Encrypted files and pages with no text layer are refused with the reason. Attachments: /attach (a path or files dropped on the terminal) now take images, PDFs and text. A PDF is stored as its text layer with the mime still saying application/pdf and its page count; anything else has to be UTF-8 text. Documents travel inline as text parts, so CST_NO_IMAGES and text-only models no longer drop them, while images keep their old path. Tests: test_pdf_text (plain, bare and zlib-framed Flate, inline image, CID + ToUnicode, object stream, error paths), documents in test_attach, inlined documents in test_llm_body, mode in test_config.
This commit is contained in:
+90
-23
@@ -255,28 +255,78 @@ static cJSON *image_part(const cst_llm_request_t *req,
|
||||
return p;
|
||||
}
|
||||
|
||||
/* Append every attachment of m as image parts (or a note saying why not). */
|
||||
static void parts_add_images(const cst_llm_request_t *req, cJSON *parts,
|
||||
const cst_message_t *m, size_t *budget) {
|
||||
/* One stored document as a text part: its text, framed so the model knows
|
||||
* where it came from. Documents are plain text on the wire, so a text-only
|
||||
* model and CST_NO_IMAGES both still get them. */
|
||||
static cJSON *document_part(const cst_llm_request_t *req,
|
||||
const cst_attachment_t *a, size_t *budget,
|
||||
char *why, size_t why_sz) {
|
||||
why[0] = 0;
|
||||
if (!req->session_path || !req->session_path[0]) {
|
||||
snprintf(why, why_sz, "there is no session store to read it from");
|
||||
return NULL;
|
||||
}
|
||||
char err[256];
|
||||
size_t n = 0;
|
||||
char *text = cst_attach_read(req->session_path, a, &n, err, sizeof(err));
|
||||
if (!text) {
|
||||
snprintf(why, why_sz, "%s", err);
|
||||
return NULL;
|
||||
}
|
||||
int cut = 0;
|
||||
if (n > *budget) {
|
||||
n = *budget;
|
||||
cut = 1;
|
||||
}
|
||||
*budget -= n;
|
||||
char desc[128];
|
||||
cst_attach_describe(a, desc, sizeof(desc));
|
||||
cst_buf_t b = {0};
|
||||
cst_buf_printf(&b, "[document \"%.120s\" (%s)%s]\n", a->name, desc,
|
||||
cut ? " -- only the start is included" : "");
|
||||
cst_buf_append(&b, text, n);
|
||||
cst_buf_puts(&b, cut ? "\n[-- document truncated --]"
|
||||
: "\n[-- end of document --]");
|
||||
free(text);
|
||||
cJSON *p = cJSON_CreateObject();
|
||||
cJSON_AddStringToObject(p, "type", "text");
|
||||
cJSON_AddStringToObject(p, "text", b.buf ? b.buf : "");
|
||||
cst_buf_free(&b);
|
||||
return p;
|
||||
}
|
||||
|
||||
/* Append every attachment of m: images as image parts (or a note saying why
|
||||
* not), documents inline as text. */
|
||||
static void parts_add_attachments(const cst_llm_request_t *req, cJSON *parts,
|
||||
const cst_message_t *m, size_t *budget,
|
||||
size_t *doc_budget, const char *img_off) {
|
||||
int n = m->nattach;
|
||||
if (n > CST_ATTACH_MAX_COUNT) {
|
||||
char note[160];
|
||||
snprintf(note, sizeof(note), "[%d of %d images sent]", CST_ATTACH_MAX_COUNT,
|
||||
n);
|
||||
snprintf(note, sizeof(note),
|
||||
"[only the first %d of %d attachments are sent]",
|
||||
CST_ATTACH_MAX_COUNT, n);
|
||||
parts_add_text(parts, cJSON_CreateString(note));
|
||||
n = CST_ATTACH_MAX_COUNT;
|
||||
}
|
||||
for (int i = 0; i < n; i++) {
|
||||
const cst_attachment_t *a = &m->attach[i];
|
||||
int is_img = cst_attach_is_image(a);
|
||||
char why[256];
|
||||
cJSON *p = image_part(req, a, budget, why, sizeof(why));
|
||||
cJSON *p = NULL;
|
||||
if (is_img && img_off)
|
||||
snprintf(why, sizeof(why), "%s", img_off);
|
||||
else if (is_img)
|
||||
p = image_part(req, a, budget, why, sizeof(why));
|
||||
else
|
||||
p = document_part(req, a, doc_budget, why, sizeof(why));
|
||||
if (p) {
|
||||
cJSON_AddItemToArray(parts, p);
|
||||
continue;
|
||||
}
|
||||
char note[512];
|
||||
snprintf(note, sizeof(note), "[image \"%.120s\" not sent: %.200s]",
|
||||
a->name, why);
|
||||
snprintf(note, sizeof(note), "[%s \"%.120s\" not sent: %.200s]",
|
||||
is_img ? "image" : "document", a->name, why);
|
||||
parts_add_text(parts, cJSON_CreateString(note));
|
||||
}
|
||||
}
|
||||
@@ -284,8 +334,10 @@ static void parts_add_images(const cst_llm_request_t *req, cJSON *parts,
|
||||
/* Images a tool result produced are deferred: an assistant tool_call must be
|
||||
* answered by tool messages only, so they ride in one extra user message
|
||||
* after the run of tool messages ends. *pending collects it. */
|
||||
static void defer_images(const cst_llm_request_t *req, const cst_message_t *m,
|
||||
cJSON **pending, size_t *budget) {
|
||||
static void defer_attachments(const cst_llm_request_t *req,
|
||||
const cst_message_t *m, cJSON **pending,
|
||||
size_t *budget, size_t *doc_budget,
|
||||
const char *img_off) {
|
||||
cJSON *p = *pending;
|
||||
if (!p) {
|
||||
p = cJSON_CreateObject();
|
||||
@@ -293,15 +345,18 @@ static void defer_images(const cst_llm_request_t *req, const cst_message_t *m,
|
||||
cJSON_AddArrayToObject(p, "content");
|
||||
*pending = p;
|
||||
}
|
||||
parts_add_images(req, cJSON_GetObjectItemCaseSensitive(p, "content"), m,
|
||||
budget);
|
||||
parts_add_attachments(req, cJSON_GetObjectItemCaseSensitive(p, "content"), m,
|
||||
budget, doc_budget, img_off);
|
||||
}
|
||||
|
||||
/* m's text plus one line per image it would have carried (malloc'd). */
|
||||
/* m's text plus one line per image it would have carried (malloc'd); a
|
||||
* document is not affected (it is text and is sent as such). */
|
||||
static char *image_notes(const cst_message_t *m, const char *why) {
|
||||
cst_buf_t b = {0};
|
||||
cst_buf_puts(&b, m->content ? m->content : "");
|
||||
for (int i = 0; i < m->nattach; i++) {
|
||||
if (!cst_attach_is_image(&m->attach[i]))
|
||||
continue;
|
||||
char d[128];
|
||||
cst_attach_describe(&m->attach[i], d, sizeof(d));
|
||||
cst_buf_printf(&b, "%s[image \"%.120s\" (%s) not sent: %s]",
|
||||
@@ -394,6 +449,7 @@ char *cst_llm_build_body(const cst_llm_request_t *req) {
|
||||
cJSON *prev_user = NULL; /* merge consecutive user messages */
|
||||
cJSON *pending = NULL; /* images from tool results, waiting for a break */
|
||||
size_t img_budget = CST_ATTACH_MAX_TOTAL;
|
||||
size_t doc_budget = CST_ATTACH_MAX_TOTAL;
|
||||
const char *noimg = getenv("CST_NO_IMAGES");
|
||||
const char *img_off =
|
||||
noimg && noimg[0] && noimg[0] != '0'
|
||||
@@ -407,15 +463,26 @@ char *cst_llm_build_body(const cst_llm_request_t *req) {
|
||||
cst_role_t role = m->role;
|
||||
if (role == CST_ROLE_TOOL && !m->tool_call_id[0])
|
||||
role = CST_ROLE_USER;
|
||||
int has_img = m->attach && m->nattach > 0;
|
||||
int n_att = m->attach ? m->nattach : 0;
|
||||
int has_img = 0, has_doc = 0;
|
||||
for (int k = 0; k < n_att; k++) {
|
||||
if (cst_attach_is_image(&m->attach[k]))
|
||||
has_img = 1;
|
||||
else
|
||||
has_doc = 1;
|
||||
}
|
||||
char *noted = NULL;
|
||||
if (has_img && img_off) {
|
||||
/* plain text: text-only APIs may refuse content arrays */
|
||||
noted = image_notes(m, img_off);
|
||||
if (noted)
|
||||
mm.content = noted;
|
||||
has_img = 0;
|
||||
if (!has_doc) {
|
||||
/* plain text: text-only APIs may refuse content arrays */
|
||||
noted = image_notes(m, img_off);
|
||||
if (noted)
|
||||
mm.content = noted;
|
||||
}
|
||||
}
|
||||
/* a document needs a content array even when no image can go out */
|
||||
int att_parts = has_img || has_doc;
|
||||
|
||||
/* Every tool_call must be answered by tool messages, so tool-produced
|
||||
* images only leave the queue when the run of tool results is over. */
|
||||
@@ -431,7 +498,7 @@ char *cst_llm_build_body(const cst_llm_request_t *req) {
|
||||
cJSON *pc = prev_user
|
||||
? cJSON_GetObjectItemCaseSensitive(prev_user, "content")
|
||||
: NULL;
|
||||
if (!has_img && !cJSON_IsArray(pc)) {
|
||||
if (!att_parts && !cJSON_IsArray(pc)) {
|
||||
cJSON *c = clean_string(m->content, 0);
|
||||
if (prev_user) {
|
||||
cst_buf_t b = {0};
|
||||
@@ -463,8 +530,8 @@ char *cst_llm_build_body(const cst_llm_request_t *req) {
|
||||
}
|
||||
cJSON *parts = content_parts(o);
|
||||
parts_add_text(parts, clean_string(m->content, 0));
|
||||
if (has_img)
|
||||
parts_add_images(req, parts, m, &img_budget);
|
||||
if (att_parts)
|
||||
parts_add_attachments(req, parts, m, &img_budget, &doc_budget, img_off);
|
||||
free(noted);
|
||||
continue;
|
||||
}
|
||||
@@ -498,8 +565,8 @@ char *cst_llm_build_body(const cst_llm_request_t *req) {
|
||||
cJSON_AddTrueToObject(o, "prefix");
|
||||
}
|
||||
cJSON_AddItemToArray(msgs, o);
|
||||
if (role == CST_ROLE_TOOL && has_img)
|
||||
defer_images(req, m, &pending, &img_budget);
|
||||
if (role == CST_ROLE_TOOL && n_att)
|
||||
defer_attachments(req, m, &pending, &img_budget, &doc_budget, img_off);
|
||||
free(noted);
|
||||
}
|
||||
if (pending)
|
||||
|
||||
Reference in New Issue
Block a user