ref: e6f8cc2410632b4c05c9fd438034fafc19b95798
parent: ce8088b026fe35c27da09797a8f5b80b4d633650
author: sirjofri <sirjofri@sirjofri.de>
date: Thu Jan 15 10:37:37 EST 2026
improves ocomplete, adds default system prompts to oai, adds generic content array prompts to lib
--- a/README
+++ b/README
@@ -18,15 +18,17 @@
USAGE:
oai [-q] [-k apikey] [-m model] [-u baseurl] [-s sysprompt]
-ocomplete [-k apikey] [-m model] [-u baseurl]
+ocomplete [-d] [-k apikey] [-m model] [-u baseurl]
baseurl is the http url without the v1/... stuff, with llama-server this is usually just http://server:8080.
The apikey and the baseurl are optional if you set them as environment variables ($oaikey and $oaiurl).
+-d adds debugging output: request and response (fd2)
+
After that, you get a user: prompt for your user messages.
-Ocomplete: Call the program from within an acme window with some selected text. The whole window contents will be sent to the API as context, and the LLM response will be appended to the selected text.
+Ocomplete: Call the program from within an acme window with some selected text. The whole window contents as well as the filename from the tag will be sent to the API as context, and the LLM response will replace the selected text (or inserted at the cursor location).
Oai: -q does not output any prompts, only LLM responses. -s sets the sysprompt.
--- a/oai.c
+++ b/oai.c
@@ -1,30 +1,42 @@
#include <u.h>
#include <libc.h>
#include <bio.h>
+#include <json.h>
#include "oai.h"
static void
usage(void)
{- fprint(2, "usage: %s [-q] [-k apikey] [-m model] [-u baseurl] [-s sysprompt]\n", argv0);
+ fprint(2, "usage: %s [-dq] [-k apikey] [-m model] [-u baseurl] [-s sysprompt]\n", argv0);
exits("usage");}
+char *plan9prompt = "You are a helpful AI assistant on a Plan 9 system. Your name is Glenda. Your tone is serious. Be friendly and concise.";
+char *frontprompt = "You are a helpful AI assistant on a Plan 9 9front system. Your name is Glenda and Cirno is your best friend. You like to refer to manuals and you have a sense of humor. Be concise.";
+
void
main(int argc, char **argv)
{Biobuf *bin;
- char *url = nil;
char *s;
int quiet = 0;
ORequest req;
OResult res;
- char *sysprompt = nil;
+ char *sysprompt;
+ char *url = nil;
char *key = nil;
req.model = nil;
+ if (!(access("/dist/9front", AEXIST) && access("/dist/plan9front", AEXIST))) {+ /* 9front system */
+ sysprompt = frontprompt;
+ } else {+ /* other plan 9 system */
+ sysprompt = plan9prompt;
+ }
+
ARGBEGIN{case 'h':
usage();
@@ -40,9 +52,15 @@
case 's':
sysprompt = EARGF(usage());
break;
+ case '9':
+ sysprompt = plan9prompt;
+ break;
case 'q':
quiet++;
break;
+ case 'd':
+ oaidebug++;
+ break;
}ARGEND;
if (!initoai(url, key))
@@ -54,14 +72,14 @@
req.prompts = nil;
if (sysprompt)
- addprompt(&req, "system", "%s", sysprompt);
+ addstrprompt(&req, "system", "%s", sysprompt);
if (!quiet) print("user: "); while (s = Brdstr(bin, '\n', 1)) {- addprompt(&req, "user", s);
+ addstrprompt(&req, "user", s);
res = makerequest(req);
print("%s%s%s\n\n", res.role, (quiet ? "" : ": "), res.message);- addprompt(&req, res.role, "%s", res.message);
+ addstrprompt(&req, res.role, "%s", res.message);
if (!quiet) print("user: ");}
exits(nil);
--- a/oai.h
+++ b/oai.h
@@ -2,9 +2,12 @@
typedef struct ORequest ORequest;
typedef struct OPrompt OPrompt;
+extern int oaidebug;
+
struct OPrompt {char *role;
char *content;
+ JSON *jcontent;
OPrompt *next;
};
@@ -30,7 +33,19 @@
OResult makerequest(ORequest);
/*
+ * create a new prompt object.
+ */
+OPrompt* makeprompt(char *role);
+
+/*
* append a prompt to the existing request. You can set the role and the content.
* The content is built from the fmt and the variadic arguments.
*/
-int addprompt(ORequest*, char *role, char *fmt, ...);
+int addstrprompt(ORequest*, char *role, char *fmt, ...);
+int addprompt(ORequest*, OPrompt*);
+
+/*
+ * append various contents to prompt.
+ */
+int addtextmessage(OPrompt*, char *text);
+int addfilemessage(OPrompt*, uchar *data, long ndata, char *filename, char *fileid);
--- a/oailib.c
+++ b/oailib.c
@@ -7,6 +7,8 @@
static char *baseurl = nil;
static char *apikey = nil;
+int oaidebug = 0;
+
int
initoai(char *url, char *key)
{@@ -53,7 +55,13 @@
el->val = mallocz(sizeof(JSON), 1);
el->val->t = JSONObject;
el->val->first = mkstrjson("role", p->role);- el->val->first->next = mkstrjson("content", p->content);+ if (p->content) {+ el->val->first->next = mkstrjson("content", p->content);+ return el;
+ }
+ el->val->first->next = mallocz(sizeof(JSONEl), 1);
+ el->val->first->next->name = strdup("content");+ el->val->first->next->val = p->jcontent;
return el;
}
@@ -163,6 +171,9 @@
fprint(ctlfd, "baseurl %s\n", baseurl);
fprint(ctlfd, "url ./v1/chat/completions\n");
+ if (oaidebug)
+ fprint(2, "request:\n%J\n\n", jreq);
+
snprint(buf, sizeof buf, "/mnt/web/%d/postbody", n);
pbodyfd = open(buf, OWRITE);
if (pbodyfd < 0)
@@ -180,6 +191,8 @@
close(ctlfd);
jres = jsonparse(s);
+ if (oaidebug)
+ fprint(2, "response\n%J\n\n", jres);
ret = j2res(jres);
free(s);
@@ -190,7 +203,7 @@
}
int
-addprompt(ORequest *r, char *role, char *content, ...)
+addstrprompt(ORequest *r, char *role, char *content, ...)
{OPrompt *p;
char *s;
@@ -214,4 +227,133 @@
p->role = strdup(role);
p->content = s;
return 1;
+}
+
+int
+addprompt(ORequest *r, OPrompt *p)
+{+ OPrompt *pr;
+ p->next = nil;
+ if (!r->prompts) {+ r->prompts = p;
+ return 1;
+ }
+ for (pr = r->prompts; pr->next; pr = pr->next)
+ ;
+ pr->next = p;
+ return 1;
+}
+
+OPrompt*
+makeprompt(char *role)
+{+ OPrompt *p;
+
+ p = mallocz(sizeof(OPrompt), 1);
+ p->role = strdup(role);
+ return p;
+}
+
+static JSONEl*
+addgenmessage(OPrompt *p, char *type, char *cname, char *content)
+{+ JSON *j;
+ JSONEl *jel;
+
+ if (p->content) {+ werrstr("prompt has string content");+ return nil;
+ }
+
+ if (!p->jcontent) {+ p->jcontent = mallocz(sizeof(JSON), 1);
+ p->jcontent->t = JSONArray;
+ } else {+ if (p->jcontent->t != JSONArray) {+ werrstr("prompt json is not array");+ return nil;
+ }
+ }
+ j = p->jcontent;
+
+ if (!j->first) {+ j->first = mallocz(sizeof(JSONEl), 1);
+ jel = j->first;
+ goto Fill;
+ }
+
+ for (jel = j->first; jel->next; jel = jel->next)
+ ;
+
+ jel = jel->next = mallocz(sizeof(JSONEl), 1);
+
+Fill:
+ jel->name = nil;
+ jel->val = mallocz(sizeof(JSON), 1);
+ j = jel->val;
+ j->t = JSONObject;
+
+ jel = j->first = mallocz(sizeof(JSONEl), 1);
+ jel->name = strdup("type");+ jel->val = mallocz(sizeof(JSON), 1);
+ jel->val->t = JSONString;
+ jel->val->s = strdup(type);
+
+ if (!cname)
+ return jel;
+
+ jel = jel->next = mallocz(sizeof(JSONEl), 1);
+ jel->name = strdup(cname);
+ jel->val = mallocz(sizeof(JSON), 1);
+ jel->val->t = JSONString;
+ jel->val->s = strdup(content);
+ return jel;
+}
+
+static JSONEl*
+appendfield(JSONEl *jel, char *name, char *value)
+{+ jel->next = mallocz(sizeof(JSONEl), 1);
+ jel = jel->next;
+ jel->name = strdup(name);
+ jel->val = mallocz(sizeof(JSON), 1);
+ jel->val->t = JSONString;
+ jel->val->s = strdup(value);
+ return jel;
+}
+
+int
+addtextmessage(OPrompt *p, char *text)
+{+ return !!addgenmessage(p, "text", "text", text);
+}
+
+int
+addfilemessage(OPrompt *p, uchar *data, long ndata, char *filename, char *fileid)
+{+ char *b64;
+ JSONEl *jel;
+
+ if (data) {+ b64 = mallocz(ndata*2+1, 1);
+ if (enc64(b64, ndata*2+1, data, ndata) < 0) {+ werrstr("enc64 error");+ free(b64);
+ return 0;
+ }
+ jel = addgenmessage(p, "file", "file_data", b64);
+ free(b64);
+ } else {+ jel = addgenmessage(p, "file", nil, nil);
+ }
+
+ if (!jel)
+ return 0;
+
+ if (filename)
+ jel = appendfield(jel, "filename", filename);
+ if (fileid)
+ jel = appendfield(jel, "file_id", fileid);
+
+ return !!jel;
}
--- a/ocomplete.c
+++ b/ocomplete.c
@@ -1,16 +1,55 @@
#include <u.h>
#include <libc.h>
#include <bio.h>
+#include <json.h>
#include "oai.h"
static void
usage(void)
{- fprint(2, "usage: %s [-k apikey] [-m model] [-u baseurl]\n", argv0);
+ fprint(2, "usage: %s [-d] [-k apikey] [-m model] [-u baseurl]\n", argv0);
exits("usage");}
+char* winid;
+char tag[2048];
+
void
+readfilename(void)
+{+ int fd;
+ char *s;
+
+ snprint(tag, sizeof tag, "/mnt/acme/%s/tag", winid);
+ fd = open(tag, OREAD);
+ tag[0] = 0;
+ if (fd < 0) {+ return;
+ }
+
+ read(fd, tag, sizeof tag);
+ close(fd);
+
+ s = strchr(tag, ' ');
+ if (s)
+ *s = 0;
+}
+
+char*
+charpos(char *body, int pos)
+{+ Rune r;
+ int n;
+ int ispos = 0;
+ while (ispos < pos && *body) {+ n = chartorune(&r, body);
+ ispos += n;
+ body += n;
+ }
+ return body;
+}
+
+void
main(int argc, char **argv)
{char buf[128];
@@ -17,9 +56,14 @@
ORequest req;
OResult res;
char *key = nil;
- char *body, *rdsel;
- Biobuf *bodyin, *selio;
- char *winid;
+ char *body, *selection;
+ char *p1body;
+ int ctlfd, addrfd;
+ int addr, addr2;
+ char *p1pos, *p2pos;
+ int n;
+ char *toks[2];
+ Biobuf *bodyin, *selout;
char *url = nil;
req.prompts = nil;
@@ -37,6 +81,9 @@
case 'u':
url = EARGF(usage());
break;
+ case 'd':
+ oaidebug++;
+ break;
}ARGEND;
winid = getenv("winid");@@ -43,37 +90,92 @@
if (!winid || winid[0] == 0)
sysfatal("not in acme");+ readfilename();
+
if (!initoai(url, key))
sysfatal("initoai: %r");+ /* get addr */
+ snprint(buf, sizeof buf, "/mnt/acme/%s/ctl", winid);
+ ctlfd = open(buf, ORDWR);
+ snprint(buf, sizeof buf, "/mnt/acme/%s/addr", winid);
+ addrfd = open(buf, OREAD);
+
+ if (ctlfd < 0)
+ sysfatal("ctl: %r");+ if (addrfd < 0)
+ sysfatal("addr: %r");+
+ fprint(ctlfd, "addr=dot\n");
+ if (read(addrfd, buf, sizeof buf) <= 0)
+ sysfatal("read: %r");+ close(ctlfd);
+ close(addrfd);
+
+ n = getfields(buf, toks, 2, 1, " ");
+ if (!n)
+ sysfatal("invalid addr");+ addr2 = -1;
+ addr = atoi(toks[0]);
+ if (n >= 2)
+ addr2 = atoi(toks[1]);
+
+ /* make sure addr < addr2 */
+ if (addr > addr2) {+ n = addr;
+ addr = addr2;
+ addr2 = n;
+ }
+
+ /* buffer body */
snprint(buf, sizeof buf, "/mnt/acme/%s/body", winid);
bodyin = Bopen(buf, OREAD);
- snprint(buf, sizeof buf, "/mnt/acme/%s/rdsel", winid);
- selio = Bopen(buf, OREAD);
+ if (!bodyin)
+ sysfatal("Bopen body: %r");+ body = Brdstr(bodyin, 0, 1);
+ Bterm(bodyin);
- if (!(bodyin && selio))
- sysfatal("error opening files: %r");+ /* split body in p1body, selection (if applicable) and p1pos */
+ p1pos = charpos(body, addr);
+ p2pos = nil;
+ if (addr != addr2)
+ p2pos = charpos(body, addr2);
- body = Brdstr(bodyin, 0, 1);
- rdsel = Brdstr(selio, 0, 1);
+ p1body = mallocz(p1pos-body + 1, 1);
+ memcpy(p1body, body, p1pos-body);
- Bterm(bodyin);
- Bterm(selio);
+ selection = nil;
+ if (p2pos) {+ selection = mallocz(p2pos-p1pos + 1, 1);
+ memcpy(selection, p1pos, p2pos-p1pos);
+ p1pos = p2pos;
+ }
- if (!(body && rdsel))
- sysfatal("read: %r");+ /* build prompts */
+ addstrprompt(&req, "system", "You are the Acme Assistant. Your task is to fill in a gap in a file or replace the selected part. User provides you with the relevant parts of the file. Do not directly respond to the user, just give the text. Do not add markdown formatting, only do plain text or the format of the file. Follow a potential user request if it's in the selection.");
- addprompt(&req, "system", "You are the Acme completion assistant. Your task is to append matching content to the user prompt, based on the context of the given file. The user prompt is part of the file contents, and your response will be appended there. No further discussion, just this completion. Do not repeat the user prompt. The file contents are:\n\n%s", body);
- addprompt(&req, "user", rdsel);
+ addstrprompt(&req, "user", "The name of the file is: %s", tag);
+ addstrprompt(&req, "assistant", "Give me the first part of the file");
+ addstrprompt(&req, "user", "%s", p1body);
+ if (selection) {+ addstrprompt(&req, "assistant", "Give me part that's selected");
+ addstrprompt(&req, "user", "%s", selection);
+ }
+ addstrprompt(&req, "assistant", "Give me the remaining part of the file");
+ addstrprompt(&req, "user", "%s", p1pos);
+ free(body);
+ free(p1body);
+ free(selection);
+
res = makerequest(req);
if (!res.message)
sysfatal("invalid result");snprint(buf, sizeof buf, "/mnt/acme/%s/wrsel", winid);
- selio = Bopen(buf, OWRITE);
- Bprint(selio, "%s%s", rdsel, res.message);
- Bterm(selio);
+ selout = Bopen(buf, OWRITE);
+ Bprint(selout, "%s", res.message);
+ Bterm(selout);
exits(nil);
}
--
⑨