357 lines
11 KiB
C
357 lines
11 KiB
C
#define _POSIX_C_SOURCE 200809L
|
|
|
|
#include <signal.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <time.h>
|
|
|
|
#include "../../nostr_core_lib/nostr_core/nostr_core.h"
|
|
#include "main.h"
|
|
#include "agent.h"
|
|
#include "config.h"
|
|
#include "llm.h"
|
|
#include "nostr_handler.h"
|
|
#include "debug.h"
|
|
#include "trigger_manager.h"
|
|
#include "tools.h"
|
|
#include "cjson/cJSON.h"
|
|
#include "http_api.h"
|
|
|
|
static volatile sig_atomic_t g_running = 1;
|
|
|
|
static void signal_handler(int signum) {
|
|
(void)signum;
|
|
g_running = 0;
|
|
}
|
|
|
|
static void print_usage(const char* prog) {
|
|
fprintf(stderr,
|
|
"Usage: %s [-h|--help] [-v|--version] [--config <path>] [--debug <0-5>] [--dump-schemas] [--test-tool <name> <args_json>]\n",
|
|
prog ? prog : "didactyl");
|
|
}
|
|
|
|
static int tool_result_success(const char* json) {
|
|
if (!json) {
|
|
return 0;
|
|
}
|
|
|
|
cJSON* root = cJSON_Parse(json);
|
|
if (!root || !cJSON_IsObject(root)) {
|
|
cJSON_Delete(root);
|
|
return 0;
|
|
}
|
|
|
|
cJSON* success = cJSON_GetObjectItemCaseSensitive(root, "success");
|
|
int ok = (success && cJSON_IsBool(success) && cJSON_IsTrue(success)) ? 1 : 0;
|
|
cJSON_Delete(root);
|
|
return ok;
|
|
}
|
|
|
|
static int wait_for_connected_relays(int timeout_ms) {
|
|
if (timeout_ms <= 0) {
|
|
return nostr_handler_connected_relay_count() > 0 ? 1 : 0;
|
|
}
|
|
|
|
const int step_ms = 100;
|
|
int elapsed_ms = 0;
|
|
|
|
while (elapsed_ms < timeout_ms) {
|
|
if (nostr_handler_connected_relay_count() > 0) {
|
|
return 1;
|
|
}
|
|
|
|
(void)nostr_handler_poll(step_ms);
|
|
elapsed_ms += step_ms;
|
|
}
|
|
|
|
return nostr_handler_connected_relay_count() > 0 ? 1 : 0;
|
|
}
|
|
|
|
int main(int argc, char** argv) {
|
|
const char* config_path = "./config.jsonc";
|
|
int debug_level = DEBUG_LEVEL_TRACE;
|
|
int dump_schemas = 0;
|
|
const char* test_tool_name = NULL;
|
|
const char* test_tool_args = "{}";
|
|
|
|
for (int i = 1; i < argc; i++) {
|
|
if (strcmp(argv[i], "-h") == 0 || strcmp(argv[i], "--help") == 0) {
|
|
print_usage(argv[0]);
|
|
return 0;
|
|
} else if (strcmp(argv[i], "-v") == 0 || strcmp(argv[i], "--version") == 0) {
|
|
printf("%s %s\n", DIDACTYL_NAME, DIDACTYL_VERSION);
|
|
return 0;
|
|
} else if (strcmp(argv[i], "--config") == 0 && i + 1 < argc) {
|
|
config_path = argv[++i];
|
|
} else if (strcmp(argv[i], "--debug") == 0 && i + 1 < argc) {
|
|
debug_level = atoi(argv[++i]);
|
|
} else if (strcmp(argv[i], "--dump-schemas") == 0) {
|
|
dump_schemas = 1;
|
|
} else if (strcmp(argv[i], "--test-tool") == 0 && i + 2 < argc) {
|
|
test_tool_name = argv[++i];
|
|
test_tool_args = argv[++i];
|
|
} else {
|
|
print_usage(argv[0]);
|
|
return 1;
|
|
}
|
|
}
|
|
|
|
debug_init(debug_level);
|
|
|
|
DEBUG_INFO("%s %s starting", DIDACTYL_NAME, DIDACTYL_VERSION);
|
|
|
|
if (nostr_init() != NOSTR_SUCCESS) {
|
|
fprintf(stderr, "Failed to initialize nostr core\n");
|
|
return 1;
|
|
}
|
|
|
|
didactyl_config_t cfg;
|
|
if (config_load(config_path, &cfg) != 0) {
|
|
fprintf(stderr, "Failed to load config: %s\n", config_path);
|
|
fprintf(stderr, "Config error: %s\n", config_last_error());
|
|
nostr_cleanup();
|
|
return 1;
|
|
}
|
|
|
|
if (dump_schemas || test_tool_name) {
|
|
if (llm_init(&cfg.llm) != 0) {
|
|
fprintf(stderr, "Failed to initialize llm client\n");
|
|
config_free(&cfg);
|
|
nostr_cleanup();
|
|
return 1;
|
|
}
|
|
|
|
if (nostr_handler_init(&cfg) != 0) {
|
|
fprintf(stderr, "Failed to initialize nostr handler\n");
|
|
config_free(&cfg);
|
|
llm_cleanup();
|
|
nostr_cleanup();
|
|
return 1;
|
|
}
|
|
|
|
tools_context_t tools_ctx;
|
|
if (tools_init(&tools_ctx, &cfg) != 0) {
|
|
fprintf(stderr, "Failed to initialize tools context\n");
|
|
nostr_handler_cleanup();
|
|
config_free(&cfg);
|
|
llm_cleanup();
|
|
nostr_cleanup();
|
|
return 1;
|
|
}
|
|
|
|
int exit_code = 0;
|
|
|
|
if (test_tool_name) {
|
|
const int timeout_ms = 15000;
|
|
if (!wait_for_connected_relays(timeout_ms)) {
|
|
fprintf(stderr,
|
|
"No relay connection established within %d ms; refusing to run --test-tool '%s'\n",
|
|
timeout_ms,
|
|
test_tool_name);
|
|
tools_cleanup(&tools_ctx);
|
|
nostr_handler_cleanup();
|
|
config_free(&cfg);
|
|
llm_cleanup();
|
|
nostr_cleanup();
|
|
return 1;
|
|
}
|
|
}
|
|
|
|
if (dump_schemas) {
|
|
char* schemas = tools_build_openai_schema_json(&tools_ctx);
|
|
if (!schemas) {
|
|
fprintf(stderr, "Failed to build tool schemas\n");
|
|
exit_code = 1;
|
|
} else {
|
|
printf("%s\n", schemas);
|
|
free(schemas);
|
|
}
|
|
} else {
|
|
char* result = tools_execute(&tools_ctx, test_tool_name, test_tool_args);
|
|
if (!result) {
|
|
fprintf(stderr, "Failed to execute tool\n");
|
|
exit_code = 1;
|
|
} else {
|
|
printf("%s\n", result);
|
|
exit_code = tool_result_success(result) ? 0 : 1;
|
|
free(result);
|
|
}
|
|
}
|
|
|
|
tools_cleanup(&tools_ctx);
|
|
nostr_handler_cleanup();
|
|
config_free(&cfg);
|
|
llm_cleanup();
|
|
nostr_cleanup();
|
|
return exit_code;
|
|
}
|
|
|
|
if (llm_init(&cfg.llm) != 0) {
|
|
fprintf(stderr, "Failed to initialize llm client\n");
|
|
config_free(&cfg);
|
|
nostr_cleanup();
|
|
return 1;
|
|
}
|
|
|
|
if (nostr_handler_init(&cfg) != 0) {
|
|
fprintf(stderr, "Failed to initialize nostr handler\n");
|
|
llm_cleanup();
|
|
config_free(&cfg);
|
|
nostr_cleanup();
|
|
return 1;
|
|
}
|
|
|
|
if (nostr_handler_reconcile_startup_events() != 0) {
|
|
DEBUG_WARN("[didactyl] startup phase: reconcile startup events failed (continuing)");
|
|
}
|
|
|
|
const char* system_context = nostr_handler_get_system_context();
|
|
if (!system_context || system_context[0] == '\0') {
|
|
system_context = "You are Didactyl, a sovereign AI agent living on Nostr.";
|
|
}
|
|
|
|
if (agent_init(&cfg, system_context) != 0) {
|
|
fprintf(stderr, "Failed to initialize agent\n");
|
|
nostr_handler_cleanup();
|
|
llm_cleanup();
|
|
config_free(&cfg);
|
|
nostr_cleanup();
|
|
return 1;
|
|
}
|
|
|
|
trigger_manager_t trigger_manager;
|
|
if (trigger_manager_init(&trigger_manager, &cfg) != 0) {
|
|
fprintf(stderr, "Failed to initialize trigger manager\n");
|
|
agent_cleanup();
|
|
nostr_handler_cleanup();
|
|
llm_cleanup();
|
|
config_free(&cfg);
|
|
nostr_cleanup();
|
|
return 1;
|
|
}
|
|
agent_set_trigger_manager(&trigger_manager);
|
|
|
|
if (trigger_manager_load_from_skills(&trigger_manager) != 0) {
|
|
DEBUG_WARN("[didactyl] startup phase: trigger manager load from skills failed (continuing)");
|
|
}
|
|
|
|
DEBUG_INFO("[didactyl] startup phase: subscribe admin context begin");
|
|
if (nostr_handler_subscribe_admin_context() != 0) {
|
|
DEBUG_WARN("[didactyl] startup phase: subscribe admin context failed (continuing)");
|
|
}
|
|
DEBUG_INFO("[didactyl] startup phase: subscribe admin context end");
|
|
|
|
DEBUG_INFO("[didactyl] startup phase: subscribe self skill cache begin");
|
|
if (nostr_handler_subscribe_self_skills() != 0) {
|
|
DEBUG_WARN("[didactyl] startup phase: subscribe self skill cache failed (continuing)");
|
|
}
|
|
DEBUG_INFO("[didactyl] startup phase: subscribe self skill cache end");
|
|
|
|
char startup_dm[768];
|
|
const char* startup_name = nostr_handler_get_startup_display_name();
|
|
int connected_relays = nostr_handler_connected_relay_count();
|
|
time_t startup_now = time(NULL);
|
|
struct tm startup_tm;
|
|
char startup_time_str[32] = {0};
|
|
if (localtime_r(&startup_now, &startup_tm)) {
|
|
(void)strftime(startup_time_str, sizeof(startup_time_str), "%Y-%m-%d %H:%M:%S", &startup_tm);
|
|
} else {
|
|
snprintf(startup_time_str, sizeof(startup_time_str), "%ld", (long)startup_now);
|
|
}
|
|
snprintf(startup_dm,
|
|
sizeof(startup_dm),
|
|
"%s has started up and is online at %s (version %s, connected relays: %d/%d).",
|
|
(startup_name && startup_name[0] != '\0') ? startup_name : "Didactyl",
|
|
startup_time_str,
|
|
DIDACTYL_VERSION,
|
|
connected_relays,
|
|
cfg.relay_count);
|
|
if (nostr_handler_send_dm_auto(cfg.admin.pubkey, startup_dm) != 0) {
|
|
DEBUG_WARN("[didactyl] startup phase: failed to send startup status DM to admin");
|
|
}
|
|
|
|
DEBUG_INFO("[didactyl] startup phase: subscribe DMs begin");
|
|
if (nostr_handler_subscribe_dms(agent_on_message, NULL) != 0) {
|
|
DEBUG_ERROR("[didactyl] startup phase: subscribe DMs failed");
|
|
fprintf(stderr, "Failed to subscribe to DMs\n");
|
|
agent_cleanup();
|
|
nostr_handler_cleanup();
|
|
llm_cleanup();
|
|
trigger_manager_cleanup(&trigger_manager);
|
|
config_free(&cfg);
|
|
nostr_cleanup();
|
|
return 1;
|
|
}
|
|
|
|
DEBUG_INFO("[didactyl] startup phase: subscribe DMs end");
|
|
|
|
signal(SIGINT, signal_handler);
|
|
signal(SIGTERM, signal_handler);
|
|
signal(SIGPIPE, SIG_IGN);
|
|
|
|
int http_api_started = 0;
|
|
if (cfg.api.enabled) {
|
|
http_api_context_t http_ctx;
|
|
memset(&http_ctx, 0, sizeof(http_ctx));
|
|
http_ctx.cfg = &cfg;
|
|
http_ctx.tools_ctx = agent_tools_context();
|
|
http_ctx.trigger_manager = &trigger_manager;
|
|
|
|
if (http_api_init(&http_ctx) != 0) {
|
|
fprintf(stderr,
|
|
"Failed to initialize HTTP API on %s:%d\n",
|
|
cfg.api.bind_address,
|
|
cfg.api.port);
|
|
agent_cleanup();
|
|
nostr_handler_cleanup();
|
|
llm_cleanup();
|
|
trigger_manager_cleanup(&trigger_manager);
|
|
config_free(&cfg);
|
|
nostr_cleanup();
|
|
return 1;
|
|
}
|
|
http_api_started = 1;
|
|
DEBUG_INFO("[didactyl] HTTP API listening at http://%s:%d",
|
|
cfg.api.bind_address[0] ? cfg.api.bind_address : "127.0.0.1",
|
|
cfg.api.port > 0 ? cfg.api.port : 8484);
|
|
DEBUG_INFO("[didactyl] HTTP API endpoints: http://%s:%d/api/context/current http://%s:%d/api/context/parts",
|
|
cfg.api.bind_address[0] ? cfg.api.bind_address : "127.0.0.1",
|
|
cfg.api.port > 0 ? cfg.api.port : 8484,
|
|
cfg.api.bind_address[0] ? cfg.api.bind_address : "127.0.0.1",
|
|
cfg.api.port > 0 ? cfg.api.port : 8484);
|
|
} else {
|
|
DEBUG_INFO("[didactyl] HTTP API disabled (set api.enabled=true in config to enable)");
|
|
}
|
|
|
|
nostr_handler_refresh_relay_statuses();
|
|
|
|
DEBUG_INFO("[didactyl] entering main poll loop");
|
|
DEBUG_INFO("[didactyl] running with pubkey %s", cfg.keys.public_key_hex);
|
|
|
|
while (g_running) {
|
|
(void)nostr_handler_poll(100);
|
|
(void)trigger_manager_poll(&trigger_manager);
|
|
if (http_api_started) {
|
|
(void)http_api_poll(0);
|
|
}
|
|
struct timespec ts = {0, 10 * 1000 * 1000};
|
|
nanosleep(&ts, NULL);
|
|
}
|
|
|
|
DEBUG_INFO("[didactyl] shutting down");
|
|
|
|
if (http_api_started) {
|
|
http_api_cleanup();
|
|
}
|
|
|
|
agent_cleanup();
|
|
nostr_handler_cleanup();
|
|
llm_cleanup();
|
|
trigger_manager_cleanup(&trigger_manager);
|
|
config_free(&cfg);
|
|
nostr_cleanup();
|
|
|
|
return 0;
|
|
}
|