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:
@@ -406,9 +406,118 @@ static void test_images_on(void) {
|
||||
cst_attach_free(img);
|
||||
}
|
||||
|
||||
/* Every text part of message i, joined (malloc'd, "" when there is none). */
|
||||
static char *text_of(const cJSON *root, int i) {
|
||||
const cJSON *m =
|
||||
cJSON_GetArrayItem(cJSON_GetObjectItemCaseSensitive(root, "messages"), i);
|
||||
const cJSON *c = cJSON_GetObjectItemCaseSensitive(m, "content");
|
||||
if (cJSON_IsString(c)) {
|
||||
size_t n = strlen(c->valuestring);
|
||||
char *out = malloc(n + 1);
|
||||
if (out)
|
||||
memcpy(out, c->valuestring, n + 1);
|
||||
return out;
|
||||
}
|
||||
size_t len = 1;
|
||||
const cJSON *p, *t, *s;
|
||||
cJSON_ArrayForEach(p, c) {
|
||||
t = cJSON_GetObjectItemCaseSensitive(p, "type");
|
||||
s = cJSON_GetObjectItemCaseSensitive(p, "text");
|
||||
if (cJSON_IsString(t) && strcmp(t->valuestring, "text") == 0 &&
|
||||
cJSON_IsString(s))
|
||||
len += strlen(s->valuestring) + 1;
|
||||
}
|
||||
char *out = calloc(1, len);
|
||||
if (!out)
|
||||
return NULL;
|
||||
cJSON_ArrayForEach(p, c) {
|
||||
t = cJSON_GetObjectItemCaseSensitive(p, "type");
|
||||
s = cJSON_GetObjectItemCaseSensitive(p, "text");
|
||||
if (cJSON_IsString(t) && strcmp(t->valuestring, "text") == 0 &&
|
||||
cJSON_IsString(s)) {
|
||||
strcat(out, s->valuestring);
|
||||
strcat(out, "\n");
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/* A document is inlined as text, so a text-only model and a message that
|
||||
* also carries an image both keep it; the image is what becomes a note. */
|
||||
static void test_document_parts(void) {
|
||||
char dir[1024], sess[1100], err[256], src[1200];
|
||||
if (test_tmpdir(dir, sizeof(dir), "cst-body-doc") != 0) {
|
||||
EXPECT(0, "scratch dir");
|
||||
return;
|
||||
}
|
||||
snprintf(sess, sizeof(sess), "%s/s.jsonl", dir);
|
||||
snprintf(src, sizeof(src), "%s/paper.pdf", dir);
|
||||
static const char PDF[] =
|
||||
"%PDF-1.4\n"
|
||||
"1 0 obj\n<< /Type /Catalog /Pages 2 0 R >>\nendobj\n"
|
||||
"2 0 obj\n<< /Type /Pages /Kids [3 0 R] /Count 1 >>\nendobj\n"
|
||||
"3 0 obj\n<< /Type /Page /Parent 2 0 R /Resources << /Font << /F1 4 0 R "
|
||||
">> >> /Contents 5 0 R >>\nendobj\n"
|
||||
"4 0 obj\n<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>\n"
|
||||
"endobj\n"
|
||||
"5 0 obj\n<< >>\nstream\n"
|
||||
"BT /F1 12 Tf 72 720 Td (Inlined paper text) Tj ET\n"
|
||||
"endstream\nendobj\n"
|
||||
"trailer\n<< /Root 1 0 R >>\n%%EOF\n";
|
||||
FILE *f = fopen(src, "wb");
|
||||
EXPECT(f != NULL, "write the PDF");
|
||||
if (!f)
|
||||
return;
|
||||
fwrite(PDF, 1, strlen(PDF), f);
|
||||
fclose(f);
|
||||
cst_attachment_t *doc = cst_attach_import_file(sess, src, err, sizeof(err));
|
||||
EXPECT(doc != NULL, "PDF stored");
|
||||
if (!doc)
|
||||
return;
|
||||
static const unsigned char png[32] = {0x89, 'P', 'N', 'G', '\r', '\n', 0x1A,
|
||||
'\n', 0, 0, 0, 13, 'I', 'H', 'D', 'R',
|
||||
0, 0, 0, 2, 0, 0, 0, 2};
|
||||
cst_attachment_t *img = cst_attach_import_bytes(
|
||||
sess, "shot.png", (const char *)png, sizeof(png), err, sizeof(err));
|
||||
EXPECT(img != NULL, "image stored");
|
||||
if (!img) {
|
||||
cst_attach_free(doc);
|
||||
return;
|
||||
}
|
||||
cst_attachment_t both[2] = {*doc, *img};
|
||||
cst_message_t m[2];
|
||||
m[0] = msg(CST_ROLE_USER, "what does this say?");
|
||||
m[0].attach = both;
|
||||
m[0].nattach = 2;
|
||||
m[1] = msg(CST_ROLE_USER, "thanks");
|
||||
cst_llm_request_t r;
|
||||
memset(&r, 0, sizeof(r));
|
||||
r.model = "deepseek-flash";
|
||||
r.msgs = m;
|
||||
r.nmsgs = 2;
|
||||
r.images_off = 1; /* a text-only model */
|
||||
r.session_path = sess;
|
||||
cJSON *j = build_req(&r);
|
||||
char *txt = text_of(j, 0);
|
||||
EXPECT(txt && strstr(txt, "[document \"paper.pdf\"") &&
|
||||
strstr(txt, "Inlined paper text") &&
|
||||
strstr(txt, "[-- end of document --]"),
|
||||
"the PDF text is inlined even when images are off");
|
||||
EXPECT(txt && strstr(txt, "[image \"shot.png\"") && strstr(txt, "not sent"),
|
||||
"the image beside it is still only a note");
|
||||
free(txt);
|
||||
char *s = cJSON_PrintUnformatted(j);
|
||||
EXPECT(s && !strstr(s, "image_url"), "no image part with images off");
|
||||
free(s);
|
||||
cJSON_Delete(j);
|
||||
cst_attach_free(doc);
|
||||
cst_attach_free(img);
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
test_images_off();
|
||||
test_images_on();
|
||||
test_document_parts();
|
||||
test_reasoning_and_prefix();
|
||||
test_cache_hints();
|
||||
test_tools_snapshot();
|
||||
|
||||
Reference in New Issue
Block a user