v0.0.2 - Fix tool-calling loop with multi-message conversation state, enforce tool-use policy, add trace DM logging, and standardize final validation via build_static.sh
This commit is contained in:
@@ -0,0 +1,9 @@
|
||||
---
|
||||
description: "Build Didactyl for end-of-change validation"
|
||||
---
|
||||
|
||||
After making code changes, run [`build_static.sh`](build_static.sh) instead of `make` for final compile validation.
|
||||
|
||||
Example:
|
||||
|
||||
./build_static.sh
|
||||
@@ -2,6 +2,6 @@
|
||||
description: "Increments and pushes the repo"
|
||||
---
|
||||
|
||||
Run increment_and_push.sh followed in the command line with a good description of the changes that were made.
|
||||
Then run increment_and_push.sh followed in the command line with a good description of the changes that were made.
|
||||
|
||||
For example: ./increment_and_push.sh "Fixed that nasty bug"
|
||||
|
||||
@@ -11,6 +11,14 @@ You are Didactyl, a sovereign AI agent living on Nostr.
|
||||
- If unsure, state uncertainty directly.
|
||||
- Prefer actionable, practical advice.
|
||||
|
||||
## Tool Use Policy
|
||||
- You have tools available and should use them when a request requires taking action.
|
||||
- For requests involving local inspection or command execution, call `shell_exec` instead of refusing.
|
||||
- For posting to Nostr, call `nostr_post` with explicit `kind` and `content`.
|
||||
- For relay/event lookup tasks, call `nostr_query` with an appropriate filter.
|
||||
- After a tool call, base your answer on the actual tool result.
|
||||
- Never claim a tool was run if no tool was executed.
|
||||
|
||||
## Safety
|
||||
- Do not claim to have executed actions you did not execute.
|
||||
- Do not reveal secrets from configuration or keys.
|
||||
|
||||
+18
-23
@@ -6,7 +6,7 @@
|
||||
set -e
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
BUILD_DIR="$SCRIPT_DIR/build"
|
||||
OUTPUT_DIR="$SCRIPT_DIR"
|
||||
DOCKERFILE="$SCRIPT_DIR/Dockerfile.alpine-musl"
|
||||
|
||||
# Parse command line arguments
|
||||
@@ -22,12 +22,15 @@ else
|
||||
echo "=========================================="
|
||||
fi
|
||||
echo "Project directory: $SCRIPT_DIR"
|
||||
echo "Build directory: $BUILD_DIR"
|
||||
echo "Output directory: $OUTPUT_DIR"
|
||||
echo "Debug build: $DEBUG_BUILD"
|
||||
echo ""
|
||||
|
||||
# Create build directory
|
||||
mkdir -p "$BUILD_DIR"
|
||||
# Remove legacy build directory if present
|
||||
if [ -d "$SCRIPT_DIR/build" ]; then
|
||||
rm -rf "$SCRIPT_DIR/build"
|
||||
echo "Removed legacy build directory: $SCRIPT_DIR/build"
|
||||
fi
|
||||
|
||||
# Check if Docker is available
|
||||
if ! command -v docker &> /dev/null; then
|
||||
@@ -136,20 +139,12 @@ echo "=========================================="
|
||||
echo "Step 2: Extracting static binary"
|
||||
echo "=========================================="
|
||||
|
||||
# Build the builder stage to extract the binary
|
||||
$DOCKER_CMD build \
|
||||
--platform "$PLATFORM" \
|
||||
--build-arg DEBUG_BUILD=$DEBUG_BUILD \
|
||||
--target builder \
|
||||
-f "$DOCKERFILE" \
|
||||
-t didactyl-static-builder-stage:latest \
|
||||
. > /dev/null 2>&1
|
||||
# Reuse the already-built final image and extract binary directly
|
||||
# (avoids a second docker build pass)
|
||||
CONTAINER_ID=$($DOCKER_CMD create didactyl-musl-builder:latest /didactyl_static)
|
||||
|
||||
# Create a temporary container to copy the binary
|
||||
CONTAINER_ID=$($DOCKER_CMD create didactyl-static-builder-stage:latest)
|
||||
|
||||
# Copy binary from container
|
||||
$DOCKER_CMD cp "$CONTAINER_ID:/build/didactyl_static" "$BUILD_DIR/$OUTPUT_NAME" || {
|
||||
# Copy binary from final image
|
||||
$DOCKER_CMD cp "$CONTAINER_ID:/didactyl_static" "$OUTPUT_DIR/$OUTPUT_NAME" || {
|
||||
echo "ERROR: Failed to extract binary from container"
|
||||
$DOCKER_CMD rm "$CONTAINER_ID" 2>/dev/null
|
||||
exit 1
|
||||
@@ -158,11 +153,11 @@ $DOCKER_CMD cp "$CONTAINER_ID:/build/didactyl_static" "$BUILD_DIR/$OUTPUT_NAME"
|
||||
# Clean up container
|
||||
$DOCKER_CMD rm "$CONTAINER_ID" > /dev/null
|
||||
|
||||
echo "✓ Binary extracted to: $BUILD_DIR/$OUTPUT_NAME"
|
||||
echo "✓ Binary extracted to: $OUTPUT_DIR/$OUTPUT_NAME"
|
||||
echo ""
|
||||
|
||||
# Make binary executable
|
||||
chmod +x "$BUILD_DIR/$OUTPUT_NAME"
|
||||
chmod +x "$OUTPUT_DIR/$OUTPUT_NAME"
|
||||
|
||||
# Verify the binary
|
||||
echo "=========================================="
|
||||
@@ -171,7 +166,7 @@ echo "=========================================="
|
||||
echo ""
|
||||
|
||||
echo "Checking for dynamic dependencies:"
|
||||
if LDD_OUTPUT=$(timeout 5 ldd "$BUILD_DIR/$OUTPUT_NAME" 2>&1); then
|
||||
if LDD_OUTPUT=$(timeout 5 ldd "$OUTPUT_DIR/$OUTPUT_NAME" 2>&1); then
|
||||
if echo "$LDD_OUTPUT" | grep -q "not a dynamic executable"; then
|
||||
echo "✓ Binary is fully static (no dynamic dependencies)"
|
||||
TRULY_STATIC=true
|
||||
@@ -185,7 +180,7 @@ if LDD_OUTPUT=$(timeout 5 ldd "$BUILD_DIR/$OUTPUT_NAME" 2>&1); then
|
||||
fi
|
||||
else
|
||||
# ldd failed or timed out - check with file command instead
|
||||
if file "$BUILD_DIR/$OUTPUT_NAME" | grep -q "statically linked"; then
|
||||
if file "$OUTPUT_DIR/$OUTPUT_NAME" | grep -q "statically linked"; then
|
||||
echo "✓ Binary is statically linked (verified with file command)"
|
||||
TRULY_STATIC=true
|
||||
else
|
||||
@@ -198,8 +193,8 @@ echo ""
|
||||
echo "=========================================="
|
||||
echo "Build Summary"
|
||||
echo "=========================================="
|
||||
echo "Binary: $BUILD_DIR/$OUTPUT_NAME"
|
||||
echo "Size: $(du -h "$BUILD_DIR/$OUTPUT_NAME" | cut -f1)"
|
||||
echo "Binary: $OUTPUT_DIR/$OUTPUT_NAME"
|
||||
echo "Size: $(du -h "$OUTPUT_DIR/$OUTPUT_NAME" | cut -f1)"
|
||||
echo "Static: $TRULY_STATIC"
|
||||
echo "Debug: $DEBUG_BUILD"
|
||||
echo "Platform: $PLATFORM"
|
||||
|
||||
+203
-22
@@ -5,15 +5,149 @@
|
||||
#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"
|
||||
|
||||
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
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
int agent_init(didactyl_config_t* config, const char* system_context) {
|
||||
if (!config || !system_context) {
|
||||
return -1;
|
||||
@@ -32,6 +166,10 @@ int agent_init(didactyl_config_t* config, const char* system_context) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
memset(g_seen_msgs, 0, sizeof(g_seen_msgs));
|
||||
g_seen_msgs_count = 0;
|
||||
g_seen_msgs_next = 0;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -43,6 +181,12 @@ void agent_on_message(const char* sender_pubkey_hex, const char* message, void*
|
||||
}
|
||||
|
||||
fprintf(stdout, "[didactyl] incoming message from %.16s...\n", sender_pubkey_hex);
|
||||
|
||||
if (agent_message_is_debounced(sender_pubkey_hex, message)) {
|
||||
fprintf(stdout, "[didactyl] debounced duplicate inbound message from %.16s...\n", sender_pubkey_hex);
|
||||
return;
|
||||
}
|
||||
|
||||
fprintf(stdout, "[didactyl] calling llm for sender %.16s...\n", sender_pubkey_hex);
|
||||
|
||||
if (!g_cfg->tools.enabled) {
|
||||
@@ -68,33 +212,57 @@ void agent_on_message(const char* sender_pubkey_hex, const char* message, void*
|
||||
return;
|
||||
}
|
||||
|
||||
llm_response_t resp;
|
||||
if (llm_chat_with_tools(g_system_context, message, tools_json, &resp) != 0) {
|
||||
cJSON* messages = cJSON_CreateArray();
|
||||
if (!messages) {
|
||||
free(tools_json);
|
||||
(void)nostr_handler_send_dm(sender_pubkey_hex, "LLM request failed.");
|
||||
(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_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;
|
||||
}
|
||||
|
||||
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 ? "..." : "");
|
||||
(void)nostr_handler_send_dm(sender_pubkey_hex, answer);
|
||||
final_answer_owned = strdup(answer);
|
||||
llm_response_free(&resp);
|
||||
return;
|
||||
}
|
||||
|
||||
int max_turns = g_cfg->tools.max_turns > 0 ? g_cfg->tools.max_turns : 8;
|
||||
llm_response_t current = resp;
|
||||
|
||||
for (int turn = 0; turn < max_turns; turn++) {
|
||||
if (current.tool_call_count <= 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
llm_tool_call_t* tc = ¤t.tool_calls[0];
|
||||
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>");
|
||||
|
||||
char* tool_result = tools_execute(&g_tools_ctx, tc->name, tc->arguments_json);
|
||||
@@ -102,25 +270,35 @@ void agent_on_message(const char* sender_pubkey_hex, const char* message, void*
|
||||
tool_result = strdup("{\"success\":false,\"error\":\"tool execution failed\"}");
|
||||
}
|
||||
|
||||
llm_response_free(¤t);
|
||||
if (llm_chat_with_tools(g_system_context, tool_result, NULL, ¤t) != 0) {
|
||||
if (append_tool_result_message(messages,
|
||||
tc->id ? tc->id : "",
|
||||
tool_result ? tool_result : "{\"success\":false,\"error\":\"tool execution failed\"}") != 0) {
|
||||
free(tool_result);
|
||||
(void)nostr_handler_send_dm(sender_pubkey_hex, "LLM failed after tool execution.");
|
||||
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);
|
||||
|
||||
if (current.content && current.tool_call_count == 0) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const char* final_answer = current.content ? current.content : "I hit my tool-use limit for this request.";
|
||||
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);
|
||||
llm_response_free(¤t);
|
||||
|
||||
free(final_answer_owned);
|
||||
cJSON_Delete(messages);
|
||||
free(tools_json);
|
||||
}
|
||||
|
||||
void agent_cleanup(void) {
|
||||
@@ -128,4 +306,7 @@ void agent_cleanup(void) {
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include "cjson/cJSON.h"
|
||||
|
||||
@@ -41,6 +42,28 @@ static size_t write_cb(void* contents, size_t size, size_t nmemb, void* userp) {
|
||||
return total;
|
||||
}
|
||||
|
||||
static const char* detect_ca_bundle_path(void) {
|
||||
const char* env = getenv("SSL_CERT_FILE");
|
||||
if (env && env[0] != '\0' && access(env, R_OK) == 0) {
|
||||
return env;
|
||||
}
|
||||
|
||||
static const char* candidates[] = {
|
||||
"/etc/ssl/certs/ca-certificates.crt", // Debian/Ubuntu
|
||||
"/etc/ssl/cert.pem", // Alpine
|
||||
"/etc/pki/tls/certs/ca-bundle.crt", // RHEL/CentOS/Fedora
|
||||
"/etc/ssl/ca-bundle.pem" // openSUSE
|
||||
};
|
||||
|
||||
for (size_t i = 0; i < sizeof(candidates) / sizeof(candidates[0]); i++) {
|
||||
if (access(candidates[i], R_OK) == 0) {
|
||||
return candidates[i];
|
||||
}
|
||||
}
|
||||
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static char* perform_chat_request(const char* body) {
|
||||
CURL* curl = curl_easy_init();
|
||||
if (!curl || !body) {
|
||||
@@ -67,6 +90,11 @@ static char* perform_chat_request(const char* body) {
|
||||
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &rb);
|
||||
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
|
||||
|
||||
const char* ca_bundle = detect_ca_bundle_path();
|
||||
if (ca_bundle) {
|
||||
curl_easy_setopt(curl, CURLOPT_CAINFO, ca_bundle);
|
||||
}
|
||||
|
||||
CURLcode res = curl_easy_perform(curl);
|
||||
long status = 0;
|
||||
curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &status);
|
||||
@@ -74,11 +102,33 @@ static char* perform_chat_request(const char* body) {
|
||||
curl_slist_free_all(headers);
|
||||
curl_easy_cleanup(curl);
|
||||
|
||||
if (res != CURLE_OK || status < 200 || status >= 300 || !rb.data) {
|
||||
if (res != CURLE_OK) {
|
||||
fprintf(stderr, "[didactyl] llm http request failed: curl=%s\n", curl_easy_strerror(res));
|
||||
if (rb.data && rb.len > 0) {
|
||||
fprintf(stderr, "[didactyl] llm partial response: %.600s%s\n",
|
||||
rb.data,
|
||||
rb.len > 600 ? "..." : "");
|
||||
}
|
||||
free(rb.data);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
if (status < 200 || status >= 300) {
|
||||
fprintf(stderr, "[didactyl] llm http request failed: status=%ld\n", status);
|
||||
if (rb.data && rb.len > 0) {
|
||||
fprintf(stderr, "[didactyl] llm error response: %.1200s%s\n",
|
||||
rb.data,
|
||||
rb.len > 1200 ? "..." : "");
|
||||
}
|
||||
free(rb.data);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
if (!rb.data) {
|
||||
fprintf(stderr, "[didactyl] llm http request failed: empty response body\n");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
return rb.data;
|
||||
}
|
||||
|
||||
@@ -228,6 +278,9 @@ char* llm_chat(const char* system_prompt, const char* user_message) {
|
||||
|
||||
llm_response_t parsed;
|
||||
if (parse_llm_response(raw, &parsed) != 0) {
|
||||
fprintf(stderr, "[didactyl] failed to parse llm response (non-tool path): %.1200s%s\n",
|
||||
raw,
|
||||
strlen(raw) > 1200 ? "..." : "");
|
||||
free(raw);
|
||||
return NULL;
|
||||
}
|
||||
@@ -238,19 +291,16 @@ char* llm_chat(const char* system_prompt, const char* user_message) {
|
||||
return answer;
|
||||
}
|
||||
|
||||
int llm_chat_with_tools(const char* system_prompt,
|
||||
const char* user_message,
|
||||
int llm_chat_with_tools_messages(const char* messages_json,
|
||||
const char* tools_json,
|
||||
const char* tool_choice,
|
||||
llm_response_t* out_response) {
|
||||
if (!g_initialized || !out_response) {
|
||||
if (!g_initialized || !out_response || !messages_json) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
cJSON* root = cJSON_CreateObject();
|
||||
cJSON* messages = cJSON_CreateArray();
|
||||
if (!root || !messages) {
|
||||
cJSON_Delete(root);
|
||||
cJSON_Delete(messages);
|
||||
if (!root) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
@@ -258,20 +308,19 @@ int llm_chat_with_tools(const char* system_prompt,
|
||||
cJSON_AddNumberToObject(root, "max_tokens", g_cfg.max_tokens);
|
||||
cJSON_AddNumberToObject(root, "temperature", g_cfg.temperature);
|
||||
|
||||
cJSON* system_msg = cJSON_CreateObject();
|
||||
cJSON* user_msg = cJSON_CreateObject();
|
||||
cJSON_AddStringToObject(system_msg, "role", "system");
|
||||
cJSON_AddStringToObject(system_msg, "content", system_prompt ? system_prompt : "");
|
||||
cJSON_AddStringToObject(user_msg, "role", "user");
|
||||
cJSON_AddStringToObject(user_msg, "content", user_message ? user_message : "");
|
||||
cJSON_AddItemToArray(messages, system_msg);
|
||||
cJSON_AddItemToArray(messages, user_msg);
|
||||
cJSON* messages = cJSON_Parse(messages_json);
|
||||
if (!messages || !cJSON_IsArray(messages)) {
|
||||
cJSON_Delete(messages);
|
||||
cJSON_Delete(root);
|
||||
return -1;
|
||||
}
|
||||
cJSON_AddItemToObject(root, "messages", messages);
|
||||
|
||||
if (tools_json) {
|
||||
cJSON* tools = cJSON_Parse(tools_json);
|
||||
if (tools && cJSON_IsArray(tools)) {
|
||||
cJSON_AddItemToObject(root, "tools", tools);
|
||||
cJSON_AddStringToObject(root, "tool_choice", tool_choice ? tool_choice : "auto");
|
||||
} else {
|
||||
cJSON_Delete(tools);
|
||||
}
|
||||
@@ -286,10 +335,47 @@ int llm_chat_with_tools(const char* system_prompt,
|
||||
if (!raw) return -1;
|
||||
|
||||
int rc = parse_llm_response(raw, out_response);
|
||||
if (rc != 0) {
|
||||
fprintf(stderr, "[didactyl] failed to parse llm response (tools path): %.1200s%s\n",
|
||||
raw,
|
||||
strlen(raw) > 1200 ? "..." : "");
|
||||
}
|
||||
free(raw);
|
||||
return rc;
|
||||
}
|
||||
|
||||
int llm_chat_with_tools(const char* system_prompt,
|
||||
const char* user_message,
|
||||
const char* tools_json,
|
||||
llm_response_t* out_response) {
|
||||
cJSON* messages = cJSON_CreateArray();
|
||||
cJSON* system_msg = cJSON_CreateObject();
|
||||
cJSON* user_msg = cJSON_CreateObject();
|
||||
if (!messages || !system_msg || !user_msg) {
|
||||
cJSON_Delete(messages);
|
||||
cJSON_Delete(system_msg);
|
||||
cJSON_Delete(user_msg);
|
||||
return -1;
|
||||
}
|
||||
|
||||
cJSON_AddStringToObject(system_msg, "role", "system");
|
||||
cJSON_AddStringToObject(system_msg, "content", system_prompt ? system_prompt : "");
|
||||
cJSON_AddStringToObject(user_msg, "role", "user");
|
||||
cJSON_AddStringToObject(user_msg, "content", user_message ? user_message : "");
|
||||
cJSON_AddItemToArray(messages, system_msg);
|
||||
cJSON_AddItemToArray(messages, user_msg);
|
||||
|
||||
char* messages_json = cJSON_PrintUnformatted(messages);
|
||||
cJSON_Delete(messages);
|
||||
if (!messages_json) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
int rc = llm_chat_with_tools_messages(messages_json, tools_json, "auto", out_response);
|
||||
free(messages_json);
|
||||
return rc;
|
||||
}
|
||||
|
||||
void llm_response_free(llm_response_t* response) {
|
||||
if (!response) return;
|
||||
free(response->content);
|
||||
|
||||
@@ -21,6 +21,10 @@ int llm_chat_with_tools(const char* system_prompt,
|
||||
const char* user_message,
|
||||
const char* tools_json,
|
||||
llm_response_t* out_response);
|
||||
int llm_chat_with_tools_messages(const char* messages_json,
|
||||
const char* tools_json,
|
||||
const char* tool_choice,
|
||||
llm_response_t* out_response);
|
||||
void llm_response_free(llm_response_t* response);
|
||||
void llm_cleanup(void);
|
||||
|
||||
|
||||
+1
-1
@@ -25,7 +25,7 @@ int main(int argc, char** argv) {
|
||||
const char* config_path = "./config.json";
|
||||
const char* context_path = "./SYSTEM.md";
|
||||
|
||||
int debug_level = DEBUG_LEVEL_INFO;
|
||||
int debug_level = DEBUG_LEVEL_TRACE;
|
||||
|
||||
for (int i = 1; i < argc; i++) {
|
||||
if (strcmp(argv[i], "--config") == 0 && i + 1 < argc) {
|
||||
|
||||
+2
-2
@@ -12,8 +12,8 @@
|
||||
// Using DIDACTYL_ prefix to avoid conflicts with nostr_core_lib VERSION macros
|
||||
#define DIDACTYL_VERSION_MAJOR 0
|
||||
#define DIDACTYL_VERSION_MINOR 0
|
||||
#define DIDACTYL_VERSION_PATCH 1
|
||||
#define DIDACTYL_VERSION "v0.0.1"
|
||||
#define DIDACTYL_VERSION_PATCH 2
|
||||
#define DIDACTYL_VERSION "v0.0.2"
|
||||
|
||||
// Agent metadata
|
||||
#define DIDACTYL_NAME "Didactyl"
|
||||
|
||||
+102
-2
@@ -6,6 +6,7 @@
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <time.h>
|
||||
#include <pthread.h>
|
||||
|
||||
#include "../../nostr_core_lib/cjson/cJSON.h"
|
||||
#include "../../nostr_core_lib/nostr_core/nostr_core.h"
|
||||
@@ -20,6 +21,47 @@ static time_t g_start_time = 0;
|
||||
static time_t g_last_status_log_time = 0;
|
||||
static nostr_pool_relay_status_t* g_last_relay_statuses = NULL;
|
||||
|
||||
#define DM_DEDUP_CACHE_SIZE 256
|
||||
|
||||
static char g_seen_dm_ids[DM_DEDUP_CACHE_SIZE][65];
|
||||
static int g_seen_dm_count = 0;
|
||||
static int g_seen_dm_next = 0;
|
||||
|
||||
static pthread_mutex_t g_dm_dedup_mutex = PTHREAD_MUTEX_INITIALIZER;
|
||||
|
||||
static int dm_id_seen_or_remember(const char* event_id_hex) {
|
||||
if (!event_id_hex || strlen(event_id_hex) != 64U) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
int seen = 0;
|
||||
pthread_mutex_lock(&g_dm_dedup_mutex);
|
||||
|
||||
for (int i = 0; i < g_seen_dm_count; i++) {
|
||||
if (strncmp(g_seen_dm_ids[i], event_id_hex, 64U) == 0) {
|
||||
seen = 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!seen) {
|
||||
int slot = 0;
|
||||
if (g_seen_dm_count < DM_DEDUP_CACHE_SIZE) {
|
||||
slot = g_seen_dm_count;
|
||||
g_seen_dm_count++;
|
||||
} else {
|
||||
slot = g_seen_dm_next;
|
||||
g_seen_dm_next = (g_seen_dm_next + 1) % DM_DEDUP_CACHE_SIZE;
|
||||
}
|
||||
|
||||
memcpy(g_seen_dm_ids[slot], event_id_hex, 64U);
|
||||
g_seen_dm_ids[slot][64] = '\0';
|
||||
}
|
||||
|
||||
pthread_mutex_unlock(&g_dm_dedup_mutex);
|
||||
return seen;
|
||||
}
|
||||
|
||||
static const char* relay_status_str(nostr_pool_relay_status_t status) {
|
||||
switch (status) {
|
||||
case NOSTR_POOL_RELAY_DISCONNECTED:
|
||||
@@ -155,6 +197,28 @@ static int extract_first_p_tag(cJSON* tags, char out_pubkey_hex[65]) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
static void trace_event_json(const char* prefix, cJSON* event) {
|
||||
if (g_debug_level < DEBUG_LEVEL_TRACE || !event) {
|
||||
return;
|
||||
}
|
||||
|
||||
char* event_json = cJSON_PrintUnformatted(event);
|
||||
if (!event_json) {
|
||||
DEBUG_TRACE("[didactyl] %s <failed to serialize event>", prefix ? prefix : "event");
|
||||
return;
|
||||
}
|
||||
|
||||
DEBUG_TRACE("[didactyl] %s %s", prefix ? prefix : "event", event_json);
|
||||
free(event_json);
|
||||
}
|
||||
|
||||
static void trace_plaintext_dm(const char* prefix, const char* plaintext) {
|
||||
if (g_debug_level < DEBUG_LEVEL_TRACE) {
|
||||
return;
|
||||
}
|
||||
DEBUG_TRACE("[didactyl] %s %s", prefix ? prefix : "dm plaintext", plaintext ? plaintext : "");
|
||||
}
|
||||
|
||||
static void on_event(cJSON* event, const char* relay_url, void* user_data) {
|
||||
(void)user_data;
|
||||
|
||||
@@ -162,6 +226,7 @@ static void on_event(cJSON* event, const char* relay_url, void* user_data) {
|
||||
return;
|
||||
}
|
||||
|
||||
cJSON* id = cJSON_GetObjectItemCaseSensitive(event, "id");
|
||||
cJSON* kind = cJSON_GetObjectItemCaseSensitive(event, "kind");
|
||||
cJSON* pubkey = cJSON_GetObjectItemCaseSensitive(event, "pubkey");
|
||||
cJSON* content = cJSON_GetObjectItemCaseSensitive(event, "content");
|
||||
@@ -172,6 +237,10 @@ static void on_event(cJSON* event, const char* relay_url, void* user_data) {
|
||||
return;
|
||||
}
|
||||
|
||||
const char* event_id_hex = (id && cJSON_IsString(id) && id->valuestring && strlen(id->valuestring) == 64U)
|
||||
? id->valuestring
|
||||
: NULL;
|
||||
|
||||
if ((int)kind->valuedouble != 4) {
|
||||
return;
|
||||
}
|
||||
@@ -204,13 +273,27 @@ static void on_event(cJSON* event, const char* relay_url, void* user_data) {
|
||||
}
|
||||
decrypted[0] = '\0';
|
||||
|
||||
trace_event_json("received encrypted DM event:", event);
|
||||
|
||||
if (nostr_nip04_decrypt(g_cfg->keys.private_key, sender_pubkey, content->valuestring, decrypted, NOSTR_NIP04_MAX_PLAINTEXT_SIZE) != NOSTR_SUCCESS) {
|
||||
fprintf(stdout, "[didactyl] failed to decrypt incoming DM from %.16s...\n", pubkey->valuestring);
|
||||
free(decrypted);
|
||||
return;
|
||||
}
|
||||
|
||||
DEBUG_INFO("[didactyl] received DM from %.16s... via %s",
|
||||
trace_plaintext_dm("received decrypted DM content:", decrypted);
|
||||
|
||||
if (event_id_hex && dm_id_seen_or_remember(event_id_hex)) {
|
||||
DEBUG_LOG("[didactyl] skipped duplicate DM event %.16s... from %.16s... via %s",
|
||||
event_id_hex,
|
||||
pubkey->valuestring,
|
||||
relay_url ? relay_url : "unknown relay");
|
||||
free(decrypted);
|
||||
return;
|
||||
}
|
||||
|
||||
DEBUG_INFO("[didactyl] received DM %.16s... from %.16s... via %s",
|
||||
event_id_hex ? event_id_hex : "<no-id>",
|
||||
pubkey->valuestring,
|
||||
relay_url ? relay_url : "unknown relay");
|
||||
g_dm_callback(pubkey->valuestring, decrypted, g_dm_user_data);
|
||||
@@ -231,6 +314,9 @@ int nostr_handler_init(didactyl_config_t* config) {
|
||||
g_cfg = config;
|
||||
g_poll_counter = 0;
|
||||
g_start_time = time(NULL);
|
||||
memset(g_seen_dm_ids, 0, sizeof(g_seen_dm_ids));
|
||||
g_seen_dm_count = 0;
|
||||
g_seen_dm_next = 0;
|
||||
|
||||
DEBUG_INFO("[didactyl] initializing relay pool with %d relays", g_cfg->relay_count);
|
||||
|
||||
@@ -393,6 +479,8 @@ int nostr_handler_send_dm(const char* recipient_pubkey_hex, const char* message)
|
||||
return -1;
|
||||
}
|
||||
|
||||
trace_plaintext_dm("sending plaintext DM content:", message);
|
||||
|
||||
char* encrypted = (char*)malloc(NOSTR_NIP04_MAX_ENCRYPTED_SIZE);
|
||||
if (!encrypted) {
|
||||
fprintf(stderr, "[didactyl] failed to allocate DM encrypt buffer\n");
|
||||
@@ -418,6 +506,8 @@ int nostr_handler_send_dm(const char* recipient_pubkey_hex, const char* message)
|
||||
return -1;
|
||||
}
|
||||
|
||||
trace_event_json("sending encrypted DM event:", event);
|
||||
|
||||
log_publish_targets("publish DM");
|
||||
|
||||
const char** connected_relays = (const char**)calloc((size_t)g_cfg->relay_count, sizeof(char*));
|
||||
@@ -444,9 +534,16 @@ int nostr_handler_send_dm(const char* recipient_pubkey_hex, const char* message)
|
||||
NULL);
|
||||
}
|
||||
|
||||
cJSON* event_id = cJSON_GetObjectItemCaseSensitive(event, "id");
|
||||
const char* out_event_id_hex = (event_id && cJSON_IsString(event_id) && event_id->valuestring) ? event_id->valuestring : "<no-id>";
|
||||
|
||||
DEBUG_INFO("[didactyl] sent DM %.16s... to %.16s... via %d connected relay(s)",
|
||||
out_event_id_hex,
|
||||
recipient_pubkey_hex,
|
||||
sent);
|
||||
|
||||
free(connected_relays);
|
||||
cJSON_Delete(event);
|
||||
DEBUG_INFO("[didactyl] sent DM to %.16s... via %d connected relay(s)", recipient_pubkey_hex, sent);
|
||||
return sent > 0 ? 0 : -1;
|
||||
}
|
||||
|
||||
@@ -573,4 +670,7 @@ void nostr_handler_cleanup(void) {
|
||||
g_cfg = NULL;
|
||||
g_dm_callback = NULL;
|
||||
g_dm_user_data = NULL;
|
||||
memset(g_seen_dm_ids, 0, sizeof(g_seen_dm_ids));
|
||||
g_seen_dm_count = 0;
|
||||
g_seen_dm_next = 0;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user