shithub: oai

Download patch

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 */
--