676 lines
22 KiB
C
676 lines
22 KiB
C
#define _POSIX_C_SOURCE 200809L
|
|
|
|
#include "agent.h"
|
|
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <stdint.h>
|
|
#include <time.h>
|
|
#include <pthread.h>
|
|
|
|
#include "llm.h"
|
|
#include "nostr_handler.h"
|
|
#include "tools.h"
|
|
#include "cjson/cJSON.h"
|
|
#include "debug.h"
|
|
#include "../../nostr_core_lib/nostr_core/nostr_core.h"
|
|
|
|
static didactyl_config_t* g_cfg = NULL;
|
|
static char* g_system_context = NULL;
|
|
static tools_context_t g_tools_ctx;
|
|
|
|
#define AGENT_DEBOUNCE_WINDOW_SECONDS 5
|
|
#define AGENT_DEBOUNCE_CACHE_SIZE 256
|
|
#define AGENT_HISTORY_TURNS 12
|
|
#define AGENT_HISTORY_QUERY_LIMIT 200
|
|
|
|
typedef struct {
|
|
uint64_t fingerprint;
|
|
time_t seen_at;
|
|
} agent_seen_msg_t;
|
|
|
|
static agent_seen_msg_t g_seen_msgs[AGENT_DEBOUNCE_CACHE_SIZE];
|
|
static int g_seen_msgs_count = 0;
|
|
static int g_seen_msgs_next = 0;
|
|
static pthread_mutex_t g_seen_msgs_mutex = PTHREAD_MUTEX_INITIALIZER;
|
|
|
|
static uint64_t fnv1a64(const char* s) {
|
|
uint64_t h = 1469598103934665603ULL;
|
|
if (!s) return h;
|
|
while (*s) {
|
|
h ^= (unsigned char)(*s++);
|
|
h *= 1099511628211ULL;
|
|
}
|
|
return h;
|
|
}
|
|
|
|
static uint64_t message_fingerprint(const char* sender_pubkey_hex, const char* message) {
|
|
uint64_t a = fnv1a64(sender_pubkey_hex);
|
|
uint64_t b = fnv1a64(message);
|
|
return a ^ (b + 0x9e3779b97f4a7c15ULL + (a << 6) + (a >> 2));
|
|
}
|
|
|
|
static int agent_message_is_debounced(const char* sender_pubkey_hex, const char* message) {
|
|
time_t now = time(NULL);
|
|
uint64_t fp = message_fingerprint(sender_pubkey_hex, message);
|
|
int duplicate = 0;
|
|
|
|
pthread_mutex_lock(&g_seen_msgs_mutex);
|
|
|
|
for (int i = 0; i < g_seen_msgs_count; i++) {
|
|
if (g_seen_msgs[i].fingerprint == fp &&
|
|
g_seen_msgs[i].seen_at > 0 &&
|
|
(now - g_seen_msgs[i].seen_at) <= AGENT_DEBOUNCE_WINDOW_SECONDS) {
|
|
duplicate = 1;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!duplicate) {
|
|
int slot = 0;
|
|
if (g_seen_msgs_count < AGENT_DEBOUNCE_CACHE_SIZE) {
|
|
slot = g_seen_msgs_count;
|
|
g_seen_msgs_count++;
|
|
} else {
|
|
slot = g_seen_msgs_next;
|
|
g_seen_msgs_next = (g_seen_msgs_next + 1) % AGENT_DEBOUNCE_CACHE_SIZE;
|
|
}
|
|
g_seen_msgs[slot].fingerprint = fp;
|
|
g_seen_msgs[slot].seen_at = now;
|
|
}
|
|
|
|
pthread_mutex_unlock(&g_seen_msgs_mutex);
|
|
return duplicate;
|
|
}
|
|
|
|
static int append_simple_message(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(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(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 void append_context_log(const char* sender_pubkey_hex, const char* phase, const char* context_payload) {
|
|
FILE* fp = fopen("context.log", "a");
|
|
if (!fp) {
|
|
return;
|
|
}
|
|
|
|
time_t now = time(NULL);
|
|
struct tm tm_info;
|
|
localtime_r(&now, &tm_info);
|
|
char timestamp[32] = {0};
|
|
strftime(timestamp, sizeof(timestamp), "%Y-%m-%d %H:%M:%S", &tm_info);
|
|
|
|
fprintf(fp,
|
|
"[%s] phase=%s sender=%s\n%s\n\n---\n\n",
|
|
timestamp,
|
|
phase ? phase : "unknown",
|
|
sender_pubkey_hex ? sender_pubkey_hex : "unknown",
|
|
context_payload ? context_payload : "");
|
|
fclose(fp);
|
|
}
|
|
|
|
typedef struct {
|
|
time_t created_at;
|
|
int role_is_user;
|
|
char* content;
|
|
} agent_history_item_t;
|
|
|
|
static int extract_first_p_tag_local(cJSON* tags, char out_pubkey_hex[65]) {
|
|
if (!tags || !cJSON_IsArray(tags) || !out_pubkey_hex) {
|
|
return -1;
|
|
}
|
|
|
|
int n = cJSON_GetArraySize(tags);
|
|
for (int i = 0; i < n; i++) {
|
|
cJSON* tag = cJSON_GetArrayItem(tags, i);
|
|
if (!tag || !cJSON_IsArray(tag) || cJSON_GetArraySize(tag) < 2) {
|
|
continue;
|
|
}
|
|
|
|
cJSON* key = cJSON_GetArrayItem(tag, 0);
|
|
cJSON* val = cJSON_GetArrayItem(tag, 1);
|
|
if (!key || !val || !cJSON_IsString(key) || !cJSON_IsString(val) || !val->valuestring) {
|
|
continue;
|
|
}
|
|
|
|
if (strcmp(key->valuestring, "p") == 0 && strlen(val->valuestring) == 64U) {
|
|
memcpy(out_pubkey_hex, val->valuestring, 64U);
|
|
out_pubkey_hex[64] = '\0';
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
return -1;
|
|
}
|
|
|
|
static int history_item_cmp_created_at(const void* a, const void* b) {
|
|
const agent_history_item_t* ia = (const agent_history_item_t*)a;
|
|
const agent_history_item_t* ib = (const agent_history_item_t*)b;
|
|
if (ia->created_at < ib->created_at) return -1;
|
|
if (ia->created_at > ib->created_at) return 1;
|
|
return 0;
|
|
}
|
|
|
|
static void free_history_items(agent_history_item_t* items, int count) {
|
|
if (!items) return;
|
|
for (int i = 0; i < count; i++) {
|
|
free(items[i].content);
|
|
}
|
|
free(items);
|
|
}
|
|
|
|
static int append_startup_events_context(cJSON* messages) {
|
|
if (!messages || !g_cfg || g_cfg->startup_event_count <= 0 || !g_cfg->startup_events) {
|
|
return 0;
|
|
}
|
|
|
|
cJSON* arr = cJSON_CreateArray();
|
|
if (!arr) {
|
|
return -1;
|
|
}
|
|
|
|
for (int i = 0; i < g_cfg->startup_event_count; i++) {
|
|
startup_event_t* se = &g_cfg->startup_events[i];
|
|
cJSON* item = cJSON_CreateObject();
|
|
if (!item) {
|
|
cJSON_Delete(arr);
|
|
return -1;
|
|
}
|
|
|
|
cJSON_AddNumberToObject(item, "kind", se->kind);
|
|
cJSON_AddStringToObject(item, "content", se->content ? se->content : "");
|
|
|
|
cJSON* tags = NULL;
|
|
if (se->tags_json) {
|
|
tags = cJSON_Parse(se->tags_json);
|
|
}
|
|
if (!tags || !cJSON_IsArray(tags)) {
|
|
cJSON_Delete(tags);
|
|
tags = cJSON_CreateArray();
|
|
}
|
|
cJSON_AddItemToObject(item, "tags", tags);
|
|
cJSON_AddItemToArray(arr, item);
|
|
}
|
|
|
|
char* events_json = cJSON_PrintUnformatted(arr);
|
|
cJSON_Delete(arr);
|
|
if (!events_json) {
|
|
return -1;
|
|
}
|
|
|
|
const char* prefix = "Startup events memory (kinds/content/tags): ";
|
|
size_t out_len = strlen(prefix) + strlen(events_json) + 1U;
|
|
char* payload = (char*)malloc(out_len);
|
|
if (!payload) {
|
|
free(events_json);
|
|
return -1;
|
|
}
|
|
|
|
snprintf(payload, out_len, "%s%s", prefix, events_json);
|
|
free(events_json);
|
|
|
|
int rc = append_simple_message(messages, "system", payload);
|
|
free(payload);
|
|
return rc;
|
|
}
|
|
|
|
static int append_admin_identity_context(cJSON* messages) {
|
|
if (!messages || !g_cfg) {
|
|
return -1;
|
|
}
|
|
|
|
char admin_header[256];
|
|
snprintf(admin_header,
|
|
sizeof(admin_header),
|
|
"This is your administrator! Admin pubkey (hex): %s",
|
|
g_cfg->admin.pubkey ? g_cfg->admin.pubkey : "unknown");
|
|
if (append_simple_message(messages, "system", admin_header) != 0) {
|
|
return -1;
|
|
}
|
|
|
|
char* kind0 = nostr_handler_get_admin_kind0_context();
|
|
if (kind0) {
|
|
const char* prefix = "Administrator kind 0 profile content (JSON): ";
|
|
size_t n = strlen(prefix) + strlen(kind0) + 1U;
|
|
char* payload = (char*)malloc(n);
|
|
if (!payload) {
|
|
free(kind0);
|
|
return -1;
|
|
}
|
|
snprintf(payload, n, "%s%s", prefix, kind0);
|
|
int rc = append_simple_message(messages, "system", payload);
|
|
free(payload);
|
|
free(kind0);
|
|
if (rc != 0) {
|
|
return -1;
|
|
}
|
|
}
|
|
|
|
char* kind10002 = nostr_handler_get_admin_kind10002_context();
|
|
if (kind10002) {
|
|
const char* prefix = "Administrator kind 10002 relay-list content (JSON): ";
|
|
size_t n = strlen(prefix) + strlen(kind10002) + 1U;
|
|
char* payload = (char*)malloc(n);
|
|
if (!payload) {
|
|
free(kind10002);
|
|
return -1;
|
|
}
|
|
snprintf(payload, n, "%s%s", prefix, kind10002);
|
|
int rc = append_simple_message(messages, "system", payload);
|
|
free(payload);
|
|
free(kind10002);
|
|
if (rc != 0) {
|
|
return -1;
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int append_recent_admin_dm_history(cJSON* messages, const char* current_message) {
|
|
if (!messages || !g_cfg) {
|
|
return -1;
|
|
}
|
|
|
|
unsigned char admin_pubkey[32];
|
|
if (nostr_hex_to_bytes(g_cfg->admin.pubkey, admin_pubkey, 32) != 0) {
|
|
return -1;
|
|
}
|
|
|
|
cJSON* filter = cJSON_CreateObject();
|
|
cJSON* kinds = cJSON_CreateArray();
|
|
cJSON* authors = cJSON_CreateArray();
|
|
if (!filter || !kinds || !authors) {
|
|
cJSON_Delete(filter);
|
|
cJSON_Delete(kinds);
|
|
cJSON_Delete(authors);
|
|
return -1;
|
|
}
|
|
|
|
cJSON_AddItemToArray(kinds, cJSON_CreateNumber(4));
|
|
cJSON_AddItemToObject(filter, "kinds", kinds);
|
|
cJSON_AddItemToArray(authors, cJSON_CreateString(g_cfg->admin.pubkey));
|
|
cJSON_AddItemToArray(authors, cJSON_CreateString(g_cfg->keys.public_key_hex));
|
|
cJSON_AddItemToObject(filter, "authors", authors);
|
|
cJSON_AddNumberToObject(filter, "limit", AGENT_HISTORY_QUERY_LIMIT);
|
|
|
|
char* events_json = nostr_handler_query_json(filter, 8000);
|
|
cJSON_Delete(filter);
|
|
if (!events_json) {
|
|
return 0;
|
|
}
|
|
|
|
cJSON* events = cJSON_Parse(events_json);
|
|
free(events_json);
|
|
if (!events || !cJSON_IsArray(events)) {
|
|
cJSON_Delete(events);
|
|
return 0;
|
|
}
|
|
|
|
agent_history_item_t* items = NULL;
|
|
int item_count = 0;
|
|
|
|
int n = cJSON_GetArraySize(events);
|
|
for (int i = 0; i < n; i++) {
|
|
cJSON* ev = cJSON_GetArrayItem(events, i);
|
|
cJSON* kind = ev ? cJSON_GetObjectItemCaseSensitive(ev, "kind") : NULL;
|
|
cJSON* pubkey = ev ? cJSON_GetObjectItemCaseSensitive(ev, "pubkey") : NULL;
|
|
cJSON* content = ev ? cJSON_GetObjectItemCaseSensitive(ev, "content") : NULL;
|
|
cJSON* tags = ev ? cJSON_GetObjectItemCaseSensitive(ev, "tags") : NULL;
|
|
cJSON* created_at = ev ? cJSON_GetObjectItemCaseSensitive(ev, "created_at") : NULL;
|
|
|
|
if (!kind || !pubkey || !content || !tags || !created_at ||
|
|
!cJSON_IsNumber(kind) || !cJSON_IsString(pubkey) ||
|
|
!cJSON_IsString(content) || !cJSON_IsArray(tags) || !cJSON_IsNumber(created_at) ||
|
|
(int)kind->valuedouble != 4 || !pubkey->valuestring || !content->valuestring) {
|
|
continue;
|
|
}
|
|
|
|
char p_tag_pubkey[65] = {0};
|
|
if (extract_first_p_tag_local(tags, p_tag_pubkey) != 0) {
|
|
continue;
|
|
}
|
|
|
|
int role_is_user = 0;
|
|
if (strcmp(pubkey->valuestring, g_cfg->admin.pubkey) == 0 &&
|
|
strcmp(p_tag_pubkey, g_cfg->keys.public_key_hex) == 0) {
|
|
role_is_user = 1;
|
|
} else if (strcmp(pubkey->valuestring, g_cfg->keys.public_key_hex) == 0 &&
|
|
strcmp(p_tag_pubkey, g_cfg->admin.pubkey) == 0) {
|
|
role_is_user = 0;
|
|
} else {
|
|
continue;
|
|
}
|
|
|
|
char* plaintext = (char*)malloc(NOSTR_NIP04_MAX_PLAINTEXT_SIZE);
|
|
if (!plaintext) {
|
|
continue;
|
|
}
|
|
plaintext[0] = '\0';
|
|
|
|
if (nostr_nip04_decrypt(g_cfg->keys.private_key,
|
|
admin_pubkey,
|
|
content->valuestring,
|
|
plaintext,
|
|
NOSTR_NIP04_MAX_PLAINTEXT_SIZE) != NOSTR_SUCCESS) {
|
|
free(plaintext);
|
|
continue;
|
|
}
|
|
|
|
agent_history_item_t* grown = (agent_history_item_t*)realloc(items, (size_t)(item_count + 1) * sizeof(agent_history_item_t));
|
|
if (!grown) {
|
|
free(plaintext);
|
|
free_history_items(items, item_count);
|
|
cJSON_Delete(events);
|
|
return -1;
|
|
}
|
|
|
|
items = grown;
|
|
items[item_count].created_at = (time_t)created_at->valuedouble;
|
|
items[item_count].role_is_user = role_is_user;
|
|
items[item_count].content = plaintext;
|
|
item_count++;
|
|
}
|
|
|
|
cJSON_Delete(events);
|
|
|
|
if (item_count > 1) {
|
|
qsort(items, (size_t)item_count, sizeof(agent_history_item_t), history_item_cmp_created_at);
|
|
}
|
|
|
|
int start = item_count > AGENT_HISTORY_TURNS ? item_count - AGENT_HISTORY_TURNS : 0;
|
|
for (int i = start; i < item_count; i++) {
|
|
if (i == item_count - 1 &&
|
|
items[i].role_is_user &&
|
|
current_message &&
|
|
strcmp(items[i].content, current_message) == 0) {
|
|
continue;
|
|
}
|
|
|
|
if (append_simple_message(messages,
|
|
items[i].role_is_user ? "user" : "assistant",
|
|
items[i].content ? items[i].content : "") != 0) {
|
|
free_history_items(items, item_count);
|
|
return -1;
|
|
}
|
|
}
|
|
|
|
free_history_items(items, item_count);
|
|
return 0;
|
|
}
|
|
|
|
int agent_init(didactyl_config_t* config, const char* system_context) {
|
|
if (!config || !system_context) {
|
|
return -1;
|
|
}
|
|
|
|
g_cfg = config;
|
|
g_system_context = strdup(system_context);
|
|
if (!g_system_context) {
|
|
return -1;
|
|
}
|
|
|
|
if (tools_init(&g_tools_ctx, g_cfg) != 0) {
|
|
free(g_system_context);
|
|
g_system_context = NULL;
|
|
g_cfg = NULL;
|
|
return -1;
|
|
}
|
|
|
|
memset(g_seen_msgs, 0, sizeof(g_seen_msgs));
|
|
g_seen_msgs_count = 0;
|
|
g_seen_msgs_next = 0;
|
|
|
|
return 0;
|
|
}
|
|
|
|
void agent_on_message(const char* sender_pubkey_hex,
|
|
const char* message,
|
|
didactyl_sender_tier_t tier,
|
|
void* user_data) {
|
|
(void)user_data;
|
|
|
|
if (!g_cfg || !g_system_context || !sender_pubkey_hex || !message) {
|
|
return;
|
|
}
|
|
|
|
if (tier == DIDACTYL_SENDER_STRANGER) {
|
|
return;
|
|
}
|
|
|
|
fprintf(stdout, "[didactyl] incoming message from %.16s... tier=%d\n", sender_pubkey_hex, (int)tier);
|
|
|
|
if (agent_message_is_debounced(sender_pubkey_hex, message)) {
|
|
fprintf(stdout, "[didactyl] debounced duplicate inbound message from %.16s...\n", sender_pubkey_hex);
|
|
return;
|
|
}
|
|
|
|
int allow_tools = (tier == DIDACTYL_SENDER_ADMIN) && g_cfg->tools.enabled && g_cfg->security.admin.tools_enabled;
|
|
|
|
if (!allow_tools) {
|
|
const char* tier_prefix = (tier == DIDACTYL_SENDER_WOT)
|
|
? "You are responding to a web-of-trust contact. Keep the response helpful and concise. Tool use is disabled for this tier."
|
|
: "You are responding in chat-only mode. Tool use is disabled.";
|
|
|
|
size_t ctx_len = strlen(g_system_context) + strlen("\n\n") + strlen(tier_prefix) + 1U;
|
|
char* system_for_chat = (char*)malloc(ctx_len);
|
|
if (!system_for_chat) {
|
|
return;
|
|
}
|
|
|
|
snprintf(system_for_chat, ctx_len, "%s\n\n%s", g_system_context, tier_prefix);
|
|
|
|
size_t context_len = strlen("system:\n\nuser:\n") + strlen(system_for_chat) + strlen(message) + 1U;
|
|
char* plain_context = (char*)malloc(context_len);
|
|
if (plain_context) {
|
|
snprintf(plain_context, context_len, "system:\n%s\n\nuser:\n%s", system_for_chat, message);
|
|
append_context_log(sender_pubkey_hex, "llm_chat", plain_context);
|
|
free(plain_context);
|
|
}
|
|
|
|
char* response = llm_chat(system_for_chat, message);
|
|
free(system_for_chat);
|
|
if (!response) {
|
|
const char* fallback = "I could not get a response from the LLM right now.";
|
|
fprintf(stdout, "[didactyl] llm response unavailable, sending fallback\n");
|
|
(void)nostr_handler_send_dm(sender_pubkey_hex, fallback);
|
|
return;
|
|
}
|
|
|
|
fprintf(stdout, "[didactyl] llm response: %.240s%s\n",
|
|
response,
|
|
strlen(response) > 240 ? "..." : "");
|
|
(void)nostr_handler_send_dm(sender_pubkey_hex, response);
|
|
free(response);
|
|
return;
|
|
}
|
|
|
|
char* tools_json = tools_build_openai_schema_json(&g_tools_ctx);
|
|
if (!tools_json) {
|
|
(void)nostr_handler_send_dm(sender_pubkey_hex, "Tool schema generation failed.");
|
|
return;
|
|
}
|
|
|
|
cJSON* messages = cJSON_CreateArray();
|
|
if (!messages) {
|
|
free(tools_json);
|
|
(void)nostr_handler_send_dm(sender_pubkey_hex, "Failed to initialize conversation state.");
|
|
return;
|
|
}
|
|
|
|
if (append_simple_message(messages, "system", g_system_context) != 0 ||
|
|
append_admin_identity_context(messages) != 0 ||
|
|
append_startup_events_context(messages) != 0 ||
|
|
append_recent_admin_dm_history(messages, message) != 0) {
|
|
cJSON_Delete(messages);
|
|
free(tools_json);
|
|
(void)nostr_handler_send_dm(sender_pubkey_hex, "Failed to initialize conversation messages.");
|
|
return;
|
|
}
|
|
|
|
char* admin_notes = nostr_handler_get_admin_kind1_notes_context();
|
|
if (admin_notes) {
|
|
(void)append_simple_message(messages, "system", admin_notes);
|
|
free(admin_notes);
|
|
}
|
|
|
|
if (append_simple_message(messages, "user", message) != 0) {
|
|
cJSON_Delete(messages);
|
|
free(tools_json);
|
|
(void)nostr_handler_send_dm(sender_pubkey_hex, "Failed to initialize conversation messages.");
|
|
return;
|
|
}
|
|
|
|
int max_turns = g_cfg->tools.max_turns > 0 ? g_cfg->tools.max_turns : 8;
|
|
char* final_answer_owned = NULL;
|
|
|
|
for (int turn = 0; turn < max_turns; turn++) {
|
|
char* messages_json = cJSON_PrintUnformatted(messages);
|
|
if (!messages_json) {
|
|
break;
|
|
}
|
|
|
|
append_context_log(sender_pubkey_hex, "llm_chat_with_tools_messages", messages_json);
|
|
|
|
llm_response_t resp;
|
|
int rc = llm_chat_with_tools_messages(messages_json, tools_json, "auto", &resp);
|
|
free(messages_json);
|
|
if (rc != 0) {
|
|
(void)nostr_handler_send_dm(sender_pubkey_hex, "LLM request failed.");
|
|
cJSON_Delete(messages);
|
|
free(tools_json);
|
|
return;
|
|
}
|
|
|
|
if (resp.tool_call_count <= 0) {
|
|
const char* answer = resp.content ? resp.content : "No response content.";
|
|
fprintf(stdout, "[didactyl] llm response (no tool call): %.240s%s\n",
|
|
answer,
|
|
strlen(answer) > 240 ? "..." : "");
|
|
final_answer_owned = strdup(answer);
|
|
llm_response_free(&resp);
|
|
break;
|
|
}
|
|
|
|
if (append_assistant_tool_calls_message(messages, &resp) != 0) {
|
|
llm_response_free(&resp);
|
|
break;
|
|
}
|
|
|
|
for (int i = 0; i < resp.tool_call_count; i++) {
|
|
llm_tool_call_t* tc = &resp.tool_calls[i];
|
|
fprintf(stdout, "[didactyl] executing tool call: %s\n", tc->name ? tc->name : "<null>");
|
|
DEBUG_TRACE("[didactyl] tool call args: %s", tc->arguments_json ? tc->arguments_json : "{}");
|
|
|
|
char* tool_result = tools_execute(&g_tools_ctx, tc->name, tc->arguments_json);
|
|
if (!tool_result) {
|
|
tool_result = strdup("{\"success\":false,\"error\":\"tool execution failed\"}");
|
|
}
|
|
|
|
DEBUG_TRACE("[didactyl] tool call result: %s", tool_result ? tool_result : "{\"success\":false,\"error\":\"tool execution failed\"}");
|
|
|
|
if (append_tool_result_message(messages,
|
|
tc->id ? tc->id : "",
|
|
tool_result ? tool_result : "{\"success\":false,\"error\":\"tool execution failed\"}") != 0) {
|
|
free(tool_result);
|
|
llm_response_free(&resp);
|
|
cJSON_Delete(messages);
|
|
free(tools_json);
|
|
(void)nostr_handler_send_dm(sender_pubkey_hex, "Failed to append tool result.");
|
|
return;
|
|
}
|
|
free(tool_result);
|
|
}
|
|
|
|
llm_response_free(&resp);
|
|
}
|
|
|
|
if (!final_answer_owned) {
|
|
final_answer_owned = strdup("I hit my tool-use limit for this request.");
|
|
}
|
|
|
|
const char* final_answer = final_answer_owned ? final_answer_owned : "I hit my tool-use limit for this request.";
|
|
fprintf(stdout, "[didactyl] final response: %.240s%s\n",
|
|
final_answer,
|
|
strlen(final_answer) > 240 ? "..." : "");
|
|
(void)nostr_handler_send_dm(sender_pubkey_hex, final_answer);
|
|
|
|
free(final_answer_owned);
|
|
cJSON_Delete(messages);
|
|
free(tools_json);
|
|
}
|
|
|
|
void agent_cleanup(void) {
|
|
tools_cleanup(&g_tools_ctx);
|
|
free(g_system_context);
|
|
g_system_context = NULL;
|
|
g_cfg = NULL;
|
|
memset(g_seen_msgs, 0, sizeof(g_seen_msgs));
|
|
g_seen_msgs_count = 0;
|
|
g_seen_msgs_next = 0;
|
|
}
|