1681 lines
58 KiB
C
1681 lines
58 KiB
C
#define _POSIX_C_SOURCE 200809L
|
|
|
|
#include "http_api.h"
|
|
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <stdint.h>
|
|
#include <time.h>
|
|
|
|
#include "agent.h"
|
|
#include "llm.h"
|
|
#include "main.h"
|
|
#include "nostr_handler.h"
|
|
#include "trigger_manager.h"
|
|
#include "tools/tools.h"
|
|
#include "cjson/cJSON.h"
|
|
#include "debug.h"
|
|
#include "mongoose.h"
|
|
|
|
static http_api_context_t g_api_ctx;
|
|
static int g_api_initialized = 0;
|
|
static struct mg_mgr g_mgr;
|
|
static struct mg_connection* g_listener = NULL;
|
|
static int g_api_tls_enabled = 0;
|
|
static struct mg_str g_tls_cert = {NULL, 0};
|
|
static struct mg_str g_tls_key = {NULL, 0};
|
|
|
|
static const char* k_json_headers =
|
|
"Content-Type: application/json\r\n"
|
|
"Access-Control-Allow-Origin: *\r\n"
|
|
"Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS\r\n"
|
|
"Access-Control-Allow-Headers: Content-Type\r\n"
|
|
"Access-Control-Allow-Private-Network: true\r\n";
|
|
|
|
static int method_is(const struct mg_http_message* hm, const char* method) {
|
|
size_t n = strlen(method);
|
|
return hm && hm->method.len == n && strncmp(hm->method.buf, method, n) == 0;
|
|
}
|
|
|
|
static void reply_json(struct mg_connection* c, int status, cJSON* root) {
|
|
char* out = cJSON_PrintUnformatted(root);
|
|
if (!out) {
|
|
mg_http_reply(c, 500, k_json_headers, "{\"success\":false,\"error\":\"json serialization failed\"}");
|
|
return;
|
|
}
|
|
mg_http_reply(c, status, k_json_headers, "%s", out);
|
|
free(out);
|
|
}
|
|
|
|
static void reply_error(struct mg_connection* c, int status, const char* err) {
|
|
cJSON* root = cJSON_CreateObject();
|
|
if (!root) {
|
|
mg_http_reply(c, 500, k_json_headers, "{\"success\":false,\"error\":\"oom\"}");
|
|
return;
|
|
}
|
|
cJSON_AddBoolToObject(root, "success", 0);
|
|
cJSON_AddStringToObject(root, "error", err ? err : "error");
|
|
reply_json(c, status, root);
|
|
cJSON_Delete(root);
|
|
}
|
|
|
|
static cJSON* parse_body_json(const struct mg_http_message* hm) {
|
|
if (!hm || hm->body.len == 0) {
|
|
return cJSON_CreateObject();
|
|
}
|
|
|
|
char* buf = (char*)malloc(hm->body.len + 1U);
|
|
if (!buf) {
|
|
return NULL;
|
|
}
|
|
memcpy(buf, hm->body.buf, hm->body.len);
|
|
buf[hm->body.len] = '\0';
|
|
|
|
cJSON* root = cJSON_Parse(buf);
|
|
free(buf);
|
|
return root;
|
|
}
|
|
|
|
static int estimate_tokens_from_chars(int chars) {
|
|
if (chars <= 0) return 0;
|
|
return (chars + 3) / 4;
|
|
}
|
|
|
|
static uint64_t fnv1a64_local(const char* s) {
|
|
uint64_t h = 1469598103934665603ULL;
|
|
if (!s) return h;
|
|
while (*s) {
|
|
h ^= (unsigned char)(*s++);
|
|
h *= 1099511628211ULL;
|
|
}
|
|
return h;
|
|
}
|
|
|
|
static uint64_t tool_calls_fingerprint_local(const llm_response_t* resp) {
|
|
if (!resp || resp->tool_call_count <= 0 || !resp->tool_calls) {
|
|
return 1469598103934665603ULL;
|
|
}
|
|
|
|
uint64_t h = 1469598103934665603ULL;
|
|
for (int i = 0; i < resp->tool_call_count; i++) {
|
|
const llm_tool_call_t* tc = &resp->tool_calls[i];
|
|
uint64_t name_h = fnv1a64_local(tc->name ? tc->name : "");
|
|
uint64_t args_h = fnv1a64_local(tc->arguments_json ? tc->arguments_json : "{}");
|
|
uint64_t part = name_h ^ (args_h + 0x9e3779b97f4a7c15ULL + (name_h << 6) + (name_h >> 2));
|
|
h ^= part + (uint64_t)(i + 1) * 1099511628211ULL;
|
|
h *= 1099511628211ULL;
|
|
}
|
|
|
|
h ^= (uint64_t)resp->tool_call_count;
|
|
h *= 1099511628211ULL;
|
|
return h;
|
|
}
|
|
|
|
static void notify_admin_limit_diagnostic_http(const char* source,
|
|
const char* reason,
|
|
const char* subject,
|
|
int max_turns,
|
|
int turns_run,
|
|
int stall_repeat_threshold,
|
|
int repeated_tool_turns,
|
|
int current_max_tokens,
|
|
const char* possible_cause,
|
|
const char* hint,
|
|
const char* final_answer) {
|
|
if (!g_api_ctx.cfg || g_api_ctx.cfg->admin.pubkey[0] == '\0') {
|
|
return;
|
|
}
|
|
|
|
char diag[1800];
|
|
snprintf(diag,
|
|
sizeof(diag),
|
|
"⚠️ Didactyl limit diagnostic\n"
|
|
"source=%s\n"
|
|
"reason=%s\n"
|
|
"subject=%s\n"
|
|
"max_turns=%d\n"
|
|
"turns_run=%d\n"
|
|
"stall_repeat_threshold=%d\n"
|
|
"repeated_tool_turns=%d\n"
|
|
"current_max_tokens=%d\n"
|
|
"possible_cause=%s\n"
|
|
"hint=%s\n"
|
|
"final_answer_preview=%.*s",
|
|
source ? source : "unknown",
|
|
reason ? reason : "unknown",
|
|
subject ? subject : "n/a",
|
|
max_turns,
|
|
turns_run,
|
|
stall_repeat_threshold,
|
|
repeated_tool_turns,
|
|
current_max_tokens,
|
|
possible_cause ? possible_cause : "n/a",
|
|
hint ? hint : "n/a",
|
|
360,
|
|
final_answer ? final_answer : "");
|
|
|
|
(void)nostr_handler_send_dm_auto(g_api_ctx.cfg->admin.pubkey, diag);
|
|
}
|
|
|
|
static int uri_extract_after_prefix(const struct mg_str* uri,
|
|
const char* prefix,
|
|
char* out,
|
|
size_t out_sz) {
|
|
if (!uri || !prefix || !out || out_sz == 0) {
|
|
return -1;
|
|
}
|
|
out[0] = '\0';
|
|
|
|
size_t prefix_len = strlen(prefix);
|
|
if (uri->len <= prefix_len || strncmp(uri->buf, prefix, prefix_len) != 0) {
|
|
return -1;
|
|
}
|
|
|
|
size_t rem = uri->len - prefix_len;
|
|
if (rem == 0 || rem >= out_sz) {
|
|
return -1;
|
|
}
|
|
|
|
memcpy(out, uri->buf + prefix_len, rem);
|
|
out[rem] = '\0';
|
|
return 0;
|
|
}
|
|
|
|
|
|
static void handle_trigger_webhook(struct mg_connection* c,
|
|
const struct mg_http_message* hm,
|
|
const char* d_tag) {
|
|
if (!g_api_ctx.trigger_manager) {
|
|
reply_error(c, 503, "trigger manager unavailable");
|
|
return;
|
|
}
|
|
|
|
active_trigger_t trigger;
|
|
if (trigger_manager_find(g_api_ctx.trigger_manager, d_tag, &trigger) != 0) {
|
|
reply_error(c, 404, "active trigger not found for d_tag");
|
|
return;
|
|
}
|
|
|
|
if (trigger.trigger_type != TRIGGER_TYPE_WEBHOOK) {
|
|
reply_error(c, 400, "trigger type is not webhook");
|
|
return;
|
|
}
|
|
|
|
if (!trigger.enabled) {
|
|
reply_error(c, 400, "webhook trigger is disabled");
|
|
return;
|
|
}
|
|
|
|
cJSON* payload = parse_body_json(hm);
|
|
if (!payload || !cJSON_IsObject(payload)) {
|
|
cJSON_Delete(payload);
|
|
reply_error(c, 400, "invalid JSON body");
|
|
return;
|
|
}
|
|
|
|
cJSON* event = cJSON_CreateObject();
|
|
if (!event) {
|
|
cJSON_Delete(payload);
|
|
reply_error(c, 500, "oom");
|
|
return;
|
|
}
|
|
|
|
cJSON_AddStringToObject(event, "type", "webhook");
|
|
cJSON_AddStringToObject(event, "skill_d_tag", d_tag);
|
|
cJSON_AddNumberToObject(event, "created_at", (double)time(NULL));
|
|
cJSON_AddItemToObject(event, "payload", payload);
|
|
|
|
int fired = trigger_manager_fire(g_api_ctx.trigger_manager, d_tag, event, "webhook");
|
|
|
|
cJSON_Delete(event);
|
|
|
|
if (fired < 0) {
|
|
reply_error(c, 404, "active trigger not found for d_tag");
|
|
return;
|
|
}
|
|
|
|
cJSON* out = cJSON_CreateObject();
|
|
if (!out) {
|
|
reply_error(c, 500, "oom");
|
|
return;
|
|
}
|
|
|
|
cJSON_AddBoolToObject(out, "success", 1);
|
|
cJSON_AddStringToObject(out, "d_tag", d_tag);
|
|
cJSON_AddBoolToObject(out, "fired", fired > 0 ? 1 : 0);
|
|
reply_json(c, 200, out);
|
|
cJSON_Delete(out);
|
|
}
|
|
|
|
static char* maybe_model_override_begin(cJSON* body, llm_config_t* out_old_cfg, int* out_overridden) {
|
|
if (!body || !out_old_cfg || !out_overridden) return NULL;
|
|
*out_overridden = 0;
|
|
|
|
cJSON* model = cJSON_GetObjectItemCaseSensitive(body, "model");
|
|
if (!model || !cJSON_IsString(model) || !model->valuestring || model->valuestring[0] == '\0') {
|
|
return NULL;
|
|
}
|
|
|
|
if (llm_get_config(out_old_cfg) != 0) {
|
|
return "failed to read current llm config";
|
|
}
|
|
|
|
llm_config_t next = *out_old_cfg;
|
|
snprintf(next.model, sizeof(next.model), "%s", model->valuestring);
|
|
if (llm_set_config(&next) != 0) {
|
|
return "failed to set temporary model";
|
|
}
|
|
|
|
*out_overridden = 1;
|
|
return NULL;
|
|
}
|
|
|
|
static void maybe_model_override_end(const llm_config_t* old_cfg, int overridden) {
|
|
if (overridden && old_cfg) {
|
|
(void)llm_set_config(old_cfg);
|
|
}
|
|
}
|
|
|
|
static const char* classify_part_name(cJSON* msg, int idx) {
|
|
return agent_classify_message_part(msg, idx);
|
|
}
|
|
|
|
static int build_context_parts_response(cJSON* out_root) {
|
|
if (!out_root) return -1;
|
|
|
|
char* messages_json = NULL;
|
|
if (agent_build_admin_messages_json("", DIDACTYL_SENDER_ADMIN, &messages_json) != 0 || !messages_json) {
|
|
return -1;
|
|
}
|
|
|
|
cJSON* arr = cJSON_Parse(messages_json);
|
|
free(messages_json);
|
|
if (!arr || !cJSON_IsArray(arr)) {
|
|
cJSON_Delete(arr);
|
|
return -1;
|
|
}
|
|
|
|
cJSON* parts = cJSON_CreateArray();
|
|
if (!parts) {
|
|
cJSON_Delete(arr);
|
|
return -1;
|
|
}
|
|
|
|
int total_chars = 0;
|
|
int n = cJSON_GetArraySize(arr);
|
|
for (int i = 0; i < n; i++) {
|
|
cJSON* msg = cJSON_GetArrayItem(arr, i);
|
|
if (!msg || !cJSON_IsObject(msg)) continue;
|
|
|
|
cJSON* role = cJSON_GetObjectItemCaseSensitive(msg, "role");
|
|
cJSON* content = cJSON_GetObjectItemCaseSensitive(msg, "content");
|
|
const char* role_s = (role && cJSON_IsString(role) && role->valuestring) ? role->valuestring : "unknown";
|
|
const char* content_s = (content && cJSON_IsString(content) && content->valuestring) ? content->valuestring : "";
|
|
|
|
int chars = (int)strlen(content_s);
|
|
total_chars += chars;
|
|
|
|
cJSON* part = cJSON_CreateObject();
|
|
if (!part) continue;
|
|
cJSON_AddStringToObject(part, "name", classify_part_name(msg, i));
|
|
cJSON_AddStringToObject(part, "role", role_s);
|
|
cJSON_AddNumberToObject(part, "chars", chars);
|
|
cJSON_AddNumberToObject(part, "estimated_tokens", estimate_tokens_from_chars(chars));
|
|
cJSON_AddStringToObject(part, "content", content_s);
|
|
cJSON_AddItemToArray(parts, part);
|
|
}
|
|
|
|
cJSON_AddBoolToObject(out_root, "success", 1);
|
|
cJSON_AddNumberToObject(out_root, "total_chars", total_chars);
|
|
cJSON_AddNumberToObject(out_root, "total_estimated_tokens", estimate_tokens_from_chars(total_chars));
|
|
cJSON_AddItemToObject(out_root, "parts", parts);
|
|
|
|
cJSON_AddItemToObject(out_root, "messages", arr);
|
|
return 0;
|
|
}
|
|
|
|
static int append_simple_message_local(cJSON* messages, const char* role, const char* content) {
|
|
if (!messages || !role) return -1;
|
|
cJSON* msg = cJSON_CreateObject();
|
|
if (!msg) return -1;
|
|
cJSON_AddStringToObject(msg, "role", role);
|
|
cJSON_AddStringToObject(msg, "content", content ? content : "");
|
|
cJSON_AddItemToArray(messages, msg);
|
|
return 0;
|
|
}
|
|
|
|
static int append_assistant_tool_calls_message_local(cJSON* messages, const llm_response_t* resp) {
|
|
if (!messages || !resp || resp->tool_call_count <= 0) return -1;
|
|
|
|
cJSON* assistant = cJSON_CreateObject();
|
|
cJSON* tool_calls = cJSON_CreateArray();
|
|
if (!assistant || !tool_calls) {
|
|
cJSON_Delete(assistant);
|
|
cJSON_Delete(tool_calls);
|
|
return -1;
|
|
}
|
|
|
|
cJSON_AddStringToObject(assistant, "role", "assistant");
|
|
if (resp->content) cJSON_AddStringToObject(assistant, "content", resp->content);
|
|
else cJSON_AddNullToObject(assistant, "content");
|
|
|
|
for (int i = 0; i < resp->tool_call_count; i++) {
|
|
const llm_tool_call_t* tc = &resp->tool_calls[i];
|
|
cJSON* tc_obj = cJSON_CreateObject();
|
|
cJSON* fn_obj = cJSON_CreateObject();
|
|
if (!tc_obj || !fn_obj) {
|
|
cJSON_Delete(tc_obj);
|
|
cJSON_Delete(fn_obj);
|
|
cJSON_Delete(assistant);
|
|
cJSON_Delete(tool_calls);
|
|
return -1;
|
|
}
|
|
|
|
cJSON_AddStringToObject(tc_obj, "id", tc->id ? tc->id : "");
|
|
cJSON_AddStringToObject(tc_obj, "type", "function");
|
|
cJSON_AddStringToObject(fn_obj, "name", tc->name ? tc->name : "");
|
|
cJSON_AddStringToObject(fn_obj, "arguments", tc->arguments_json ? tc->arguments_json : "{}");
|
|
cJSON_AddItemToObject(tc_obj, "function", fn_obj);
|
|
cJSON_AddItemToArray(tool_calls, tc_obj);
|
|
}
|
|
|
|
cJSON_AddItemToObject(assistant, "tool_calls", tool_calls);
|
|
cJSON_AddItemToArray(messages, assistant);
|
|
return 0;
|
|
}
|
|
|
|
static int append_tool_result_message_local(cJSON* messages, const char* tool_call_id, const char* tool_result_json) {
|
|
if (!messages || !tool_call_id) return -1;
|
|
|
|
cJSON* msg = cJSON_CreateObject();
|
|
if (!msg) return -1;
|
|
|
|
cJSON_AddStringToObject(msg, "role", "tool");
|
|
cJSON_AddStringToObject(msg, "tool_call_id", tool_call_id);
|
|
cJSON_AddStringToObject(msg,
|
|
"content",
|
|
tool_result_json ? tool_result_json : "{\"success\":false,\"error\":\"empty tool result\"}");
|
|
cJSON_AddItemToArray(messages, msg);
|
|
return 0;
|
|
}
|
|
|
|
static int maybe_append_loaded_skill_instructions_local(cJSON* messages,
|
|
const char* tool_name,
|
|
const char* tool_result_json) {
|
|
if (!messages || !tool_name || strcmp(tool_name, "skill_load") != 0 || !tool_result_json) {
|
|
return 0;
|
|
}
|
|
|
|
cJSON* root = cJSON_Parse(tool_result_json);
|
|
if (!root || !cJSON_IsObject(root)) {
|
|
cJSON_Delete(root);
|
|
return 0;
|
|
}
|
|
|
|
cJSON* ok = cJSON_GetObjectItemCaseSensitive(root, "success");
|
|
cJSON* type = cJSON_GetObjectItemCaseSensitive(root, "type");
|
|
cJSON* instructions = cJSON_GetObjectItemCaseSensitive(root, "instructions");
|
|
|
|
int should_apply = (ok && cJSON_IsBool(ok) && cJSON_IsTrue(ok) &&
|
|
type && cJSON_IsString(type) && type->valuestring &&
|
|
strcmp(type->valuestring, "skill_instructions") == 0 &&
|
|
instructions && cJSON_IsString(instructions) && instructions->valuestring &&
|
|
instructions->valuestring[0] != '\0');
|
|
|
|
int rc = 0;
|
|
if (should_apply) {
|
|
rc = append_simple_message_local(messages, "system", instructions->valuestring);
|
|
}
|
|
|
|
cJSON_Delete(root);
|
|
return rc;
|
|
}
|
|
|
|
static int is_space_char_local(char c) {
|
|
return c == ' ' || c == '\t' || c == '\n' || c == '\r';
|
|
}
|
|
|
|
static const char* skip_spaces_local(const char* s) {
|
|
while (s && *s && is_space_char_local(*s)) s++;
|
|
return s ? s : "";
|
|
}
|
|
|
|
static char* json_error_string_local(const char* msg) {
|
|
cJSON* out = cJSON_CreateObject();
|
|
if (!out) return NULL;
|
|
cJSON_AddBoolToObject(out, "success", 0);
|
|
cJSON_AddStringToObject(out, "error", msg ? msg : "error");
|
|
char* json = cJSON_PrintUnformatted(out);
|
|
cJSON_Delete(out);
|
|
return json;
|
|
}
|
|
|
|
static char* build_slash_args_json_local(const char* raw_args) {
|
|
const char* s = skip_spaces_local(raw_args);
|
|
if (!s || s[0] == '\0') {
|
|
return strdup("{}");
|
|
}
|
|
|
|
cJSON* parsed = cJSON_Parse(s);
|
|
if (parsed) {
|
|
char* out = cJSON_PrintUnformatted(parsed);
|
|
cJSON_Delete(parsed);
|
|
return out;
|
|
}
|
|
|
|
cJSON* obj = cJSON_CreateObject();
|
|
if (!obj) return NULL;
|
|
cJSON_AddStringToObject(obj, "input", s);
|
|
char* out = cJSON_PrintUnformatted(obj);
|
|
cJSON_Delete(obj);
|
|
return out;
|
|
}
|
|
|
|
static char* build_run_args_json_local(const char* raw_args) {
|
|
const char* s = skip_spaces_local(raw_args);
|
|
if (!s || s[0] == '\0') {
|
|
return json_error_string_local("/run requires a skill identifier");
|
|
}
|
|
|
|
const char* ident_start = s;
|
|
while (*s && !is_space_char_local(*s)) s++;
|
|
|
|
size_t ident_len = (size_t)(s - ident_start);
|
|
if (ident_len == 0 || ident_len >= 256U) {
|
|
return json_error_string_local("/run skill identifier is invalid");
|
|
}
|
|
|
|
char ident[256];
|
|
memcpy(ident, ident_start, ident_len);
|
|
ident[ident_len] = '\0';
|
|
|
|
const char* remaining = skip_spaces_local(s);
|
|
|
|
cJSON* out = cJSON_CreateObject();
|
|
if (!out) return NULL;
|
|
|
|
const char* c1 = strchr(ident, ':');
|
|
const char* c2 = c1 ? strchr(c1 + 1, ':') : NULL;
|
|
if (c1 && c2 && c1 != ident && (size_t)(c2 - c1) == 65U && c2[1] != '\0') {
|
|
size_t pubkey_len = (size_t)(c2 - (c1 + 1));
|
|
char pubkey[65];
|
|
memcpy(pubkey, c1 + 1, pubkey_len);
|
|
pubkey[pubkey_len] = '\0';
|
|
cJSON_AddStringToObject(out, "pubkey", pubkey);
|
|
cJSON_AddStringToObject(out, "d_tag", c2 + 1);
|
|
} else {
|
|
cJSON_AddStringToObject(out, "d_tag", ident);
|
|
}
|
|
|
|
if (remaining && remaining[0] != '\0') {
|
|
cJSON_AddStringToObject(out, "args", remaining);
|
|
}
|
|
|
|
char* json = cJSON_PrintUnformatted(out);
|
|
cJSON_Delete(out);
|
|
return json;
|
|
}
|
|
|
|
static int append_textf_http(char** buf, size_t* cap, size_t* used, const char* fmt, ...) {
|
|
if (!buf || !cap || !used || !fmt) return -1;
|
|
|
|
while (1) {
|
|
if (*used + 128U >= *cap) {
|
|
size_t next_cap = (*cap) * 2U;
|
|
char* grown = (char*)realloc(*buf, next_cap);
|
|
if (!grown) return -1;
|
|
*buf = grown;
|
|
*cap = next_cap;
|
|
}
|
|
|
|
va_list ap;
|
|
va_start(ap, fmt);
|
|
int n = vsnprintf(*buf + *used, *cap - *used, fmt, ap);
|
|
va_end(ap);
|
|
|
|
if (n < 0) return -1;
|
|
if ((size_t)n < (*cap - *used)) {
|
|
*used += (size_t)n;
|
|
return 0;
|
|
}
|
|
|
|
size_t next_cap = (*cap) * 2U + (size_t)n + 64U;
|
|
char* grown = (char*)realloc(*buf, next_cap);
|
|
if (!grown) return -1;
|
|
*buf = grown;
|
|
*cap = next_cap;
|
|
}
|
|
}
|
|
|
|
static int append_markdown_indent_http(char** buf, size_t* cap, size_t* used, int depth) {
|
|
for (int i = 0; i < depth; i++) {
|
|
if (append_textf_http(buf, cap, used, " ") != 0) {
|
|
return -1;
|
|
}
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static int append_cjson_markdown_http(char** buf, size_t* cap, size_t* used, cJSON* node, int depth) {
|
|
if (!node) {
|
|
return append_markdown_indent_http(buf, cap, used, depth) == 0 &&
|
|
append_textf_http(buf, cap, used, "- null\n") == 0 ? 0 : -1;
|
|
}
|
|
|
|
if (cJSON_IsObject(node)) {
|
|
cJSON* child = NULL;
|
|
cJSON_ArrayForEach(child, node) {
|
|
const char* key = child->string ? child->string : "item";
|
|
if (cJSON_IsString(child)) {
|
|
if (append_markdown_indent_http(buf, cap, used, depth) != 0 ||
|
|
append_textf_http(buf, cap, used, "- **%s:** %s\n", key,
|
|
child->valuestring ? child->valuestring : "") != 0) {
|
|
return -1;
|
|
}
|
|
} else if (cJSON_IsBool(child)) {
|
|
if (append_markdown_indent_http(buf, cap, used, depth) != 0 ||
|
|
append_textf_http(buf, cap, used, "- **%s:** %s\n", key,
|
|
cJSON_IsTrue(child) ? "true" : "false") != 0) {
|
|
return -1;
|
|
}
|
|
} else if (cJSON_IsNumber(child)) {
|
|
if (append_markdown_indent_http(buf, cap, used, depth) != 0 ||
|
|
append_textf_http(buf, cap, used, "- **%s:** %g\n", key, child->valuedouble) != 0) {
|
|
return -1;
|
|
}
|
|
} else if (cJSON_IsNull(child)) {
|
|
if (append_markdown_indent_http(buf, cap, used, depth) != 0 ||
|
|
append_textf_http(buf, cap, used, "- **%s:** null\n", key) != 0) {
|
|
return -1;
|
|
}
|
|
} else {
|
|
if (append_markdown_indent_http(buf, cap, used, depth) != 0 ||
|
|
append_textf_http(buf, cap, used, "- **%s:**\n", key) != 0) {
|
|
return -1;
|
|
}
|
|
if (append_cjson_markdown_http(buf, cap, used, child, depth + 1) != 0) {
|
|
return -1;
|
|
}
|
|
}
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
if (cJSON_IsArray(node)) {
|
|
int n = cJSON_GetArraySize(node);
|
|
for (int i = 0; i < n; i++) {
|
|
cJSON* item = cJSON_GetArrayItem(node, i);
|
|
if (cJSON_IsString(item)) {
|
|
if (append_markdown_indent_http(buf, cap, used, depth) != 0 ||
|
|
append_textf_http(buf, cap, used, "- %s\n", item->valuestring ? item->valuestring : "") != 0) {
|
|
return -1;
|
|
}
|
|
} else if (cJSON_IsBool(item)) {
|
|
if (append_markdown_indent_http(buf, cap, used, depth) != 0 ||
|
|
append_textf_http(buf, cap, used, "- %s\n", cJSON_IsTrue(item) ? "true" : "false") != 0) {
|
|
return -1;
|
|
}
|
|
} else if (cJSON_IsNumber(item)) {
|
|
if (append_markdown_indent_http(buf, cap, used, depth) != 0 ||
|
|
append_textf_http(buf, cap, used, "- %g\n", item->valuedouble) != 0) {
|
|
return -1;
|
|
}
|
|
} else if (cJSON_IsNull(item)) {
|
|
if (append_markdown_indent_http(buf, cap, used, depth) != 0 ||
|
|
append_textf_http(buf, cap, used, "- null\n") != 0) {
|
|
return -1;
|
|
}
|
|
} else {
|
|
if (append_markdown_indent_http(buf, cap, used, depth) != 0 ||
|
|
append_textf_http(buf, cap, used, "-\n") != 0) {
|
|
return -1;
|
|
}
|
|
if (append_cjson_markdown_http(buf, cap, used, item, depth + 1) != 0) {
|
|
return -1;
|
|
}
|
|
}
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
if (cJSON_IsString(node)) {
|
|
return append_markdown_indent_http(buf, cap, used, depth) == 0 &&
|
|
append_textf_http(buf, cap, used, "- %s\n", node->valuestring ? node->valuestring : "") == 0 ? 0 : -1;
|
|
}
|
|
if (cJSON_IsBool(node)) {
|
|
return append_markdown_indent_http(buf, cap, used, depth) == 0 &&
|
|
append_textf_http(buf, cap, used, "- %s\n", cJSON_IsTrue(node) ? "true" : "false") == 0 ? 0 : -1;
|
|
}
|
|
if (cJSON_IsNumber(node)) {
|
|
return append_markdown_indent_http(buf, cap, used, depth) == 0 &&
|
|
append_textf_http(buf, cap, used, "- %g\n", node->valuedouble) == 0 ? 0 : -1;
|
|
}
|
|
|
|
return append_markdown_indent_http(buf, cap, used, depth) == 0 &&
|
|
append_textf_http(buf, cap, used, "- null\n") == 0 ? 0 : -1;
|
|
}
|
|
|
|
static char* format_result_markdown_http(const char* raw_result) {
|
|
if (!raw_result) {
|
|
return strdup("No output.");
|
|
}
|
|
|
|
cJSON* root = cJSON_Parse(raw_result);
|
|
if (!root) {
|
|
return strdup(raw_result);
|
|
}
|
|
|
|
size_t cap = strlen(raw_result) * 2U + 512U;
|
|
if (cap < 2048U) cap = 2048U;
|
|
size_t used = 0U;
|
|
char* out = (char*)malloc(cap);
|
|
if (!out) {
|
|
cJSON_Delete(root);
|
|
return strdup(raw_result);
|
|
}
|
|
out[0] = '\0';
|
|
|
|
if (append_cjson_markdown_http(&out, &cap, &used, root, 0) != 0) {
|
|
cJSON_Delete(root);
|
|
free(out);
|
|
return strdup(raw_result);
|
|
}
|
|
|
|
cJSON_Delete(root);
|
|
return out;
|
|
}
|
|
|
|
static char* build_slash_help_all_json_local(void) {
|
|
size_t cap = 2048U;
|
|
size_t used = 0U;
|
|
char* out = (char*)malloc(cap);
|
|
if (!out) return NULL;
|
|
out[0] = '\0';
|
|
|
|
if (append_textf_http(&out, &cap, &used, "AVAILABLE TOOLS\n") != 0) {
|
|
free(out);
|
|
return NULL;
|
|
}
|
|
|
|
char* tool_list_json = tools_execute(g_api_ctx.tools_ctx, "tool_list", "{}");
|
|
cJSON* tool_root = tool_list_json ? cJSON_Parse(tool_list_json) : NULL;
|
|
cJSON* tools = tool_root ? cJSON_GetObjectItemCaseSensitive(tool_root, "tools") : NULL;
|
|
|
|
if (!tools || !cJSON_IsArray(tools) || cJSON_GetArraySize(tools) <= 0) {
|
|
(void)append_textf_http(&out, &cap, &used, "- (none)\n");
|
|
} else {
|
|
int tn = cJSON_GetArraySize(tools);
|
|
int* used_idx = (int*)calloc((size_t)tn, sizeof(int));
|
|
if (!used_idx) {
|
|
cJSON_Delete(tool_root);
|
|
free(tool_list_json);
|
|
free(out);
|
|
return NULL;
|
|
}
|
|
|
|
for (int k = 0; k < tn; k++) {
|
|
int best = -1;
|
|
const char* best_name = NULL;
|
|
|
|
for (int i = 0; i < tn; i++) {
|
|
if (used_idx[i]) continue;
|
|
cJSON* row = cJSON_GetArrayItem(tools, i);
|
|
cJSON* name = row ? cJSON_GetObjectItemCaseSensitive(row, "name") : NULL;
|
|
const char* name_s = (name && cJSON_IsString(name) && name->valuestring) ? name->valuestring : "unknown";
|
|
if (best < 0 || strcmp(name_s, best_name) < 0) {
|
|
best = i;
|
|
best_name = name_s;
|
|
}
|
|
}
|
|
|
|
if (best < 0) {
|
|
break;
|
|
}
|
|
|
|
used_idx[best] = 1;
|
|
cJSON* row = cJSON_GetArrayItem(tools, best);
|
|
cJSON* name = row ? cJSON_GetObjectItemCaseSensitive(row, "name") : NULL;
|
|
cJSON* desc = row ? cJSON_GetObjectItemCaseSensitive(row, "description") : NULL;
|
|
const char* name_s = (name && cJSON_IsString(name) && name->valuestring) ? name->valuestring : "unknown";
|
|
const char* desc_s = (desc && cJSON_IsString(desc) && desc->valuestring) ? desc->valuestring : "";
|
|
if (append_textf_http(&out, &cap, &used, "- %s%s%s\n", name_s, desc_s[0] ? " — " : "", desc_s) != 0) {
|
|
free(used_idx);
|
|
cJSON_Delete(tool_root);
|
|
free(tool_list_json);
|
|
free(out);
|
|
return NULL;
|
|
}
|
|
}
|
|
|
|
free(used_idx);
|
|
}
|
|
|
|
cJSON_Delete(tool_root);
|
|
free(tool_list_json);
|
|
|
|
if (append_textf_http(&out, &cap, &used, "\nAVAILABLE SKILLS\n") != 0) {
|
|
free(out);
|
|
return NULL;
|
|
}
|
|
|
|
char* skill_list_json = tools_execute(g_api_ctx.tools_ctx, "skill_list", "{}");
|
|
cJSON* skill_root = skill_list_json ? cJSON_Parse(skill_list_json) : NULL;
|
|
cJSON* skills = skill_root ? cJSON_GetObjectItemCaseSensitive(skill_root, "skills") : NULL;
|
|
|
|
int available_count = 0;
|
|
if (skills && cJSON_IsArray(skills) && cJSON_GetArraySize(skills) > 0) {
|
|
int sn = cJSON_GetArraySize(skills);
|
|
for (int i = 0; i < sn; i++) {
|
|
cJSON* row = cJSON_GetArrayItem(skills, i);
|
|
cJSON* owner = row ? cJSON_GetObjectItemCaseSensitive(row, "owner") : NULL;
|
|
cJSON* d_tag = row ? cJSON_GetObjectItemCaseSensitive(row, "d_tag") : NULL;
|
|
cJSON* desc = row ? cJSON_GetObjectItemCaseSensitive(row, "description") : NULL;
|
|
const char* owner_s = (owner && cJSON_IsString(owner) && owner->valuestring) ? owner->valuestring : "";
|
|
const char* d_tag_s = (d_tag && cJSON_IsString(d_tag) && d_tag->valuestring) ? d_tag->valuestring : NULL;
|
|
const char* desc_s = (desc && cJSON_IsString(desc) && desc->valuestring) ? desc->valuestring : "";
|
|
|
|
if (strcmp(owner_s, "agent") != 0 || !d_tag_s || d_tag_s[0] == '\0') {
|
|
continue;
|
|
}
|
|
|
|
if (append_textf_http(&out, &cap, &used, "- %s%s%s\n", d_tag_s, desc_s[0] ? " — " : "", desc_s) != 0) {
|
|
cJSON_Delete(skill_root);
|
|
free(skill_list_json);
|
|
free(out);
|
|
return NULL;
|
|
}
|
|
available_count++;
|
|
}
|
|
}
|
|
|
|
if (available_count <= 0) {
|
|
(void)append_textf_http(&out, &cap, &used, "- (none)\n");
|
|
}
|
|
|
|
cJSON_Delete(skill_root);
|
|
free(skill_list_json);
|
|
|
|
if (append_textf_http(&out, &cap, &used, "\nADOPTED SKILLS\n") != 0) {
|
|
free(out);
|
|
return NULL;
|
|
}
|
|
|
|
char* adopted_json = tools_execute(g_api_ctx.tools_ctx, "adopted_skills", "{}");
|
|
cJSON* adopted_root = adopted_json ? cJSON_Parse(adopted_json) : NULL;
|
|
cJSON* adoption_events_content = adopted_root ? cJSON_GetObjectItemCaseSensitive(adopted_root, "content") : NULL;
|
|
cJSON* adoption_events_json = adopted_root ? cJSON_GetObjectItemCaseSensitive(adopted_root, "adoption_events_json") : NULL;
|
|
const char* adoption_events_text = NULL;
|
|
if (adoption_events_content && cJSON_IsString(adoption_events_content) && adoption_events_content->valuestring) {
|
|
adoption_events_text = adoption_events_content->valuestring;
|
|
} else if (adoption_events_json && cJSON_IsString(adoption_events_json) && adoption_events_json->valuestring) {
|
|
adoption_events_text = adoption_events_json->valuestring;
|
|
}
|
|
cJSON* adoption_events = adoption_events_text ? cJSON_Parse(adoption_events_text) : NULL;
|
|
|
|
int adopted_count = 0;
|
|
if (adoption_events && cJSON_IsArray(adoption_events) && cJSON_GetArraySize(adoption_events) > 0) {
|
|
cJSON* ev0 = cJSON_GetArrayItem(adoption_events, 0);
|
|
cJSON* tags = ev0 ? cJSON_GetObjectItemCaseSensitive(ev0, "tags") : NULL;
|
|
if (tags && cJSON_IsArray(tags)) {
|
|
int tn = cJSON_GetArraySize(tags);
|
|
for (int i = 0; i < tn; i++) {
|
|
cJSON* tag = cJSON_GetArrayItem(tags, i);
|
|
if (!tag || !cJSON_IsArray(tag) || cJSON_GetArraySize(tag) < 2) {
|
|
continue;
|
|
}
|
|
|
|
cJSON* k = cJSON_GetArrayItem(tag, 0);
|
|
cJSON* v = cJSON_GetArrayItem(tag, 1);
|
|
if (!k || !v || !cJSON_IsString(k) || !k->valuestring ||
|
|
!cJSON_IsString(v) || !v->valuestring) {
|
|
continue;
|
|
}
|
|
|
|
if (strcmp(k->valuestring, "a") != 0) {
|
|
continue;
|
|
}
|
|
|
|
const char* addr = v->valuestring;
|
|
const char* d_tag = strrchr(addr, ':');
|
|
const char* label = (d_tag && d_tag[1] != '\0') ? (d_tag + 1) : addr;
|
|
|
|
if (append_textf_http(&out, &cap, &used, "- %s\n", label) != 0) {
|
|
cJSON_Delete(adoption_events);
|
|
cJSON_Delete(adopted_root);
|
|
free(adopted_json);
|
|
free(out);
|
|
return NULL;
|
|
}
|
|
adopted_count++;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (adopted_count <= 0) {
|
|
(void)append_textf_http(&out, &cap, &used, "- (none)\n");
|
|
}
|
|
|
|
cJSON_Delete(adoption_events);
|
|
cJSON_Delete(adopted_root);
|
|
free(adopted_json);
|
|
return out;
|
|
}
|
|
|
|
static char* build_slash_help_tool_json_local(const char* tool_name) {
|
|
if (!tool_name || tool_name[0] == '\0') {
|
|
return json_error_string_local("missing tool name");
|
|
}
|
|
|
|
char* schema_json = tools_build_openai_schema_json(g_api_ctx.tools_ctx);
|
|
if (!schema_json) return json_error_string_local("failed to build tool schema");
|
|
|
|
cJSON* schema = cJSON_Parse(schema_json);
|
|
free(schema_json);
|
|
if (!schema || !cJSON_IsArray(schema)) {
|
|
cJSON_Delete(schema);
|
|
return json_error_string_local("tool schema parse failure");
|
|
}
|
|
|
|
cJSON* matched_fn = NULL;
|
|
int n = cJSON_GetArraySize(schema);
|
|
for (int i = 0; i < n; i++) {
|
|
cJSON* item = cJSON_GetArrayItem(schema, i);
|
|
cJSON* fn = item ? cJSON_GetObjectItemCaseSensitive(item, "function") : NULL;
|
|
cJSON* name = fn ? cJSON_GetObjectItemCaseSensitive(fn, "name") : NULL;
|
|
if (name && cJSON_IsString(name) && name->valuestring && strcmp(name->valuestring, tool_name) == 0) {
|
|
matched_fn = fn;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!matched_fn) {
|
|
cJSON_Delete(schema);
|
|
return json_error_string_local("unknown tool");
|
|
}
|
|
|
|
cJSON* out = cJSON_CreateObject();
|
|
if (!out) {
|
|
cJSON_Delete(schema);
|
|
return NULL;
|
|
}
|
|
|
|
cJSON_AddBoolToObject(out, "success", 1);
|
|
cJSON_AddStringToObject(out, "mode", "slash_help_tool");
|
|
cJSON_AddStringToObject(out, "tool", tool_name);
|
|
cJSON_AddItemToObject(out, "schema", cJSON_Duplicate(matched_fn, 1));
|
|
|
|
char* result = cJSON_PrintUnformatted(out);
|
|
cJSON_Delete(out);
|
|
cJSON_Delete(schema);
|
|
return result;
|
|
}
|
|
|
|
static char* execute_slash_command_http(const char* message) {
|
|
if (!message || message[0] != '/') return NULL;
|
|
|
|
const char* p = message + 1;
|
|
while (*p && !is_space_char_local(*p)) p++;
|
|
|
|
size_t tool_len = (size_t)(p - (message + 1));
|
|
if (tool_len == 0 || tool_len >= 128U) {
|
|
return strdup("{\"success\":false,\"error\":\"invalid slash command\"}");
|
|
}
|
|
|
|
char tool_name[128];
|
|
memcpy(tool_name, message + 1, tool_len);
|
|
tool_name[tool_len] = '\0';
|
|
|
|
const char* raw_args = skip_spaces_local(p);
|
|
|
|
char* result_json = NULL;
|
|
if (raw_args && (strcmp(raw_args, "-h") == 0 || strcmp(raw_args, "--help") == 0)) {
|
|
const char* help_name = (strcmp(tool_name, "run") == 0) ? "skill_run" : tool_name;
|
|
result_json = build_slash_help_tool_json_local(help_name);
|
|
} else if (strcmp(tool_name, "help") == 0) {
|
|
if (!raw_args || raw_args[0] == '\0') {
|
|
result_json = build_slash_help_all_json_local();
|
|
} else {
|
|
char help_tool[128];
|
|
size_t i = 0;
|
|
while (raw_args[i] && !is_space_char_local(raw_args[i]) && i < sizeof(help_tool) - 1U) {
|
|
help_tool[i] = raw_args[i];
|
|
i++;
|
|
}
|
|
help_tool[i] = '\0';
|
|
const char* help_name = (strcmp(help_tool, "run") == 0) ? "skill_run" : help_tool;
|
|
result_json = build_slash_help_tool_json_local(help_name);
|
|
}
|
|
} else {
|
|
char* args_json = NULL;
|
|
const char* exec_tool_name = tool_name;
|
|
|
|
if (strcmp(tool_name, "run") == 0) {
|
|
args_json = build_run_args_json_local(raw_args);
|
|
exec_tool_name = "skill_run";
|
|
} else {
|
|
args_json = build_slash_args_json_local(raw_args);
|
|
}
|
|
|
|
if (!args_json) {
|
|
result_json = json_error_string_local("failed to parse slash args");
|
|
} else {
|
|
result_json = tools_execute(g_api_ctx.tools_ctx, exec_tool_name, args_json);
|
|
free(args_json);
|
|
}
|
|
}
|
|
|
|
if (!result_json) {
|
|
result_json = strdup("{\"success\":false,\"error\":\"direct tool execution failed\"}");
|
|
}
|
|
|
|
char* markdown_result = format_result_markdown_http(result_json);
|
|
|
|
free(result_json);
|
|
return markdown_result ? markdown_result : strdup("Command executed with no output.");
|
|
}
|
|
|
|
static cJSON* run_prompt_with_tools_convo(cJSON* convo,
|
|
int max_turns,
|
|
const char* log_sender,
|
|
const char* tool_limit_message) {
|
|
if (!convo || !cJSON_IsArray(convo)) return NULL;
|
|
|
|
int max_ceiling = 16;
|
|
if (g_api_ctx.cfg && g_api_ctx.cfg->tools.api_max_turns_ceiling > 0) {
|
|
max_ceiling = g_api_ctx.cfg->tools.api_max_turns_ceiling;
|
|
}
|
|
|
|
if (max_turns < 1) max_turns = 1;
|
|
if (max_turns > max_ceiling) max_turns = max_ceiling;
|
|
|
|
char* tools_json = tools_build_openai_schema_json(g_api_ctx.tools_ctx);
|
|
if (!tools_json) {
|
|
return NULL;
|
|
}
|
|
|
|
cJSON* root = cJSON_CreateObject();
|
|
cJSON* turns = cJSON_CreateArray();
|
|
if (!root || !turns) {
|
|
cJSON_Delete(root);
|
|
cJSON_Delete(turns);
|
|
free(tools_json);
|
|
return NULL;
|
|
}
|
|
|
|
int stall_repeat_threshold = 3;
|
|
if (g_api_ctx.cfg && g_api_ctx.cfg->tools.stall_repeat_threshold > 1) {
|
|
stall_repeat_threshold = g_api_ctx.cfg->tools.stall_repeat_threshold;
|
|
}
|
|
|
|
char* final_response = NULL;
|
|
uint64_t last_tool_fp = 0;
|
|
int repeated_tool_turns = 0;
|
|
int exited_on_stall = 0;
|
|
int stall_due_to_length = 0;
|
|
int turns_run = 0;
|
|
|
|
for (int turn = 0; turn < max_turns; turn++) {
|
|
turns_run = turn + 1;
|
|
char* messages_json = cJSON_PrintUnformatted(convo);
|
|
if (!messages_json) break;
|
|
|
|
llm_response_t resp;
|
|
int rc = llm_chat_with_tools_messages(messages_json, tools_json, "auto", &resp);
|
|
free(messages_json);
|
|
|
|
if (rc != 0) {
|
|
final_response = strdup("LLM request failed.");
|
|
break;
|
|
}
|
|
|
|
cJSON* turn_obj = cJSON_CreateObject();
|
|
if (turn_obj) {
|
|
cJSON_AddNumberToObject(turn_obj, "turn", turn + 1);
|
|
cJSON* tool_calls = cJSON_CreateArray();
|
|
if (tool_calls) cJSON_AddItemToObject(turn_obj, "tool_calls", tool_calls);
|
|
cJSON_AddItemToArray(turns, turn_obj);
|
|
}
|
|
|
|
if (resp.tool_call_count <= 0) {
|
|
final_response = strdup(resp.content ? resp.content : "No response content.");
|
|
llm_response_free(&resp);
|
|
break;
|
|
}
|
|
|
|
uint64_t current_tool_fp = tool_calls_fingerprint_local(&resp);
|
|
if (turn == 0 || current_tool_fp != last_tool_fp) {
|
|
repeated_tool_turns = 1;
|
|
last_tool_fp = current_tool_fp;
|
|
} else {
|
|
repeated_tool_turns++;
|
|
}
|
|
|
|
if (repeated_tool_turns >= stall_repeat_threshold) {
|
|
exited_on_stall = 1;
|
|
stall_due_to_length = (resp.finish_reason && strcmp(resp.finish_reason, "length") == 0) ? 1 : 0;
|
|
llm_response_free(&resp);
|
|
break;
|
|
}
|
|
|
|
if (append_assistant_tool_calls_message_local(convo, &resp) != 0) {
|
|
llm_response_free(&resp);
|
|
final_response = strdup("Failed to append assistant tool calls.");
|
|
break;
|
|
}
|
|
|
|
cJSON* current_turn = cJSON_GetArrayItem(turns, cJSON_GetArraySize(turns) - 1);
|
|
cJSON* turn_tool_calls = current_turn ? cJSON_GetObjectItemCaseSensitive(current_turn, "tool_calls") : NULL;
|
|
|
|
for (int i = 0; i < resp.tool_call_count; i++) {
|
|
llm_tool_call_t* tc = &resp.tool_calls[i];
|
|
char* tool_result = tools_execute(g_api_ctx.tools_ctx, tc->name, tc->arguments_json);
|
|
if (!tool_result) {
|
|
tool_result = strdup("{\"success\":false,\"error\":\"tool execution failed\"}");
|
|
}
|
|
|
|
if (turn_tool_calls && cJSON_IsArray(turn_tool_calls)) {
|
|
cJSON* tr = cJSON_CreateObject();
|
|
if (tr) {
|
|
cJSON_AddStringToObject(tr, "name", tc->name ? tc->name : "");
|
|
cJSON_AddStringToObject(tr, "arguments", tc->arguments_json ? tc->arguments_json : "{}");
|
|
cJSON_AddStringToObject(tr, "result", tool_result ? tool_result : "{}");
|
|
cJSON_AddItemToArray(turn_tool_calls, tr);
|
|
}
|
|
}
|
|
|
|
if (append_tool_result_message_local(convo,
|
|
tc->id ? tc->id : "",
|
|
tool_result ? tool_result : "{\"success\":false,\"error\":\"tool execution failed\"}") != 0) {
|
|
free(tool_result);
|
|
llm_response_free(&resp);
|
|
final_response = strdup("Failed to append tool result.");
|
|
goto done;
|
|
}
|
|
|
|
if (maybe_append_loaded_skill_instructions_local(convo,
|
|
tc->name,
|
|
tool_result) != 0) {
|
|
free(tool_result);
|
|
llm_response_free(&resp);
|
|
final_response = strdup("Failed to apply loaded skill instructions.");
|
|
goto done;
|
|
}
|
|
free(tool_result);
|
|
}
|
|
|
|
llm_response_free(&resp);
|
|
}
|
|
|
|
done:;
|
|
int hit_max_turn_limit = (!final_response && !exited_on_stall && turns_run >= max_turns);
|
|
|
|
if (!final_response) {
|
|
char* messages_json = cJSON_PrintUnformatted(convo);
|
|
if (messages_json) {
|
|
llm_response_t final_resp;
|
|
int final_rc = llm_chat_with_tools_messages(messages_json, tools_json, "none", &final_resp);
|
|
free(messages_json);
|
|
if (final_rc == 0) {
|
|
const char* forced_answer = final_resp.content ? final_resp.content : "";
|
|
if (forced_answer[0] != '\0') {
|
|
final_response = strdup(forced_answer);
|
|
}
|
|
llm_response_free(&final_resp);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!final_response) {
|
|
final_response = strdup(exited_on_stall
|
|
? "Stopped repeated tool calls after detecting a loop, and no final summary was produced."
|
|
: (tool_limit_message ? tool_limit_message : "I hit my tool-use limit for this prompt run."));
|
|
}
|
|
|
|
if (exited_on_stall || hit_max_turn_limit) {
|
|
notify_admin_limit_diagnostic_http("http_api_tool_loop",
|
|
exited_on_stall ? "stall_repetition_detected" : "max_turns_exhausted",
|
|
log_sender,
|
|
max_turns,
|
|
turns_run,
|
|
stall_repeat_threshold,
|
|
repeated_tool_turns,
|
|
g_api_ctx.cfg ? g_api_ctx.cfg->llm.max_tokens : 0,
|
|
(exited_on_stall && stall_due_to_length) ? "max_tokens_truncation" : NULL,
|
|
(exited_on_stall && stall_due_to_length) ? "Increase max_tokens (example: /model_set {\"max_tokens\": 4096})" : NULL,
|
|
final_response ? final_response : "");
|
|
}
|
|
|
|
cJSON_AddBoolToObject(root, "success", 1);
|
|
cJSON_AddStringToObject(root, "final_response", final_response ? final_response : "");
|
|
cJSON_AddItemToObject(root, "turns", turns);
|
|
|
|
llm_config_t cfg;
|
|
if (llm_get_config(&cfg) == 0) {
|
|
cJSON_AddStringToObject(root, "model_used", cfg.model);
|
|
}
|
|
|
|
int input_chars = 0;
|
|
char* convo_json = cJSON_PrintUnformatted(convo);
|
|
if (convo_json) {
|
|
input_chars = (int)strlen(convo_json);
|
|
free(convo_json);
|
|
}
|
|
cJSON_AddNumberToObject(root, "total_input_tokens_estimate", estimate_tokens_from_chars(input_chars));
|
|
cJSON_AddNumberToObject(root,
|
|
"total_output_tokens_estimate",
|
|
estimate_tokens_from_chars((int)strlen(final_response ? final_response : "")));
|
|
|
|
free(final_response);
|
|
free(tools_json);
|
|
return root;
|
|
}
|
|
|
|
static cJSON* run_prompt_with_tools(cJSON* body) {
|
|
if (!body || !cJSON_IsObject(body)) return NULL;
|
|
|
|
cJSON* messages_node = cJSON_GetObjectItemCaseSensitive(body, "messages");
|
|
if (!messages_node || !cJSON_IsArray(messages_node)) return NULL;
|
|
|
|
cJSON* convo = cJSON_Duplicate(messages_node, 1);
|
|
if (!convo || !cJSON_IsArray(convo)) {
|
|
cJSON_Delete(convo);
|
|
return NULL;
|
|
}
|
|
|
|
int max_turns = 4;
|
|
if (g_api_ctx.cfg && g_api_ctx.cfg->tools.api_default_max_turns > 0) {
|
|
max_turns = g_api_ctx.cfg->tools.api_default_max_turns;
|
|
}
|
|
cJSON* max_turns_node = cJSON_GetObjectItemCaseSensitive(body, "max_turns");
|
|
if (max_turns_node && cJSON_IsNumber(max_turns_node)) {
|
|
max_turns = (int)max_turns_node->valuedouble;
|
|
}
|
|
|
|
cJSON* root = run_prompt_with_tools_convo(convo,
|
|
max_turns,
|
|
"http_api",
|
|
"I hit my tool-use limit for this prompt run.");
|
|
cJSON_Delete(convo);
|
|
return root;
|
|
}
|
|
|
|
static void handle_status(struct mg_connection* c) {
|
|
cJSON* root = cJSON_CreateObject();
|
|
if (!root) {
|
|
reply_error(c, 500, "oom");
|
|
return;
|
|
}
|
|
|
|
cJSON_AddBoolToObject(root, "success", 1);
|
|
cJSON_AddStringToObject(root, "name", DIDACTYL_NAME);
|
|
cJSON_AddStringToObject(root, "version", DIDACTYL_VERSION);
|
|
|
|
if (g_api_ctx.cfg) {
|
|
cJSON_AddStringToObject(root, "pubkey", g_api_ctx.cfg->keys.public_key_hex);
|
|
cJSON_AddNumberToObject(root, "relay_count", g_api_ctx.cfg->relay_count);
|
|
}
|
|
|
|
cJSON_AddNumberToObject(root, "connected_relays", nostr_handler_connected_relay_count());
|
|
if (g_api_ctx.trigger_manager) {
|
|
cJSON_AddNumberToObject(root, "active_triggers", trigger_manager_active_count(g_api_ctx.trigger_manager));
|
|
}
|
|
|
|
reply_json(c, 200, root);
|
|
cJSON_Delete(root);
|
|
}
|
|
|
|
static void handle_context_current(struct mg_connection* c) {
|
|
char* messages_json = NULL;
|
|
if (agent_build_admin_messages_json("", DIDACTYL_SENDER_ADMIN, &messages_json) != 0 || !messages_json) {
|
|
reply_error(c, 500, "failed to build context messages");
|
|
return;
|
|
}
|
|
|
|
cJSON* messages = cJSON_Parse(messages_json);
|
|
cJSON* root = cJSON_CreateObject();
|
|
if (!root || !messages || !cJSON_IsArray(messages)) {
|
|
free(messages_json);
|
|
cJSON_Delete(messages);
|
|
cJSON_Delete(root);
|
|
reply_error(c, 500, "failed to serialize context");
|
|
return;
|
|
}
|
|
|
|
int chars = (int)strlen(messages_json);
|
|
cJSON_AddBoolToObject(root, "success", 1);
|
|
cJSON_AddNumberToObject(root, "total_chars", chars);
|
|
cJSON_AddNumberToObject(root, "total_estimated_tokens", estimate_tokens_from_chars(chars));
|
|
cJSON_AddItemToObject(root, "messages", messages);
|
|
|
|
reply_json(c, 200, root);
|
|
|
|
free(messages_json);
|
|
cJSON_Delete(root);
|
|
}
|
|
|
|
static void handle_context_parts(struct mg_connection* c) {
|
|
cJSON* root = cJSON_CreateObject();
|
|
if (!root) {
|
|
reply_error(c, 500, "oom");
|
|
return;
|
|
}
|
|
|
|
if (build_context_parts_response(root) != 0) {
|
|
cJSON_Delete(root);
|
|
reply_error(c, 500, "failed to build context parts");
|
|
return;
|
|
}
|
|
|
|
reply_json(c, 200, root);
|
|
cJSON_Delete(root);
|
|
}
|
|
|
|
static void handle_prompt_run_simple(struct mg_connection* c, const struct mg_http_message* hm) {
|
|
cJSON* body = parse_body_json(hm);
|
|
if (!body || !cJSON_IsObject(body)) {
|
|
cJSON_Delete(body);
|
|
reply_error(c, 400, "invalid JSON body");
|
|
return;
|
|
}
|
|
|
|
cJSON* system = cJSON_GetObjectItemCaseSensitive(body, "system");
|
|
cJSON* user = cJSON_GetObjectItemCaseSensitive(body, "user");
|
|
if (!system || !user || !cJSON_IsString(system) || !cJSON_IsString(user) ||
|
|
!system->valuestring || !user->valuestring) {
|
|
cJSON_Delete(body);
|
|
reply_error(c, 400, "system and user strings are required");
|
|
return;
|
|
}
|
|
|
|
llm_config_t old_cfg;
|
|
int overridden = 0;
|
|
char* model_err = maybe_model_override_begin(body, &old_cfg, &overridden);
|
|
if (model_err) {
|
|
cJSON_Delete(body);
|
|
reply_error(c, 400, model_err);
|
|
return;
|
|
}
|
|
|
|
char* response = llm_chat(system->valuestring, user->valuestring);
|
|
maybe_model_override_end(&old_cfg, overridden);
|
|
|
|
if (!response) {
|
|
cJSON_Delete(body);
|
|
reply_error(c, 500, "llm request failed");
|
|
return;
|
|
}
|
|
|
|
cJSON* root = cJSON_CreateObject();
|
|
if (!root) {
|
|
free(response);
|
|
cJSON_Delete(body);
|
|
reply_error(c, 500, "oom");
|
|
return;
|
|
}
|
|
|
|
cJSON_AddBoolToObject(root, "success", 1);
|
|
cJSON_AddStringToObject(root, "response", response);
|
|
|
|
llm_config_t cfg;
|
|
if (llm_get_config(&cfg) == 0) {
|
|
cJSON_AddStringToObject(root, "model_used", cfg.model);
|
|
}
|
|
|
|
int in_chars = (int)strlen(system->valuestring) + (int)strlen(user->valuestring);
|
|
cJSON_AddNumberToObject(root, "input_tokens_estimate", estimate_tokens_from_chars(in_chars));
|
|
cJSON_AddNumberToObject(root, "output_tokens_estimate", estimate_tokens_from_chars((int)strlen(response)));
|
|
|
|
free(response);
|
|
cJSON_Delete(body);
|
|
reply_json(c, 200, root);
|
|
cJSON_Delete(root);
|
|
}
|
|
|
|
static void handle_prompt_run(struct mg_connection* c, const struct mg_http_message* hm) {
|
|
cJSON* body = parse_body_json(hm);
|
|
if (!body || !cJSON_IsObject(body)) {
|
|
cJSON_Delete(body);
|
|
reply_error(c, 400, "invalid JSON body");
|
|
return;
|
|
}
|
|
|
|
llm_config_t old_cfg;
|
|
int overridden = 0;
|
|
char* model_err = maybe_model_override_begin(body, &old_cfg, &overridden);
|
|
if (model_err) {
|
|
cJSON_Delete(body);
|
|
reply_error(c, 400, model_err);
|
|
return;
|
|
}
|
|
|
|
cJSON* result = run_prompt_with_tools(body);
|
|
maybe_model_override_end(&old_cfg, overridden);
|
|
cJSON_Delete(body);
|
|
|
|
if (!result) {
|
|
reply_error(c, 500, "failed to run prompt");
|
|
return;
|
|
}
|
|
|
|
reply_json(c, 200, result);
|
|
cJSON_Delete(result);
|
|
}
|
|
|
|
static void handle_prompt_agent(struct mg_connection* c, const struct mg_http_message* hm) {
|
|
cJSON* body = parse_body_json(hm);
|
|
if (!body || !cJSON_IsObject(body)) {
|
|
cJSON_Delete(body);
|
|
reply_error(c, 400, "invalid JSON body");
|
|
return;
|
|
}
|
|
|
|
cJSON* message = cJSON_GetObjectItemCaseSensitive(body, "message");
|
|
if (!message || !cJSON_IsString(message) || !message->valuestring) {
|
|
cJSON_Delete(body);
|
|
reply_error(c, 400, "message string is required");
|
|
return;
|
|
}
|
|
|
|
char* slash_result = execute_slash_command_http(message->valuestring);
|
|
if (slash_result) {
|
|
cJSON* out = cJSON_CreateObject();
|
|
cJSON* turns = cJSON_CreateArray();
|
|
if (!out || !turns) {
|
|
free(slash_result);
|
|
cJSON_Delete(out);
|
|
cJSON_Delete(turns);
|
|
cJSON_Delete(body);
|
|
reply_error(c, 500, "oom");
|
|
return;
|
|
}
|
|
cJSON_AddBoolToObject(out, "success", 1);
|
|
cJSON_AddStringToObject(out, "final_response", slash_result);
|
|
cJSON_AddItemToObject(out, "turns", turns);
|
|
cJSON_AddNumberToObject(out, "total_input_tokens_estimate", estimate_tokens_from_chars((int)strlen(message->valuestring)));
|
|
cJSON_AddNumberToObject(out, "total_output_tokens_estimate", estimate_tokens_from_chars((int)strlen(slash_result)));
|
|
free(slash_result);
|
|
cJSON_Delete(body);
|
|
reply_json(c, 200, out);
|
|
cJSON_Delete(out);
|
|
return;
|
|
}
|
|
|
|
int max_turns = 4;
|
|
if (g_api_ctx.cfg && g_api_ctx.cfg->tools.api_default_max_turns > 0) {
|
|
max_turns = g_api_ctx.cfg->tools.api_default_max_turns;
|
|
}
|
|
cJSON* max_turns_node = cJSON_GetObjectItemCaseSensitive(body, "max_turns");
|
|
if (max_turns_node && cJSON_IsNumber(max_turns_node)) {
|
|
max_turns = (int)max_turns_node->valuedouble;
|
|
}
|
|
|
|
llm_config_t old_cfg;
|
|
int overridden = 0;
|
|
char* model_err = maybe_model_override_begin(body, &old_cfg, &overridden);
|
|
if (model_err) {
|
|
cJSON_Delete(body);
|
|
reply_error(c, 400, model_err);
|
|
return;
|
|
}
|
|
|
|
char* base_messages_json = NULL;
|
|
cJSON* convo = NULL;
|
|
cJSON* result = NULL;
|
|
|
|
if (agent_build_admin_messages_json(message->valuestring,
|
|
DIDACTYL_SENDER_ADMIN,
|
|
&base_messages_json) != 0 || !base_messages_json) {
|
|
maybe_model_override_end(&old_cfg, overridden);
|
|
cJSON_Delete(body);
|
|
reply_error(c, 500, "failed to build agent context");
|
|
return;
|
|
}
|
|
|
|
convo = cJSON_Parse(base_messages_json);
|
|
free(base_messages_json);
|
|
if (!convo || !cJSON_IsArray(convo)) {
|
|
cJSON_Delete(convo);
|
|
maybe_model_override_end(&old_cfg, overridden);
|
|
cJSON_Delete(body);
|
|
reply_error(c, 500, "failed to initialize conversation messages");
|
|
return;
|
|
}
|
|
|
|
if (append_simple_message_local(convo, "user", message->valuestring) != 0) {
|
|
cJSON_Delete(convo);
|
|
maybe_model_override_end(&old_cfg, overridden);
|
|
cJSON_Delete(body);
|
|
reply_error(c, 500, "failed to append user message");
|
|
return;
|
|
}
|
|
|
|
cJSON* live_user_msg = cJSON_GetArrayItem(convo, cJSON_GetArraySize(convo) - 1);
|
|
if (live_user_msg && cJSON_IsObject(live_user_msg)) {
|
|
cJSON_AddNumberToObject(live_user_msg, "_ts", (double)time(NULL));
|
|
}
|
|
|
|
result = run_prompt_with_tools_convo(convo,
|
|
max_turns,
|
|
"http_api_agent",
|
|
"I hit my tool-use limit for this request.");
|
|
cJSON_Delete(convo);
|
|
|
|
maybe_model_override_end(&old_cfg, overridden);
|
|
cJSON_Delete(body);
|
|
|
|
if (!result) {
|
|
reply_error(c, 500, "failed to run prompt");
|
|
return;
|
|
}
|
|
|
|
reply_json(c, 200, result);
|
|
cJSON_Delete(result);
|
|
}
|
|
|
|
static cJSON* run_variant(cJSON* variant) {
|
|
if (!variant || !cJSON_IsObject(variant)) return NULL;
|
|
cJSON* body_copy = cJSON_Duplicate(variant, 1);
|
|
if (!body_copy) return NULL;
|
|
|
|
llm_config_t old_cfg;
|
|
int overridden = 0;
|
|
char* model_err = maybe_model_override_begin(body_copy, &old_cfg, &overridden);
|
|
if (model_err) {
|
|
cJSON* err = cJSON_CreateObject();
|
|
if (err) {
|
|
cJSON_AddBoolToObject(err, "success", 0);
|
|
cJSON_AddStringToObject(err, "error", model_err);
|
|
}
|
|
cJSON_Delete(body_copy);
|
|
return err;
|
|
}
|
|
|
|
cJSON* out = run_prompt_with_tools(body_copy);
|
|
maybe_model_override_end(&old_cfg, overridden);
|
|
cJSON_Delete(body_copy);
|
|
return out;
|
|
}
|
|
|
|
static void handle_prompt_compare(struct mg_connection* c, const struct mg_http_message* hm) {
|
|
cJSON* body = parse_body_json(hm);
|
|
if (!body || !cJSON_IsObject(body)) {
|
|
cJSON_Delete(body);
|
|
reply_error(c, 400, "invalid JSON body");
|
|
return;
|
|
}
|
|
|
|
cJSON* a = cJSON_GetObjectItemCaseSensitive(body, "variant_a");
|
|
cJSON* b = cJSON_GetObjectItemCaseSensitive(body, "variant_b");
|
|
if (!a || !b || !cJSON_IsObject(a) || !cJSON_IsObject(b)) {
|
|
cJSON_Delete(body);
|
|
reply_error(c, 400, "variant_a and variant_b objects are required");
|
|
return;
|
|
}
|
|
|
|
cJSON* out_a = run_variant(a);
|
|
cJSON* out_b = run_variant(b);
|
|
|
|
if (!out_a || !out_b) {
|
|
cJSON_Delete(out_a);
|
|
cJSON_Delete(out_b);
|
|
cJSON_Delete(body);
|
|
reply_error(c, 500, "failed to run one or more variants");
|
|
return;
|
|
}
|
|
|
|
cJSON* root = cJSON_CreateObject();
|
|
if (!root) {
|
|
cJSON_Delete(out_a);
|
|
cJSON_Delete(out_b);
|
|
cJSON_Delete(body);
|
|
reply_error(c, 500, "oom");
|
|
return;
|
|
}
|
|
|
|
cJSON_AddBoolToObject(root, "success", 1);
|
|
cJSON_AddItemToObject(root, "variant_a", out_a);
|
|
cJSON_AddItemToObject(root, "variant_b", out_b);
|
|
|
|
cJSON_Delete(body);
|
|
reply_json(c, 200, root);
|
|
cJSON_Delete(root);
|
|
}
|
|
|
|
static void http_handler(struct mg_connection* c, int ev, void* ev_data) {
|
|
if (ev == MG_EV_ACCEPT) {
|
|
if (g_api_tls_enabled) {
|
|
struct mg_tls_opts opts;
|
|
memset(&opts, 0, sizeof(opts));
|
|
opts.cert = g_tls_cert;
|
|
opts.key = g_tls_key;
|
|
mg_tls_init(c, &opts);
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (ev != MG_EV_HTTP_MSG) {
|
|
return;
|
|
}
|
|
|
|
struct mg_http_message* hm = (struct mg_http_message*)ev_data;
|
|
|
|
if (method_is(hm, "OPTIONS")) {
|
|
mg_http_reply(c, 204, k_json_headers, "");
|
|
return;
|
|
}
|
|
|
|
if (method_is(hm, "GET") && mg_match(hm->uri, mg_str("/api/status"), NULL)) {
|
|
handle_status(c);
|
|
return;
|
|
}
|
|
if (method_is(hm, "GET") && mg_match(hm->uri, mg_str("/api/context/current"), NULL)) {
|
|
handle_context_current(c);
|
|
return;
|
|
}
|
|
if (method_is(hm, "GET") && mg_match(hm->uri, mg_str("/api/context/parts"), NULL)) {
|
|
handle_context_parts(c);
|
|
return;
|
|
}
|
|
if (method_is(hm, "POST") && mg_match(hm->uri, mg_str("/api/prompt/run-simple"), NULL)) {
|
|
handle_prompt_run_simple(c, hm);
|
|
return;
|
|
}
|
|
if (method_is(hm, "POST") && mg_match(hm->uri, mg_str("/api/prompt/run"), NULL)) {
|
|
handle_prompt_run(c, hm);
|
|
return;
|
|
}
|
|
if (method_is(hm, "POST") && mg_match(hm->uri, mg_str("/api/prompt/agent"), NULL)) {
|
|
handle_prompt_agent(c, hm);
|
|
return;
|
|
}
|
|
if (method_is(hm, "POST") && mg_match(hm->uri, mg_str("/api/prompt/compare"), NULL)) {
|
|
handle_prompt_compare(c, hm);
|
|
return;
|
|
}
|
|
if (method_is(hm, "POST") && mg_match(hm->uri, mg_str("/api/trigger/*"), NULL)) {
|
|
char d_tag[TRIGGER_SKILL_D_TAG_MAX] = {0};
|
|
if (uri_extract_after_prefix(&hm->uri, "/api/trigger/", d_tag, sizeof(d_tag)) != 0) {
|
|
reply_error(c, 400, "missing or invalid d_tag");
|
|
return;
|
|
}
|
|
handle_trigger_webhook(c, hm, d_tag);
|
|
return;
|
|
}
|
|
|
|
reply_error(c, 404, "not found");
|
|
}
|
|
|
|
int http_api_init(const http_api_context_t* ctx) {
|
|
if (!ctx || !ctx->cfg || !ctx->tools_ctx) {
|
|
return -1;
|
|
}
|
|
|
|
g_api_ctx = *ctx;
|
|
|
|
mg_mgr_init(&g_mgr);
|
|
|
|
g_api_tls_enabled = 0;
|
|
g_tls_cert = mg_file_read(&mg_fs_posix, "/home/teknari/.ssl_for_local_servers/cert.pem");
|
|
g_tls_key = mg_file_read(&mg_fs_posix, "/home/teknari/.ssl_for_local_servers/key.pem");
|
|
if (g_tls_cert.buf && g_tls_cert.len > 0 && g_tls_key.buf && g_tls_key.len > 0) {
|
|
g_api_tls_enabled = 1;
|
|
} else {
|
|
if (g_tls_cert.buf) mg_free((void*)g_tls_cert.buf);
|
|
if (g_tls_key.buf) mg_free((void*)g_tls_key.buf);
|
|
g_tls_cert.buf = NULL;
|
|
g_tls_cert.len = 0;
|
|
g_tls_key.buf = NULL;
|
|
g_tls_key.len = 0;
|
|
}
|
|
|
|
char listen_url[320];
|
|
snprintf(listen_url,
|
|
sizeof(listen_url),
|
|
"%s://%s:%d",
|
|
g_api_tls_enabled ? "https" : "http",
|
|
g_api_ctx.cfg->api.bind_address[0] ? g_api_ctx.cfg->api.bind_address : "127.0.0.1",
|
|
g_api_ctx.cfg->api.port > 0 ? g_api_ctx.cfg->api.port : 8484);
|
|
|
|
g_listener = mg_http_listen(&g_mgr, listen_url, http_handler, NULL);
|
|
if (!g_listener) {
|
|
mg_mgr_free(&g_mgr);
|
|
memset(&g_mgr, 0, sizeof(g_mgr));
|
|
return -1;
|
|
}
|
|
|
|
g_api_initialized = 1;
|
|
DEBUG_INFO("[didactyl] http api listening on %s", listen_url);
|
|
return 0;
|
|
}
|
|
|
|
int http_api_poll(int timeout_ms) {
|
|
if (!g_api_initialized) {
|
|
return 0;
|
|
}
|
|
|
|
if (timeout_ms < 0) timeout_ms = 0;
|
|
mg_mgr_poll(&g_mgr, timeout_ms);
|
|
return 0;
|
|
}
|
|
|
|
void http_api_cleanup(void) {
|
|
if (!g_api_initialized) {
|
|
return;
|
|
}
|
|
|
|
mg_mgr_free(&g_mgr);
|
|
memset(&g_mgr, 0, sizeof(g_mgr));
|
|
g_listener = NULL;
|
|
|
|
if (g_tls_cert.buf) mg_free((void*)g_tls_cert.buf);
|
|
if (g_tls_key.buf) mg_free((void*)g_tls_key.buf);
|
|
g_tls_cert.buf = NULL;
|
|
g_tls_cert.len = 0;
|
|
g_tls_key.buf = NULL;
|
|
g_tls_key.len = 0;
|
|
g_api_tls_enabled = 0;
|
|
|
|
memset(&g_api_ctx, 0, sizeof(g_api_ctx));
|
|
g_api_initialized = 0;
|
|
} |