Telegram bot with functions tools.
- In comparison with popstas/telegram-chatgpt-bot
- Single answer to several forwarded messages to bot
- Bot can use tools to get answer
- Better fallback answer when telegram markdown is wrong
- Agent-like pipelines: bot can use several tools to get answer
- MCP support: use external tools and services to get answer
- Langfuse support: track chat history and tool usage
- Use agents as tools
- Agents can be triggered by name via HTTP or MQTT
- Incoming audio transcription using Whisper service
- Prompt placeholders:
{url:...}and{tool:...}for dynamic content - Photo messages and image documents are processed with OCR to extract text
- Dedicated log files for HTTP and MQTT activity
- Desktop launcher with tray controls and live log viewer (docs/desktop-launcher.md)
- Docker healthcheck endpoint for container monitoring
- GET
/agent/:agentreturns agent status - Per-chat
http_tokenoverrides the global HTTP token - Mark known users in history using
markOurUsers - Automatic history cleanup with
forgetTimeout - Abort previous answer if user sends a new message
- Optional delay between split messages
- Vector memory with
memory_searchandmemory_deletetools (confirmation required for delete, optional automatic search) - Dynamic reply buttons returned from LLM responses (enable with
chatParams.responseButtons) - Enforce structured outputs by setting
response_formatin chat configuration
The project ships with a lightweight Electron wrapper so you can monitor the bot from the system tray on macOS, Windows, or Linux.
- Start the desktop shell:
npm run desktop. The command launches Electron with the existing Node runtime so the same configuration is reused. - Tray controls: start or stop the bot, show or hide the window, and open the local
data/directory where log files live. - Live log viewer: the window streams new entries from the running bot in real time, mirroring what hits
data/messages.logwithout replaying historical lines. You can pause the stream, clear the list, switch between message and Desktop channels, and toggle automatic scrolling. - Desktop log file: Electron lifecycle activity is also persisted to
data/electron.logso you can review startup issues even if the renderer fails to load. - Graceful shutdown: quitting the app stops running bots and MQTT connections before exiting.
For distribution, run npm run build:dist to bundle the Electron entry points and generate a Windows .exe installer in the
dist/ directory. If you only need the JavaScript bundles (for custom packaging workflows), use npm run build:electron.
Vector memory features rely on the native better-sqlite3 bindings. The CLI entry point bundles a binary compatible with your local Node.js runtime, but Electron ships with its own Node version. To avoid repeated ERR_DLOPEN_FAILED traces the desktop launcher skips loading better-sqlite3 until you explicitly opt back in. When you are ready to enable vector memory inside Electron:
- Rebuild the module for Electron:
npx electron-rebuild --only better-sqlite3
- Launch the desktop shell with the opt-in flag so the runtime will load the rebuilt binding:
BETTER_SQLITE3_ALLOW_ELECTRON=1 npm run desktop
Until the rebuild succeeds (or the opt-in flag is omitted) the bot continues to run, vector memory tools stay disabled, and a warning is logged to both the desktop console and data/electron.log.
- Receive question
- Use tool to get answer, send tool usage to user
- Read tool answer, answer user
brainstorm- Useful tool for brainstorming and planning taskchange_chat_settings- Change chat settings in config.ymlchange_access_settings- Add/remove users to admin and private user lists in config.ymlget_next_offday- count 4-days cycle: day, night, sleep, offdayforget- Forget chat historyjavascript_interpreter- exec JavaScript codeobsidian_read- return the contents of an Obsidian file specified byfile_path, list of files pass to the promptobsidian_write- append text to a markdown file specified byout_filepowershell- exec PowerShell command, single server from configread_google_sheet- read Google Sheetread_knowledge_google_sheet- questions and answers from Google Sheetread_knowledge_json- questions and answers from json file/urlmemory_search- search messages saved with vector memorymemory_delete- delete messages from vector memory after confirmationssh_command- exec ssh shell command, single server from configweb_search_preview- use OpenAI internal web search tool (only for Responses API)image_generation- generate images using OpenAI image model (only for Responses API)- ... and thousands of tools from MCP
Empty config.yml should be generated. Fill it with your data:
- agent_name (optional, autogenerated from bot_name or chat name)
- bot_name (deprecated)
- auth.token
- auth.chatgpt_api_key
- stt.whisperBaseUrl
- http.http_token (per-chat tokens use chat.http_token)
- useChatsDir (optional, default
false) – when enabled, chat configs are loaded from separate files insidechatsDirinstead of thechatssection ofconfig.yml. - chatsDir (optional, default
data/chats) – directory where per-chat YAML files are stored whenuseChatsDiris turned on. Private chats are saved asprivate_<username>.yml.
When useChatsDir is enabled, the bot watches both config.yml and each chat file for changes and
automatically reloads updated settings. New chat files placed in the directory are also watched
automatically. Configuration files are written only when their content changes to avoid unnecessary
reloads.
You can convert your configuration between a single config.yml and per-chat files:
npm run config:convert split # save chats to data/chats and enable useChatsDir
npm run config:convert merge # read chats from data/chats and merge into config.ymlYou can run multiple Telegram bots from a single instance using the bot_token field in each chat config.
- Run several bots with different tokens from the same codebase (e.g., main bot and test bot, or bots for different groups).
- Per-chat bot tokens: assign a unique bot token to a specific chat, while others use the global token.
- The bot will launch an instance for every unique
bot_tokenfound inconfig.chatsand for the globalauth.bot_token. - If a chat does not specify its own
bot_token, it will use the globalauth.bot_token. - Only one instance per unique token is launched (deduplicated automatically).
auth:
bot_token: "123456:main-token"
chatgpt_api_key: "sk-..."
chats:
- name: "Main Chat"
id: 123456789
# uses global auth.bot_token
- name: "Secondary Bot Chat"
id: 987654321
bot_token: "987654:secondary-token"
agent_name: "secondary_bot"- If you launch two bots with the same token, Telegram will throw a 409 Conflict error. The bot automatically avoids this by deduplication.
- You must set
agent_name(autogenerated if missing).bot_nameis deprecated. - You can set
privateUsersin a chat config for extended access control.
Prompt placeholders allow you to include dynamic content in your prompts by fetching data from external sources or executing tools.
Fetches content from a URL and inserts it into the prompt.
- Syntax:
{url:https://example.com} - Caching: Results are cached for 1 hour (3600 seconds) by default, change with
placeholderCacheTime - Example:
Check this article: {url:https://example.com/latest-news}
# Example usage in a prompt
systemMessage: |
Here's the latest news:
{url:https://example.com/breaking-news}
Summarize the key points above
chatParams:
placeholderCacheTime: 60Executes a tool and inserts its output into the prompt.
- Syntax:
{tool:toolName(arguments)}- Arguments can be a JSON object or a string
- If no arguments, use empty parentheses:
{tool:getTime()}
- Caching: Results are not cached by default (set
placeholderCacheTimeto enable) - Example:
Current weather: {tool:getWeather({"city": "New York"})}
# Example usage in a prompt
systemMessage: |
Current weather:
{tool:getWeather({"city": "London"})}
Based on this weather, what should I wear today?Responses API is a new feature of OpenAI that allows you to use tools and web search to get answers to user questions.
To use it, set useResponsesApi to true in the chat config.
You can tune reasoning effort and verbosity for the Responses API via the optional responsesParams section of the chat
configuration. Set responsesParams.reasoning.effort to control the model's planning depth and responsesParams.text.verbosity
to control how long the final answer should be:
responsesParams:
reasoning:
effort: minimal # or low | medium | high
text:
verbosity: low # or medium | highThese values are passed to client.responses.create calls so you can balance speed, reasoning depth and verbosity depending on
the chat requirements.
Work only with OpenAI models.
When enabled, the bot can use the web_search_preview tool to get web search results.
It can also generate images using the image_generation tool.
Learn how to stream model responses from the OpenAI API using server-sent events.
By default, when you make a request to the OpenAI API, we generate the model's entire output before sending it back in a single HTTP response. When generating long outputs, waiting for a response can take time. Streaming responses lets you start printing or processing the beginning of the model's output while it continues generating the full response.
To start streaming responses, set stream=True in your request to the Responses endpoint:
import { OpenAI } from "openai";
const client = new OpenAI();
const stream = await client.responses.create({
model: "gpt-4.1",
input: [
{
role: "user",
content: "Say 'double bubble bath' ten times fast.",
},
],
stream: true,
});
for await (const event of stream) {
console.log(event);
}The Responses API uses semantic events for streaming. Each event is typed with a predefined schema, so you can listen for events you care about.
For a full list of event types, see the API reference for streaming. Here are a few examples:
type StreamingEvent =
| ResponseCreatedEvent
| ResponseInProgressEvent
| ResponseFailedEvent
| ResponseCompletedEvent
| ResponseOutputItemAdded
| ResponseOutputItemDone
| ResponseContentPartAdded
| ResponseContentPartDone
| ResponseOutputTextDelta
| ResponseOutputTextAnnotationAdded
| ResponseTextDone
| ResponseRefusalDelta
| ResponseRefusalDone
| ResponseFunctionCallArgumentsDelta
| ResponseFunctionCallArgumentsDone
| ResponseFileSearchCallInProgress
| ResponseFileSearchCallSearching
| ResponseFileSearchCallCompleted
| ResponseCodeInterpreterInProgress
| ResponseCodeInterpreterCallCodeDelta
| ResponseCodeInterpreterCallCodeDone
| ResponseCodeInterpreterCallIntepreting
| ResponseCodeInterpreterCallCompleted
| ErrorIf you're using our SDK, every event is a typed instance. You can also identity individual events using the type property of the event.
Some key lifecycle events are emitted only once, while others are emitted multiple times as the response is generated. Common events to listen for when streaming text are:
- `response.created`
- `response.output_text.delta`
- `response.completed`
- `error`
For a full list of events you can listen for, see the API reference for streaming.
For more advanced use cases, like streaming tool calls, check out the following dedicated guides:
Note that streaming the model's output in a production application makes it more difficult to moderate the content of the completions, as partial completions may be more difficult to evaluate. This may have implications for approved usage.
You can use one bot as a tool (agent) inside another bot. This allows you to compose complex workflows, delegate tasks, or chain multiple bots together.
- In your chat config, add a tool entry with
agent_name,name, anddescription. - The main bot will expose this agent as a tool function. When called, it will internally send the request to the specified bot, as if a user messaged it.
- The agent bot processes the request and returns the result to the main bot, which includes it in the final answer.
chats:
- name: Main Bot
id: 10001
tools:
- agent_name: tool_bot
name: add_task
description: "Adds a task to the task list."
- name: Bot as tool
id: 10002
agent_name: tool_bot
bot_token: "987654:tool-token"
systemMessage: "You accept a task text and return a structured task."- The main bot exposes the
add_tasktool. - When the tool is called (e.g., by function-calling or via a button), the main bot sends the input text to
tool_bot. - The result (e.g., task created or error) is sent back and included in the main bot’s response.
- The agent bot must be configured in
config.ymlwith a uniqueagent_name. - The tool interface expects an
inputargument (the text to send to the agent). - You can chain multiple agents and tools for advanced workflows.
You can run any configured agent outside Telegram.
CLI isn't working at this time, use scripts that calling curl.
npm run agent <agent_name> "your text"
POST /agent/:agentName with JSON { "text": "hi", "webhook": "<url>" }.
Use header Authorization: Bearer <http_token>.
GET /agent/:agentName returns current agent status.
You can set http_token per chat in config.yml; it overrides the global token.
POST /agent/:agentName/tool/:toolName with JSON { "args": { ... } }.
Authorization is the same as for /agent.
Publish text to <base><agent_name>.
Progress messages go to <base><agent_name>_progress and the final answer to <base><agent_name>_answer.
Add to config.yml local model, use ollama url and model name, then define local_model in the chat settings:
local_models:
- name: qwen3:4b
model: qwen3:4b
url: http://192.168.1.1:11434
chats:
- id: 123
name: Chat with qwen
local_model: qwen3:4b
/info should return actual using model.
MCP (Model Context Protocol) provides external tools and services to the bot. MCP servers are defined in the config.mcpServers file, which lists available MCP endpoints used by all chats.
- The format of
config.mcpServersmatches the structure used in Claude Desktop. - It is a list of MCP server configurations, each specifying the server address and connection details.
- Example:
{ "mcpServers": { "memory": { "command": "npx", "args": [ "-y", "@modelcontextprotocol/server-memory" ] } }
- All MCP servers listed in
config.mcpServersare shared between all chats. - There is currently no per-chat isolation of MCP servers; every chat can access all configured MCP tools.
Evaluators are special agents that assess the quality and completeness of the bot's responses. They help ensure that the bot provides useful and complete answers to user queries.
- After generating a response, the bot can optionally send both the original user request and the generated response to an evaluator.
- The evaluator rates the response on a scale from 0 to 5 based on completeness and usefulness.
- The evaluator provides a justification for the score and determines if the response is considered complete.
- If the response is incomplete (score < 4), the evaluator can suggest improvements or additional information to include.
Evaluators return a JSON object with the following structure:
{
"score": 4,
"justification": "The response addresses the main points but could provide more specific examples.",
"is_complete": true
}To enable evaluators for a chat, add an evaluators array to your chat settings. Each evaluator is configured with the following properties:
chats:
- name: "Chat with Evaluators"
id: 123456789
evaluators:
- agent_name: "url-checker" # Name of the agent to use for evaluation
threshold: 4 # Optional: minimum score to consider the response complete (default: 4)
maxIterations: 3 # Optional: maximum number of evaluation iterations (default: 3)
- name: "URL evaluator agent"
agent_name: "url-checker"
systemMessage: "Check for url in answer."
completionParams:
model: "gpt-4.1-nano"- The
agent_namespecifies which agent to use for the evaluation. This agent should be defined in your configuration. - The
threshold(default: 4) sets the minimum score required for a response to be considered complete. - The
maxIterations(default: 3) limits how many times the evaluator will attempt to improve a response.
To disable evaluators for a specific chat, simply omit the evaluators array from the chat configuration.
- Each chat's configuration should specify a
toolslist. - The
toolslist should include the names of tools (from MCP) that are available to that chat.
Other useful chat parameters include:
markOurUsers– suffix to append to known users in historyforgetTimeout– auto-forget history after N seconds- Example chat config snippet:
- name: Memory MCP agent id: -123123 tools: - create_entities - create_relations - add_observations - delete_entities - delete_observations - delete_relations - read_graph - search_nodes - open_nodes
Enable semantic memory with chatParams.vector_memory. Messages starting with запомни (any punctuation immediately after the keyword is ignored) are embedded and stored in a SQLite database using sqlite-vec. Use the memory_search tool to find related snippets or memory_delete to remove them after a preview and confirmation. Set toolParams.vector_memory.alwaysSearch to automatically search memory before answering. Adjust toolParams.vector_memory.deleteMaxDistance (default 1.1) to limit how far results can be for deletions.
To prevent duplicates, each new entry is compared against existing memories; if the text is already present or the closest embedding is nearly identical, the save is skipped.
chatParams:
vector_memory: true
toolParams:
vector_memory:
dbPath: data/memory/default.sqlite
dimension: 1536
alwaysSearch: false
deleteMaxDistance: 1.1By default, databases are stored under data/memory/:
-
private chats:
data/memory/private/{username}.sqlite -
chats for specific bots:
data/memory/bots/{bot_name}.sqlite -
group chats:
data/memory/groups/{chat_name_or_id_safe}.sqlite -
The available tool names are fetched from the MCP servers listed in
config.mcpServers.
Refer to the MCP and Claude Desktop documentation for further details on server configuration and tool discovery.
Enable the bot to return temporary reply buttons from the model's response. When chatParams.responseButtons is true, the model must return JSON with message and buttons fields (use an empty array if no buttons), which are shown to the user as a keyboard.
This feature works both with the OpenAI Responses API and with streaming mode; the JSON envelope is hidden from users.
chatParams:
responseButtons: trueEach button should contain name and prompt. When a user clicks a button, its prompt is sent as their next message.
Set response_format in a chat configuration to force the model to reply in a specific structure.
response_format:
type: json_objectYou can also provide a JSON Schema:
response_format:
type: json_schema
json_schema:
name: response
schema:
type: object
properties:
message: { type: string }
required: [message]This bot supports Langfuse for tracing, analytics, and observability of chat and tool usage.
Add your Langfuse credentials to your config (e.g., config.yml):
langfuse:
secretKey: <your_secret_key>
publicKey: <your_public_key>
baseUrl: https://cloud.langfuse.comTo run the tests, use the following command:
npm testThis will execute all unit and integration tests in the tests directory using the jest framework.
The project uses a TypeScript configuration optimized for fast type checking:
- NodeNext modules –
moduleandmoduleResolutionare set toNodeNext. All relative imports therefore require explicit file extensions (e.g.import { x } from "./file.ts"). - allowImportingTsExtensions – enables importing
.tsfiles directly during development. - incremental and assumeChangesOnlyAffectDirectDependencies – cache build info in
node_modules/.cache/tsconfig.tsbuildinfoand speed up subsequent runs oftsc --noEmit. - skipLibCheck – skips type checking of declaration files.
Run npm run typecheck to perform a fast type-only build using these settings.
Run npm run typecheck:native to experiment with the TypeScript Native preview (tsgo) compiler.
Helper to ask a user for confirmation with inline Yes/No buttons.
import { telegramConfirm } from "./telegram/confirm";
await telegramConfirm({
chatId,
msg: message,
chatConfig,
text: "Are you sure?",
onConfirm: async () => {
/* confirmed */
},
onCancel: async () => {
/* canceled */
},
});