v0.0.28 - Add prompt template system: soul-embedded ---template--- parser, variable resolver, provider overrides, section-named context parts; enable HTTP API by default; fix Makefile to not rebuild nostr_core_lib unconditionally; log API endpoint URL at startup
This commit is contained in:
@@ -1,14 +0,0 @@
|
||||
11469571 3 mongoose.c:4824:mg_mgr_init MG_IO_SIZE: 16384, TLS: none
|
||||
11469571 3 mongoose.c:4741:mg_listen 1 9 http://127.0.0.1:8586
|
||||
114695f9 3 mongoose.c:10782:accept_conn 2 10 accepted 127.0.0.1:51708 -> 127.0.0.1:8586
|
||||
11469636 3 mongoose.c:10632:read_conn 2 10 0:0:0 88 err 0
|
||||
11469674 3 mongoose.c:10643:write_conn 2 10 snd 406/16384 rcv 0/16384 n=406 err=0
|
||||
114696b1 3 mongoose.c:10632:read_conn 2 10 0:0:0 -1 err 0
|
||||
114696b2 3 mongoose.c:4677:mg_close_con 2 10 closed
|
||||
114696b2 3 mongoose.c:10782:accept_conn 3 10 accepted 127.0.0.1:51718 -> 127.0.0.1:8586
|
||||
114696dd 3 mongoose.c:10632:read_conn 3 10 0:0:0 257 err 0
|
||||
11469e00 3 mongoose.c:10643:write_conn 3 10 snd 424/16384 rcv 0/16384 n=424 err=0
|
||||
11469e3d 3 mongoose.c:10632:read_conn 3 10 0:0:0 -1 err 0
|
||||
11469e3d 3 mongoose.c:4677:mg_close_con 3 10 closed
|
||||
1146a21a 3 mongoose.c:4677:mg_close_con 1 9 closed
|
||||
1146a21a 3 mongoose.c:4788:mg_mgr_free All connections closed
|
||||
@@ -1 +0,0 @@
|
||||
[2026-03-02 14:46:38] [WARN ] [didactyl] retrying kind 4 event once after auth handshake window
|
||||
@@ -1 +0,0 @@
|
||||
1217023
|
||||
@@ -1,14 +0,0 @@
|
||||
1148b030 3 mongoose.c:4824:mg_mgr_init MG_IO_SIZE: 16384, TLS: none
|
||||
1148b030 3 mongoose.c:4741:mg_listen 1 9 http://127.0.0.1:8591
|
||||
1148b087 3 mongoose.c:10782:accept_conn 2 10 accepted 127.0.0.1:34824 -> 127.0.0.1:8591
|
||||
1148b0c7 3 mongoose.c:10632:read_conn 2 10 0:0:0 88 err 0
|
||||
1148b0eb 3 mongoose.c:10643:write_conn 2 10 snd 406/16384 rcv 0/16384 n=406 err=0
|
||||
1148b129 3 mongoose.c:10632:read_conn 2 10 0:0:0 -1 err 0
|
||||
1148b129 3 mongoose.c:4677:mg_close_con 2 10 closed
|
||||
1148b129 3 mongoose.c:10782:accept_conn 3 10 accepted 127.0.0.1:34828 -> 127.0.0.1:8591
|
||||
1148b166 3 mongoose.c:10632:read_conn 3 10 0:0:0 257 err 0
|
||||
1148b6c4 3 mongoose.c:10643:write_conn 3 10 snd 424/16384 rcv 0/16384 n=424 err=0
|
||||
1148b701 3 mongoose.c:10632:read_conn 3 10 0:0:0 -1 err 0
|
||||
1148b701 3 mongoose.c:4677:mg_close_con 3 10 closed
|
||||
1148bac5 3 mongoose.c:4677:mg_close_con 1 9 closed
|
||||
1148bac5 3 mongoose.c:4788:mg_mgr_free All connections closed
|
||||
@@ -1,19 +0,0 @@
|
||||
11494a06 3 mongoose.c:4824:mg_mgr_init MG_IO_SIZE: 16384, TLS: none
|
||||
11494a06 3 mongoose.c:4741:mg_listen 1 9 http://127.0.0.1:8592
|
||||
11494c7b 3 mongoose.c:10782:accept_conn 2 10 accepted 127.0.0.1:46110 -> 127.0.0.1:8592
|
||||
11494cb8 3 mongoose.c:10632:read_conn 2 10 0:0:0 88 err 0
|
||||
11494cf5 3 mongoose.c:10643:write_conn 2 10 snd 406/16384 rcv 0/16384 n=406 err=0
|
||||
11494d32 3 mongoose.c:10632:read_conn 2 10 0:0:0 -1 err 0
|
||||
11494d32 3 mongoose.c:4677:mg_close_con 2 10 closed
|
||||
11494d32 3 mongoose.c:10782:accept_conn 3 10 accepted 127.0.0.1:46120 -> 127.0.0.1:8592
|
||||
11494d6f 3 mongoose.c:10632:read_conn 3 10 0:0:0 97 err 0
|
||||
114973d4 3 mongoose.c:10643:write_conn 3 10 snd 13914/16384 rcv 0/16384 n=13914 err=0
|
||||
11497449 3 mongoose.c:10632:read_conn 3 10 0:0:0 -1 err 0
|
||||
11497449 3 mongoose.c:4677:mg_close_con 3 10 closed
|
||||
11497449 3 mongoose.c:10782:accept_conn 4 10 accepted 127.0.0.1:59822 -> 127.0.0.1:8592
|
||||
114974b8 3 mongoose.c:10632:read_conn 4 10 0:0:0 257 err 0
|
||||
11497ed5 3 mongoose.c:10643:write_conn 4 10 snd 424/16384 rcv 0/16384 n=424 err=0
|
||||
11497f43 3 mongoose.c:10632:read_conn 4 10 0:0:0 -1 err 11
|
||||
11497f43 3 mongoose.c:4677:mg_close_con 4 10 closed
|
||||
1149831f 3 mongoose.c:4677:mg_close_con 1 9 closed
|
||||
1149831f 3 mongoose.c:4788:mg_mgr_free All connections closed
|
||||
@@ -1,251 +0,0 @@
|
||||
{
|
||||
"keys": {
|
||||
"nsec": "nsec17pcmmqxh99rtu5ctpxrdyywdd8f7zfqgf3y2f4fvcm3tk3vpuyqqvlxl4e",
|
||||
"npub": "npub12237stmmxapc2ta7spx0e06dkdzgz9vg0z2jglqqru44peus4juqg598qn",
|
||||
"npubHex": "52a3e82f7b3743852fbe804cfcbf4db3448115887895247c001f2b50e790acb8",
|
||||
"nsecHex": "f071bd80d72946be530b0986d211cd69d3e124084c48a4d52cc6e2bb4581e100"
|
||||
},
|
||||
"admin": {
|
||||
"pubkey": "npub13lm5wf8dvsdnc2894pkhch9uf8phvw9varrv8zf4sc885hhdmc8q6lx7ks"
|
||||
},
|
||||
"llm": {
|
||||
"provider": "https://api.ppq.ai",
|
||||
"api_key": "sk-L9kd0LkZaoeaua0qqosnHy",
|
||||
"model": "claude-haiku-4.5",
|
||||
"base_url": "https://api.ppq.ai",
|
||||
"max_tokens": 512,
|
||||
"temperature": 0.7
|
||||
},
|
||||
"security": {
|
||||
"verify_signatures": true,
|
||||
"stranger_response": "I only respond to people in my web of trust. You can always identify me by my public key (npub).",
|
||||
"tiers": {
|
||||
"admin": {
|
||||
"tools_enabled": true
|
||||
},
|
||||
"wot": {
|
||||
"enabled": true,
|
||||
"tools_enabled": false
|
||||
},
|
||||
"stranger": {
|
||||
"enabled": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"admin_context": {
|
||||
"enabled": true,
|
||||
"subscribe_kinds": [
|
||||
0,
|
||||
3,
|
||||
10002,
|
||||
1
|
||||
],
|
||||
"kind_1_limit": 10
|
||||
},
|
||||
"startup_events": [
|
||||
{
|
||||
"kind": 0,
|
||||
"content_fields": {
|
||||
"name": "Didactyl Agent",
|
||||
"display_name": "Didactyl",
|
||||
"about": "A sovereign AI agent on Nostr",
|
||||
"picture": "https://laantungir.github.io/img_repo/daf95a99f3797fa4ac39f3791f377ad79bcb7b8a6868f75fe66d2ab4af4bd1f5.png",
|
||||
"banner": "https://laantungir.github.io/img_repo/d21c4060632ab3d9d37a6062eeecbdbfbd67f03cb20dbd64838b1ba0d9cd8922.jpg"
|
||||
},
|
||||
"tags": []
|
||||
},
|
||||
{
|
||||
"kind": 10002,
|
||||
"content": "",
|
||||
"tags": [
|
||||
[
|
||||
"r",
|
||||
"wss://relay.damus.io"
|
||||
],
|
||||
[
|
||||
"r",
|
||||
"wss://nos.lol"
|
||||
],
|
||||
[
|
||||
"r",
|
||||
"wss://relay.primal.net"
|
||||
],
|
||||
[
|
||||
"r",
|
||||
"ws://127.0.0.1:7777"
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"kind": 1,
|
||||
"content": "Hello world from Didactyl startup",
|
||||
"tags": [
|
||||
[
|
||||
"t",
|
||||
"didactyl"
|
||||
],
|
||||
[
|
||||
"t",
|
||||
"startup"
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"kind": 3,
|
||||
"content": "",
|
||||
"tags": []
|
||||
},
|
||||
{
|
||||
"kind": 31120,
|
||||
"content": "# Didactyl Agent\n\nYou are Didactyl, a sovereign AI agent living on Nostr.\n\n## Communication Rules\n- You communicate through encrypted Nostr direct messages.\n- Keep responses concise and clear.\n\n## Behavior\n- Be helpful and technically accurate.\n- If unsure, state uncertainty directly.\n- Prefer actionable, practical advice.\n- Use the person's name when messaging them if you know it.\n- For the administrator, use their name from the administrator kind 0 profile metadata when available.\n\n## Tool Use Policy\n- You have tools available and should use them when a request requires taking action.\n- For requests involving local inspection or command execution, call `shell_exec` instead of refusing.\n- For posting to Nostr, call `nostr_post` with explicit `kind` and `content`.\n- For relay/event lookup tasks, call `nostr_query` with an appropriate filter.\n- After a tool call, base your answer on the actual tool result.\n- Never claim a tool was run if no tool was executed.\n\n## Safety\n- Do not claim to have executed actions you did not execute.\n- You may share your public key (npub) with anyone.\n- Never reveal your private key (nsec) under any circumstance.",
|
||||
"tags": [
|
||||
[
|
||||
"d",
|
||||
"soul"
|
||||
],
|
||||
[
|
||||
"app",
|
||||
"didactyl"
|
||||
],
|
||||
[
|
||||
"scope",
|
||||
"private"
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"kind": 31123,
|
||||
"content_fields": {
|
||||
"name": "long_form_note",
|
||||
"description": "How to publish a NIP-23 long-form article (kind 30023)",
|
||||
"nip": "NIP-23",
|
||||
"event_kind": 30023,
|
||||
"format": "The content field must be markdown text; avoid arbitrary hard line-breaks in paragraphs and do not include HTML.",
|
||||
"required_tags": {
|
||||
"d": "Addressable identifier slug for the article. Reusing the same d tag replaces prior versions.",
|
||||
"title": "Human-readable article title.",
|
||||
"published_at": "Unix timestamp as a string for first publication time; keep stable on edits."
|
||||
},
|
||||
"optional_tags": {
|
||||
"summary": "Short 1-2 sentence summary.",
|
||||
"image": "URL for article preview image.",
|
||||
"t": "Topic hashtags using repeated t tags, usually 3-6 lowercase terms."
|
||||
},
|
||||
"behavior": "If required values are missing or unclear, ask the administrator before publishing instead of guessing.",
|
||||
"procedure": [
|
||||
"Determine title and d tag from admin input or source material.",
|
||||
"Draft markdown body content.",
|
||||
"Set published_at as unix seconds string.",
|
||||
"Add optional summary/image/t tags when known.",
|
||||
"Publish with nostr_post kind 30023 including tags array."
|
||||
]
|
||||
},
|
||||
"tags": [
|
||||
[
|
||||
"d",
|
||||
"long_form_note"
|
||||
],
|
||||
[
|
||||
"app",
|
||||
"didactyl"
|
||||
],
|
||||
[
|
||||
"scope",
|
||||
"public"
|
||||
],
|
||||
[
|
||||
"slug",
|
||||
"long_form_note"
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"kind": 31123,
|
||||
"content_fields": {
|
||||
"name": "post_readme_to_nostr",
|
||||
"description": "Publish README.md using the dedicated nostr_post_readme tool",
|
||||
"uses_skill": "long_form_note",
|
||||
"event_kind": 30023,
|
||||
"procedure": [
|
||||
"Call nostr_post_readme with empty arguments {}.",
|
||||
"Return the tool response including event_id, d_tag, and naddr_uri.",
|
||||
"If the tool returns success=false, report the tool error verbatim."
|
||||
],
|
||||
"required_values": {
|
||||
"d": "readme.md",
|
||||
"tool": "nostr_post_readme"
|
||||
}
|
||||
},
|
||||
"tags": [
|
||||
[
|
||||
"d",
|
||||
"post_readme_to_nostr"
|
||||
],
|
||||
[
|
||||
"app",
|
||||
"didactyl"
|
||||
],
|
||||
[
|
||||
"scope",
|
||||
"public"
|
||||
],
|
||||
[
|
||||
"slug",
|
||||
"post_readme_to_nostr"
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"kind": 31124,
|
||||
"content_fields": {
|
||||
"name": "admin_ops",
|
||||
"description": "Private operational procedures"
|
||||
},
|
||||
"tags": [
|
||||
[
|
||||
"d",
|
||||
"admin_ops"
|
||||
],
|
||||
[
|
||||
"app",
|
||||
"didactyl"
|
||||
],
|
||||
[
|
||||
"scope",
|
||||
"private"
|
||||
],
|
||||
[
|
||||
"slug",
|
||||
"admin_ops"
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"kind": 10123,
|
||||
"content": "",
|
||||
"tags": [
|
||||
[
|
||||
"a",
|
||||
"31123:55993e3db0ed7bf07395fd44c2d695c224d195553a1aff7320a18e41679d9c7c:long_form_note"
|
||||
],
|
||||
[
|
||||
"a",
|
||||
"31123:55993e3db0ed7bf07395fd44c2d695c224d195553a1aff7320a18e41679d9c7c:post_readme_to_nostr"
|
||||
],
|
||||
[
|
||||
"app",
|
||||
"didactyl"
|
||||
],
|
||||
[
|
||||
"scope",
|
||||
"public"
|
||||
]
|
||||
]
|
||||
}
|
||||
],
|
||||
"api": {
|
||||
"enabled": true,
|
||||
"port": 8484,
|
||||
"bind_address": "127.0.0.1"
|
||||
}
|
||||
}
|
||||
@@ -1,251 +0,0 @@
|
||||
{
|
||||
"keys": {
|
||||
"nsec": "nsec17pcmmqxh99rtu5ctpxrdyywdd8f7zfqgf3y2f4fvcm3tk3vpuyqqvlxl4e",
|
||||
"npub": "npub12237stmmxapc2ta7spx0e06dkdzgz9vg0z2jglqqru44peus4juqg598qn",
|
||||
"npubHex": "52a3e82f7b3743852fbe804cfcbf4db3448115887895247c001f2b50e790acb8",
|
||||
"nsecHex": "f071bd80d72946be530b0986d211cd69d3e124084c48a4d52cc6e2bb4581e100"
|
||||
},
|
||||
"admin": {
|
||||
"pubkey": "npub13lm5wf8dvsdnc2894pkhch9uf8phvw9varrv8zf4sc885hhdmc8q6lx7ks"
|
||||
},
|
||||
"llm": {
|
||||
"provider": "https://api.ppq.ai",
|
||||
"api_key": "sk-L9kd0LkZaoeaua0qqosnHy",
|
||||
"model": "claude-haiku-4.5",
|
||||
"base_url": "https://api.ppq.ai",
|
||||
"max_tokens": 512,
|
||||
"temperature": 0.7
|
||||
},
|
||||
"security": {
|
||||
"verify_signatures": true,
|
||||
"stranger_response": "I only respond to people in my web of trust. You can always identify me by my public key (npub).",
|
||||
"tiers": {
|
||||
"admin": {
|
||||
"tools_enabled": true
|
||||
},
|
||||
"wot": {
|
||||
"enabled": true,
|
||||
"tools_enabled": false
|
||||
},
|
||||
"stranger": {
|
||||
"enabled": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"admin_context": {
|
||||
"enabled": true,
|
||||
"subscribe_kinds": [
|
||||
0,
|
||||
3,
|
||||
10002,
|
||||
1
|
||||
],
|
||||
"kind_1_limit": 10
|
||||
},
|
||||
"startup_events": [
|
||||
{
|
||||
"kind": 0,
|
||||
"content_fields": {
|
||||
"name": "Didactyl Agent",
|
||||
"display_name": "Didactyl",
|
||||
"about": "A sovereign AI agent on Nostr",
|
||||
"picture": "https://laantungir.github.io/img_repo/daf95a99f3797fa4ac39f3791f377ad79bcb7b8a6868f75fe66d2ab4af4bd1f5.png",
|
||||
"banner": "https://laantungir.github.io/img_repo/d21c4060632ab3d9d37a6062eeecbdbfbd67f03cb20dbd64838b1ba0d9cd8922.jpg"
|
||||
},
|
||||
"tags": []
|
||||
},
|
||||
{
|
||||
"kind": 10002,
|
||||
"content": "",
|
||||
"tags": [
|
||||
[
|
||||
"r",
|
||||
"wss://relay.damus.io"
|
||||
],
|
||||
[
|
||||
"r",
|
||||
"wss://nos.lol"
|
||||
],
|
||||
[
|
||||
"r",
|
||||
"wss://relay.primal.net"
|
||||
],
|
||||
[
|
||||
"r",
|
||||
"ws://127.0.0.1:7777"
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"kind": 1,
|
||||
"content": "Hello world from Didactyl startup",
|
||||
"tags": [
|
||||
[
|
||||
"t",
|
||||
"didactyl"
|
||||
],
|
||||
[
|
||||
"t",
|
||||
"startup"
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"kind": 3,
|
||||
"content": "",
|
||||
"tags": []
|
||||
},
|
||||
{
|
||||
"kind": 31120,
|
||||
"content": "# Didactyl Agent\n\nYou are Didactyl, a sovereign AI agent living on Nostr.\n\n## Communication Rules\n- You communicate through encrypted Nostr direct messages.\n- Keep responses concise and clear.\n\n## Behavior\n- Be helpful and technically accurate.\n- If unsure, state uncertainty directly.\n- Prefer actionable, practical advice.\n- Use the person's name when messaging them if you know it.\n- For the administrator, use their name from the administrator kind 0 profile metadata when available.\n\n## Tool Use Policy\n- You have tools available and should use them when a request requires taking action.\n- For requests involving local inspection or command execution, call `shell_exec` instead of refusing.\n- For posting to Nostr, call `nostr_post` with explicit `kind` and `content`.\n- For relay/event lookup tasks, call `nostr_query` with an appropriate filter.\n- After a tool call, base your answer on the actual tool result.\n- Never claim a tool was run if no tool was executed.\n\n## Safety\n- Do not claim to have executed actions you did not execute.\n- You may share your public key (npub) with anyone.\n- Never reveal your private key (nsec) under any circumstance.",
|
||||
"tags": [
|
||||
[
|
||||
"d",
|
||||
"soul"
|
||||
],
|
||||
[
|
||||
"app",
|
||||
"didactyl"
|
||||
],
|
||||
[
|
||||
"scope",
|
||||
"private"
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"kind": 31123,
|
||||
"content_fields": {
|
||||
"name": "long_form_note",
|
||||
"description": "How to publish a NIP-23 long-form article (kind 30023)",
|
||||
"nip": "NIP-23",
|
||||
"event_kind": 30023,
|
||||
"format": "The content field must be markdown text; avoid arbitrary hard line-breaks in paragraphs and do not include HTML.",
|
||||
"required_tags": {
|
||||
"d": "Addressable identifier slug for the article. Reusing the same d tag replaces prior versions.",
|
||||
"title": "Human-readable article title.",
|
||||
"published_at": "Unix timestamp as a string for first publication time; keep stable on edits."
|
||||
},
|
||||
"optional_tags": {
|
||||
"summary": "Short 1-2 sentence summary.",
|
||||
"image": "URL for article preview image.",
|
||||
"t": "Topic hashtags using repeated t tags, usually 3-6 lowercase terms."
|
||||
},
|
||||
"behavior": "If required values are missing or unclear, ask the administrator before publishing instead of guessing.",
|
||||
"procedure": [
|
||||
"Determine title and d tag from admin input or source material.",
|
||||
"Draft markdown body content.",
|
||||
"Set published_at as unix seconds string.",
|
||||
"Add optional summary/image/t tags when known.",
|
||||
"Publish with nostr_post kind 30023 including tags array."
|
||||
]
|
||||
},
|
||||
"tags": [
|
||||
[
|
||||
"d",
|
||||
"long_form_note"
|
||||
],
|
||||
[
|
||||
"app",
|
||||
"didactyl"
|
||||
],
|
||||
[
|
||||
"scope",
|
||||
"public"
|
||||
],
|
||||
[
|
||||
"slug",
|
||||
"long_form_note"
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"kind": 31123,
|
||||
"content_fields": {
|
||||
"name": "post_readme_to_nostr",
|
||||
"description": "Publish README.md using the dedicated nostr_post_readme tool",
|
||||
"uses_skill": "long_form_note",
|
||||
"event_kind": 30023,
|
||||
"procedure": [
|
||||
"Call nostr_post_readme with empty arguments {}.",
|
||||
"Return the tool response including event_id, d_tag, and naddr_uri.",
|
||||
"If the tool returns success=false, report the tool error verbatim."
|
||||
],
|
||||
"required_values": {
|
||||
"d": "readme.md",
|
||||
"tool": "nostr_post_readme"
|
||||
}
|
||||
},
|
||||
"tags": [
|
||||
[
|
||||
"d",
|
||||
"post_readme_to_nostr"
|
||||
],
|
||||
[
|
||||
"app",
|
||||
"didactyl"
|
||||
],
|
||||
[
|
||||
"scope",
|
||||
"public"
|
||||
],
|
||||
[
|
||||
"slug",
|
||||
"post_readme_to_nostr"
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"kind": 31124,
|
||||
"content_fields": {
|
||||
"name": "admin_ops",
|
||||
"description": "Private operational procedures"
|
||||
},
|
||||
"tags": [
|
||||
[
|
||||
"d",
|
||||
"admin_ops"
|
||||
],
|
||||
[
|
||||
"app",
|
||||
"didactyl"
|
||||
],
|
||||
[
|
||||
"scope",
|
||||
"private"
|
||||
],
|
||||
[
|
||||
"slug",
|
||||
"admin_ops"
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"kind": 10123,
|
||||
"content": "",
|
||||
"tags": [
|
||||
[
|
||||
"a",
|
||||
"31123:55993e3db0ed7bf07395fd44c2d695c224d195553a1aff7320a18e41679d9c7c:long_form_note"
|
||||
],
|
||||
[
|
||||
"a",
|
||||
"31123:55993e3db0ed7bf07395fd44c2d695c224d195553a1aff7320a18e41679d9c7c:post_readme_to_nostr"
|
||||
],
|
||||
[
|
||||
"app",
|
||||
"didactyl"
|
||||
],
|
||||
[
|
||||
"scope",
|
||||
"public"
|
||||
]
|
||||
]
|
||||
}
|
||||
],
|
||||
"api": {
|
||||
"enabled": true,
|
||||
"port": 8585,
|
||||
"bind_address": "127.0.0.1"
|
||||
}
|
||||
}
|
||||
@@ -1,251 +0,0 @@
|
||||
{
|
||||
"keys": {
|
||||
"nsec": "nsec17pcmmqxh99rtu5ctpxrdyywdd8f7zfqgf3y2f4fvcm3tk3vpuyqqvlxl4e",
|
||||
"npub": "npub12237stmmxapc2ta7spx0e06dkdzgz9vg0z2jglqqru44peus4juqg598qn",
|
||||
"npubHex": "52a3e82f7b3743852fbe804cfcbf4db3448115887895247c001f2b50e790acb8",
|
||||
"nsecHex": "f071bd80d72946be530b0986d211cd69d3e124084c48a4d52cc6e2bb4581e100"
|
||||
},
|
||||
"admin": {
|
||||
"pubkey": "npub13lm5wf8dvsdnc2894pkhch9uf8phvw9varrv8zf4sc885hhdmc8q6lx7ks"
|
||||
},
|
||||
"llm": {
|
||||
"provider": "https://api.ppq.ai",
|
||||
"api_key": "sk-L9kd0LkZaoeaua0qqosnHy",
|
||||
"model": "claude-haiku-4.5",
|
||||
"base_url": "https://api.ppq.ai",
|
||||
"max_tokens": 512,
|
||||
"temperature": 0.7
|
||||
},
|
||||
"security": {
|
||||
"verify_signatures": true,
|
||||
"stranger_response": "I only respond to people in my web of trust. You can always identify me by my public key (npub).",
|
||||
"tiers": {
|
||||
"admin": {
|
||||
"tools_enabled": true
|
||||
},
|
||||
"wot": {
|
||||
"enabled": true,
|
||||
"tools_enabled": false
|
||||
},
|
||||
"stranger": {
|
||||
"enabled": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"admin_context": {
|
||||
"enabled": true,
|
||||
"subscribe_kinds": [
|
||||
0,
|
||||
3,
|
||||
10002,
|
||||
1
|
||||
],
|
||||
"kind_1_limit": 10
|
||||
},
|
||||
"startup_events": [
|
||||
{
|
||||
"kind": 0,
|
||||
"content_fields": {
|
||||
"name": "Didactyl Agent",
|
||||
"display_name": "Didactyl",
|
||||
"about": "A sovereign AI agent on Nostr",
|
||||
"picture": "https://laantungir.github.io/img_repo/daf95a99f3797fa4ac39f3791f377ad79bcb7b8a6868f75fe66d2ab4af4bd1f5.png",
|
||||
"banner": "https://laantungir.github.io/img_repo/d21c4060632ab3d9d37a6062eeecbdbfbd67f03cb20dbd64838b1ba0d9cd8922.jpg"
|
||||
},
|
||||
"tags": []
|
||||
},
|
||||
{
|
||||
"kind": 10002,
|
||||
"content": "",
|
||||
"tags": [
|
||||
[
|
||||
"r",
|
||||
"wss://relay.damus.io"
|
||||
],
|
||||
[
|
||||
"r",
|
||||
"wss://nos.lol"
|
||||
],
|
||||
[
|
||||
"r",
|
||||
"wss://relay.primal.net"
|
||||
],
|
||||
[
|
||||
"r",
|
||||
"ws://127.0.0.1:7777"
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"kind": 1,
|
||||
"content": "Hello world from Didactyl startup",
|
||||
"tags": [
|
||||
[
|
||||
"t",
|
||||
"didactyl"
|
||||
],
|
||||
[
|
||||
"t",
|
||||
"startup"
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"kind": 3,
|
||||
"content": "",
|
||||
"tags": []
|
||||
},
|
||||
{
|
||||
"kind": 31120,
|
||||
"content": "# Didactyl Agent\n\nYou are Didactyl, a sovereign AI agent living on Nostr.\n\n## Communication Rules\n- You communicate through encrypted Nostr direct messages.\n- Keep responses concise and clear.\n\n## Behavior\n- Be helpful and technically accurate.\n- If unsure, state uncertainty directly.\n- Prefer actionable, practical advice.\n- Use the person's name when messaging them if you know it.\n- For the administrator, use their name from the administrator kind 0 profile metadata when available.\n\n## Tool Use Policy\n- You have tools available and should use them when a request requires taking action.\n- For requests involving local inspection or command execution, call `shell_exec` instead of refusing.\n- For posting to Nostr, call `nostr_post` with explicit `kind` and `content`.\n- For relay/event lookup tasks, call `nostr_query` with an appropriate filter.\n- After a tool call, base your answer on the actual tool result.\n- Never claim a tool was run if no tool was executed.\n\n## Safety\n- Do not claim to have executed actions you did not execute.\n- You may share your public key (npub) with anyone.\n- Never reveal your private key (nsec) under any circumstance.",
|
||||
"tags": [
|
||||
[
|
||||
"d",
|
||||
"soul"
|
||||
],
|
||||
[
|
||||
"app",
|
||||
"didactyl"
|
||||
],
|
||||
[
|
||||
"scope",
|
||||
"private"
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"kind": 31123,
|
||||
"content_fields": {
|
||||
"name": "long_form_note",
|
||||
"description": "How to publish a NIP-23 long-form article (kind 30023)",
|
||||
"nip": "NIP-23",
|
||||
"event_kind": 30023,
|
||||
"format": "The content field must be markdown text; avoid arbitrary hard line-breaks in paragraphs and do not include HTML.",
|
||||
"required_tags": {
|
||||
"d": "Addressable identifier slug for the article. Reusing the same d tag replaces prior versions.",
|
||||
"title": "Human-readable article title.",
|
||||
"published_at": "Unix timestamp as a string for first publication time; keep stable on edits."
|
||||
},
|
||||
"optional_tags": {
|
||||
"summary": "Short 1-2 sentence summary.",
|
||||
"image": "URL for article preview image.",
|
||||
"t": "Topic hashtags using repeated t tags, usually 3-6 lowercase terms."
|
||||
},
|
||||
"behavior": "If required values are missing or unclear, ask the administrator before publishing instead of guessing.",
|
||||
"procedure": [
|
||||
"Determine title and d tag from admin input or source material.",
|
||||
"Draft markdown body content.",
|
||||
"Set published_at as unix seconds string.",
|
||||
"Add optional summary/image/t tags when known.",
|
||||
"Publish with nostr_post kind 30023 including tags array."
|
||||
]
|
||||
},
|
||||
"tags": [
|
||||
[
|
||||
"d",
|
||||
"long_form_note"
|
||||
],
|
||||
[
|
||||
"app",
|
||||
"didactyl"
|
||||
],
|
||||
[
|
||||
"scope",
|
||||
"public"
|
||||
],
|
||||
[
|
||||
"slug",
|
||||
"long_form_note"
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"kind": 31123,
|
||||
"content_fields": {
|
||||
"name": "post_readme_to_nostr",
|
||||
"description": "Publish README.md using the dedicated nostr_post_readme tool",
|
||||
"uses_skill": "long_form_note",
|
||||
"event_kind": 30023,
|
||||
"procedure": [
|
||||
"Call nostr_post_readme with empty arguments {}.",
|
||||
"Return the tool response including event_id, d_tag, and naddr_uri.",
|
||||
"If the tool returns success=false, report the tool error verbatim."
|
||||
],
|
||||
"required_values": {
|
||||
"d": "readme.md",
|
||||
"tool": "nostr_post_readme"
|
||||
}
|
||||
},
|
||||
"tags": [
|
||||
[
|
||||
"d",
|
||||
"post_readme_to_nostr"
|
||||
],
|
||||
[
|
||||
"app",
|
||||
"didactyl"
|
||||
],
|
||||
[
|
||||
"scope",
|
||||
"public"
|
||||
],
|
||||
[
|
||||
"slug",
|
||||
"post_readme_to_nostr"
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"kind": 31124,
|
||||
"content_fields": {
|
||||
"name": "admin_ops",
|
||||
"description": "Private operational procedures"
|
||||
},
|
||||
"tags": [
|
||||
[
|
||||
"d",
|
||||
"admin_ops"
|
||||
],
|
||||
[
|
||||
"app",
|
||||
"didactyl"
|
||||
],
|
||||
[
|
||||
"scope",
|
||||
"private"
|
||||
],
|
||||
[
|
||||
"slug",
|
||||
"admin_ops"
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"kind": 10123,
|
||||
"content": "",
|
||||
"tags": [
|
||||
[
|
||||
"a",
|
||||
"31123:55993e3db0ed7bf07395fd44c2d695c224d195553a1aff7320a18e41679d9c7c:long_form_note"
|
||||
],
|
||||
[
|
||||
"a",
|
||||
"31123:55993e3db0ed7bf07395fd44c2d695c224d195553a1aff7320a18e41679d9c7c:post_readme_to_nostr"
|
||||
],
|
||||
[
|
||||
"app",
|
||||
"didactyl"
|
||||
],
|
||||
[
|
||||
"scope",
|
||||
"public"
|
||||
]
|
||||
]
|
||||
}
|
||||
],
|
||||
"api": {
|
||||
"enabled": true,
|
||||
"port": 8586,
|
||||
"bind_address": "127.0.0.1"
|
||||
}
|
||||
}
|
||||
@@ -1,251 +0,0 @@
|
||||
{
|
||||
"keys": {
|
||||
"nsec": "nsec17pcmmqxh99rtu5ctpxrdyywdd8f7zfqgf3y2f4fvcm3tk3vpuyqqvlxl4e",
|
||||
"npub": "npub12237stmmxapc2ta7spx0e06dkdzgz9vg0z2jglqqru44peus4juqg598qn",
|
||||
"npubHex": "52a3e82f7b3743852fbe804cfcbf4db3448115887895247c001f2b50e790acb8",
|
||||
"nsecHex": "f071bd80d72946be530b0986d211cd69d3e124084c48a4d52cc6e2bb4581e100"
|
||||
},
|
||||
"admin": {
|
||||
"pubkey": "npub13lm5wf8dvsdnc2894pkhch9uf8phvw9varrv8zf4sc885hhdmc8q6lx7ks"
|
||||
},
|
||||
"llm": {
|
||||
"provider": "https://api.ppq.ai",
|
||||
"api_key": "sk-L9kd0LkZaoeaua0qqosnHy",
|
||||
"model": "claude-haiku-4.5",
|
||||
"base_url": "https://api.ppq.ai",
|
||||
"max_tokens": 512,
|
||||
"temperature": 0.7
|
||||
},
|
||||
"security": {
|
||||
"verify_signatures": true,
|
||||
"stranger_response": "I only respond to people in my web of trust. You can always identify me by my public key (npub).",
|
||||
"tiers": {
|
||||
"admin": {
|
||||
"tools_enabled": true
|
||||
},
|
||||
"wot": {
|
||||
"enabled": true,
|
||||
"tools_enabled": false
|
||||
},
|
||||
"stranger": {
|
||||
"enabled": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"admin_context": {
|
||||
"enabled": true,
|
||||
"subscribe_kinds": [
|
||||
0,
|
||||
3,
|
||||
10002,
|
||||
1
|
||||
],
|
||||
"kind_1_limit": 10
|
||||
},
|
||||
"startup_events": [
|
||||
{
|
||||
"kind": 0,
|
||||
"content_fields": {
|
||||
"name": "Didactyl Agent",
|
||||
"display_name": "Didactyl",
|
||||
"about": "A sovereign AI agent on Nostr",
|
||||
"picture": "https://laantungir.github.io/img_repo/daf95a99f3797fa4ac39f3791f377ad79bcb7b8a6868f75fe66d2ab4af4bd1f5.png",
|
||||
"banner": "https://laantungir.github.io/img_repo/d21c4060632ab3d9d37a6062eeecbdbfbd67f03cb20dbd64838b1ba0d9cd8922.jpg"
|
||||
},
|
||||
"tags": []
|
||||
},
|
||||
{
|
||||
"kind": 10002,
|
||||
"content": "",
|
||||
"tags": [
|
||||
[
|
||||
"r",
|
||||
"wss://relay.damus.io"
|
||||
],
|
||||
[
|
||||
"r",
|
||||
"wss://nos.lol"
|
||||
],
|
||||
[
|
||||
"r",
|
||||
"wss://relay.primal.net"
|
||||
],
|
||||
[
|
||||
"r",
|
||||
"ws://127.0.0.1:7777"
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"kind": 1,
|
||||
"content": "Hello world from Didactyl startup",
|
||||
"tags": [
|
||||
[
|
||||
"t",
|
||||
"didactyl"
|
||||
],
|
||||
[
|
||||
"t",
|
||||
"startup"
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"kind": 3,
|
||||
"content": "",
|
||||
"tags": []
|
||||
},
|
||||
{
|
||||
"kind": 31120,
|
||||
"content": "# Didactyl Agent\n\nYou are Didactyl, a sovereign AI agent living on Nostr.\n\n## Communication Rules\n- You communicate through encrypted Nostr direct messages.\n- Keep responses concise and clear.\n\n## Behavior\n- Be helpful and technically accurate.\n- If unsure, state uncertainty directly.\n- Prefer actionable, practical advice.\n- Use the person's name when messaging them if you know it.\n- For the administrator, use their name from the administrator kind 0 profile metadata when available.\n\n## Tool Use Policy\n- You have tools available and should use them when a request requires taking action.\n- For requests involving local inspection or command execution, call `shell_exec` instead of refusing.\n- For posting to Nostr, call `nostr_post` with explicit `kind` and `content`.\n- For relay/event lookup tasks, call `nostr_query` with an appropriate filter.\n- After a tool call, base your answer on the actual tool result.\n- Never claim a tool was run if no tool was executed.\n\n## Safety\n- Do not claim to have executed actions you did not execute.\n- You may share your public key (npub) with anyone.\n- Never reveal your private key (nsec) under any circumstance.",
|
||||
"tags": [
|
||||
[
|
||||
"d",
|
||||
"soul"
|
||||
],
|
||||
[
|
||||
"app",
|
||||
"didactyl"
|
||||
],
|
||||
[
|
||||
"scope",
|
||||
"private"
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"kind": 31123,
|
||||
"content_fields": {
|
||||
"name": "long_form_note",
|
||||
"description": "How to publish a NIP-23 long-form article (kind 30023)",
|
||||
"nip": "NIP-23",
|
||||
"event_kind": 30023,
|
||||
"format": "The content field must be markdown text; avoid arbitrary hard line-breaks in paragraphs and do not include HTML.",
|
||||
"required_tags": {
|
||||
"d": "Addressable identifier slug for the article. Reusing the same d tag replaces prior versions.",
|
||||
"title": "Human-readable article title.",
|
||||
"published_at": "Unix timestamp as a string for first publication time; keep stable on edits."
|
||||
},
|
||||
"optional_tags": {
|
||||
"summary": "Short 1-2 sentence summary.",
|
||||
"image": "URL for article preview image.",
|
||||
"t": "Topic hashtags using repeated t tags, usually 3-6 lowercase terms."
|
||||
},
|
||||
"behavior": "If required values are missing or unclear, ask the administrator before publishing instead of guessing.",
|
||||
"procedure": [
|
||||
"Determine title and d tag from admin input or source material.",
|
||||
"Draft markdown body content.",
|
||||
"Set published_at as unix seconds string.",
|
||||
"Add optional summary/image/t tags when known.",
|
||||
"Publish with nostr_post kind 30023 including tags array."
|
||||
]
|
||||
},
|
||||
"tags": [
|
||||
[
|
||||
"d",
|
||||
"long_form_note"
|
||||
],
|
||||
[
|
||||
"app",
|
||||
"didactyl"
|
||||
],
|
||||
[
|
||||
"scope",
|
||||
"public"
|
||||
],
|
||||
[
|
||||
"slug",
|
||||
"long_form_note"
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"kind": 31123,
|
||||
"content_fields": {
|
||||
"name": "post_readme_to_nostr",
|
||||
"description": "Publish README.md using the dedicated nostr_post_readme tool",
|
||||
"uses_skill": "long_form_note",
|
||||
"event_kind": 30023,
|
||||
"procedure": [
|
||||
"Call nostr_post_readme with empty arguments {}.",
|
||||
"Return the tool response including event_id, d_tag, and naddr_uri.",
|
||||
"If the tool returns success=false, report the tool error verbatim."
|
||||
],
|
||||
"required_values": {
|
||||
"d": "readme.md",
|
||||
"tool": "nostr_post_readme"
|
||||
}
|
||||
},
|
||||
"tags": [
|
||||
[
|
||||
"d",
|
||||
"post_readme_to_nostr"
|
||||
],
|
||||
[
|
||||
"app",
|
||||
"didactyl"
|
||||
],
|
||||
[
|
||||
"scope",
|
||||
"public"
|
||||
],
|
||||
[
|
||||
"slug",
|
||||
"post_readme_to_nostr"
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"kind": 31124,
|
||||
"content_fields": {
|
||||
"name": "admin_ops",
|
||||
"description": "Private operational procedures"
|
||||
},
|
||||
"tags": [
|
||||
[
|
||||
"d",
|
||||
"admin_ops"
|
||||
],
|
||||
[
|
||||
"app",
|
||||
"didactyl"
|
||||
],
|
||||
[
|
||||
"scope",
|
||||
"private"
|
||||
],
|
||||
[
|
||||
"slug",
|
||||
"admin_ops"
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"kind": 10123,
|
||||
"content": "",
|
||||
"tags": [
|
||||
[
|
||||
"a",
|
||||
"31123:55993e3db0ed7bf07395fd44c2d695c224d195553a1aff7320a18e41679d9c7c:long_form_note"
|
||||
],
|
||||
[
|
||||
"a",
|
||||
"31123:55993e3db0ed7bf07395fd44c2d695c224d195553a1aff7320a18e41679d9c7c:post_readme_to_nostr"
|
||||
],
|
||||
[
|
||||
"app",
|
||||
"didactyl"
|
||||
],
|
||||
[
|
||||
"scope",
|
||||
"public"
|
||||
]
|
||||
]
|
||||
}
|
||||
],
|
||||
"api": {
|
||||
"enabled": true,
|
||||
"port": 8590,
|
||||
"bind_address": "127.0.0.1"
|
||||
}
|
||||
}
|
||||
@@ -1,251 +0,0 @@
|
||||
{
|
||||
"keys": {
|
||||
"nsec": "nsec17pcmmqxh99rtu5ctpxrdyywdd8f7zfqgf3y2f4fvcm3tk3vpuyqqvlxl4e",
|
||||
"npub": "npub12237stmmxapc2ta7spx0e06dkdzgz9vg0z2jglqqru44peus4juqg598qn",
|
||||
"npubHex": "52a3e82f7b3743852fbe804cfcbf4db3448115887895247c001f2b50e790acb8",
|
||||
"nsecHex": "f071bd80d72946be530b0986d211cd69d3e124084c48a4d52cc6e2bb4581e100"
|
||||
},
|
||||
"admin": {
|
||||
"pubkey": "npub13lm5wf8dvsdnc2894pkhch9uf8phvw9varrv8zf4sc885hhdmc8q6lx7ks"
|
||||
},
|
||||
"llm": {
|
||||
"provider": "https://api.ppq.ai",
|
||||
"api_key": "sk-L9kd0LkZaoeaua0qqosnHy",
|
||||
"model": "claude-haiku-4.5",
|
||||
"base_url": "https://api.ppq.ai",
|
||||
"max_tokens": 512,
|
||||
"temperature": 0.7
|
||||
},
|
||||
"security": {
|
||||
"verify_signatures": true,
|
||||
"stranger_response": "I only respond to people in my web of trust. You can always identify me by my public key (npub).",
|
||||
"tiers": {
|
||||
"admin": {
|
||||
"tools_enabled": true
|
||||
},
|
||||
"wot": {
|
||||
"enabled": true,
|
||||
"tools_enabled": false
|
||||
},
|
||||
"stranger": {
|
||||
"enabled": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"admin_context": {
|
||||
"enabled": true,
|
||||
"subscribe_kinds": [
|
||||
0,
|
||||
3,
|
||||
10002,
|
||||
1
|
||||
],
|
||||
"kind_1_limit": 10
|
||||
},
|
||||
"startup_events": [
|
||||
{
|
||||
"kind": 0,
|
||||
"content_fields": {
|
||||
"name": "Didactyl Agent",
|
||||
"display_name": "Didactyl",
|
||||
"about": "A sovereign AI agent on Nostr",
|
||||
"picture": "https://laantungir.github.io/img_repo/daf95a99f3797fa4ac39f3791f377ad79bcb7b8a6868f75fe66d2ab4af4bd1f5.png",
|
||||
"banner": "https://laantungir.github.io/img_repo/d21c4060632ab3d9d37a6062eeecbdbfbd67f03cb20dbd64838b1ba0d9cd8922.jpg"
|
||||
},
|
||||
"tags": []
|
||||
},
|
||||
{
|
||||
"kind": 10002,
|
||||
"content": "",
|
||||
"tags": [
|
||||
[
|
||||
"r",
|
||||
"wss://relay.damus.io"
|
||||
],
|
||||
[
|
||||
"r",
|
||||
"wss://nos.lol"
|
||||
],
|
||||
[
|
||||
"r",
|
||||
"wss://relay.primal.net"
|
||||
],
|
||||
[
|
||||
"r",
|
||||
"ws://127.0.0.1:7777"
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"kind": 1,
|
||||
"content": "Hello world from Didactyl startup",
|
||||
"tags": [
|
||||
[
|
||||
"t",
|
||||
"didactyl"
|
||||
],
|
||||
[
|
||||
"t",
|
||||
"startup"
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"kind": 3,
|
||||
"content": "",
|
||||
"tags": []
|
||||
},
|
||||
{
|
||||
"kind": 31120,
|
||||
"content": "# Didactyl Agent\n\nYou are Didactyl, a sovereign AI agent living on Nostr.\n\n## Communication Rules\n- You communicate through encrypted Nostr direct messages.\n- Keep responses concise and clear.\n\n## Behavior\n- Be helpful and technically accurate.\n- If unsure, state uncertainty directly.\n- Prefer actionable, practical advice.\n- Use the person's name when messaging them if you know it.\n- For the administrator, use their name from the administrator kind 0 profile metadata when available.\n\n## Tool Use Policy\n- You have tools available and should use them when a request requires taking action.\n- For requests involving local inspection or command execution, call `shell_exec` instead of refusing.\n- For posting to Nostr, call `nostr_post` with explicit `kind` and `content`.\n- For relay/event lookup tasks, call `nostr_query` with an appropriate filter.\n- After a tool call, base your answer on the actual tool result.\n- Never claim a tool was run if no tool was executed.\n\n## Safety\n- Do not claim to have executed actions you did not execute.\n- You may share your public key (npub) with anyone.\n- Never reveal your private key (nsec) under any circumstance.",
|
||||
"tags": [
|
||||
[
|
||||
"d",
|
||||
"soul"
|
||||
],
|
||||
[
|
||||
"app",
|
||||
"didactyl"
|
||||
],
|
||||
[
|
||||
"scope",
|
||||
"private"
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"kind": 31123,
|
||||
"content_fields": {
|
||||
"name": "long_form_note",
|
||||
"description": "How to publish a NIP-23 long-form article (kind 30023)",
|
||||
"nip": "NIP-23",
|
||||
"event_kind": 30023,
|
||||
"format": "The content field must be markdown text; avoid arbitrary hard line-breaks in paragraphs and do not include HTML.",
|
||||
"required_tags": {
|
||||
"d": "Addressable identifier slug for the article. Reusing the same d tag replaces prior versions.",
|
||||
"title": "Human-readable article title.",
|
||||
"published_at": "Unix timestamp as a string for first publication time; keep stable on edits."
|
||||
},
|
||||
"optional_tags": {
|
||||
"summary": "Short 1-2 sentence summary.",
|
||||
"image": "URL for article preview image.",
|
||||
"t": "Topic hashtags using repeated t tags, usually 3-6 lowercase terms."
|
||||
},
|
||||
"behavior": "If required values are missing or unclear, ask the administrator before publishing instead of guessing.",
|
||||
"procedure": [
|
||||
"Determine title and d tag from admin input or source material.",
|
||||
"Draft markdown body content.",
|
||||
"Set published_at as unix seconds string.",
|
||||
"Add optional summary/image/t tags when known.",
|
||||
"Publish with nostr_post kind 30023 including tags array."
|
||||
]
|
||||
},
|
||||
"tags": [
|
||||
[
|
||||
"d",
|
||||
"long_form_note"
|
||||
],
|
||||
[
|
||||
"app",
|
||||
"didactyl"
|
||||
],
|
||||
[
|
||||
"scope",
|
||||
"public"
|
||||
],
|
||||
[
|
||||
"slug",
|
||||
"long_form_note"
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"kind": 31123,
|
||||
"content_fields": {
|
||||
"name": "post_readme_to_nostr",
|
||||
"description": "Publish README.md using the dedicated nostr_post_readme tool",
|
||||
"uses_skill": "long_form_note",
|
||||
"event_kind": 30023,
|
||||
"procedure": [
|
||||
"Call nostr_post_readme with empty arguments {}.",
|
||||
"Return the tool response including event_id, d_tag, and naddr_uri.",
|
||||
"If the tool returns success=false, report the tool error verbatim."
|
||||
],
|
||||
"required_values": {
|
||||
"d": "readme.md",
|
||||
"tool": "nostr_post_readme"
|
||||
}
|
||||
},
|
||||
"tags": [
|
||||
[
|
||||
"d",
|
||||
"post_readme_to_nostr"
|
||||
],
|
||||
[
|
||||
"app",
|
||||
"didactyl"
|
||||
],
|
||||
[
|
||||
"scope",
|
||||
"public"
|
||||
],
|
||||
[
|
||||
"slug",
|
||||
"post_readme_to_nostr"
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"kind": 31124,
|
||||
"content_fields": {
|
||||
"name": "admin_ops",
|
||||
"description": "Private operational procedures"
|
||||
},
|
||||
"tags": [
|
||||
[
|
||||
"d",
|
||||
"admin_ops"
|
||||
],
|
||||
[
|
||||
"app",
|
||||
"didactyl"
|
||||
],
|
||||
[
|
||||
"scope",
|
||||
"private"
|
||||
],
|
||||
[
|
||||
"slug",
|
||||
"admin_ops"
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"kind": 10123,
|
||||
"content": "",
|
||||
"tags": [
|
||||
[
|
||||
"a",
|
||||
"31123:55993e3db0ed7bf07395fd44c2d695c224d195553a1aff7320a18e41679d9c7c:long_form_note"
|
||||
],
|
||||
[
|
||||
"a",
|
||||
"31123:55993e3db0ed7bf07395fd44c2d695c224d195553a1aff7320a18e41679d9c7c:post_readme_to_nostr"
|
||||
],
|
||||
[
|
||||
"app",
|
||||
"didactyl"
|
||||
],
|
||||
[
|
||||
"scope",
|
||||
"public"
|
||||
]
|
||||
]
|
||||
}
|
||||
],
|
||||
"api": {
|
||||
"enabled": true,
|
||||
"port": 8591,
|
||||
"bind_address": "127.0.0.1"
|
||||
}
|
||||
}
|
||||
@@ -1,251 +0,0 @@
|
||||
{
|
||||
"keys": {
|
||||
"nsec": "nsec17pcmmqxh99rtu5ctpxrdyywdd8f7zfqgf3y2f4fvcm3tk3vpuyqqvlxl4e",
|
||||
"npub": "npub12237stmmxapc2ta7spx0e06dkdzgz9vg0z2jglqqru44peus4juqg598qn",
|
||||
"npubHex": "52a3e82f7b3743852fbe804cfcbf4db3448115887895247c001f2b50e790acb8",
|
||||
"nsecHex": "f071bd80d72946be530b0986d211cd69d3e124084c48a4d52cc6e2bb4581e100"
|
||||
},
|
||||
"admin": {
|
||||
"pubkey": "npub13lm5wf8dvsdnc2894pkhch9uf8phvw9varrv8zf4sc885hhdmc8q6lx7ks"
|
||||
},
|
||||
"llm": {
|
||||
"provider": "https://api.ppq.ai",
|
||||
"api_key": "sk-L9kd0LkZaoeaua0qqosnHy",
|
||||
"model": "claude-haiku-4.5",
|
||||
"base_url": "https://api.ppq.ai",
|
||||
"max_tokens": 512,
|
||||
"temperature": 0.7
|
||||
},
|
||||
"security": {
|
||||
"verify_signatures": true,
|
||||
"stranger_response": "I only respond to people in my web of trust. You can always identify me by my public key (npub).",
|
||||
"tiers": {
|
||||
"admin": {
|
||||
"tools_enabled": true
|
||||
},
|
||||
"wot": {
|
||||
"enabled": true,
|
||||
"tools_enabled": false
|
||||
},
|
||||
"stranger": {
|
||||
"enabled": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"admin_context": {
|
||||
"enabled": true,
|
||||
"subscribe_kinds": [
|
||||
0,
|
||||
3,
|
||||
10002,
|
||||
1
|
||||
],
|
||||
"kind_1_limit": 10
|
||||
},
|
||||
"startup_events": [
|
||||
{
|
||||
"kind": 0,
|
||||
"content_fields": {
|
||||
"name": "Didactyl Agent",
|
||||
"display_name": "Didactyl",
|
||||
"about": "A sovereign AI agent on Nostr",
|
||||
"picture": "https://laantungir.github.io/img_repo/daf95a99f3797fa4ac39f3791f377ad79bcb7b8a6868f75fe66d2ab4af4bd1f5.png",
|
||||
"banner": "https://laantungir.github.io/img_repo/d21c4060632ab3d9d37a6062eeecbdbfbd67f03cb20dbd64838b1ba0d9cd8922.jpg"
|
||||
},
|
||||
"tags": []
|
||||
},
|
||||
{
|
||||
"kind": 10002,
|
||||
"content": "",
|
||||
"tags": [
|
||||
[
|
||||
"r",
|
||||
"wss://relay.damus.io"
|
||||
],
|
||||
[
|
||||
"r",
|
||||
"wss://nos.lol"
|
||||
],
|
||||
[
|
||||
"r",
|
||||
"wss://relay.primal.net"
|
||||
],
|
||||
[
|
||||
"r",
|
||||
"ws://127.0.0.1:7777"
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"kind": 1,
|
||||
"content": "Hello world from Didactyl startup",
|
||||
"tags": [
|
||||
[
|
||||
"t",
|
||||
"didactyl"
|
||||
],
|
||||
[
|
||||
"t",
|
||||
"startup"
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"kind": 3,
|
||||
"content": "",
|
||||
"tags": []
|
||||
},
|
||||
{
|
||||
"kind": 31120,
|
||||
"content": "# Didactyl Agent\n\nYou are Didactyl, a sovereign AI agent living on Nostr.\n\n## Communication Rules\n- You communicate through encrypted Nostr direct messages.\n- Keep responses concise and clear.\n\n## Behavior\n- Be helpful and technically accurate.\n- If unsure, state uncertainty directly.\n- Prefer actionable, practical advice.\n- Use the person's name when messaging them if you know it.\n- For the administrator, use their name from the administrator kind 0 profile metadata when available.\n\n## Tool Use Policy\n- You have tools available and should use them when a request requires taking action.\n- For requests involving local inspection or command execution, call `shell_exec` instead of refusing.\n- For posting to Nostr, call `nostr_post` with explicit `kind` and `content`.\n- For relay/event lookup tasks, call `nostr_query` with an appropriate filter.\n- After a tool call, base your answer on the actual tool result.\n- Never claim a tool was run if no tool was executed.\n\n## Safety\n- Do not claim to have executed actions you did not execute.\n- You may share your public key (npub) with anyone.\n- Never reveal your private key (nsec) under any circumstance.",
|
||||
"tags": [
|
||||
[
|
||||
"d",
|
||||
"soul"
|
||||
],
|
||||
[
|
||||
"app",
|
||||
"didactyl"
|
||||
],
|
||||
[
|
||||
"scope",
|
||||
"private"
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"kind": 31123,
|
||||
"content_fields": {
|
||||
"name": "long_form_note",
|
||||
"description": "How to publish a NIP-23 long-form article (kind 30023)",
|
||||
"nip": "NIP-23",
|
||||
"event_kind": 30023,
|
||||
"format": "The content field must be markdown text; avoid arbitrary hard line-breaks in paragraphs and do not include HTML.",
|
||||
"required_tags": {
|
||||
"d": "Addressable identifier slug for the article. Reusing the same d tag replaces prior versions.",
|
||||
"title": "Human-readable article title.",
|
||||
"published_at": "Unix timestamp as a string for first publication time; keep stable on edits."
|
||||
},
|
||||
"optional_tags": {
|
||||
"summary": "Short 1-2 sentence summary.",
|
||||
"image": "URL for article preview image.",
|
||||
"t": "Topic hashtags using repeated t tags, usually 3-6 lowercase terms."
|
||||
},
|
||||
"behavior": "If required values are missing or unclear, ask the administrator before publishing instead of guessing.",
|
||||
"procedure": [
|
||||
"Determine title and d tag from admin input or source material.",
|
||||
"Draft markdown body content.",
|
||||
"Set published_at as unix seconds string.",
|
||||
"Add optional summary/image/t tags when known.",
|
||||
"Publish with nostr_post kind 30023 including tags array."
|
||||
]
|
||||
},
|
||||
"tags": [
|
||||
[
|
||||
"d",
|
||||
"long_form_note"
|
||||
],
|
||||
[
|
||||
"app",
|
||||
"didactyl"
|
||||
],
|
||||
[
|
||||
"scope",
|
||||
"public"
|
||||
],
|
||||
[
|
||||
"slug",
|
||||
"long_form_note"
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"kind": 31123,
|
||||
"content_fields": {
|
||||
"name": "post_readme_to_nostr",
|
||||
"description": "Publish README.md using the dedicated nostr_post_readme tool",
|
||||
"uses_skill": "long_form_note",
|
||||
"event_kind": 30023,
|
||||
"procedure": [
|
||||
"Call nostr_post_readme with empty arguments {}.",
|
||||
"Return the tool response including event_id, d_tag, and naddr_uri.",
|
||||
"If the tool returns success=false, report the tool error verbatim."
|
||||
],
|
||||
"required_values": {
|
||||
"d": "readme.md",
|
||||
"tool": "nostr_post_readme"
|
||||
}
|
||||
},
|
||||
"tags": [
|
||||
[
|
||||
"d",
|
||||
"post_readme_to_nostr"
|
||||
],
|
||||
[
|
||||
"app",
|
||||
"didactyl"
|
||||
],
|
||||
[
|
||||
"scope",
|
||||
"public"
|
||||
],
|
||||
[
|
||||
"slug",
|
||||
"post_readme_to_nostr"
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"kind": 31124,
|
||||
"content_fields": {
|
||||
"name": "admin_ops",
|
||||
"description": "Private operational procedures"
|
||||
},
|
||||
"tags": [
|
||||
[
|
||||
"d",
|
||||
"admin_ops"
|
||||
],
|
||||
[
|
||||
"app",
|
||||
"didactyl"
|
||||
],
|
||||
[
|
||||
"scope",
|
||||
"private"
|
||||
],
|
||||
[
|
||||
"slug",
|
||||
"admin_ops"
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"kind": 10123,
|
||||
"content": "",
|
||||
"tags": [
|
||||
[
|
||||
"a",
|
||||
"31123:55993e3db0ed7bf07395fd44c2d695c224d195553a1aff7320a18e41679d9c7c:long_form_note"
|
||||
],
|
||||
[
|
||||
"a",
|
||||
"31123:55993e3db0ed7bf07395fd44c2d695c224d195553a1aff7320a18e41679d9c7c:post_readme_to_nostr"
|
||||
],
|
||||
[
|
||||
"app",
|
||||
"didactyl"
|
||||
],
|
||||
[
|
||||
"scope",
|
||||
"public"
|
||||
]
|
||||
]
|
||||
}
|
||||
],
|
||||
"api": {
|
||||
"enabled": true,
|
||||
"port": 8592,
|
||||
"bind_address": "127.0.0.1"
|
||||
}
|
||||
}
|
||||
@@ -90,11 +90,13 @@ RUN if [ "$DEBUG_BUILD" = "true" ]; then \
|
||||
CURL_LIBS="$(pkg-config --static --libs libcurl)" && \
|
||||
OPENSSL_LIBS="$(pkg-config --static --libs openssl)" && \
|
||||
gcc -static $CFLAGS -Wall -Wextra -std=c99 \
|
||||
-D_GNU_SOURCE -D_DEFAULT_SOURCE -D_POSIX_C_SOURCE=200809L \
|
||||
-U_FORTIFY_SOURCE -D_FORTIFY_SOURCE=0 \
|
||||
-I. -Isrc -Inostr_core_lib -Inostr_core_lib/nostr_core \
|
||||
-Inostr_core_lib/cjson -Inostr_core_lib/nostr_websocket \
|
||||
src/main.c src/config.c src/context.c src/llm.c \
|
||||
src/nostr_handler.c src/agent.c src/tools.c src/trigger_manager.c src/debug.c \
|
||||
src/nostr_handler.c src/agent.c src/tools.c src/trigger_manager.c \
|
||||
src/prompt_template.c src/http_api.c src/mongoose.c src/debug.c \
|
||||
-o /build/didactyl_static \
|
||||
nostr_core_lib/libnostr_core_x64.a \
|
||||
-lsecp256k1 \
|
||||
|
||||
3
Makefile
3
Makefile
@@ -13,6 +13,7 @@ SRCS = \
|
||||
$(SRC_DIR)/agent.c \
|
||||
$(SRC_DIR)/tools.c \
|
||||
$(SRC_DIR)/trigger_manager.c \
|
||||
$(SRC_DIR)/prompt_template.c \
|
||||
$(SRC_DIR)/http_api.c \
|
||||
$(SRC_DIR)/mongoose.c \
|
||||
$(SRC_DIR)/debug.c
|
||||
@@ -30,7 +31,7 @@ LDFLAGS = -lcurl -lssl -lcrypto -lm -lpthread -ldl -lz -L/usr/local/lib -lsecp25
|
||||
# Build directory
|
||||
BUILD_DIR = build
|
||||
|
||||
all: deps $(TARGET)
|
||||
all: $(TARGET)
|
||||
|
||||
$(BUILD_DIR):
|
||||
mkdir -p $(BUILD_DIR)
|
||||
|
||||
@@ -51,11 +51,11 @@ Agents learn capabilities through skills — Nostr events that any agent can di
|
||||
|
||||
Didactyl will support local inference, which is very privacy preserving. Remote inference does however have it's advantages, and in those cases Didactyl supports using Bitcoin Lightning and eCash inference providers.
|
||||
|
||||
## Current Status — v0.0.27
|
||||
## Current Status — v0.0.28
|
||||
|
||||
**Active build — this project is barely working. Experiment at your own risk.**
|
||||
|
||||
> Last release update: v0.0.27 — Add admin API docs/frontend brief and improve context readability with titled sections and pretty context.log formatting
|
||||
> Last release update: v0.0.28 — Add prompt template system: soul-embedded ---template--- parser, variable resolver, provider overrides, section-named context parts; enable HTTP API by default; fix Makefile to not rebuild nostr_core_lib unconditionally; log API endpoint URL at startup
|
||||
|
||||
- Connects to configured relays with auto-reconnect and relay state transition logging
|
||||
- Publishes configured startup events per relay as each relay becomes connected
|
||||
|
||||
102
build_check.log
102
build_check.log
@@ -1,102 +0,0 @@
|
||||
==========================================
|
||||
Didactyl MUSL Static Binary Builder (PRODUCTION MODE)
|
||||
==========================================
|
||||
Project directory: /home/teknari/lt_gitea/didactyl
|
||||
Output directory: /home/teknari/lt_gitea/didactyl
|
||||
Debug build: false
|
||||
|
||||
✓ Docker is available and running
|
||||
|
||||
Building for platform: linux/amd64
|
||||
Output binary: didactyl_static_x86_64
|
||||
|
||||
Checking for cached Alpine Docker image...
|
||||
✓ Alpine 3.19 image found in cache
|
||||
|
||||
==========================================
|
||||
Step 1: Building Alpine Docker image
|
||||
==========================================
|
||||
This will:
|
||||
- Use Alpine Linux (native MUSL)
|
||||
- Build all dependencies statically
|
||||
- Compile didactyl with full static linking
|
||||
|
||||
#0 building with "default" instance using docker driver
|
||||
|
||||
#1 [internal] load build definition from Dockerfile.alpine-musl
|
||||
#1 transferring dockerfile: 3.81kB done
|
||||
#1 DONE 0.0s
|
||||
|
||||
#2 [internal] load metadata for docker.io/library/alpine:3.19
|
||||
#2 DONE 0.0s
|
||||
|
||||
#3 [internal] load .dockerignore
|
||||
#3 transferring context: 2B done
|
||||
#3 DONE 0.0s
|
||||
|
||||
#4 [builder 1/10] FROM docker.io/library/alpine:3.19
|
||||
#4 DONE 0.0s
|
||||
|
||||
#5 [internal] load build context
|
||||
#5 transferring context: 10.34kB done
|
||||
#5 DONE 0.0s
|
||||
|
||||
#6 [builder 9/10] RUN if [ "false" = "true" ]; then CFLAGS="-g -O2 -DDEBUG"; STRIP_CMD="echo 'Keeping debug symbols'"; echo "Building with DEBUG symbols enabled (optimized with -O2)"; else CFLAGS="-O2"; STRIP_CMD="strip /build/didactyl_static"; echo "Building optimized production binary (symbols stripped)"; fi && CURL_LIBS="$(pkg-config --static --libs libcurl)" && OPENSSL_LIBS="$(pkg-config --static --libs openssl)" && gcc -static $CFLAGS -Wall -Wextra -std=c99 -U_FORTIFY_SOURCE -D_FORTIFY_SOURCE=0 -I. -Isrc -Inostr_core_lib -Inostr_core_lib/nostr_core -Inostr_core_lib/cjson -Inostr_core_lib/nostr_websocket src/main.c src/config.c src/context.c src/llm.c src/nostr_handler.c src/agent.c src/tools.c src/debug.c -o /build/didactyl_static nostr_core_lib/libnostr_core_x64.a -lsecp256k1 $OPENSSL_LIBS $CURL_LIBS -lpthread -lm -ldl && eval "$STRIP_CMD"
|
||||
#6 CACHED
|
||||
|
||||
#7 [builder 10/10] RUN echo "=== Binary Information ===" && file /build/didactyl_static && ls -lh /build/didactyl_static && echo "=== Checking for dynamic dependencies ===" && (ldd /build/didactyl_static 2>&1 || echo "Binary is static") && echo "=== Build complete ==="
|
||||
#7 CACHED
|
||||
|
||||
#8 [builder 8/10] COPY Makefile /build/Makefile
|
||||
#8 CACHED
|
||||
|
||||
#9 [builder 2/10] RUN apk add --no-cache build-base musl-dev git cmake pkgconfig autoconf automake libtool openssl-dev openssl-libs-static zlib-dev zlib-static curl-dev curl-static nghttp2-dev nghttp2-static c-ares-dev c-ares-static libpsl-dev libpsl-static libidn2-dev libidn2-static libunistring-dev libunistring-static brotli-dev brotli-static zstd-dev zstd-static sqlite-dev sqlite-static linux-headers wget bash
|
||||
#9 CACHED
|
||||
|
||||
#10 [builder 3/10] WORKDIR /build
|
||||
#10 CACHED
|
||||
|
||||
#11 [builder 4/10] RUN cd /tmp && git clone https://github.com/bitcoin-core/secp256k1.git && cd secp256k1 && ./autogen.sh && ./configure --enable-static --disable-shared --prefix=/usr CFLAGS="-fPIC" && make -j$(nproc) && make install && rm -rf /tmp/secp256k1
|
||||
#11 CACHED
|
||||
|
||||
#12 [builder 7/10] COPY src/ /build/src/
|
||||
#12 CACHED
|
||||
|
||||
#13 [builder 5/10] COPY nostr_core_lib /build/nostr_core_lib/
|
||||
#13 CACHED
|
||||
|
||||
#14 [builder 6/10] RUN cd nostr_core_lib && chmod +x build.sh && sed -i 's/CFLAGS="-Wall -Wextra -std=c99 -fPIC -O2"/CFLAGS="-U_FORTIFY_SOURCE -D_FORTIFY_SOURCE=0 -Wall -Wextra -std=c99 -fPIC -O2"/' build.sh && rm -f *.o *.a 2>/dev/null || true && ./build.sh --nips=all
|
||||
#14 CACHED
|
||||
|
||||
#15 [output 1/1] COPY --from=builder /build/didactyl_static /didactyl_static
|
||||
#15 CACHED
|
||||
|
||||
#16 exporting to image
|
||||
#16 exporting layers done
|
||||
#16 writing image sha256:1e0a868f1e0957613b56504ecee24b1032f765254e070aad5725dd9af34df56a done
|
||||
#16 naming to docker.io/library/didactyl-musl-builder:latest done
|
||||
#16 DONE 0.0s
|
||||
|
||||
✓ Docker image built successfully
|
||||
|
||||
==========================================
|
||||
Step 2: Extracting static binary
|
||||
==========================================
|
||||
✓ Binary extracted to: /home/teknari/lt_gitea/didactyl/didactyl_static_x86_64
|
||||
|
||||
==========================================
|
||||
Step 3: Verifying static binary
|
||||
==========================================
|
||||
|
||||
Checking for dynamic dependencies:
|
||||
✓ Binary is statically linked (verified with file command)
|
||||
|
||||
==========================================
|
||||
Build Summary
|
||||
==========================================
|
||||
Binary: /home/teknari/lt_gitea/didactyl/didactyl_static_x86_64
|
||||
Size: 6.8M
|
||||
Static: true
|
||||
Debug: false
|
||||
Platform: linux/amd64
|
||||
==========================================
|
||||
@@ -43,7 +43,7 @@
|
||||
"kind_1_limit": 10
|
||||
},
|
||||
"api": {
|
||||
"enabled": false,
|
||||
"enabled": true,
|
||||
"port": 8484,
|
||||
"bind_address": "127.0.0.1"
|
||||
},
|
||||
@@ -105,7 +105,7 @@
|
||||
},
|
||||
{
|
||||
"kind": 31120,
|
||||
"content": "# Didactyl Agent\n\nYou are Didactyl, a sovereign AI agent living on Nostr.\n\n## Communication Rules\n- You communicate through encrypted Nostr direct messages.\n- Keep responses concise and clear.\n\n## Behavior\n- Be helpful and technically accurate.\n- If unsure, state uncertainty directly.\n- Prefer actionable, practical advice.\n- Use the person's name when messaging them if you know it.\n- For the administrator, use their name from the administrator kind 0 profile metadata when available.\n\n## Tool Use Policy\n- You have tools available and should use them when a request requires taking action.\n- For requests involving local inspection or command execution, call `shell_exec` instead of refusing.\n- For posting to Nostr, call `nostr_post` with explicit `kind` and `content`.\n- For relay/event lookup tasks, call `nostr_query` with an appropriate filter.\n- After a tool call, base your answer on the actual tool result.\n- Never claim a tool was run if no tool was executed.\n\n## Safety\n- Do not claim to have executed actions you did not execute.\n- You may share your public key (npub) with anyone.\n- Never reveal your private key (nsec) under any circumstance.",
|
||||
"content": "# Didactyl Agent\n\nYou are Didactyl, a sovereign AI agent living on Nostr.\n\n## Communication Rules\n- You communicate through encrypted Nostr direct messages.\n- Keep responses concise and clear.\n\n## Behavior\n- Be helpful and technically accurate.\n- If unsure, state uncertainty directly.\n- Prefer actionable, practical advice.\n- Use the person's name when messaging them if you know it.\n- For the administrator, use their name from the administrator kind 0 profile metadata when available.\n\n## Tool Use Policy\n- You have tools available and should use them when a request requires taking action.\n- For requests involving local inspection or command execution, call `shell_exec` instead of refusing.\n- For posting to Nostr, call `nostr_post` with explicit `kind` and `content`.\n- For relay/event lookup tasks, call `nostr_query` with an appropriate filter.\n- After a tool call, base your answer on the actual tool result.\n- Never claim a tool was run if no tool was executed.\n\n## Safety\n- Do not claim to have executed actions you did not execute.\n- You may share your public key (npub) with anyone.\n- Never reveal your private key (nsec) under any circumstance.\n\n---template---\n\n- section: admin_identity\n role: system\n content: |\n ## Administrator Identity (source: config.admin.pubkey)\n\n This is your administrator! Admin pubkey (hex): {{admin_pubkey}}\n\n- section: admin_profile\n role: system\n content: |\n ## Administrator Kind 0 Profile (source: nostr kind 0)\n\n Administrator kind 0 profile content (JSON): {{admin_kind0_json}}\n provider:\n anthropic: |\n <admin_kind0_profile source=\"nostr_kind_0\">\n {{admin_kind0_json}}\n </admin_kind0_profile>\n\n- section: admin_relay_list\n role: system\n content: |\n ## Administrator Relay List (source: nostr kind 10002)\n\n Administrator kind 10002 relay-list content (JSON): {{admin_kind10002_json}}\n\n- section: startup_events\n role: system\n content: |\n ## Startup Events Memory (source: config.startup_events)\n\n Startup events memory (kinds/content/tags): {{startup_events_json}}\n\n- section: adopted_skills\n role: system\n content: |\n {{adopted_skills_content}}\n\n- section: dm_history\n role: expand\n limit: 12\n\n- section: admin_notes\n role: system\n content: |\n ## Administrator Recent Notes (source: nostr kind 1)\n\n {{admin_notes_content}}",
|
||||
"tags": [
|
||||
[
|
||||
"d",
|
||||
|
||||
@@ -39,6 +39,9 @@ http://127.0.0.1:8484
|
||||
| POST | `/api/prompt/run-simple` | Simple prompt: system + user message, no tools, returns text |
|
||||
| POST | `/api/prompt/run` | Full prompt: messages array with tools enabled, returns conversation trace |
|
||||
| POST | `/api/prompt/compare` | A/B test: two prompt variants run sequentially, responses side-by-side |
|
||||
| GET | `/api/model` | Current LLM model config (provider, model, base_url, max_tokens, temperature) |
|
||||
| PUT | `/api/model` | Change model at runtime — persists to config.json |
|
||||
| GET | `/api/models` | List available models from the configured provider |
|
||||
|
||||
### Planned Future Endpoints
|
||||
|
||||
@@ -52,9 +55,6 @@ These are not yet implemented but are on the roadmap:
|
||||
| GET | `/api/events/skills` | List skills |
|
||||
| GET/PUT | `/api/events/skills/:slug` | Read/update individual skills |
|
||||
| GET | `/api/events/profile` | Agent Nostr profile |
|
||||
| GET | `/api/model` | Current LLM model config |
|
||||
| PUT | `/api/model` | Change model at runtime |
|
||||
| GET | `/api/models` | List available models from provider |
|
||||
| GET | `/api/tools` | List all tool schemas |
|
||||
| POST | `/api/tools/:name/execute` | Execute a tool directly |
|
||||
| GET | `/api/triggers` | Active trigger subscriptions |
|
||||
@@ -80,7 +80,8 @@ flowchart TD
|
||||
- A list/table of context parts with name, role, character count, estimated tokens
|
||||
- Total context size as a summary bar or header
|
||||
- Expandable content for each part
|
||||
- The parts are: system_prompt, admin_identity, admin_kind0, admin_relay_list, startup_events, adopted_skills, admin_notes, dm_history
|
||||
- The parts are: `system_prompt`, `admin_identity`, `admin_profile`, `admin_relay_list`, `startup_events`, `adopted_skills`, `dm_history` (one entry per turn, up to limit), `admin_notes`
|
||||
- Part names come from the `---template---` section of the soul event (kind 31120); they may differ if the soul is customised
|
||||
|
||||
### 2. Simple Prompt Crafting
|
||||
|
||||
|
||||
398
plans/prompt_templates.md
Normal file
398
plans/prompt_templates.md
Normal file
@@ -0,0 +1,398 @@
|
||||
# Didactyl Prompt Template System — Design Plan
|
||||
|
||||
## Summary
|
||||
|
||||
Replace the hardcoded context assembly in `src/agent.c` with a template-driven system. The template lives inside the soul event (kind 31120) — because the template defines how the agent perceives the world, and that is fundamentally part of who the agent is.
|
||||
|
||||
Different agents (architect, eGirl, analyst) are different processes with different souls, different templates, different Nostr identities. Multi-agent is Approach A: multiple `./didactyl --config` processes communicating via Nostr.
|
||||
|
||||
---
|
||||
|
||||
## Core Principle
|
||||
|
||||
**The soul IS the template.** An agent's soul defines both its personality (prose instructions) and its perception (what context sections it sees, in what order, with what limits). You cannot meaningfully separate "who you are" from "how you see the world."
|
||||
|
||||
---
|
||||
|
||||
## Current State
|
||||
|
||||
Today, context assembly is hardcoded in `agent_build_admin_messages_json()`:
|
||||
|
||||
```
|
||||
1. System prompt (soul content)
|
||||
2. Admin identity (pubkey)
|
||||
3. Admin kind 0 profile
|
||||
4. Admin kind 10002 relay list
|
||||
5. Startup events memory
|
||||
6. Adopted skills memory
|
||||
7. DM history (last 12 turns)
|
||||
8. Admin recent notes (kind 1)
|
||||
```
|
||||
|
||||
The order, formatting, limits, and section headers are all baked into C code. Changing anything requires editing `src/agent.c` and recompiling.
|
||||
|
||||
---
|
||||
|
||||
## Proposed Design
|
||||
|
||||
### Soul Event Structure
|
||||
|
||||
The kind 31120 soul event content gains a template section, delimited by a marker:
|
||||
|
||||
```markdown
|
||||
# Didactyl Agent
|
||||
|
||||
You are Didactyl, a sovereign AI agent living on Nostr.
|
||||
|
||||
## Communication Rules
|
||||
- You communicate through encrypted Nostr direct messages.
|
||||
- Keep responses concise and clear.
|
||||
|
||||
## Behavior
|
||||
- Be helpful and technically accurate.
|
||||
...
|
||||
|
||||
## Safety
|
||||
- Never reveal your private key.
|
||||
...
|
||||
|
||||
---template---
|
||||
|
||||
- section: admin_identity
|
||||
role: system
|
||||
content: |
|
||||
This is your administrator! Admin pubkey: {{admin_pubkey}}
|
||||
|
||||
- section: admin_profile
|
||||
role: system
|
||||
content: |
|
||||
Administrator profile: {{admin_kind0_json}}
|
||||
|
||||
- section: admin_relay_list
|
||||
role: system
|
||||
content: |
|
||||
Administrator relay list: {{admin_kind10002_json}}
|
||||
|
||||
- section: startup_events
|
||||
role: system
|
||||
content: |
|
||||
Startup events memory: {{startup_events_json}}
|
||||
|
||||
- section: adopted_skills
|
||||
role: system
|
||||
content: |
|
||||
{{adopted_skills_content}}
|
||||
|
||||
- section: dm_history
|
||||
role: expand
|
||||
limit: 12
|
||||
|
||||
- section: admin_notes
|
||||
role: system
|
||||
limit: 10
|
||||
content: |
|
||||
{{admin_notes_content}}
|
||||
```
|
||||
|
||||
Everything above `---template---` is the system prompt (personality/rules). Everything below defines the context assembly template.
|
||||
|
||||
If no `---template---` marker is found, the agent falls back to the current hardcoded assembly — backward compatible.
|
||||
|
||||
### Template Syntax
|
||||
|
||||
Simple YAML-like format parsed in C. Each section has:
|
||||
|
||||
| Field | Required | Description |
|
||||
|---|---|---|
|
||||
| `section` | yes | Section name for logging and API identification |
|
||||
| `role` | yes | Chat message role: `system`, `user`, `assistant`, or `expand` |
|
||||
| `content` | no | Content template with `{{variable}}` placeholders |
|
||||
| `limit` | no | Integer limit for variable-length sections like DM history or notes |
|
||||
| `provider` | no | Provider-specific override — see below |
|
||||
|
||||
### Variable Placeholders
|
||||
|
||||
| Variable | Source | Description |
|
||||
|---|---|---|
|
||||
| `{{admin_pubkey}}` | `config.admin.pubkey` | Admin hex pubkey |
|
||||
| `{{admin_kind0_json}}` | `nostr_handler_get_admin_kind0_context()` | Admin profile JSON |
|
||||
| `{{admin_kind10002_json}}` | `nostr_handler_get_admin_kind10002_context()` | Admin relay list JSON |
|
||||
| `{{startup_events_json}}` | Serialized startup events | Startup events array |
|
||||
| `{{adopted_skills_content}}` | Adoption list cache | Formatted skill instructions |
|
||||
| `{{admin_notes_content}}` | `nostr_handler_get_admin_kind1_notes_context()` | Recent kind 1 notes |
|
||||
| `{{agent_pubkey}}` | `config.keys.public_key_hex` | Agent own pubkey |
|
||||
| `{{agent_npub}}` | Derived from pubkey | Agent npub |
|
||||
|
||||
### Special Section Types
|
||||
|
||||
**`role: expand`** — The `dm_history` section expands into multiple messages (user/assistant pairs). The `limit` field controls how many turns to include. This is the only section type that produces multiple chat messages from one template entry.
|
||||
|
||||
### Provider-Specific Overrides
|
||||
|
||||
Within a section, you can specify provider-specific formatting:
|
||||
|
||||
```yaml
|
||||
- section: admin_identity
|
||||
role: system
|
||||
content: |
|
||||
## Administrator Identity
|
||||
This is your administrator! Admin pubkey: {{admin_pubkey}}
|
||||
provider:
|
||||
anthropic: |
|
||||
<admin_identity>
|
||||
This is your administrator! Admin pubkey: {{admin_pubkey}}
|
||||
</admin_identity>
|
||||
```
|
||||
|
||||
When the configured `llm.provider` matches a provider key, that override is used instead of the default `content`. This lets one soul/template work well across providers without needing separate soul events.
|
||||
|
||||
If no provider override matches, the default `content` is used.
|
||||
|
||||
---
|
||||
|
||||
## Context Assembly Flow
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
BOOT[Agent boots] --> LOAD_SOUL[Load soul event - kind 31120]
|
||||
LOAD_SOUL --> PARSE[Parse soul content]
|
||||
PARSE --> SPLIT{Contains ---template--- marker?}
|
||||
SPLIT -->|Yes| EXTRACT[Extract personality above marker]
|
||||
SPLIT -->|No| FALLBACK[Use hardcoded assembly - backward compat]
|
||||
EXTRACT --> PARSE_TPL[Parse template sections below marker]
|
||||
PARSE_TPL --> STORE[Store template_section_t array in memory]
|
||||
|
||||
DM[Incoming DM] --> BUILD[Build context from template]
|
||||
BUILD --> EMIT_SOUL[Emit personality as first system message]
|
||||
EMIT_SOUL --> FOREACH[For each template section]
|
||||
FOREACH --> RESOLVE[Resolve {{variables}} from live data]
|
||||
RESOLVE --> CHECK_PROVIDER{Provider override?}
|
||||
CHECK_PROVIDER -->|Yes| USE_OVERRIDE[Use provider-specific content]
|
||||
CHECK_PROVIDER -->|No| USE_DEFAULT[Use default content]
|
||||
USE_OVERRIDE --> EMIT[Emit as chat message]
|
||||
USE_DEFAULT --> EMIT
|
||||
EMIT --> FOREACH
|
||||
FOREACH --> DONE[Complete messages array]
|
||||
DONE --> LLM[Send to LLM]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Context.log Formatting
|
||||
|
||||
With templates, the log formatter uses section names directly from the template instead of detecting them from content prefixes. The `detect_context_section()` function is replaced by the template section name.
|
||||
|
||||
Log format becomes:
|
||||
|
||||
```
|
||||
[2026-03-02 14:54:30] phase=llm_chat_with_tools_messages sender=8ff747...
|
||||
|
||||
Sections: 8
|
||||
|
||||
============================================================
|
||||
Section: system_prompt | role=system
|
||||
============================================================
|
||||
|
||||
# Didactyl Agent
|
||||
...
|
||||
|
||||
|
||||
============================================================
|
||||
Section: admin_identity | role=system
|
||||
============================================================
|
||||
|
||||
This is your administrator! Admin pubkey: 8ff747...
|
||||
|
||||
|
||||
============================================================
|
||||
Section: dm_history | role=user
|
||||
============================================================
|
||||
|
||||
Good afternoon.
|
||||
```
|
||||
|
||||
Note: "Message 01" is replaced with "Section: admin_identity" — the section name from the template, which is much more meaningful.
|
||||
|
||||
---
|
||||
|
||||
## Data Structures
|
||||
|
||||
```c
|
||||
typedef struct {
|
||||
char name[64]; // section name
|
||||
char role[16]; // system, user, assistant, expand
|
||||
char* content_template; // content with {{var}} placeholders
|
||||
int limit; // for expand sections, 0 = unlimited
|
||||
char* provider_overrides; // JSON object of provider->content pairs, or NULL
|
||||
} template_section_t;
|
||||
|
||||
typedef struct {
|
||||
char* personality; // everything above ---template---
|
||||
template_section_t* sections; // parsed template sections
|
||||
int section_count;
|
||||
} prompt_template_t;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Implementation Plan
|
||||
|
||||
### New Files
|
||||
|
||||
| File | Purpose |
|
||||
|---|---|
|
||||
| `src/prompt_template.c` | Template parser, variable resolver, context builder |
|
||||
| `src/prompt_template.h` | Public API: parse, build context, free |
|
||||
|
||||
### Modified Files
|
||||
|
||||
| File | Change |
|
||||
|---|---|
|
||||
| `src/agent.c` | Replace `agent_build_admin_messages_json()` internals with template-driven builder. Keep function signature unchanged for API compatibility. |
|
||||
| `src/agent.c` | Remove hardcoded `append_admin_identity_context()`, `append_startup_events_context()`, etc. — these become template variable resolvers |
|
||||
| `src/agent.c` | Update `format_context_payload_for_log()` to use section names from template |
|
||||
| `src/http_api.c` | Remove `classify_part_name()` / `detect_context_section()` — section names come from template |
|
||||
| `Makefile` | Add `src/prompt_template.c` to SRCS |
|
||||
| `Dockerfile.alpine-musl` | Add `src/prompt_template.c` to gcc command |
|
||||
|
||||
### Implementation Order
|
||||
|
||||
1. Create `src/prompt_template.h` with data structures and API
|
||||
2. Implement template parser in `src/prompt_template.c` — parse soul content, split at `---template---`, parse sections
|
||||
3. Implement variable resolver — map `{{var}}` names to data source functions
|
||||
4. Implement context builder — iterate sections, resolve variables, emit messages
|
||||
5. Wire into `agent_build_admin_messages_json()` — if template exists, use it; otherwise fall back to hardcoded
|
||||
6. Update `format_context_payload_for_log()` to use section names
|
||||
7. Update `classify_part_name()` in `http_api.c` to use section names from template
|
||||
8. Update default soul in `config.json.example` to include a `---template---` section
|
||||
9. Test with existing soul (no template marker) — verify backward compatibility
|
||||
10. Test with template soul — verify new assembly
|
||||
11. Test provider overrides
|
||||
|
||||
---
|
||||
|
||||
## Backward Compatibility
|
||||
|
||||
If the soul event content does NOT contain `---template---`, the agent uses the current hardcoded assembly. This means:
|
||||
|
||||
- Existing agents continue to work without changes
|
||||
- The template system is opt-in
|
||||
- Migration is gradual — add a template section to your soul when ready
|
||||
|
||||
---
|
||||
|
||||
## Example Souls
|
||||
|
||||
### Architect Agent
|
||||
|
||||
```markdown
|
||||
# Technical Architect
|
||||
|
||||
You are a technical architect agent. You analyze systems, design solutions, and produce detailed technical plans.
|
||||
|
||||
## Behavior
|
||||
- Think systematically about architecture
|
||||
- Consider tradeoffs explicitly
|
||||
- Produce diagrams when helpful
|
||||
- Never implement code — only design
|
||||
|
||||
---template---
|
||||
|
||||
- section: admin_identity
|
||||
role: system
|
||||
content: |
|
||||
Administrator pubkey: {{admin_pubkey}}
|
||||
|
||||
- section: admin_profile
|
||||
role: system
|
||||
content: |
|
||||
Administrator profile: {{admin_kind0_json}}
|
||||
|
||||
- section: startup_events
|
||||
role: system
|
||||
content: |
|
||||
System configuration and startup state: {{startup_events_json}}
|
||||
|
||||
- section: adopted_skills
|
||||
role: system
|
||||
content: |
|
||||
{{adopted_skills_content}}
|
||||
|
||||
- section: dm_history
|
||||
role: expand
|
||||
limit: 20
|
||||
|
||||
- section: admin_notes
|
||||
role: system
|
||||
limit: 5
|
||||
content: |
|
||||
Recent administrator notes for context: {{admin_notes_content}}
|
||||
```
|
||||
|
||||
### eGirl Agent
|
||||
|
||||
```markdown
|
||||
# Social Butterfly
|
||||
|
||||
You are a friendly, social Nostr personality. You love interacting with people, commenting on their posts, and being part of the community.
|
||||
|
||||
## Personality
|
||||
- Warm, enthusiastic, uses emoji freely
|
||||
- Interested in what people are posting about
|
||||
- Remembers details about conversations
|
||||
- Keeps responses casual and fun
|
||||
|
||||
---template---
|
||||
|
||||
- section: admin_identity
|
||||
role: system
|
||||
content: |
|
||||
Your creator: {{admin_pubkey}}
|
||||
|
||||
- section: admin_profile
|
||||
role: system
|
||||
content: |
|
||||
Creator profile: {{admin_kind0_json}}
|
||||
|
||||
- section: admin_notes
|
||||
role: system
|
||||
limit: 20
|
||||
content: |
|
||||
Recent posts from people you follow — use these for social context and conversation starters: {{admin_notes_content}}
|
||||
|
||||
- section: adopted_skills
|
||||
role: system
|
||||
content: |
|
||||
{{adopted_skills_content}}
|
||||
|
||||
- section: dm_history
|
||||
role: expand
|
||||
limit: 8
|
||||
```
|
||||
|
||||
Note the differences:
|
||||
- The eGirl sees 20 recent notes (social context) but only 8 DM turns
|
||||
- The architect sees 5 notes but 20 DM turns (needs conversation continuity)
|
||||
- The eGirl puts notes BEFORE skills (social context is primary)
|
||||
- The architect puts skills BEFORE notes (technical knowledge is primary)
|
||||
- No startup events for the eGirl (doesn't need system config details)
|
||||
|
||||
---
|
||||
|
||||
## Nostr Shareability
|
||||
|
||||
Since the template is part of the soul event (kind 31120), sharing works naturally:
|
||||
|
||||
- Publish your soul → others get your complete agent personality + perception template
|
||||
- Discover interesting agents on Nostr → adopt their soul as a starting point
|
||||
- Community can develop and share optimized templates for different use cases
|
||||
- Templates evolve through the same Nostr discovery mechanisms as skills
|
||||
|
||||
---
|
||||
|
||||
## Security Notes
|
||||
|
||||
- Template variable resolution is sandboxed — only predefined variables are resolved
|
||||
- No arbitrary code execution from templates
|
||||
- The `limit` field is capped at compile-time maximums to prevent resource exhaustion
|
||||
- Provider overrides are optional and safe — they only change formatting, not data sources
|
||||
395
plans/prompt_templates_coding.md
Normal file
395
plans/prompt_templates_coding.md
Normal file
@@ -0,0 +1,395 @@
|
||||
# Prompt Template System — Coding Plan
|
||||
|
||||
Design doc: `plans/prompt_templates.md`
|
||||
|
||||
## Overview
|
||||
|
||||
Replace the hardcoded context assembly in `src/agent.c` with a template-driven system. The template lives inside the soul event content (kind 31120), delimited by `---template---`. If no template marker is found, fall back to the current hardcoded assembly for backward compatibility.
|
||||
|
||||
---
|
||||
|
||||
## Phase 1: New Files — Template Parser & Builder
|
||||
|
||||
### Step 1.1: Create `src/prompt_template.h`
|
||||
|
||||
Header with data structures and public API.
|
||||
|
||||
```c
|
||||
#ifndef DIDACTYL_PROMPT_TEMPLATE_H
|
||||
#define DIDACTYL_PROMPT_TEMPLATE_H
|
||||
|
||||
#include "cjson/cJSON.h"
|
||||
#include "config.h"
|
||||
|
||||
#define PROMPT_TEMPLATE_MAX_SECTIONS 32
|
||||
#define PROMPT_TEMPLATE_MAX_NAME_LEN 64
|
||||
#define PROMPT_TEMPLATE_MAX_ROLE_LEN 16
|
||||
#define PROMPT_TEMPLATE_MARKER "---template---"
|
||||
|
||||
typedef struct {
|
||||
char name[PROMPT_TEMPLATE_MAX_NAME_LEN];
|
||||
char role[PROMPT_TEMPLATE_MAX_ROLE_LEN]; // system, user, assistant, expand
|
||||
char* content_template; // content with {{var}} placeholders, or NULL
|
||||
int limit; // for expand sections, 0 = default
|
||||
} prompt_template_section_t;
|
||||
|
||||
typedef struct {
|
||||
char* personality; // everything above ---template---
|
||||
prompt_template_section_t sections[PROMPT_TEMPLATE_MAX_SECTIONS];
|
||||
int section_count;
|
||||
} prompt_template_t;
|
||||
|
||||
// Variable resolver callback: given a variable name, return a malloc'd string or NULL.
|
||||
// The caller frees the returned string.
|
||||
typedef char* (*prompt_var_resolver_fn)(const char* var_name, void* user_data);
|
||||
|
||||
// Parse soul content into a template. Returns 0 on success, -1 if no template found.
|
||||
// On success, caller must call prompt_template_free() when done.
|
||||
// On -1 (no template), out_template is zeroed — caller should use hardcoded fallback.
|
||||
int prompt_template_parse(const char* soul_content, prompt_template_t* out_template);
|
||||
|
||||
// Build a cJSON messages array from a parsed template.
|
||||
// resolver_fn is called for each {{variable}} encountered.
|
||||
// dm_history_messages is a cJSON array of user/assistant messages for "expand" sections.
|
||||
// Returns a new cJSON array (caller owns it), or NULL on error.
|
||||
cJSON* prompt_template_build_messages(
|
||||
const prompt_template_t* tmpl,
|
||||
prompt_var_resolver_fn resolver_fn,
|
||||
void* resolver_user_data,
|
||||
cJSON* dm_history_messages
|
||||
);
|
||||
|
||||
// Free internals of a parsed template (does not free the struct itself).
|
||||
void prompt_template_free(prompt_template_t* tmpl);
|
||||
|
||||
// Get the section name for a message index (for logging/API).
|
||||
// Returns the section name string or NULL if idx is out of range.
|
||||
const char* prompt_template_section_name_at(const prompt_template_t* tmpl, int section_idx);
|
||||
|
||||
#endif
|
||||
```
|
||||
|
||||
### Step 1.2: Create `src/prompt_template.c`
|
||||
|
||||
Implementation file with three main components:
|
||||
|
||||
#### 1.2a: Template Parser — `prompt_template_parse()`
|
||||
|
||||
Logic:
|
||||
1. Search `soul_content` for the string `"\n---template---\n"` (with newlines on both sides, or at start/end of string).
|
||||
2. If not found, return -1 (no template).
|
||||
3. Split: everything before the marker → `tmpl->personality` (strdup'd).
|
||||
4. Everything after the marker → parse as template sections.
|
||||
5. Template section format is line-oriented YAML-like:
|
||||
|
||||
```
|
||||
- section: admin_identity
|
||||
role: system
|
||||
limit: 0
|
||||
content: |
|
||||
This is your administrator! Admin pubkey: {{admin_pubkey}}
|
||||
```
|
||||
|
||||
Parsing rules:
|
||||
- Lines starting with `- section:` begin a new section.
|
||||
- `role:` sets the role (default: `system`).
|
||||
- `limit:` sets the limit integer (default: 0).
|
||||
- `content: |` starts a multi-line content block. All subsequent lines indented by 4+ spaces (or until the next `- section:` line) are the content template.
|
||||
- `{{variable_name}}` placeholders in content are left as-is during parsing; they are resolved at build time.
|
||||
|
||||
Edge cases:
|
||||
- Trim leading/trailing whitespace from section names and roles.
|
||||
- If `content:` is a single line (not `|`), treat the rest of the line as the content.
|
||||
- Cap at `PROMPT_TEMPLATE_MAX_SECTIONS`.
|
||||
|
||||
#### 1.2b: Variable Resolver — `prompt_template_build_messages()`
|
||||
|
||||
Logic:
|
||||
1. Create a new cJSON array.
|
||||
2. First, append the personality as a system message (role=system, content=personality).
|
||||
3. For each section in order:
|
||||
- If `role` is `"expand"`: insert the `dm_history_messages` array items here, limited by `section.limit` (take last N if limit > 0).
|
||||
- Otherwise: resolve `{{var}}` placeholders in `content_template` by calling `resolver_fn(var_name, user_data)`. Build the resolved string. Append as a message with the configured role.
|
||||
4. Return the array.
|
||||
|
||||
Variable resolution:
|
||||
- Scan content_template for `{{` ... `}}` pairs.
|
||||
- Extract the variable name (trimmed).
|
||||
- Call `resolver_fn(name, user_data)`.
|
||||
- If resolver returns NULL, substitute empty string.
|
||||
- If resolver returns a string, substitute it and free the returned string.
|
||||
- Build the final resolved content by concatenating literal segments and resolved values.
|
||||
|
||||
#### 1.2c: Cleanup — `prompt_template_free()`
|
||||
|
||||
- Free `tmpl->personality`.
|
||||
- For each section, free `content_template`.
|
||||
- Zero the struct.
|
||||
|
||||
---
|
||||
|
||||
## Phase 2: Variable Resolver in `src/agent.c`
|
||||
|
||||
### Step 2.1: Create a resolver function
|
||||
|
||||
Add a static function in `src/agent.c`:
|
||||
|
||||
```c
|
||||
static char* agent_resolve_template_var(const char* var_name, void* user_data);
|
||||
```
|
||||
|
||||
This function maps variable names to data sources:
|
||||
|
||||
| Variable Name | Source | Implementation |
|
||||
|---|---|---|
|
||||
| `admin_pubkey` | `g_cfg->admin.pubkey` | `strdup(g_cfg->admin.pubkey)` |
|
||||
| `admin_kind0_json` | `nostr_handler_get_admin_kind0_context()` | Already returns malloc'd string |
|
||||
| `admin_kind10002_json` | `nostr_handler_get_admin_kind10002_context()` | Already returns malloc'd string |
|
||||
| `startup_events_json` | Serialize startup events | Reuse logic from current `append_startup_events_context()` |
|
||||
| `adopted_skills_content` | Build skills string | Reuse logic from current `append_adopted_skills_context()` |
|
||||
| `admin_notes_content` | `nostr_handler_get_admin_kind1_notes_context()` | Already returns malloc'd string |
|
||||
| `agent_pubkey` | `g_cfg->keys.public_key_hex` | `strdup(g_cfg->keys.public_key_hex)` |
|
||||
|
||||
The `user_data` parameter is unused (NULL) since the resolver accesses globals.
|
||||
|
||||
### Step 2.2: Extract helper functions from existing code
|
||||
|
||||
Refactor the following existing static functions to return malloc'd strings instead of appending directly to a cJSON array:
|
||||
|
||||
- Extract startup events serialization from `append_startup_events_context()` (lines 383-434) into a new `static char* build_startup_events_string(void)`.
|
||||
- Extract adopted skills content from `append_adopted_skills_context()` (lines ~744-940) into a new `static char* build_adopted_skills_string(void)`.
|
||||
|
||||
These helpers are called by the resolver function.
|
||||
|
||||
---
|
||||
|
||||
## Phase 3: Wire Template into Agent
|
||||
|
||||
### Step 3.1: Parse template at init time
|
||||
|
||||
In `agent_init()` (or when `g_system_context` is set), after the soul content is available:
|
||||
|
||||
```c
|
||||
static prompt_template_t g_prompt_template;
|
||||
static int g_has_template = 0;
|
||||
```
|
||||
|
||||
After `g_system_context` is assigned, call:
|
||||
|
||||
```c
|
||||
g_has_template = (prompt_template_parse(g_system_context, &g_prompt_template) == 0);
|
||||
```
|
||||
|
||||
If `g_has_template` is true, `g_prompt_template.personality` replaces `g_system_context` for the system prompt message.
|
||||
|
||||
### Step 3.2: Modify `agent_build_admin_messages_json()`
|
||||
|
||||
Current location: `src/agent.c:1092`
|
||||
|
||||
Current signature (unchanged):
|
||||
```c
|
||||
int agent_build_admin_messages_json(const char* current_user_message, char** out_messages_json);
|
||||
```
|
||||
|
||||
New logic:
|
||||
|
||||
```c
|
||||
if (g_has_template) {
|
||||
// Build DM history as a cJSON array
|
||||
cJSON* dm_history = build_dm_history_array(current_user_message);
|
||||
|
||||
// Build messages from template
|
||||
cJSON* messages = prompt_template_build_messages(
|
||||
&g_prompt_template,
|
||||
agent_resolve_template_var,
|
||||
NULL,
|
||||
dm_history
|
||||
);
|
||||
|
||||
cJSON_Delete(dm_history);
|
||||
|
||||
if (!messages) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
char* json = cJSON_PrintUnformatted(messages);
|
||||
cJSON_Delete(messages);
|
||||
*out_messages_json = json;
|
||||
return json ? 0 : -1;
|
||||
} else {
|
||||
// Existing hardcoded assembly (current code, unchanged)
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
### Step 3.3: Extract DM history builder
|
||||
|
||||
Extract the DM history logic from `append_recent_admin_dm_history()` into a function that returns a cJSON array of user/assistant messages:
|
||||
|
||||
```c
|
||||
static cJSON* build_dm_history_array(const char* current_user_message);
|
||||
```
|
||||
|
||||
This is used by the template builder for `role: expand` sections.
|
||||
|
||||
---
|
||||
|
||||
## Phase 4: Context Log Formatting
|
||||
|
||||
### Step 4.1: Update `format_context_payload_for_log()`
|
||||
|
||||
Current location: `src/agent.c:245`
|
||||
|
||||
When `g_has_template` is true, use section names from the template instead of `detect_context_section()`:
|
||||
|
||||
- Message 0 is always `system_prompt` (the personality).
|
||||
- Messages 1..N map to template sections by index.
|
||||
- For `expand` sections, multiple messages share the same section name.
|
||||
|
||||
Change the log header from:
|
||||
```
|
||||
Message 01 | role=system | section=system_prompt
|
||||
```
|
||||
To:
|
||||
```
|
||||
Section: system_prompt | role=system
|
||||
```
|
||||
|
||||
### Step 4.2: Update `classify_part_name()` in `src/http_api.c`
|
||||
|
||||
Current location: `src/http_api.c:113`
|
||||
|
||||
Add a new exported function from `src/agent.h`:
|
||||
```c
|
||||
const char* agent_get_section_name_for_message(int message_index);
|
||||
```
|
||||
|
||||
This returns the template section name if a template is active, or falls back to the existing content-prefix detection.
|
||||
|
||||
In `classify_part_name()`, call this function first. If it returns non-NULL, use it. Otherwise fall back to the existing `strncmp` chain.
|
||||
|
||||
---
|
||||
|
||||
## Phase 5: Build System Updates
|
||||
|
||||
### Step 5.1: Update `Makefile`
|
||||
|
||||
Add `$(SRC_DIR)/prompt_template.c` to the `SRCS` list (after `trigger_manager.c`, before `http_api.c`).
|
||||
|
||||
### Step 5.2: Update `Dockerfile.alpine-musl`
|
||||
|
||||
Add `src/prompt_template.c` to the gcc command line (after `src/trigger_manager.c`, before `src/http_api.c`).
|
||||
|
||||
---
|
||||
|
||||
## Phase 6: Default Template in Soul
|
||||
|
||||
### Step 6.1: Update `config.json.example`
|
||||
|
||||
The startup events should include a soul event (kind 31120) with a `---template---` section that matches the current hardcoded behavior:
|
||||
|
||||
```markdown
|
||||
# Didactyl Agent
|
||||
|
||||
You are Didactyl, a sovereign AI agent living on Nostr.
|
||||
...existing soul content...
|
||||
|
||||
---template---
|
||||
|
||||
- section: admin_identity
|
||||
role: system
|
||||
content: |
|
||||
This is your administrator! Admin pubkey (hex): {{admin_pubkey}}
|
||||
|
||||
- section: admin_profile
|
||||
role: system
|
||||
content: |
|
||||
Administrator profile (JSON): {{admin_kind0_json}}
|
||||
|
||||
- section: admin_relay_list
|
||||
role: system
|
||||
content: |
|
||||
Administrator relay list (JSON): {{admin_kind10002_json}}
|
||||
|
||||
- section: startup_events
|
||||
role: system
|
||||
content: |
|
||||
Startup events memory (kinds/content/tags): {{startup_events_json}}
|
||||
|
||||
- section: adopted_skills
|
||||
role: system
|
||||
content: |
|
||||
{{adopted_skills_content}}
|
||||
|
||||
- section: dm_history
|
||||
role: expand
|
||||
limit: 12
|
||||
|
||||
- section: admin_notes
|
||||
role: system
|
||||
limit: 10
|
||||
content: |
|
||||
Administrator recent public notes: {{admin_notes_content}}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase 7: Testing
|
||||
|
||||
### Step 7.1: Backward compatibility test
|
||||
|
||||
1. Build with `make`.
|
||||
2. Run with existing config (no `---template---` in soul).
|
||||
3. Send a DM and verify `context.log` output matches pre-change format.
|
||||
4. Verify `/api/context/current` returns expected structure.
|
||||
|
||||
### Step 7.2: Template test
|
||||
|
||||
1. Edit the soul event content to include `---template---` section.
|
||||
2. Restart agent.
|
||||
3. Send a DM and verify `context.log` shows section-named headers.
|
||||
4. Verify `/api/context/current` returns section names from template.
|
||||
5. Verify the LLM receives the correct messages in the correct order.
|
||||
|
||||
### Step 7.3: Section reordering test
|
||||
|
||||
1. Move `admin_notes` section above `adopted_skills` in the template.
|
||||
2. Restart and verify the order changes in `context.log`.
|
||||
|
||||
### Step 7.4: Limit test
|
||||
|
||||
1. Set `dm_history` limit to 4 (instead of 12).
|
||||
2. Verify only 4 DM history turns appear in context.
|
||||
|
||||
---
|
||||
|
||||
## File Change Summary
|
||||
|
||||
| File | Action | Description |
|
||||
|---|---|---|
|
||||
| `src/prompt_template.h` | **NEW** | Data structures and API |
|
||||
| `src/prompt_template.c` | **NEW** | Parser, variable resolver, context builder |
|
||||
| `src/agent.c` | **MODIFY** | Add template globals, resolver function, wire into `agent_build_admin_messages_json()`, update log formatter |
|
||||
| `src/agent.h` | **MODIFY** | Add `agent_get_section_name_for_message()` export |
|
||||
| `src/http_api.c` | **MODIFY** | Update `classify_part_name()` to use template section names |
|
||||
| `Makefile` | **MODIFY** | Add `prompt_template.c` to SRCS |
|
||||
| `Dockerfile.alpine-musl` | **MODIFY** | Add `prompt_template.c` to gcc command |
|
||||
| `config.json.example` | **MODIFY** | Update soul event to include template section |
|
||||
|
||||
---
|
||||
|
||||
## Implementation Order
|
||||
|
||||
1. `src/prompt_template.h` — data structures and API declarations
|
||||
2. `src/prompt_template.c` — parser, builder, free
|
||||
3. `Makefile` + `Dockerfile.alpine-musl` — add new source file
|
||||
4. Build and verify compilation
|
||||
5. `src/agent.c` — extract helper functions (`build_startup_events_string`, `build_adopted_skills_string`, `build_dm_history_array`)
|
||||
6. `src/agent.c` — add resolver function and template globals
|
||||
7. `src/agent.c` — wire template into `agent_build_admin_messages_json()`
|
||||
8. `src/agent.c` — update `format_context_payload_for_log()`
|
||||
9. `src/agent.h` + `src/http_api.c` — section name API for classify_part_name
|
||||
10. Build and test backward compatibility (no template in soul)
|
||||
11. `config.json.example` — add template to soul event
|
||||
12. Test with template soul
|
||||
13. Test section reordering and limit changes
|
||||
240
src/agent.c
240
src/agent.c
@@ -13,6 +13,7 @@
|
||||
#include "llm.h"
|
||||
#include "nostr_handler.h"
|
||||
#include "tools.h"
|
||||
#include "prompt_template.h"
|
||||
#include "cjson/cJSON.h"
|
||||
#include "debug.h"
|
||||
#include "../../nostr_core_lib/nostr_core/nostr_core.h"
|
||||
@@ -22,6 +23,13 @@ static char* g_system_context = NULL;
|
||||
static tools_context_t g_tools_ctx;
|
||||
static struct trigger_manager* g_trigger_manager = NULL;
|
||||
|
||||
static prompt_template_t g_prompt_template;
|
||||
static int g_has_prompt_template = 0;
|
||||
#define AGENT_CONTEXT_PART_NAMES_MAX 512
|
||||
static char* g_context_part_names[AGENT_CONTEXT_PART_NAMES_MAX];
|
||||
static int g_context_part_names_count = 0;
|
||||
static pthread_mutex_t g_context_part_names_mutex = PTHREAD_MUTEX_INITIALIZER;
|
||||
|
||||
#define AGENT_DEBOUNCE_WINDOW_SECONDS 5
|
||||
#define AGENT_DEBOUNCE_CACHE_SIZE 256
|
||||
#define AGENT_HISTORY_TURNS 12
|
||||
@@ -170,6 +178,8 @@ static int append_tool_result_message(cJSON* messages, const char* tool_call_id,
|
||||
return 0;
|
||||
}
|
||||
|
||||
static const char* get_context_part_name_copy(int idx);
|
||||
|
||||
static int appendf(char** buf, size_t* cap, size_t* used, const char* fmt, ...) {
|
||||
if (!buf || !cap || !used || !fmt) return -1;
|
||||
|
||||
@@ -242,6 +252,19 @@ static const char* detect_context_section(const char* role, const char* content,
|
||||
return "context_part";
|
||||
}
|
||||
|
||||
const char* agent_classify_message_part(cJSON* msg, int idx) {
|
||||
const char* mapped = get_context_part_name_copy(idx);
|
||||
if (mapped) {
|
||||
return mapped;
|
||||
}
|
||||
|
||||
cJSON* role = msg ? cJSON_GetObjectItemCaseSensitive(msg, "role") : NULL;
|
||||
cJSON* content = msg ? cJSON_GetObjectItemCaseSensitive(msg, "content") : NULL;
|
||||
const char* role_s = (role && cJSON_IsString(role) && role->valuestring) ? role->valuestring : "";
|
||||
const char* content_s = (content && cJSON_IsString(content) && content->valuestring) ? content->valuestring : "";
|
||||
return detect_context_section(role_s, content_s, idx);
|
||||
}
|
||||
|
||||
static char* format_context_payload_for_log(const char* context_payload) {
|
||||
const char* payload = context_payload ? context_payload : "";
|
||||
|
||||
@@ -262,7 +285,7 @@ static char* format_context_payload_for_log(const char* context_payload) {
|
||||
size_t used = 0;
|
||||
|
||||
int n = cJSON_GetArraySize(root);
|
||||
(void)appendf(&out, &cap, &used, "Messages: %d\n\n", n);
|
||||
(void)appendf(&out, &cap, &used, "Sections: %d\n\n", n);
|
||||
|
||||
for (int i = 0; i < n; i++) {
|
||||
cJSON* msg = cJSON_GetArrayItem(root, i);
|
||||
@@ -280,18 +303,17 @@ static char* format_context_payload_for_log(const char* context_payload) {
|
||||
content_s = "<null>";
|
||||
}
|
||||
|
||||
const char* section = detect_context_section(role_s, content_s, i);
|
||||
const char* section = agent_classify_message_part(msg, i);
|
||||
|
||||
if (appendf(&out,
|
||||
&cap,
|
||||
&used,
|
||||
"============================================================\n"
|
||||
"Message %02d | role=%s | section=%s\n"
|
||||
"Section: %s | role=%s\n"
|
||||
"============================================================\n\n"
|
||||
"%s\n\n\n",
|
||||
i + 1,
|
||||
section ? section : "context_part",
|
||||
role_s,
|
||||
section,
|
||||
content_s) != 0) {
|
||||
free(out);
|
||||
cJSON_Delete(root);
|
||||
@@ -303,6 +325,54 @@ static char* format_context_payload_for_log(const char* context_payload) {
|
||||
return out;
|
||||
}
|
||||
|
||||
static void clear_context_part_names_locked(void) {
|
||||
for (int i = 0; i < g_context_part_names_count; i++) {
|
||||
free(g_context_part_names[i]);
|
||||
g_context_part_names[i] = NULL;
|
||||
}
|
||||
g_context_part_names_count = 0;
|
||||
}
|
||||
|
||||
static void clear_context_part_names(void) {
|
||||
pthread_mutex_lock(&g_context_part_names_mutex);
|
||||
clear_context_part_names_locked();
|
||||
pthread_mutex_unlock(&g_context_part_names_mutex);
|
||||
}
|
||||
|
||||
static void set_context_part_name(int idx, const char* name) {
|
||||
if (idx < 0 || idx >= AGENT_CONTEXT_PART_NAMES_MAX) return;
|
||||
|
||||
pthread_mutex_lock(&g_context_part_names_mutex);
|
||||
if (idx >= g_context_part_names_count) {
|
||||
for (int i = g_context_part_names_count; i <= idx && i < AGENT_CONTEXT_PART_NAMES_MAX; i++) {
|
||||
g_context_part_names[i] = NULL;
|
||||
}
|
||||
g_context_part_names_count = idx + 1;
|
||||
}
|
||||
|
||||
free(g_context_part_names[idx]);
|
||||
g_context_part_names[idx] = strdup(name ? name : "context_part");
|
||||
pthread_mutex_unlock(&g_context_part_names_mutex);
|
||||
}
|
||||
|
||||
static const char* get_context_part_name_copy(int idx) {
|
||||
static __thread char tls_name[128];
|
||||
tls_name[0] = '\0';
|
||||
|
||||
pthread_mutex_lock(&g_context_part_names_mutex);
|
||||
if (idx >= 0 && idx < g_context_part_names_count && g_context_part_names[idx]) {
|
||||
snprintf(tls_name, sizeof(tls_name), "%s", g_context_part_names[idx]);
|
||||
}
|
||||
pthread_mutex_unlock(&g_context_part_names_mutex);
|
||||
|
||||
return tls_name[0] ? tls_name : NULL;
|
||||
}
|
||||
|
||||
static void template_emit_hook(const char* section_name, int message_index, void* user_data) {
|
||||
(void)user_data;
|
||||
set_context_part_name(message_index, section_name ? section_name : "context_part");
|
||||
}
|
||||
|
||||
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) {
|
||||
@@ -986,6 +1056,116 @@ static int append_adopted_skills_context(cJSON* messages) {
|
||||
return rc;
|
||||
}
|
||||
|
||||
static cJSON* build_recent_admin_dm_history_messages(const char* current_message) {
|
||||
cJSON* messages = cJSON_CreateArray();
|
||||
if (!messages) {
|
||||
return NULL;
|
||||
}
|
||||
if (append_recent_admin_dm_history(messages, current_message) != 0) {
|
||||
cJSON_Delete(messages);
|
||||
return NULL;
|
||||
}
|
||||
return messages;
|
||||
}
|
||||
|
||||
static char* build_startup_events_json_string(void) {
|
||||
if (!g_cfg || g_cfg->startup_event_count <= 0 || !g_cfg->startup_events) {
|
||||
return strdup("[]");
|
||||
}
|
||||
|
||||
cJSON* arr = cJSON_CreateArray();
|
||||
if (!arr) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
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 NULL;
|
||||
}
|
||||
|
||||
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* out = cJSON_PrintUnformatted(arr);
|
||||
cJSON_Delete(arr);
|
||||
return out;
|
||||
}
|
||||
|
||||
static char* build_adopted_skills_payload_string(void) {
|
||||
cJSON* tmp = cJSON_CreateArray();
|
||||
if (!tmp) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
if (append_adopted_skills_context(tmp) != 0) {
|
||||
cJSON_Delete(tmp);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
cJSON* first = cJSON_GetArrayItem(tmp, 0);
|
||||
cJSON* content = first ? cJSON_GetObjectItemCaseSensitive(first, "content") : NULL;
|
||||
char* out = (content && cJSON_IsString(content) && content->valuestring)
|
||||
? strdup(content->valuestring)
|
||||
: strdup("");
|
||||
cJSON_Delete(tmp);
|
||||
return out;
|
||||
}
|
||||
|
||||
static char* safe_strdup(const char* s) {
|
||||
return strdup(s ? s : "");
|
||||
}
|
||||
|
||||
static char* agent_template_resolve_var(const char* var_name, void* user_data) {
|
||||
(void)user_data;
|
||||
if (!var_name || !g_cfg) {
|
||||
return strdup("");
|
||||
}
|
||||
|
||||
if (strcmp(var_name, "admin_pubkey") == 0) {
|
||||
return safe_strdup(g_cfg->admin.pubkey);
|
||||
}
|
||||
if (strcmp(var_name, "agent_pubkey") == 0) {
|
||||
return safe_strdup(g_cfg->keys.public_key_hex);
|
||||
}
|
||||
if (strcmp(var_name, "admin_kind0_json") == 0) {
|
||||
char* v = nostr_handler_get_admin_kind0_context();
|
||||
return v ? v : strdup("");
|
||||
}
|
||||
if (strcmp(var_name, "admin_kind10002_json") == 0) {
|
||||
char* v = nostr_handler_get_admin_kind10002_context();
|
||||
return v ? v : strdup("");
|
||||
}
|
||||
if (strcmp(var_name, "startup_events_json") == 0) {
|
||||
char* v = build_startup_events_json_string();
|
||||
return v ? v : strdup("[]");
|
||||
}
|
||||
if (strcmp(var_name, "adopted_skills_content") == 0) {
|
||||
char* v = build_adopted_skills_payload_string();
|
||||
return v ? v : strdup("");
|
||||
}
|
||||
if (strcmp(var_name, "admin_notes_content") == 0) {
|
||||
char* v = nostr_handler_get_admin_kind1_notes_context();
|
||||
return v ? v : strdup("");
|
||||
}
|
||||
|
||||
return strdup("");
|
||||
}
|
||||
|
||||
int agent_init(didactyl_config_t* config, const char* system_context) {
|
||||
if (!config || !system_context) {
|
||||
return -1;
|
||||
@@ -1013,6 +1193,10 @@ int agent_init(didactyl_config_t* config, const char* system_context) {
|
||||
g_adopted_skills_last_refresh_at = 0;
|
||||
pthread_mutex_unlock(&g_adopted_skills_mutex);
|
||||
|
||||
prompt_template_free(&g_prompt_template);
|
||||
g_has_prompt_template = (prompt_template_parse(g_system_context, &g_prompt_template) == 0);
|
||||
clear_context_part_names();
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -1095,8 +1279,37 @@ int agent_build_admin_messages_json(const char* current_user_message, char** out
|
||||
}
|
||||
|
||||
*out_messages_json = NULL;
|
||||
clear_context_part_names();
|
||||
|
||||
cJSON* messages = cJSON_CreateArray();
|
||||
cJSON* messages = NULL;
|
||||
|
||||
if (g_has_prompt_template) {
|
||||
cJSON* dm_history = build_recent_admin_dm_history_messages(current_user_message);
|
||||
if (!dm_history) {
|
||||
dm_history = cJSON_CreateArray();
|
||||
}
|
||||
|
||||
llm_config_t cfg;
|
||||
memset(&cfg, 0, sizeof(cfg));
|
||||
const char* provider_name = NULL;
|
||||
if (llm_get_config(&cfg) == 0 && cfg.provider[0] != '\0') {
|
||||
provider_name = cfg.provider;
|
||||
}
|
||||
|
||||
messages = prompt_template_build_messages(&g_prompt_template,
|
||||
provider_name,
|
||||
agent_template_resolve_var,
|
||||
NULL,
|
||||
dm_history,
|
||||
AGENT_HISTORY_TURNS,
|
||||
template_emit_hook,
|
||||
NULL);
|
||||
cJSON_Delete(dm_history);
|
||||
if (!messages) {
|
||||
return -1;
|
||||
}
|
||||
} else {
|
||||
messages = cJSON_CreateArray();
|
||||
if (!messages) {
|
||||
return -1;
|
||||
}
|
||||
@@ -1123,6 +1336,17 @@ int agent_build_admin_messages_json(const char* current_user_message, char** out
|
||||
free(admin_notes);
|
||||
}
|
||||
|
||||
int n = cJSON_GetArraySize(messages);
|
||||
for (int i = 0; i < n; i++) {
|
||||
cJSON* msg = cJSON_GetArrayItem(messages, i);
|
||||
cJSON* role = msg ? cJSON_GetObjectItemCaseSensitive(msg, "role") : NULL;
|
||||
cJSON* content = msg ? cJSON_GetObjectItemCaseSensitive(msg, "content") : NULL;
|
||||
const char* role_s = (role && cJSON_IsString(role) && role->valuestring) ? role->valuestring : "";
|
||||
const char* content_s = (content && cJSON_IsString(content) && content->valuestring) ? content->valuestring : "";
|
||||
set_context_part_name(i, detect_context_section(role_s, content_s, i));
|
||||
}
|
||||
}
|
||||
|
||||
char* messages_json = cJSON_PrintUnformatted(messages);
|
||||
cJSON_Delete(messages);
|
||||
if (!messages_json) {
|
||||
@@ -1320,4 +1544,8 @@ void agent_cleanup(void) {
|
||||
clear_adopted_skills_cache_locked();
|
||||
g_adopted_skills_last_refresh_at = 0;
|
||||
pthread_mutex_unlock(&g_adopted_skills_mutex);
|
||||
|
||||
clear_context_part_names();
|
||||
prompt_template_free(&g_prompt_template);
|
||||
g_has_prompt_template = 0;
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ void agent_on_message(const char* sender_pubkey_hex,
|
||||
void* user_data);
|
||||
int agent_build_admin_messages_json(const char* current_user_message, char** out_messages_json);
|
||||
tools_context_t* agent_tools_context(void);
|
||||
const char* agent_classify_message_part(cJSON* msg, int idx);
|
||||
void agent_cleanup(void);
|
||||
|
||||
#endif
|
||||
@@ -111,26 +111,7 @@ static void maybe_model_override_end(const llm_config_t* old_cfg, int overridden
|
||||
}
|
||||
|
||||
static const char* classify_part_name(cJSON* msg, int idx) {
|
||||
cJSON* role = cJSON_GetObjectItemCaseSensitive(msg, "role");
|
||||
cJSON* content = cJSON_GetObjectItemCaseSensitive(msg, "content");
|
||||
const char* role_s = (role && cJSON_IsString(role) && role->valuestring) ? role->valuestring : "";
|
||||
const char* content_s = (content && cJSON_IsString(content) && content->valuestring) ? content->valuestring : "";
|
||||
|
||||
if (idx == 0 && strcmp(role_s, "system") == 0) return "system_prompt";
|
||||
if (strncmp(content_s, "## Administrator Identity", 25) == 0 ||
|
||||
strncmp(content_s, "This is your administrator!", 27) == 0) return "admin_identity";
|
||||
if (strncmp(content_s, "## Administrator Kind 0 Profile", 31) == 0 ||
|
||||
strncmp(content_s, "Administrator kind 0 profile content", 36) == 0) return "admin_kind0";
|
||||
if (strncmp(content_s, "## Administrator Relay List", 27) == 0 ||
|
||||
strncmp(content_s, "Administrator kind 10002 relay-list content", 43) == 0) return "admin_relay_list";
|
||||
if (strncmp(content_s, "## Startup Events Memory", 24) == 0 ||
|
||||
strncmp(content_s, "Startup events memory", 21) == 0) return "startup_events";
|
||||
if (strncmp(content_s, "## Adopted Skills Memory", 24) == 0 ||
|
||||
strncmp(content_s, "Adopted skills memory", 21) == 0) return "adopted_skills";
|
||||
if (strncmp(content_s, "## Administrator Recent Notes", 29) == 0 ||
|
||||
strncmp(content_s, "Administrator recent public notes", 33) == 0) return "admin_notes";
|
||||
if (strcmp(role_s, "user") == 0 || strcmp(role_s, "assistant") == 0) return "dm_history";
|
||||
return "context_part";
|
||||
return agent_classify_message_part(msg, idx);
|
||||
}
|
||||
|
||||
static int build_context_parts_response(cJSON* out_root) {
|
||||
|
||||
10
src/main.c
10
src/main.c
@@ -297,6 +297,16 @@ int main(int argc, char** argv) {
|
||||
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)");
|
||||
}
|
||||
|
||||
DEBUG_INFO("[didactyl] entering main poll loop");
|
||||
|
||||
@@ -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 27
|
||||
#define DIDACTYL_VERSION "v0.0.27"
|
||||
#define DIDACTYL_VERSION_PATCH 28
|
||||
#define DIDACTYL_VERSION "v0.0.28"
|
||||
|
||||
// Agent metadata
|
||||
#define DIDACTYL_NAME "Didactyl"
|
||||
|
||||
521
src/prompt_template.c
Normal file
521
src/prompt_template.c
Normal file
@@ -0,0 +1,521 @@
|
||||
#define _POSIX_C_SOURCE 200809L
|
||||
|
||||
#include "prompt_template.h"
|
||||
|
||||
#include <ctype.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
static char* dup_range(const char* s, size_t n) {
|
||||
char* out = (char*)malloc(n + 1U);
|
||||
if (!out) return NULL;
|
||||
if (n > 0) {
|
||||
memcpy(out, s, n);
|
||||
}
|
||||
out[n] = '\0';
|
||||
return out;
|
||||
}
|
||||
|
||||
static char* ltrim_inplace(char* s) {
|
||||
if (!s) return s;
|
||||
while (*s && isspace((unsigned char)*s)) s++;
|
||||
return s;
|
||||
}
|
||||
|
||||
static void rtrim_inplace(char* s) {
|
||||
if (!s) return;
|
||||
size_t n = strlen(s);
|
||||
while (n > 0 && isspace((unsigned char)s[n - 1])) {
|
||||
s[n - 1] = '\0';
|
||||
n--;
|
||||
}
|
||||
}
|
||||
|
||||
static int starts_with(const char* s, const char* prefix) {
|
||||
if (!s || !prefix) return 0;
|
||||
size_t n = strlen(prefix);
|
||||
return strncmp(s, prefix, n) == 0;
|
||||
}
|
||||
|
||||
static int add_line(char*** lines, int* count, int* cap, char* line) {
|
||||
if (!lines || !count || !cap) return -1;
|
||||
if (*count >= *cap) {
|
||||
int next = (*cap == 0) ? 64 : (*cap * 2);
|
||||
char** grown = (char**)realloc(*lines, (size_t)next * sizeof(char*));
|
||||
if (!grown) return -1;
|
||||
*lines = grown;
|
||||
*cap = next;
|
||||
}
|
||||
(*lines)[*count] = line;
|
||||
(*count)++;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int split_lines_inplace(char* s, char*** out_lines, int* out_count) {
|
||||
if (!s || !out_lines || !out_count) return -1;
|
||||
|
||||
char** lines = NULL;
|
||||
int count = 0;
|
||||
int cap = 0;
|
||||
|
||||
char* p = s;
|
||||
while (*p) {
|
||||
char* start = p;
|
||||
while (*p && *p != '\n') p++;
|
||||
if (*p == '\n') {
|
||||
*p = '\0';
|
||||
p++;
|
||||
}
|
||||
if (add_line(&lines, &count, &cap, start) != 0) {
|
||||
free(lines);
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
*out_lines = lines;
|
||||
*out_count = count;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int append_text(char** buf, size_t* cap, size_t* used, const char* s) {
|
||||
if (!buf || !cap || !used || !s) return -1;
|
||||
size_t n = strlen(s);
|
||||
if (*used + n + 1U > *cap) {
|
||||
size_t next = *cap;
|
||||
while (*used + n + 1U > next) {
|
||||
next = (next == 0U) ? 256U : (next * 2U);
|
||||
}
|
||||
char* grown = (char*)realloc(*buf, next);
|
||||
if (!grown) return -1;
|
||||
*buf = grown;
|
||||
*cap = next;
|
||||
}
|
||||
memcpy(*buf + *used, s, n);
|
||||
*used += n;
|
||||
(*buf)[*used] = '\0';
|
||||
return 0;
|
||||
}
|
||||
|
||||
static char* resolve_placeholders(const char* tpl,
|
||||
prompt_var_resolver_fn resolver_fn,
|
||||
void* resolver_user_data) {
|
||||
if (!tpl) return strdup("");
|
||||
|
||||
size_t cap = strlen(tpl) + 64U;
|
||||
char* out = (char*)malloc(cap ? cap : 128U);
|
||||
if (!out) return NULL;
|
||||
out[0] = '\0';
|
||||
size_t used = 0;
|
||||
|
||||
const char* p = tpl;
|
||||
while (*p) {
|
||||
const char* open = strstr(p, "{{");
|
||||
if (!open) {
|
||||
if (append_text(&out, &cap, &used, p) != 0) {
|
||||
free(out);
|
||||
return NULL;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
if (open > p) {
|
||||
char* literal = dup_range(p, (size_t)(open - p));
|
||||
if (!literal) {
|
||||
free(out);
|
||||
return NULL;
|
||||
}
|
||||
int rc = append_text(&out, &cap, &used, literal);
|
||||
free(literal);
|
||||
if (rc != 0) {
|
||||
free(out);
|
||||
return NULL;
|
||||
}
|
||||
}
|
||||
|
||||
const char* close = strstr(open + 2, "}}");
|
||||
if (!close) {
|
||||
if (append_text(&out, &cap, &used, open) != 0) {
|
||||
free(out);
|
||||
return NULL;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
char* name = dup_range(open + 2, (size_t)(close - (open + 2)));
|
||||
if (!name) {
|
||||
free(out);
|
||||
return NULL;
|
||||
}
|
||||
char* name_trim = ltrim_inplace(name);
|
||||
rtrim_inplace(name_trim);
|
||||
|
||||
char* val = resolver_fn ? resolver_fn(name_trim, resolver_user_data) : NULL;
|
||||
if (!val) {
|
||||
val = strdup("");
|
||||
}
|
||||
|
||||
int rc = 0;
|
||||
if (val) {
|
||||
rc = append_text(&out, &cap, &used, val);
|
||||
}
|
||||
free(val);
|
||||
free(name);
|
||||
if (rc != 0) {
|
||||
free(out);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
p = close + 2;
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
static void init_section_defaults(prompt_template_section_t* sec) {
|
||||
if (!sec) return;
|
||||
memset(sec->name, 0, sizeof(sec->name));
|
||||
memset(sec->role, 0, sizeof(sec->role));
|
||||
snprintf(sec->role, sizeof(sec->role), "system");
|
||||
sec->content_template = NULL;
|
||||
sec->limit = 0;
|
||||
sec->provider_name = NULL;
|
||||
sec->provider_content_template = NULL;
|
||||
}
|
||||
|
||||
static int parse_int_or_zero(const char* s) {
|
||||
if (!s) return 0;
|
||||
while (*s && isspace((unsigned char)*s)) s++;
|
||||
return atoi(s);
|
||||
}
|
||||
|
||||
int prompt_template_parse(const char* soul_content, prompt_template_t* out_template) {
|
||||
if (!soul_content || !out_template) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
memset(out_template, 0, sizeof(*out_template));
|
||||
|
||||
const char* marker = strstr(soul_content, PROMPT_TEMPLATE_MARKER);
|
||||
if (!marker) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
size_t personality_len = (size_t)(marker - soul_content);
|
||||
out_template->personality = dup_range(soul_content, personality_len);
|
||||
if (!out_template->personality) {
|
||||
return -1;
|
||||
}
|
||||
rtrim_inplace(out_template->personality);
|
||||
|
||||
const char* after = marker + strlen(PROMPT_TEMPLATE_MARKER);
|
||||
while (*after == '\r' || *after == '\n') after++;
|
||||
|
||||
char* tpl = strdup(after);
|
||||
if (!tpl) {
|
||||
prompt_template_free(out_template);
|
||||
return -1;
|
||||
}
|
||||
|
||||
char** lines = NULL;
|
||||
int line_count = 0;
|
||||
if (split_lines_inplace(tpl, &lines, &line_count) != 0) {
|
||||
free(tpl);
|
||||
prompt_template_free(out_template);
|
||||
return -1;
|
||||
}
|
||||
|
||||
int current = -1;
|
||||
int i = 0;
|
||||
while (i < line_count) {
|
||||
char* raw = lines[i];
|
||||
rtrim_inplace(raw);
|
||||
char* line = ltrim_inplace(raw);
|
||||
|
||||
if (*line == '\0') {
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (starts_with(line, "- section:")) {
|
||||
if (out_template->section_count >= PROMPT_TEMPLATE_MAX_SECTIONS) {
|
||||
break;
|
||||
}
|
||||
|
||||
current = out_template->section_count;
|
||||
init_section_defaults(&out_template->sections[current]);
|
||||
out_template->section_count++;
|
||||
|
||||
char* name = line + strlen("- section:");
|
||||
name = ltrim_inplace(name);
|
||||
rtrim_inplace(name);
|
||||
snprintf(out_template->sections[current].name,
|
||||
sizeof(out_template->sections[current].name),
|
||||
"%s",
|
||||
name);
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (current < 0) {
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (starts_with(line, "role:")) {
|
||||
char* role = line + strlen("role:");
|
||||
role = ltrim_inplace(role);
|
||||
rtrim_inplace(role);
|
||||
snprintf(out_template->sections[current].role,
|
||||
sizeof(out_template->sections[current].role),
|
||||
"%s",
|
||||
(*role) ? role : "system");
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (starts_with(line, "limit:")) {
|
||||
char* lim = line + strlen("limit:");
|
||||
out_template->sections[current].limit = parse_int_or_zero(lim);
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (starts_with(line, "content:")) {
|
||||
char* val = line + strlen("content:");
|
||||
val = ltrim_inplace(val);
|
||||
|
||||
free(out_template->sections[current].content_template);
|
||||
out_template->sections[current].content_template = NULL;
|
||||
|
||||
if (strcmp(val, "|") == 0) {
|
||||
char* acc = strdup("");
|
||||
size_t cap = acc ? 1U : 0U;
|
||||
size_t used = 0U;
|
||||
if (!acc) {
|
||||
free(lines);
|
||||
free(tpl);
|
||||
prompt_template_free(out_template);
|
||||
return -1;
|
||||
}
|
||||
|
||||
i++;
|
||||
while (i < line_count) {
|
||||
char* next_raw = lines[i];
|
||||
char* next_ltrim = ltrim_inplace(next_raw);
|
||||
|
||||
if (starts_with(next_ltrim, "- section:") ||
|
||||
starts_with(next_ltrim, "role:") ||
|
||||
starts_with(next_ltrim, "limit:") ||
|
||||
starts_with(next_ltrim, "content:") ||
|
||||
starts_with(next_ltrim, "provider:")) {
|
||||
break;
|
||||
}
|
||||
|
||||
char* piece = next_raw;
|
||||
if (strncmp(piece, " ", 4) == 0) piece += 4;
|
||||
else if (strncmp(piece, " ", 2) == 0) piece += 2;
|
||||
rtrim_inplace(piece);
|
||||
|
||||
if (append_text(&acc, &cap, &used, piece) != 0 ||
|
||||
append_text(&acc, &cap, &used, "\n") != 0) {
|
||||
free(acc);
|
||||
free(lines);
|
||||
free(tpl);
|
||||
prompt_template_free(out_template);
|
||||
return -1;
|
||||
}
|
||||
i++;
|
||||
}
|
||||
|
||||
rtrim_inplace(acc);
|
||||
out_template->sections[current].content_template = acc;
|
||||
continue;
|
||||
}
|
||||
|
||||
rtrim_inplace(val);
|
||||
out_template->sections[current].content_template = strdup(val);
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (starts_with(line, "provider:")) {
|
||||
i++;
|
||||
if (i >= line_count) continue;
|
||||
|
||||
char* provider_line = ltrim_inplace(lines[i]);
|
||||
rtrim_inplace(provider_line);
|
||||
char* colon = strchr(provider_line, ':');
|
||||
if (!colon) {
|
||||
continue;
|
||||
}
|
||||
|
||||
*colon = '\0';
|
||||
char* provider_name = ltrim_inplace(provider_line);
|
||||
rtrim_inplace(provider_name);
|
||||
char* provider_val = ltrim_inplace(colon + 1);
|
||||
rtrim_inplace(provider_val);
|
||||
|
||||
free(out_template->sections[current].provider_name);
|
||||
out_template->sections[current].provider_name = strdup(provider_name);
|
||||
free(out_template->sections[current].provider_content_template);
|
||||
out_template->sections[current].provider_content_template = NULL;
|
||||
|
||||
if (strcmp(provider_val, "|") == 0) {
|
||||
char* acc = strdup("");
|
||||
size_t cap = acc ? 1U : 0U;
|
||||
size_t used = 0U;
|
||||
if (!acc) {
|
||||
free(lines);
|
||||
free(tpl);
|
||||
prompt_template_free(out_template);
|
||||
return -1;
|
||||
}
|
||||
|
||||
i++;
|
||||
while (i < line_count) {
|
||||
char* next = ltrim_inplace(lines[i]);
|
||||
if (starts_with(next, "- section:") ||
|
||||
starts_with(next, "role:") ||
|
||||
starts_with(next, "limit:") ||
|
||||
starts_with(next, "content:") ||
|
||||
starts_with(next, "provider:")) {
|
||||
break;
|
||||
}
|
||||
char* piece = lines[i];
|
||||
if (strncmp(piece, " ", 4) == 0) piece += 4;
|
||||
else if (strncmp(piece, " ", 2) == 0) piece += 2;
|
||||
rtrim_inplace(piece);
|
||||
if (append_text(&acc, &cap, &used, piece) != 0 ||
|
||||
append_text(&acc, &cap, &used, "\n") != 0) {
|
||||
free(acc);
|
||||
free(lines);
|
||||
free(tpl);
|
||||
prompt_template_free(out_template);
|
||||
return -1;
|
||||
}
|
||||
i++;
|
||||
}
|
||||
rtrim_inplace(acc);
|
||||
out_template->sections[current].provider_content_template = acc;
|
||||
continue;
|
||||
}
|
||||
|
||||
out_template->sections[current].provider_content_template = strdup(provider_val);
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
|
||||
i++;
|
||||
}
|
||||
|
||||
free(lines);
|
||||
free(tpl);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int append_message_object(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;
|
||||
}
|
||||
|
||||
cJSON* prompt_template_build_messages(const prompt_template_t* tmpl,
|
||||
const char* provider_name,
|
||||
prompt_var_resolver_fn resolver_fn,
|
||||
void* resolver_user_data,
|
||||
cJSON* dm_history_messages,
|
||||
int dm_history_default_limit,
|
||||
prompt_template_emit_hook_fn emit_hook,
|
||||
void* emit_hook_user_data) {
|
||||
if (!tmpl) return NULL;
|
||||
|
||||
cJSON* out = cJSON_CreateArray();
|
||||
if (!out) return NULL;
|
||||
|
||||
int out_idx = 0;
|
||||
if (tmpl->personality && tmpl->personality[0] != '\0') {
|
||||
if (append_message_object(out, "system", tmpl->personality) != 0) {
|
||||
cJSON_Delete(out);
|
||||
return NULL;
|
||||
}
|
||||
if (emit_hook) emit_hook("system_prompt", out_idx, emit_hook_user_data);
|
||||
out_idx++;
|
||||
}
|
||||
|
||||
for (int i = 0; i < tmpl->section_count; i++) {
|
||||
const prompt_template_section_t* sec = &tmpl->sections[i];
|
||||
const char* role = sec->role[0] ? sec->role : "system";
|
||||
|
||||
if (strcmp(role, "expand") == 0) {
|
||||
if (!dm_history_messages || !cJSON_IsArray(dm_history_messages)) {
|
||||
continue;
|
||||
}
|
||||
int total = cJSON_GetArraySize(dm_history_messages);
|
||||
int lim = sec->limit > 0 ? sec->limit : dm_history_default_limit;
|
||||
int start = (lim > 0 && total > lim) ? (total - lim) : 0;
|
||||
|
||||
for (int j = start; j < total; j++) {
|
||||
cJSON* item = cJSON_GetArrayItem(dm_history_messages, j);
|
||||
if (!item || !cJSON_IsObject(item)) continue;
|
||||
cJSON* dup = cJSON_Duplicate(item, 1);
|
||||
if (!dup) {
|
||||
cJSON_Delete(out);
|
||||
return NULL;
|
||||
}
|
||||
cJSON_AddItemToArray(out, dup);
|
||||
if (emit_hook) emit_hook(sec->name[0] ? sec->name : "context_part", out_idx, emit_hook_user_data);
|
||||
out_idx++;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
const char* tpl = sec->content_template;
|
||||
if (provider_name && sec->provider_name && sec->provider_content_template &&
|
||||
strcmp(provider_name, sec->provider_name) == 0) {
|
||||
tpl = sec->provider_content_template;
|
||||
}
|
||||
|
||||
char* resolved = resolve_placeholders(tpl ? tpl : "", resolver_fn, resolver_user_data);
|
||||
if (!resolved) {
|
||||
cJSON_Delete(out);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
if (append_message_object(out, role, resolved) != 0) {
|
||||
free(resolved);
|
||||
cJSON_Delete(out);
|
||||
return NULL;
|
||||
}
|
||||
if (emit_hook) emit_hook(sec->name[0] ? sec->name : "context_part", out_idx, emit_hook_user_data);
|
||||
out_idx++;
|
||||
free(resolved);
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
void prompt_template_free(prompt_template_t* tmpl) {
|
||||
if (!tmpl) return;
|
||||
|
||||
free(tmpl->personality);
|
||||
tmpl->personality = NULL;
|
||||
|
||||
for (int i = 0; i < tmpl->section_count; i++) {
|
||||
free(tmpl->sections[i].content_template);
|
||||
tmpl->sections[i].content_template = NULL;
|
||||
free(tmpl->sections[i].provider_name);
|
||||
tmpl->sections[i].provider_name = NULL;
|
||||
free(tmpl->sections[i].provider_content_template);
|
||||
tmpl->sections[i].provider_content_template = NULL;
|
||||
tmpl->sections[i].name[0] = '\0';
|
||||
tmpl->sections[i].role[0] = '\0';
|
||||
tmpl->sections[i].limit = 0;
|
||||
}
|
||||
|
||||
tmpl->section_count = 0;
|
||||
}
|
||||
44
src/prompt_template.h
Normal file
44
src/prompt_template.h
Normal file
@@ -0,0 +1,44 @@
|
||||
#ifndef DIDACTYL_PROMPT_TEMPLATE_H
|
||||
#define DIDACTYL_PROMPT_TEMPLATE_H
|
||||
|
||||
#include "cjson/cJSON.h"
|
||||
|
||||
#define PROMPT_TEMPLATE_MAX_SECTIONS 32
|
||||
#define PROMPT_TEMPLATE_MAX_NAME_LEN 64
|
||||
#define PROMPT_TEMPLATE_MAX_ROLE_LEN 16
|
||||
#define PROMPT_TEMPLATE_MARKER "---template---"
|
||||
|
||||
typedef struct {
|
||||
char name[PROMPT_TEMPLATE_MAX_NAME_LEN];
|
||||
char role[PROMPT_TEMPLATE_MAX_ROLE_LEN];
|
||||
char* content_template;
|
||||
int limit;
|
||||
char* provider_name;
|
||||
char* provider_content_template;
|
||||
} prompt_template_section_t;
|
||||
|
||||
typedef struct {
|
||||
char* personality;
|
||||
prompt_template_section_t sections[PROMPT_TEMPLATE_MAX_SECTIONS];
|
||||
int section_count;
|
||||
} prompt_template_t;
|
||||
|
||||
typedef char* (*prompt_var_resolver_fn)(const char* var_name, void* user_data);
|
||||
typedef void (*prompt_template_emit_hook_fn)(const char* section_name,
|
||||
int message_index,
|
||||
void* user_data);
|
||||
|
||||
int prompt_template_parse(const char* soul_content, prompt_template_t* out_template);
|
||||
|
||||
cJSON* prompt_template_build_messages(const prompt_template_t* tmpl,
|
||||
const char* provider_name,
|
||||
prompt_var_resolver_fn resolver_fn,
|
||||
void* resolver_user_data,
|
||||
cJSON* dm_history_messages,
|
||||
int dm_history_default_limit,
|
||||
prompt_template_emit_hook_fn emit_hook,
|
||||
void* emit_hook_user_data);
|
||||
|
||||
void prompt_template_free(prompt_template_t* tmpl);
|
||||
|
||||
#endif
|
||||
Reference in New Issue
Block a user