Files
didactyl/src/agent.c
T

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;
}