Telegram bot with functions tools.
- In comparsion 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
- Docker healthcheck endpoint for container monitoring
- GET
/agent/:agent
returns agent status - Per-chat
http_token
overrides 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_search
andmemory_delete
tools (confirmation required for delete, optional automatic search) - Dynamic reply buttons returned from LLM responses (enable with
chatParams.responseButtons
) - Enforce structured outputs by setting
response_format
in chat configuration
- 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_file
powershell
- 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 insidechatsDir
instead of thechats
section ofconfig.yml
. - chatsDir (optional, default
data/chats
) – directory where per-chat YAML files are stored whenuseChatsDir
is 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.yml
You 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_token
found inconfig.chats
and 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_name
is deprecated. - You can set
privateUsers
in 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: 60
Executes 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
placeholderCacheTime
to 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.
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
| Error
If 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_task
tool. - 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.yml
with a uniqueagent_name
. - The tool interface expects an
input
argument (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.mcpServers
matches 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.mcpServers
are 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_name
specifies 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
tools
list. - The
tools
list 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.1
By 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: true
Each 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_object
You 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.com
To run the tests, use the following command:
npm test
This 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 –
module
andmoduleResolution
are set toNodeNext
. All relative imports therefore require explicit file extensions (e.g.import { x } from "./file.ts"
). - allowImportingTsExtensions – enables importing
.ts
files directly during development. - incremental and assumeChangesOnlyAffectDirectDependencies – cache build info in
node_modules/.cache/tsconfig.tsbuildinfo
and 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 */
},
});