ref: 1f68d6a68ba2b4512d963ea2815280cabe707f53
parent: e6f8cc2410632b4c05c9fd438034fafc19b95798
author: sirjofri <sirjofri@sirjofri.de>
date: Sun Feb 22 18:42:03 EST 2026
adds first working tool calling
--- a/oai.c
+++ b/oai.c
@@ -11,6 +11,24 @@
exits("usage");}
+OTool *tools;
+
+static void
+inittools(void)
+{+ tools = maketool(nil, Function, "list_files", "list all files in the current directory", nil);
+}
+
+static char*
+toolcall(OToolcall tc)
+{+ fprint(2, "toolcall!\n");
+ fprint(2, " name: %s\n", tc.name);
+ fprint(2, " args: %s\n", tc.arguments);
+ fprint(2, " id: %s\n", tc.id);
+ return strdup("a b c");+}
+
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.";
@@ -63,13 +81,16 @@
break;
}ARGEND;
- if (!initoai(url, key))
+ if (!initoai(url, key, toolcall))
usage();
bin = Bfdopen(0, OREAD);
assert(bin);
+ inittools();
+
req.prompts = nil;
+ req.tools = tools;
if (sysprompt)
addstrprompt(&req, "system", "%s", sysprompt);
@@ -78,6 +99,10 @@
while (s = Brdstr(bin, '\n', 1)) {addstrprompt(&req, "user", s);
res = makerequest(req);
+ if (!res.success) {+ fprint(2, "exiting!\n");
+ exits("fail");+ }
print("%s%s%s\n\n", res.role, (quiet ? "" : ": "), res.message);addstrprompt(&req, res.role, "%s", res.message);
if (!quiet) print("user: ");--- a/oai.h
+++ b/oai.h
@@ -1,13 +1,24 @@
typedef struct OResult OResult;
typedef struct ORequest ORequest;
typedef struct OPrompt OPrompt;
+typedef struct OTool OTool;
+typedef struct OToolcall OToolcall;
+typedef char* (*OToolcallfn)(OToolcall);
+
+typedef enum {+ Function,
+ Custom,
+} Tooltype;
+
extern int oaidebug;
struct OPrompt {char *role;
char *content;
+ char *jcname;
JSON *jcontent;
+ char *callid;
OPrompt *next;
};
@@ -14,18 +25,36 @@
struct ORequest {char *model;
OPrompt *prompts;
+ OTool *tools;
};
struct OResult {+ int success;
char *role;
char *message;
};
+struct OTool {+ Tooltype type;
+ char *name;
+ char *description;
+ char *parameters;
+ OTool *next;
+};
+
+struct OToolcall {+ Tooltype type;
+ char *name;
+ char *arguments;
+ char *id;
+ OToolcall *next;
+};
+
/*
* initoai returns 1 on success. If baseurl or apikey is nil, it'll try to fetch the data
* from the environment variables $oaiurl and $oaikey. Key is optional.
*/
-int initoai(char *baseurl, char *apikey);
+int initoai(char *baseurl, char *apikey, char* (*f)(OToolcall));
/*
* makerequest to make the request.
@@ -49,3 +78,8 @@
*/
int addtextmessage(OPrompt*, char *text);
int addfilemessage(OPrompt*, uchar *data, long ndata, char *filename, char *fileid);
+
+/*
+ * append tool
+ */
+OTool* maketool(OTool*, Tooltype, char *name, char *description, char *parameters);
--- a/oailib.c
+++ b/oailib.c
@@ -7,12 +7,14 @@
static char *baseurl = nil;
static char *apikey = nil;
+static OToolcallfn toolcall = nil;
+
int oaidebug = 0;
int
-initoai(char *url, char *key)
+initoai(char *url, char *key, OToolcallfn fn)
{- if (!url)
+ if (!url)
url = getenv("oaiurl"); if (!url) { werrstr("invalid baseurl");@@ -19,6 +21,7 @@
return 0;
}
baseurl = url;
+ toolcall = fn;
if (!key)
key = getenv("oaikey");@@ -50,18 +53,80 @@
prompt2json(OPrompt *p)
{JSONEl *el;
+ JSONEl *top;
- el = mallocz(sizeof(JSONEl), 1);
+ el = top = mallocz(sizeof(JSONEl), 1);
el->val = mallocz(sizeof(JSON), 1);
el->val->t = JSONObject;
- el->val->first = mkstrjson("role", p->role);+ el = el->val->first = mkstrjson("role", p->role);+ if (p->callid) {+ el = el->next = mkstrjson("tool_call_id", p->callid);+ }
if (p->content) {- el->val->first->next = mkstrjson("content", p->content);- return el;
+ el = el->next = mkstrjson("content", p->content);+ return top;
}
- el->val->first->next = mallocz(sizeof(JSONEl), 1);
- el->val->first->next->name = strdup("content");- el->val->first->next->val = p->jcontent;
+ el = el->next = mallocz(sizeof(JSONEl), 1);
+ el->name = p->jcname ? strdup(p->jcname) : strdup("content");+ el->val = p->jcontent;
+ return top;
+}
+
+static char*
+tooltype(Tooltype t)
+{+ switch (t) {+ case Function:
+ return "function";
+ case Custom:
+ return "custom";
+ }
+ fprint(2, "invalid tool type: %d\n", t);
+ return "";
+}
+
+static void
+func2json(OTool *t, JSONEl *el)
+{+ el->next = mallocz(sizeof(JSONEl), 1);
+ assert(el->next);
+ el = el->next;
+ el->name = strdup("function");+ el->val = mallocz(sizeof(JSON), 1);
+ assert(el->val);
+ el->val->t = JSONObject;
+ el->val->first = mkstrjson("name", t->name);+ el->val->first->next = mkstrjson("description", t->description);+ if (t->parameters)
+ el->val->first->next->next = mkstrjson("parameters", t->parameters);+}
+
+static void
+custom2json(OTool *t, JSONEl *el)
+{+ el->next = mkstrjson("name", t->name);+ el = el->next;
+ el->next = mkstrjson("description", t->description);+}
+
+static JSONEl*
+tool2json(OTool *t)
+{+ JSONEl *el;
+
+ el = mallocz(sizeof(JSONEl), 1);
+ el->val = mallocz(sizeof(JSON), 1);
+ el->val->t = JSONObject;
+ el->val->first = mkstrjson("type", tooltype(t->type));+ switch (t->type) {+ case Function:
+ func2json(t, el->val->first);
+ break;
+ case Custom:
+ custom2json(t, el->val->first);
+ break;
+ }
+
return el;
}
@@ -69,8 +134,9 @@
req2json(ORequest *req)
{JSON *j;
- JSONEl *el;
+ JSONEl *el, *tel;
OPrompt *p;
+ OTool *t;
j = mallocz(sizeof(JSON), 1);
assert(j);
@@ -85,6 +151,7 @@
assert(el->val);
el->val->t = JSONArray;
j->first = el;
+ tel = el;
for (p = req->prompts; p; p = p->next) { if (!el->val->first) {@@ -96,13 +163,62 @@
el = el->next;
}
+ el = mallocz(sizeof(JSONEl), 1);
+ assert(el);
+ el->name = strdup("tools");+ assert(el->name);
+ el->val = mallocz(sizeof(JSON), 1);
+ assert(el->val);
+ el->val->t = JSONArray;
+ tel->next = el;
+ tel = tel->next;
+
+ for (t = req->tools; t; t = t->next) {+ if (!el->val->first) {+ el->val->first = tool2json(t);
+ el = el->val->first;
+ continue;
+ }
+ el->next = tool2json(t);
+ el = el->next;
+ }
+
if (req->model) {- j->first->next = mkstrjson("model", req->model);+ tel->next = mkstrjson("model", req->model);}
return j;
}
+static JSON*
+getfirstchoice(JSON *j)
+{+ JSON *choices;
+
+ choices = jsonbyname(j, "choices");
+ if (!choices)
+ sysfatal("no choices");+ if (choices->t != JSONArray)
+ sysfatal("choices is not an array");+ if (!(choices->first && choices->first->val))
+ sysfatal("no first choices");+
+ return choices->first->val;
+}
+
+static JSON*
+getmessage(JSON *j)
+{+ JSON *message;
+
+ message = jsonbyname(j, "message");
+ if (!message)
+ sysfatal("missing message");+ if (message->t != JSONObject)
+ sysfatal("message is not an object");+ return message;
+}
+
static OResult
j2res(JSON *j)
{@@ -142,6 +258,167 @@
return r;
}
+static void
+addtcprompt(OPrompt *pr, OToolcall *tc)
+{+ JSONEl *jel;
+ JSON *j;
+
+ if (!pr->jcontent) {+ pr->jcontent = mallocz(sizeof(JSON), 1);
+ pr->jcontent->t = JSONArray;
+ pr->jcontent->first = jel = mallocz(sizeof(JSONEl), 1);
+ } else {+ for (jel = pr->jcontent->first; jel->next; jel = jel->next)
+ ;
+ jel->next = mallocz(sizeof(JSONEl), 1);
+ jel = jel->next;
+ }
+
+ jel->val = mallocz(sizeof(JSON), 1);
+ j = jel->val;
+ j->t = JSONObject;
+ jel = j->first = mkstrjson("id", tc->id);+ jel = jel->next = mkstrjson("type", tooltype(tc->type));+ jel = jel->next = mallocz(sizeof(JSONEl), 1);
+ jel->name = strdup("function");+ j = jel->val = mallocz(sizeof(JSON), 1);
+ j->t = JSONObject;
+ jel = j->first = mkstrjson("name", tc->name);+ jel = jel->next = mkstrjson("arguments", tc->arguments);+ USED(jel);
+}
+
+enum {+ Fstop,
+ Ftoolcall,
+};
+
+static int
+getfinishreason(JSON *jres)
+{+ JSON *first;
+ JSON *finish;
+ char *s;
+
+ first = getfirstchoice(jres);
+
+ finish = jsonbyname(first, "finish_reason");
+ if (!finish)
+ sysfatal("no finish reason");+ if (finish->t != JSONString)
+ sysfatal("finish reason not a string");+
+ s = jsonstr(finish);
+ if (!s)
+ sysfatal("invalid finish reason");+
+ if (oaidebug)
+ fprint(2, "finish reason: %s\n", s);
+
+ if (strcmp(s, "stop") == 0)
+ return Fstop;
+ if (strcmp(s, "tool_calls") == 0)
+ return Ftoolcall;
+
+ if (oaidebug)
+ fprint(2, "unknown finish_reason\n");
+ return -1;
+}
+
+static void
+tcparse(JSON *j, OToolcall *tc)
+{+ JSON *type, *name, *args, *id;
+ JSON *func;
+ char *s;
+
+ type = jsonbyname(j, "type");
+ if (!type)
+ sysfatal("tool_call without type");+ s = jsonstr(type);
+ if (!s)
+ sysfatal("missing tool_call type");+ if (strcmp(s, "function") == 0)
+ tc->type = Function;
+ else if (strcmp(s, "custom") == 0)
+ tc->type = Custom;
+ else
+ sysfatal("invalid tool_call type: %s", s);+
+ if (tc->type != Function)
+ sysfatal("tool_call type %s not implemented!", s);+
+ id = jsonbyname(j, "id");
+ if (!id)
+ sysfatal("missing tool_call id");+ if (id->t != JSONString)
+ sysfatal("tool_call id not a string");+ tc->id = strdup(jsonstr(id));
+
+ func = jsonbyname(j, "function");
+ if (!func)
+ sysfatal("missing tool_call function");+ if (func->t != JSONObject)
+ sysfatal("tool_call function wrong type");+
+ name = jsonbyname(func, "name");
+ if (!name)
+ sysfatal("tool_call without name");+ if (name->t != JSONString)
+ sysfatal("tool_call name not a string");+ tc->name = strdup(jsonstr(name));
+
+ args = jsonbyname(func, "arguments");
+ if (!args)
+ sysfatal("tool_call without arguments");+ if (args->t != JSONString)
+ sysfatal("tool_call arguments not a string");+ tc->arguments = strdup(jsonstr(args));
+}
+
+static OResult
+calltool(JSON *j, ORequest *req)
+{+ JSON *first;
+ JSON *calls;
+ JSONEl *el;
+ OToolcall tc;
+ OResult res;
+ OPrompt *pr, *pres;
+ char *r;
+
+ if (!toolcall)
+ sysfatal("tool calls not supported by application!");+
+ first = getfirstchoice(j);
+ first = getmessage(first);
+ calls = jsonbyname(first, "tool_calls");
+ if (!calls)
+ sysfatal("tool call with missing tool_calls");+ if (calls->t != JSONArray)
+ sysfatal("tool_calls is not an array");+
+ pr = makeprompt("assistant");+ pr->jcname = strdup("tool_calls");+ addprompt(req, pr);
+
+ for (el = calls->first; el; el = el->next) {+ tcparse(el->val, &tc);
+ r = toolcall(tc);
+ if (!r)
+ continue;
+ addtcprompt(pr, &tc);
+ pres = makeprompt("tool");+ pres->content = r;
+ pres->callid = strdup(tc.id);
+ addprompt(req, pres);
+ }
+
+ res = makerequest(*req);
+ return res;
+}
+
OResult
makerequest(ORequest req)
{@@ -193,6 +470,20 @@
jres = jsonparse(s);
if (oaidebug)
fprint(2, "response\n%J\n\n", jres);
+
+ switch (getfinishreason(jres)) {+ default:
+ ret.success = 0;
+ return ret;
+ case Fstop:
+ break;
+ case Ftoolcall:
+ ret.success = 0;
+ if (toolcall)
+ ret = calltool(jres, &req);
+ return ret;
+ }
+
ret = j2res(jres);
free(s);
@@ -356,4 +647,27 @@
jel = appendfield(jel, "file_id", fileid);
return !!jel;
+}
+
+OTool*
+maketool(OTool *tool, Tooltype type, char *name, char *description, char *parameters)
+{+ OTool *t, *n;
+
+ t = mallocz(sizeof(OTool), 1);
+ assert(t);
+
+ t->type = type;
+ t->name = strdup(name);
+ t->description = strdup(description);
+ if (parameters && parameters[0])
+ t->parameters = strdup(parameters);
+
+ if (!tool)
+ return t;
+
+ for (n = tool; n->next; n = n->next)
+ ;
+ n->next = t;
+ return t;
}
--- a/ocomplete.c
+++ b/ocomplete.c
@@ -92,7 +92,7 @@
readfilename();
- if (!initoai(url, key))
+ if (!initoai(url, key, nil))
sysfatal("initoai: %r");/* get addr */
--
⑨