You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
-`openai_compat.go` — shared OpenAI-compatible implementation using raw `net/http` (no go-openai). `buildMessages(messages, systemPrompt, vision bool)` serialises messages with full control: assistant messages with `tool_calls` and empty content use `"content": null` (not omitted) to satisfy all provider APIs. `image_url` parts are replaced with `[image]` for non-vision providers. Defines `APIError{StatusCode, Message}` for fallback routing.
49
-
-`router.go` — thread-safe routing config (`mu` protects both `override` and `cfg`). Priority: multimodal → override → classifier → reasoner → primary → fallback on 5xx/429/network. `SetRole(role, model)` + `SetClassifierMinLen(n)` allow runtime changes; `saveOverrides()` persists to `persistPath` (JSON) on every change; `LoadPersistedOverrides()` applies saved values on startup. Classifier has a 5 s timeout.
48
+
-`openai_compat.go` — shared OpenAI-compatible implementation using raw `net/http` (no go-openai). `buildMessages(messages, systemPrompt, vision bool)` serialises messages with full control: assistant messages with `tool_calls` and empty content use `"content": null` (not omitted) to satisfy all provider APIs. `image_url` parts are replaced with `[image]` for non-vision providers; `input_audio` parts likewise. Defines `APIError{StatusCode, Message}` for fallback routing.
49
+
-`ollama.go` — native Ollama provider using `/api/chat` protocol (not OpenAI-compat). Supports Ollama Cloud (`https://ollama.com`) and local instances. Handles tool calling (Ollama returns `arguments` as object → serialised to JSON string; generates `call_N` IDs since Ollama omits them). Multimodal: base64 images sent via `images[]` field (strips data URI prefix). `stream: false` for synchronous responses.
50
+
-`router.go` — thread-safe routing config (`mu` protects both `override` and `cfg`). Priority: multimodal → override → classifier → reasoner → primary → fallback on 5xx/429/network. `SetRole(role, model)` + `SetClassifierMinLen(n)` allow runtime changes; `saveOverrides()` persists to `persistPath` (JSON) on every change; `LoadPersistedOverrides()` applies saved values on startup. Classifier has 5 s timeout; input truncated to 500 chars; logs routing decisions at Info when routed to reasoner.
50
51
- All providers are optional in `main.go` — any configured model can be `routing.default`. The bot exits with a clear error if the default provider is missing.
51
52
52
53
**`internal/store`** — conversation history.
@@ -65,43 +66,49 @@ flowchart LR
65
66
- Security: `validateServerURL` blocks loopback/link-local addresses and non-http(s) schemes; tool names validated by regex + 128-char limit; descriptions truncated to 4 KB; args size capped at 1 MB; responses capped at 10 MB via `io.LimitReader`; tool results truncated to 100 KB
66
67
-**Vector tool filtering**: `EnableEmbeddings(cfg, topK)` + `EmbedTools(ctx)` at startup; `LLMToolsForQuery(ctx, query)` returns top-K most relevant tools per request via cosine similarity
67
68
-**`EmbedText(ctx, text) ([]float32, error)`** — public method; used by agent for message embedding (shared embedding layer with tool filtering)
69
+
-**`Close()`** — releases HTTP connection pools for all MCP servers; called on shutdown
-`doEmbedRequest` — shared HTTP helper with `io.LimitReader(10 MB)`
73
+
-`doEmbedRequest` — shared HTTP helper with `io.LimitReader(10 MB)` and reusable `embedHTTPClient` (package-level singleton)
72
74
-`cosineSimilarity` — used for tool filtering; `cosineSimilarityF32` (same logic) in `store/sqlite.go` for history
73
75
74
76
**`internal/agent`** — agentic loop.
75
77
-`Process(ctx, chatID, llm.Message, onToolCall)`:
76
78
1. Calls `storeUserMessage` — embeds user message via `mcp.EmbedText` and stores with embedding if `SemanticStore` available; falls back to plain `AddMessage`
77
-
2. Auto-compacts if needed
79
+
2. Auto-compacts if needed (with 2 min timeout)
78
80
3. Calls `buildCrossSessionContext` — searches past sessions, formats snippets for system prompt (top 5, cosine > 0.75, 3000 char budget; 200/300 chars per user/bot snippet)
4. Agentic loop: calls `getHistory` → `Router.Chat` → handles tool calls (max 5 iterations); tool results > 2000 chars are auto-summarised via LLM before storing in history
82
+
-`TranscribeAudio(ctx, audioData, format)` — sends audio to multimodal provider with transcription prompt; returns text. Used by handler for voice messages.
-`getHistory(chatID, queryEmb)` — uses `SemanticStore.GetSemanticHistory(chatID, emb, 10, 20)` when embedding available; falls back to `GetHistory`
81
85
-`GetStats(chatID) (store.ChatStats, bool)` — type-asserts store to `CompactableStore`
82
-
-`cache.go` — `ResponseCache`: in-memory LLM response cache keyed by query embedding. `Get(chatID, emb)` returns hit if cosine ≥ 0.97 and not expired. `Set(chatID, emb, response)` stores entry; evicts expired on write, then oldest if at capacity. TTL=1h, maxSize=200. Checked before LLM call; only pure direct responses (first iteration, no tool calls) are cached.
86
+
-`cache.go` — `ResponseCache`: in-memory LLM response cache keyed by query embedding. `Get(chatID, emb)` returns hit if cosine ≥ 0.92 and not expired. `Set(chatID, emb, response)` stores entry; evicts expired on write, then oldest if at capacity. TTL=4h, maxSize=200. Checked before LLM call; only pure direct responses (first iteration, no tool calls) are cached.
87
+
-`websearch.go` — Ollama web search built-in tool. `callWebSearch(ctx, cfg, argsJSON)` calls `/api/web_search` with Bearer auth. Returns formatted results (title, URL, content snippet). Registered as `web_search` tool visible to any LLM. Agent dispatches via `callTool()` which checks built-in tools before forwarding to MCP.
83
88
-`compact.go` — token-based threshold: 16 000 estimated tokens. Fast pre-check at 32 000 chars. `EstimateTokens`: `len(Content)/4` + text parts `/4` + images `×1000`. **Semantic compaction**: `GetAllActive` now populates `MessageRow.Embedding`; if embeddings are present, `clusterByEmbedding` groups turns into topic clusters (greedy cosine, threshold 0.65) and each cluster is summarised separately — results joined with `---`. Falls back to single-pass when no embeddings or only one cluster.
84
89
85
90
**`internal/telegram`** — Telegram Bot API handler.
86
91
-`markdown.go` — Markdown → Telegram HTML converter. No external deps.
87
92
-`handler.go`:
88
-
- 2 s debounce batch merges text, photos (max 5), forwarded messages into one `llm.Message`
93
+
- 2 s debounce batch merges text, photos (max 5), voice messages, forwarded messages into one `llm.Message`
94
+
-**Voice transcription**: `transcribeVoice()` downloads OGG from Telegram, calls `agent.TranscribeAudio()` (30 s timeout), result merged as text into the batch
89
95
-**Race condition fix**: `version` counter on each batch; timer bails if version changed
90
96
-**Reply chain**: `buildReplyQuote()` prepends `[Replying to: "..."]` (max 300 chars)
-**Graceful shutdown**: `Drain()` atomically swaps batches map, flushes synchronously (30 s timeout in main.go)
98
+
-**Concurrency limiter**: semaphore limits `handleUpdate` goroutines to 10 concurrent
92
99
-**Smart forward buffer**: forward-only batch → `bufferForwards()` embeds each entry via `agent.EmbedText` and stores `[]forwardEntry{text, emb}` with 5 min TTL. On follow-up question: question is embedded, `selectForwards()` scores buffered entries by cosine similarity and keeps those ≥ 0.25 (min 2 always included). Falls back to all entries when embeddings unavailable or ≤ 3 entries buffered.
93
100
-`/routing` — inline keyboard for live routing changes; callback `rt:set:<role>:<model>` calls `agent.SetRoutingRole`; logs `routing change requested/applied`
|`config/config.yaml`| Models, routing, tool_filter, web_search — `${ENV_VAR}` substitution. All models require `base_url`; `embedding` is exception (no `base_url`/`max_tokens`). `Validate()` checks required fields and numeric ranges on load.|
105
112
|`config/routing.json`| Runtime routing overrides from `/routing` UI — auto-created, applied over `config.yaml` at startup. **Applied before compacter init**, so effective primary is always correct. |
106
113
|`config/mcp.json`| MCP servers in Claude Desktop format |
107
114
|`config/system_prompt.md`| System prompt injected on every LLM request |
`llm.Message.Parts []ContentPart` supports `"text"`, `"image_url"`. Audio (`"input_audio"`) is defined in types but not wired in the handler.
181
+
`llm.Message.Parts []ContentPart` supports `"text"`, `"image_url"`, `"input_audio"`. Audio is used for voice message transcription (Telegram OGG → Gemini `input_audio`). Non-vision providers replace `image_url` with `[image]` and `input_audio` with `[audio]` text placeholders.
Copy file name to clipboardExpand all lines: README.md
+26-5Lines changed: 26 additions & 5 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -5,6 +5,9 @@ A lightweight Telegram bot that acts as a personal AI assistant. Written in Go
5
5
## Features
6
6
7
7
-**Multi-model routing** — any configured model can be primary; automatic fallback on errors or rate limits; dedicated reasoner for complex tasks; vision model for images; classifier-based routing to reasoner
8
+
-**Ollama Cloud support** — native `/api/chat` provider for Ollama Cloud and local Ollama instances; tool calling, multimodal, Bearer auth
9
+
-**Voice messages** — send a voice message in Telegram and it's automatically transcribed via the multimodal model (Gemini), then processed as text through the normal pipeline
10
+
-**Web search** — built-in Ollama web search tool; any LLM model can search the web for real-time information
8
11
-**Semantic conversation memory** — user messages are embedded and stored; within a session, relevant past turns are retrieved by cosine similarity instead of just "last N messages"
9
12
-**Cross-session memory** — past conversations are searched across all sessions; relevant snippets are automatically injected into the system prompt so the bot remembers what you discussed weeks ago
10
13
-**Image support** — send a photo (with or without caption) and it's routed automatically to the vision model
@@ -15,7 +18,7 @@ A lightweight Telegram bot that acts as a personal AI assistant. Written in Go
15
18
-**Configurable embeddings** — shared embedding layer used for both tool filtering and conversation memory; supports Gemini (default), HuggingFace TEI, or any OpenAI-compatible endpoint
16
19
-**Persistent memory** — SQLite-backed conversation history with automatic session management
17
20
-**Token-based compaction** — auto-summarises old history when estimated token count exceeds threshold; images count as 1000 tokens each
18
-
-**Embedding response cache** — identical or near-identical queries (cosine ≥ 0.97) return a cached response without calling the LLM; per-chat, 1-hour TTL, 200-entry capacity
21
+
-**Smart token usage** — classifier input truncated to 500 chars; large tool results (>2 KB) auto-summarised before entering history; response cache with cosine ≥ 0.92 threshold and 4-hour TTL
19
22
-**Rich formatting** — Markdown converted to Telegram HTML; responses ≥ 4096 chars sent as `response.md`
20
23
-**Access control** — allowlist by chat ID + owner-only enforcement
21
24
-**Date/time awareness** — current date and time injected into every request; timezone set via `TZ` env var
@@ -25,7 +28,7 @@ A lightweight Telegram bot that acts as a personal AI assistant. Written in Go
25
28
26
29
- Go 1.24+ (or Docker)
27
30
-[Telegram Bot Token](https://t.me/BotFather)
28
-
- At least one LLM API key (DeepSeek, Gemini, or Qwen)
31
+
- At least one LLM API key (DeepSeek, Gemini, Qwen, or Ollama Cloud)
The classifier (`qwen-flash` by default) is a lightweight call with no history and no tools that returns `yes`/`no`. It only runs for messages longer than `classifier_min_length` characters (default: 100). Set `classifier_min_length: 0` to disable.
259
+
The classifier (`qwen-flash` by default) is a lightweight call with no history and no tools that returns `yes`/`no`. It only runs for messages longer than `classifier_min_length` characters (default: 100). Input is truncated to 500 chars to save tokens. Set `classifier_min_length: 0` to disable.
245
260
246
261
All routing roles can be changed live via `/routing` — an inline keyboard menu. **Changes persist across restarts** in `config/routing.json`. On startup, the bot notifies the owner via Telegram if any routing role references an unavailable model.
0 commit comments