-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathconfig.example.toml
More file actions
427 lines (372 loc) · 20.5 KB
/
Copy pathconfig.example.toml
File metadata and controls
427 lines (372 loc) · 20.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
# inbox — example configuration
# Copy to config.toml and fill in your secrets via environment variables.
# All ${VAR} references are substituted at startup from the environment.
# ── General ───────────────────────────────────────────────────────────────────
[general]
# Org-mode file that captured items are appended to.
output_file = "/home/user/org/inbox.org"
# Base directory for downloaded attachments (org-attach-id-dir layout).
attachments_dir = "/home/user/org/attachments"
# Log level: trace | debug | info | warn | error
log_level = "info"
# Log format: pretty (human) | json (K8s / structured)
log_format = "pretty"
# ── Admin server (port 9090) ──────────────────────────────────────────────────
[admin]
# Address for the admin server (health, metrics, UI).
# Default is all interfaces for container deployments.
# If you do not need remote access, prefer 127.0.0.1 and a reverse proxy/firewall.
bind_addr = "0.0.0.0:9090"
# Username for the web UI login.
username = "admin"
# Argon2id hash of the admin password.
# Generate with: cargo run -- hash-password
password_hash = "${INBOX_ADMIN_PASSWORD_HASH}"
# Session cookie lifetime in days (default: 7).
# Cookie attributes are HttpOnly + SameSite=Lax (no Secure flag by default), so
# keep admin traffic on trusted networks and terminate TLS upstream when exposed.
session_ttl_days = 7
# Seconds between SIGTERM and actual shutdown (for load-balancer drain).
shutdown_drain_secs = 5
# ── Web UI ────────────────────────────────────────────────────────────────────
[web_ui]
enabled = true
# ── LLM ──────────────────────────────────────────────────────────────────────
[llm]
# What to do when all LLM backends fail:
# raw — write the raw text as-is (default)
# discard — drop the message
fallback = "raw"
# Maximum characters of fetched page content to include in the LLM prompt.
url_content_max_chars = 4000
# Maximum tool-call turns per message.
max_tool_turns = 5
# Retries for individual LLM API calls within a tool loop (e.g. on network blip).
# Default: 2
# inner_retries = 2
# Max depth for recursive llm_call tool invocations (default: 1).
# max_llm_tool_depth = 1
# Maximum image file size (bytes) to send to the LLM for vision analysis.
# Images larger than this are silently skipped. Default: 5 MB.
# vision_max_bytes = 5242880
# Truncate individual tool results to this many characters before feeding back to LLM.
# Prevents context overflow from large scraped pages. Set to 0 to disable truncation. Default: 20000.
# tool_result_max_chars = 20000
[llm.prompts]
base_system = """
You are a personal inbox assistant. Given a captured note or web content, respond with a JSON object containing:
- "title": a short descriptive title (max 80 chars)
- "tags": array of relevant tag strings (max 5, lowercase, no spaces — use underscores)
- "summary": a 1-3 sentence summary of the content
- "excerpt": (optional) a single key quote or sentence worth preserving verbatim, or null
Respond ONLY with the JSON object, no markdown fences.
"""
tool_guidance_header = "Tool-specific guidance:"
js_shell_tool_hint = "If URL content appears to be a JavaScript shell, call crawl_url for these URLs: {urls}. Prefer markdown output; fallback to HTML if markdown is empty."
require_tool_for_urls = true
url_tool_decision = "When URLs are present, decide the best retrieval tool first and call it before producing final JSON. Use crawl_url for JS-heavy/app-shell pages, scrape_page for normal readable pages, and download_file for direct file links. URLs: {urls}"
# Backend ordering: the chain tries each [[llm.backends]] entry in the order
# they appear, only escalating to the next when the current one exhausts its
# retries. Recommended order for mixed setups:
# 1. free_router — quality-ranked free cloud models with hedged dispatch
# 2. ollama — offline fallback for when the internet is unreachable
# 3. openrouter — optional final resort pinned to a paid model
# Primary backend: free-router cloud pool.
#
# Fetches a curated index of *free* OpenRouter models, partitions them by
# tool-call capability, and races several candidates in parallel per call.
# Still requires an OpenRouter API key because the chat-completions endpoint
# is OpenRouter's own.
[[llm.backends]]
type = "free_router"
api_key = "${OPENROUTER_API_KEY}"
# Model index. Default: https://shir-man.com/api/free-llm/top-models
# api_url = "https://shir-man.com/api/free-llm/top-models"
# Chat-completions endpoint. Default: https://openrouter.ai/api/v1
# base_url = "https://openrouter.ai/api/v1"
retries = 1 # inner fanout + per-model retries handle most failures
timeout_secs = 30
connect_timeout_secs = 10
# Number of candidate models raced in parallel per call. First valid wins.
# parallel_fanout = 3
# Attempts per model (with exponential backoff) before moving on for this call.
# per_model_retries = 2
# Total in-flight calls across all hedged requests (omit = unlimited).
# max_concurrent = 8
# Minimum seconds between reactive pool refreshes (prevents refresh storms).
# min_refresh_interval_secs = 300
# Soft preferences — reorder the pool but never drop models. All optional.
# min_context_length = 16384
# prefer_structured_outputs = true
# prefer_reasoning = false
# Vision capability is detected automatically per model from OpenRouter
# /api/v1/models metadata (architecture.input_modalities), so image requests
# are routed only to vision-capable free models. No vision_supported flag here.
# circuit_open_secs = 300 # On a transient outage (429 rate limit, 5xx, or a
# # connection timeout) skip this backend for N seconds
# # and fall through to the next one (0 = disabled).
# Alternative cloud backend: pinned OpenRouter model (commented out by default
# — prefer free_router above for free models, or keep both with openrouter
# as a deterministic fallback).
# [[llm.backends]]
# type = "openrouter"
# model = "openai/gpt-4o-mini"
# api_key = "${OPENROUTER_API_KEY}"
# base_url defaults to "https://openrouter.ai/api/v1"; override for compatible APIs.
# base_url = "https://openrouter.ai/api/v1"
# retries = 3
# timeout_secs = 30
# connect_timeout_secs = 10 # TCP connect timeout (default 10s); separate from read timeout
# vision_supported = true # set true for a vision-capable pinned model (e.g. gpt-4o);
# # image requests are routed here. Default: false.
# circuit_open_secs = 300 # Transient-outage cooldown (429/5xx/timeout), as above.
# Offline fallback: local Ollama (comment out if not available).
# [[llm.backends]]
# type = "ollama"
# model = "llama3.2"
# base_url = "http://localhost:11434"
# timeout_secs = 60 # read/response timeout; set longer for CPU-based inference
# connect_timeout_secs = 10 # TCP connect timeout; set short to fail fast if Ollama is down
# think = true # static default; model may also call activate_thinking tool
# think_timeout_secs = 300 # timeout for thinking requests (default: timeout_secs)
# thinking_supported = true # allow model to call activate_thinking (Ollama only; default: false)
# vision_supported = true # set true for a vision-capable local model (e.g. llava, llama3.2-vision);
# # image requests are routed here. Default: false.
# max_concurrent = 1 # serialize requests to avoid overloading the local model (omit = unlimited)
# context_size = 16384 # num_ctx for Ollama (KV cache / context window). Must fit the system
# # prompt + ALL enabled tool schemas + content; Ollama silently
# # left-truncates anything past num_ctx, evicting the system prompt
# # (and its JSON-output instruction) first. 2048 is far too small once
# # tools are enabled — use 16384+ for tool-calling models.
# format = "json" # Constrain Ollama output to valid JSON. Applied only on turns with no
# # tools, so it never suppresses tool calls. Recommended for small models
# # that tend to drift into Markdown prose.
# circuit_open_secs = 300 # Skip this backend for N seconds after a transient outage —
# # a connection failure OR a 429/5xx/timeout response (0 = disabled).
# Local vision fallback: a vision-capable Ollama model, placed AFTER the text
# backends so it only handles image requests the cloud vision pool could not.
# Recommended chain order: free_router (cloud vision auto-detected) → ollama
# (text) → ollama (vision). With this in place, an image whose cloud vision
# model is rate-limited (429) is re-OCR'd locally instead of being left pending.
# [[llm.backends]]
# type = "ollama"
# model = "llama3.2-vision" # or "llava"
# base_url = "http://localhost:11434"
# timeout_secs = 120 # vision inference is slower; allow more time
# connect_timeout_secs = 10
# vision_supported = true # REQUIRED — routes image requests to this backend
# max_concurrent = 1
# context_size = 16384
# circuit_open_secs = 300
# ── Adapters ──────────────────────────────────────────────────────────────────
[adapters.telegram]
enabled = true
# Bot token from @BotFather.
bot_token = "${TELEGRAM_BOT_TOKEN}"
# Restrict to specific Telegram user IDs (empty = allow all).
allowed_user_ids = []
# Per-attempt timeout for downloading attached files (seconds). Default: 60.
# file_download_timeout_secs = 60
# Number of download attempts before giving up. Default: 3.
# file_download_retries = 3
# Time (ms) to wait for additional parts of a media group (album) before
# flushing them as a single message. Default: 1500.
# media_group_timeout_ms = 1500
# Max attempts to send/edit a status message (non-terminal stages).
# Terminal stages (Done/Failed) use twice this value. Default: 5.
# status_notify_retries = 5
# Base delay (ms) for exponential backoff between status notification retries.
# Capped at 30 seconds. Default: 1000.
# status_notify_retry_base_ms = 1000
[adapters.http]
enabled = true
# Address for the inbox HTTP API.
bind_addr = "0.0.0.0:8080"
# Bearer token for POST /inbox and POST /inbox/upload (empty = no auth).
auth_token = "${INBOX_HTTP_TOKEN}"
[adapters.email]
enabled = false
host = "imap.example.com"
port = 993
username = "${EMAIL_USERNAME}"
password = "${EMAIL_PASSWORD}"
mailbox = "INBOX"
mark_as_seen = true
# tls = true # default; set to false only for local/test IMAP servers
# Move processed emails here instead of just marking as seen (optional).
# processed_mailbox = "Processed"
# ── URL fetching ──────────────────────────────────────────────────────────────
[url_fetch]
enabled = true
timeout_secs = 10
max_redirects = 5
max_body_bytes = 5242880 # 5 MiB
user_agent = "inbox/1.0 (personal capture daemon)"
# Domains to skip when auto-fetching URLs (e.g. large media sites).
# Note: if you configure nitter_base_url below, remove twitter.com and x.com from this list.
skip_domains = ["youtube.com", "twitter.com", "x.com"]
# Optional: rewrite Twitter/X URLs to a Nitter instance (serves static HTML without JavaScript).
# When set, remove twitter.com and x.com from skip_domains to allow fetching through Nitter.
# nitter_base_url = "https://nitter.example.com"
# ── Pipeline pre-processing rules ────────────────────────────────────────────
#
# Rules are evaluated in order before URL enrichment. All matching rules fire
# (not first-match-wins). Supported conditions:
# text_word_count_lt — fires when the message text has fewer than `threshold` words
# has_image_attachment — fires when at least one image is attached
# has_attachment — fires when any attachment is present
#
# Supported actions:
# force_web_search — instruct the LLM to call web_search before summarising
# add_tag — add `tag` to the org tags (merged with LLM tags)
# add_llm_hint — append `llm_hint` to the LLM system prompt
#
# All rules are optional. Uncomment and customise as needed.
#
# [[pipeline.preprocessing.rules]]
# name = "short_text_search"
# condition = "text_word_count_lt"
# threshold = 10
# action = "force_web_search"
# llm_hint = "The input is very short — use web_search to find more context before summarising."
#
# [[pipeline.preprocessing.rules]]
# name = "image_tagging"
# condition = "has_image_attachment"
# action = "add_tag"
# tag = "image"
# llm_hint = "This message contains an image — describe the visual content in the summary."
# ── Memo (research-not-needed) tags ──────────────────────────────────────────
# Messages carrying one of these hashtags are written straight to org: URL fetch
# and the LLM enrichment call (and its memory recall) are skipped; only image
# OCR still runs. The node is final (never :inbox_pending:) and recorded with
# ENRICHED_BY: memo. The tag stays on the org headline so memos remain
# searchable. An image memo sent while every vision backend is down is held
# :inbox_pending: and re-OCR'd on resume instead of being finalized empty.
# A leading "#" in a tag is optional (matched the same either way). Default: ["memo"].
[pipeline]
memo_tags = ["memo"]
# ── Pipeline content policy ───────────────────────────────────────────────────
[pipeline.web_content]
# allow = keep extracted text even if it looks like a JS shell page
# tool_only = do not trust JS-shell extracted text; guide LLM to call crawl_url
# drop = skip JS-shell text without adding tool guidance
js_shell_policy = "tool_only"
js_shell_patterns = [
"doesn't work properly without javascript enabled",
"please enable it to continue",
"requires javascript"
]
# ── Incomplete-processing resume ─────────────────────────────────────────────
# When all LLM backends fail, the message is written with an :inbox_pending: tag.
# If resume is enabled, the full context (URL contents, tool results, etc.) is
# stored in a SQLite database and retried in the background when the pipeline is
# idle.
[pipeline.resume]
# Set to true to enable the pending store and background retry task.
enabled = false
# How often (in seconds) the resume task wakes up to check for pending items.
interval_secs = 300 # 5 minutes
# Maximum number of LLM retry attempts per item before it is abandoned.
max_retries = 5
# Path to the SQLite database file. Defaults to {attachments_dir}/pending.db.
# db_path = "/path/to/pending.db"
# ── Image analysis ─────────────────────────────────────────────────────────────
# Vision-LLM stage that runs before pre-processing: it classifies each image
# attachment (interface/screenshot vs photo) and transcribes any visible text,
# which then flows into enrichment. Requires a vision-capable backend (a free
# vision model via free_router, or a pinned model with vision_supported = true).
# With no vision backend the stage is a no-op and an image still produces a
# metadata node (it is never written empty).
[pipeline.image_analysis]
# Enable the stage. Default: true.
enabled = true
# Instruction sent to the vision model. Default: transcribe visible text.
# prompt = "Transcribe all text visible in this image, preserving line breaks. If the image contains no readable text, reply with an empty response."
# Maximum images analyzed per message. Default: 4.
# max_attachments = 4
# Minimum recognized-text length (chars) to classify an image as an interface
# rather than a plain photo. Default: 24.
# interface_min_chars = 24
# Note: per-image size is capped by [llm].vision_max_bytes (default 5 MiB).
# ── Syncthing ─────────────────────────────────────────────────────────────────
[syncthing]
enabled = false
api_url = "http://localhost:8384"
api_key = "${SYNCTHING_API_KEY}"
# Syncthing folder ID that contains the org file.
org_folder_id = "org-folder"
# Syncthing folder ID for attachments (may be the same as org_folder_id).
attachments_folder_id = "org-folder"
rescan_on_write = true
# ── Memory (Grafeo graph database for persistent LLM memory) ──────────────────
# Tools: memory_save, memory_recall, memory_link, memory_context
# Also stores user feedback when enabled.
[memory]
enabled = false
# db_path = "" # default: {attachments_dir}/memory.grafeo
# embedding_endpoint = "http://localhost:11434" # Ollama base URL (uses POST /api/embed)
# embedding_model = "nomic-embed-text"
# embedding_dims = 768 # optional: auto-detected from endpoint on startup
# embedding_api_key = ""
# When memory is enabled, relevant memories are always pre-loaded into the LLM
# context before enrichment (no toggle — enabled = true means pre-load is active).
# preload_max_memories = 5 # max recalled memories injected
# preload_graph_hops = 2 # graph traversal depth for related memories
# preload_feedback = true # inject recent low-rated feedback as guidance
# preload_max_feedback = 10
# preload_feedback_max_rating = 2 # 1=bad, 2=ok, 3=good; at or below this rating
# ── Tooling (dedicated sections + per-tool prompt) ───────────────────────────
[tooling.scrape_page]
enabled = true
description = "Fetch and extract readable text from a web page URL"
prompt = "Use this for normal article pages."
backend = "internal"
timeout_secs = 15
retries = 3 # additional attempts on failure (0 = no retry); default 3
[tooling.download_file]
enabled = true
description = "Download a file from a URL and save it as an attachment"
prompt = "Use this when the URL points to a downloadable file."
backend = "internal"
timeout_secs = 15
retries = 3
[tooling.crawl_url]
enabled = true
description = "Crawl a URL and return markdown/html extracted by crawler service"
prompt = "Prefer this for dynamic pages; prefer markdown in tool output and fallback to HTML when markdown is empty."
endpoint = "http://localhost:11235/crawl"
priority = 10
timeout_secs = 30
retries = 2 # crawl4ai may need an extra attempt on cold start
# auth_header = "Authorization: Bearer ${CRAWLER_TOKEN}"
[tooling.web_search]
enabled = false
description = "Search the web via Kagi Search API and return top results"
prompt = "Use this for general web lookups when page URLs are not provided."
endpoint = "https://kagi.com/api/v0/search"
api_token = "${KAGI_API_TOKEN}"
default_limit = 5
max_snippet_chars = 320
timeout_secs = 15
retries = 1
[tooling.duckduckgo_search]
enabled = false
description = "Search the web via DuckDuckGo and return top results"
prompt = "Use this for general web lookups when page URLs are not provided."
endpoint = "https://duckduckgo.com/html/"
default_limit = 5
max_snippet_chars = 320
timeout_secs = 15
retries = 1
[tooling.keenable_search]
enabled = false
description = "Search the web via Keenable Search API and return top results"
prompt = "Use this for general web lookups when page URLs are not provided."
endpoint = "https://api.keenable.ai/v1/search"
# REST API key required (the keyless free tier is CLI/MCP only); get one at keenable.ai/console
api_key = "${KEENABLE_API_KEY}"
default_limit = 5
max_snippet_chars = 320
timeout_secs = 15
retries = 1