Metadata-Version: 2.4
Name: manolo-bot
Version: 0.4.1
Summary: AI-powered Telegram Chat Bot and Library using LLMs
Author-email: Carlos Cesar Caballero Díaz <ccesarcaball.cc@gmail.com>
Requires-Python: >=3.12
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: aiohttp>=3.11.11
Requires-Dist: aiogram>=3.17.0
Requires-Dist: beautifulsoup4>=4.13.3
Requires-Dist: ddgs>=9.6.0
Requires-Dist: deepagents>=0.7
Requires-Dist: envmodel>=0.2
Requires-Dist: google-generativeai>=0.8.4
Requires-Dist: langchain>=0.3.20
Requires-Dist: langchain-community>=0.3.19
Requires-Dist: langchain-core>=0.3.52
Requires-Dist: langchain-google-genai>=2.0.10
Requires-Dist: langchain-mcp-adapters>=0.1.4
Requires-Dist: langchain-ollama>=0.2.3
Requires-Dist: langchain-openai>=0.3.8
Requires-Dist: langgraph>=0.5.4
Requires-Dist: mcp>=1.3.1
Requires-Dist: pillow>=11.1.0
Requires-Dist: python-dotenv>=1.0.1
Requires-Dist: telegramify-markdown[mermaid]>=0.5.1
Requires-Dist: transformers>=4.53.0
Requires-Dist: youtube-transcript-api>=1.1.1
Requires-Dist: langchain-tavily>=0.2.17
Requires-Dist: html2text>=2025.4.15
Requires-Dist: pypdf>=6.12.2
Requires-Dist: python-docx>=1.2.0
Requires-Dist: langchain-text-splitters>=1.1.0
Requires-Dist: docx2txt>=0.9
Requires-Dist: redis>=5.0
Provides-Extra: docs
Requires-Dist: sphinx>=9.1.0; extra == "docs"
Requires-Dist: sphinx-rtd-theme>=3.1.0; extra == "docs"
Requires-Dist: myst-parser>=5.1.0; extra == "docs"
Requires-Dist: sphinx-autodoc-typehints>=3.10.3; extra == "docs"
Dynamic: license-file

# manolo-bot: AI-powered Telegram Chat Bot and Library

[![Documentation Status](https://readthedocs.org/projects/manolo-bot/badge/?version=latest)](https://manolo-bot.readthedocs.io/en/latest/?badge=latest)

`manolo-bot` is an AI-powered Telegram Chat Bot and Library built with Python, leveraging modern LLM frameworks and the
Model Context Protocol (MCP).
It is designed to be both a standalone application and a reusable library for building your own AI-powered bots.

## Documentation

Full documentation is available at [https://manolo-bot.readthedocs.io/](https://manolo-bot.readthedocs.io/)

## Installation

### From PyPI (Recommended)

You can install `manolo-bot` directly from PyPI:

```shell
pip install manolo-bot
```

### From Source

If you want to run the bot from the source code, you need to install the required packages
using [uv](https://docs.astral.sh/uv/):

```shell
uv sync --no-dev
```

## Configuration

You can copy and rename the provided `env.example` to `.env` and edit the file according to your data

You can create a bot on Telegram and get its API token by following
the [official instructions](https://core.telegram.org/bots#how-do-i-create-a-bot).

To use the bot in a group, you have to use the @BotFather bot
to [set the Group Privacy off](https://stackoverflow.com/questions/50204633/allow-bot-to-access-telegram-group-messages/50236522#50236522).
This allows the bot to access all group messages.

#### Required environment variables.

You can use the `GOOGLE_API_KEY`, `OPENAI_API_KEY`, `OPENAI_API_BASE_URL` or `OLLAMA_MODEL` for selecting the required
LLM provider.
The `OPENAI_API_BASE_URL` will look for an OpenAI API like, as the LM Studio API

- <b>Note:</b> When `GOOGLE_API_KEY` option is selected, the default model will be Gemini 2.0 Flash.

`TELEGRAM_BOT_NAME`: Your Telegram bot name

`TELEGRAM_BOT_USERNAME`: Your Telegram bot username

`TELEGRAM_BOT_TOKEN`: Your Telegram bot token

#### Advanced Identity Settings

`BOT_UUID`: A unique identifier for this bot instance (default: `default-bot-uuid`). Used to isolate conversation
history in storage.

`USER_ID`: Your Telegram User ID (default: `0`). Used for internal tracking and metadata.

#### Selecting OpenAI Model.

`OPENAI_API_MODEL`: LLM to use for OpenAI or OpenAI-like API; if not provided, the default model will be used.

#### Selecting Google API Model.

`GOOGLE_API_MODEL`: LLM to use for Google API; if not provided, the default model will be used.

#### Selecting AI Mode

`AI_MODE`: Selects the bot's AI mode (`llm`, `agent`, `deep_agent`). Default is `agent`.

| Mode | Class | Description |
|------|-------|-------------|
| `agent` | `LLMAgent` | LangGraph-based agent with an automatic tool loop. Uses the LLM as a reasoning engine, iterating through multiple steps (like searching the internet and analyzing results) to complete complex tasks. |
| `deep_agent` | `LLMDeepAgent` | Full Deep Agents harness on top of `LLMAgent`. Adds to-do list planning, a virtual filesystem, sub-agent support, skills, and long-term memory for complex, multi-step tasks. |
| `llm` | `LLMBot` | Simple LLM with a manual tool loop. Best for models that don't support tool calling or simpler use cases. |

`AGENT_MODE`: (Deprecated) Enable agent mode (True, False). Use `AI_MODE` instead. When `AI_MODE` is not set,
`AGENT_MODE=True` selects `agent`.

`AGENT_INSTRUCTIONS`: (Optional) Custom instructions to guide the agent's behavior and reasoning when in agent mode.

#### Deep Agent Filesystem Workspace

`DEEP_AGENT_WORKSPACE_PATH`: Directory used as the virtual filesystem root for the `deep_agent` mode
(only used by the `local_fs` backend). Defaults to a system temporary directory
(e.g. `/tmp/manolo_bot/workspace`). This is separate from `DOCUMENT_STORAGE_PATH`, which handles
temporarily uploaded documents.

> **Security note:** the default workspace lives under the system temp directory, which on Linux is
> world-readable. Files the deep-agent writes there (chat working notes, file extracts, data echoed
> back from tool results) can be read by any local user on a shared host. On multi-tenant or shared
> machines set this to a path only the bot process can read, e.g.
> `$HOME/.local/share/manolo_bot/workspace` with mode `0700`.

`DEEP_AGENT_BACKEND`: Filesystem backend type for the `deep_agent` mode (`in_memory` or `local_fs`).
Default is `local_fs`. The `local_fs` backend persists scratch files per chat under
`DEEP_AGENT_WORKSPACE_PATH/bot_uuid/chat_id` and refuses to delete anything outside that workspace.
The `in_memory` backend keeps the virtual filesystem in process memory, scoped per
`(bot_uuid, chat_id)`; state is cleared on `clean_context()` or process restart.

`DEEP_AGENT_SKILLS_PATHS`: Comma-separated list of skill source paths for the `deep_agent` mode; skills are loaded into the agent's system prompt via progressive disclosure.

`DEEP_AGENT_MEMORY_PATH`: Root directory for per-chat long-term memory in the `deep_agent` mode; each chat gets its own independent, seeded `AGENTS.md` file at `DEEP_AGENT_MEMORY_PATH/bot_uuid/chat_id/AGENTS.md` (no cross-chat leakage). Memory files are fully loaded into the agent's system prompt on every turn (keep them concise — they cost tokens on every message, unlike skills). The bot constructs a per-chat `MemoryFilesystemDeepAgentBackend` with a default process-local `InMemoryStore`; `/flushcontext` wipes the chat's memory along with its workspace.

`DEEP_AGENT_MEMORY_ADD_CACHE_CONTROL`: Add an Anthropic prompt-cache breakpoint on the memory block (default `False`; no-op on non-Anthropic models). Fed to `LLMDeepAgent(memory_add_cache_control=...)` at agent construction.

#### RAG (Retrieval-Augmented Generation)

Local document knowledge base, wired into the `agent` and `deep_agent` modes only
(the `llm` mode ignores these settings). When enabled with non-empty `RAG_SOURCES`,
documents are chunked, embedded and indexed at startup, and a `rag_search` retriever
tool is appended to the agent's tools. Failures degrade gracefully (the bot continues
without RAG).

`RAG_ENABLED`: Enable RAG indexing (True, False). Default is False.

`RAG_BACKEND`: RAG backend type (`in_memory`, `local_fs`, `redis`). Default is `in_memory` (opt-in persistence).
- `in_memory`: ephemeral vectors, kept for debugging and tests; re-ingests on every restart.
- `local_fs`: persists the index as JSON under `RAG_STORE_PATH` for simple single-node
  deployments (not horizontally scalable).
- `redis`: shared, persistent index in Redis for multi-instance deployments; needs `REDIS_URL`
  (reused from storage settings, independent of `STORAGE_TYPE`). Vectors are served from
  memory after load, so queries behave the same on every backend.

`RAG_SOURCES`: Comma-separated list of file paths, directories or glob patterns to index.
Supported formats are `.txt`, `.md`, `.pdf` and `.docx` (anything else is attempted
as plain text).
Append `::DESC=<description>` to an entry to describe its per-source `rag_search_<slug>` tool
(e.g. `docs/handbook.md::DESC=Employee handbook`); descriptions must not contain commas.
Tool slugs derive from the filename (stable); the description carries the routing meaning.
Each per-source tool searches only its own files; with more than 8 sources only the global
`rag_search` tool is exposed.

`RAG_STORE_PATH`: Directory for RAG index state. Defaults to a system temporary directory
(e.g. `/tmp/manolo_bot/rag`).

`RAG_TOP_K`: Number of chunks retrieved per query (default: `5`).

`RAG_CHUNK_SIZE` / `RAG_CHUNK_OVERLAP`: Chunking settings (defaults: `1000` / `200`).

`RAG_REINDEX`: When to ingest sources (`auto`, `always`, `never`). Default is `auto`
(only new or changed files are ingested; files removed from the configuration are
pruned from the index automatically; the ephemeral `in_memory` backend still
re-ingests everything on each restart since vectors are not persisted).

`RAG_EMBEDDING_MODEL`: Optional embedding model for the configured provider
(defaults: `models/gemini-embedding-001` for Google, `text-embedding-ada-002` for
OpenAI, `nomic-embed-text` for Ollama). Set this when your endpoint does not host
the default — e.g. an OpenAI-compatible server (LM Studio, vLLM) with its own
embedding model.

Ingest is per-file isolated: one bad file can't kill the index — failures are logged
with a warning and retried on the next run, while good files are indexed normally.
Files yielding no content warn loudly and are retried (never silently kept).
`ingest()` is idempotent: unchanged files are skipped without any embedding call,
changed files are replaced (never duplicated), so re-ingesting is free of charge.
`remove()` is the inverse — drop documents without touching the rest — and
`list_sources()` reports what is currently indexed. Long-lived library agents bake
their tools at init, so re-resolve or recreate the agent to pick up index changes.
`RAG_MAX_FILE_BYTES`: per-file size cap in bytes (default `10485760`, i.e. 10MB;
`0` = unlimited); oversized sources are skipped with a warning. Keep `RAG_CHUNK_SIZE` well below your
provider's per-request token limit; the defaults are safe.

#### Enabling image Generation with Stable Diffusion

`WEBUI_SD_API_URL`: you can define a Stable Diffusion Web UI API URL for image generation. If this option is enabled the
bot will answer image generation requests using Stable Diffusion generated images.

`WEBUI_SD_API_PARAMS`: A JSON string containing Stable Diffusion Web UI API params. If not provided, default parameters
for the SDXL Turbo model will be used.

#### Setting custom bot character instructions

`TELEGRAM_BOT_INSTRUCTIONS_CHARACTER`: You can define a custom character for the bot instructions.
This will override the default bot character. For example:
`You are a software engineer, geek and nerd, user of linux and free software technologies.`

#### Setting extra bot instructions

`TELEGRAM_BOT_INSTRUCTIONS_EXTRA`: You can include extra LLM system instructions using this variable.

#### Setting custom bot instructions

`TELEGRAM_BOT_INSTRUCTIONS`: You can define custom LLM system instructions using this variable.
This will override the default instructions, and the custom bot character instructions.

#### Limiting Bot interaction

`TELEGRAM_ALLOWED_CHATS`: You can use a comma-separated list of allowed chat IDs to limit bot interaction to those
chats.

`ALLOW_PRIVATE_CHATS`: Enable or disable direct bot interaction in private chats (True, False). Default is True.

`ADD_NO_ANSWER`: If `True`, the bot will reply with "NO_ANSWER" if it doesn't understand a message or isn't sure if it
should respond (True, False). Default is False.

#### Enable multimodal capabilities

`IMAGE_MULTIMODAL`: Enable multimodal capabilities for images (True, False). The selected model must support multimodal
capabilities.

`AUDIO_MULTIMODAL`: **(Experimental)** Enable multimodal capabilities for audio/voice messages (True, False). Currently,
this feature only works with the Gemini API.

`DOCUMENT_MULTIMODAL`: Enable document analysis for PDF, DOCX, and TXT files (True, False). Default is False.
When enabled, the bot can extract text from uploaded documents and use it as context.
For large documents, it is recommended to use a model with a large context window (like Gemini) and increase
`CONTEXT_MAX_TOKENS`.
The document text is stored separately from chat history to keep memory clean.

#### Enable group assistant

`ENABLE_GROUP_ASSISTANT`: Enable group assistant for group chats (True, False). The bot will respond to group chats with
a question mark. The default value is False.

#### File size limits and document storage

`MAX_DOCUMENT_SIZE_BYTES`: Maximum size of documents the bot will process (default: 2MB).
`MAX_VOICE_SIZE_BYTES`: Maximum size of voice messages the bot will process (default: 2MB).
`DOCUMENT_STORAGE_PATH`: Directory where extracted document text is stored. Defaults to a system temporary directory.

#### Enable rate limiting

`RATE_LIMITER_REQUESTS_PER_SECOND`: The number of requests per second allowed by the bot.
`RATE_LIMITER_CHECK_EVERY_N_SECONDS`: The number of seconds between rate limit checks.
`RATE_LIMITER_MAX_BUCKET_SIZE`: The maximum bucket size for rate limiting.

#### Set preferred language

`PREFERRED_LANGUAGE`: The preferred language for the bot. (English, Spanish, etc.)

#### Set context max tokens

`CONTEXT_MAX_TOKENS`: The maximum number of tokens allowed for the bot's context.

#### Context summarization

When the chat context exceeds `CONTEXT_MAX_TOKENS`, the bot can summarize the oldest
messages instead of silently dropping them, and include that summary in subsequent prompts.

`ENABLE_CONTEXT_SUMMARIZATION`: Enable incremental LLM summarization of the oldest messages
when the context limit is reached (True, False). Default is True. When disabled (or when
summarization fails), the bot falls back to dropping the oldest messages.

`SUMMARY_MAX_TOKENS`: Maximum number of tokens allowed for the generated summary (default: `512`).

`SUMMARY_KEEP_MESSAGES`: How many of the most recent messages to keep intact when summarizing
(minimum `2`, default: `6`).

#### Agent history policy

`AGENT_HISTORY_POLICY`: Which messages from the agent's tool loop are persisted to chat history
(`final-only`, `full-trace`). Default is `final-only`, which stores only the final answer.
`full-trace` also stores intermediate tool calls and results, at the cost of higher token usage.
Applies to `agent` and `deep_agent` modes.

#### Web Content Retrieval Configuration

`WEB_CONTENT_REQUEST_TIMEOUT_SECONDS`: Timeout in seconds for HTTP requests when retrieving web content. Default is 10
seconds.

#### Simulate typing human behavior

`SIMULATE_TYPING`: Enable simulating human typing behavior. The default is False. This typing simulation will influence
the bot's response time in all chats.

`SIMULATE_TYPING_WPM`: The words per minute for simulating human typing behavior. Default is 100.

`SIMULATE_TYPING_MAX_TIME`: The maximum time in seconds for simulating human typing behavior. Default is 10 seconds (we
usually don't want to wait too long).

#### Tools usage

`USE_TOOLS`: Enable tool usage (True, False). Default is False. When tool usage is enabled, the bot will use the LLM's
tools capabilities. When tool usage is disabled, the bot will use the prompt-based pseudo-tools implementation.

#### Search Configuration

The bot uses DuckDuckGo by default for web searches. You can optionally enable **Tavily Search** for more advanced
search capabilities.

`USE_TAVILY_SEARCH`: Enable Tavily Search (True, False). Default is False.

`TAVILY_SEARCH_KEY`: Your Tavily API key. Required if `USE_TAVILY_SEARCH` is set to True.

#### Storage Configuration

`STORAGE_TYPE`: Sets the storage type for conversation context (memory, redis). Default is memory.

`REDIS_URL`: The Redis URL for storage when `STORAGE_TYPE` is set to redis. Default is `redis://localhost:6379/0`.

### MCP (Model Context Protocol) Support

manolo_bot supports the [Model Context Protocol](https://modelcontextprotocol.io/) for connecting to external tool
servers.

#### Enabling MCP

Set the following environment variables:

`ENABLE_MCP`: Enable MCP support (True, False). Default is False.

`MCP_SERVERS_CONFIG`: MCP server configuration in JSON format.

#### MCP Server Configuration

MCP servers are configured via the `MCP_SERVERS_CONFIG` environment variable, which accepts a JSON object mapping server
names to their configurations.

**stdio transport example:**

```json
{
  "math": {
    "command": "python",
    "args": [
      "/path/to/math_server.py"
    ],
    "transport": "stdio"
  }
}
```

**streamable_http transport example:**

```json
{
  "weather": {
    "url": "http://localhost:8000/mcp/",
    "transport": "streamable_http"
  }
}
```

**Multiple servers:**

```json
{
  "math": {
    "command": "python",
    "args": [
      "/path/to/math_server.py"
    ],
    "transport": "stdio"
  },
  "weather": {
    "url": "http://localhost:8000/mcp/",
    "transport": "streamable_http"
  }
}
```

**Notes:**

- MCP tools are loaded alongside custom tools defined in `ai/tools.py`
- If tool name conflicts occur, MCP tools will override custom tools (a warning is logged)
- The bot will start successfully even if MCP initialization fails (graceful degradation)
- MCP is only loaded when both `ENABLE_MCP=True` and valid `MCP_SERVERS_CONFIG` are provided

#### Logging Level

`LOGGING_LEVEL`: Sets the logging level (DEBUG, INFO, WARNING, ERROR, CRITICAL), defaulting to INFO.

### Available Commands

The bot supports the following commands:

- `/flushcontext` - Clears the conversation context for the current chat. In group chats, only admins can use this
  command. The bot will respond with a confirmation message in the configured language.

### Running the Bot

You can run the bot using the following command:

```shell
uv run manolo-bot
```

## Developers information

Use `uv sync --dev` to install the development dependencies.

### Pre-commit hooks

After installing the development dependencies, to install pre-commit scripts, including ruff checks, you can run
the following command:

```shell
pre-commit install
```

### Running tests

You can run the tests using the following command:

```shell
uv run python -m unittest discover tests
```

## Library Usage

`manolo-bot` can also be used as a library to build your own AI assistants. It provides a clean abstraction for LLM
providers, message storage, and agentic capabilities.

### Using LLMAgent (Recommended)

The `LLMAgent` is the most powerful way to use the library. It allows the bot to use tools and iterate through multiple
steps to solve complex queries.

```python
import asyncio
from manolo_bot.ai.llmagent import LLMAgent
from manolo_bot.ai.llmbot import LLMBuilder
from manolo_bot.ai.config import BotConfig, LLMConfig
from manolo_bot.storage.messages.memory_storage import MemoryMessagesStorage
from manolo_bot.ai.tools import get_tools


async def main():
    # 1. Configure LLM (Google, OpenAI, or Ollama)
    llm_config = LLMConfig(
        google_api_key="your_api_key",
        google_api_model="",
        openai_api_key="",
        openai_api_model="",
        openai_api_base_url="",
        ollama_model="",
    )
    llm = LLMBuilder(llm_config).get_llm()

    # 2. Define Bot identity
    bot_config = BotConfig(
        bot_uuid="my-bot",
        bot_name="Assistant",
        bot_username="assistant_bot",
        bot_token="123456:ABC",
        user_id=0,
    )

    # 3. Setup Storage for a specific conversation
    storage = MemoryMessagesStorage(bot_uuid="my-bot", chat_id=123)
    await storage.refresh_messages()

    # 4. Initialize Agent with tools
    tools = get_tools()
    agent = LLMAgent(
        llm=llm,
        bot_config=bot_config,
        system_instructions="You are a helpful assistant.",
        messages_storage=storage,
        tools=tools
    )

    # 5. Interact
    # The agent can search the web, analyze content, etc.
    response = await agent.answer_message(chat_id=123, message="What is the current price of Bitcoin?")
    print(f"Agent: {response.content}")

    # 6. Persistent changes (if using Redis)
    await storage.commit()


if __name__ == "__main__":
    asyncio.run(main())
```

#### Using RAG with LLMAgent

A local knowledge base can be attached to `LLMAgent` (or `LLMDeepAgent`) via the
`manolo_bot.rag` package. RAG is only wired for agent modes — `LLMBot` ignores it.

```python
import asyncio
from manolo_bot.ai.config import BotConfig, LLMConfig
from manolo_bot.ai.llmagent import LLMAgent
from manolo_bot.ai.llmbot import LLMBuilder
from manolo_bot.rag.embeddings import build_embeddings
from manolo_bot.rag.factory import build_rag_backend
from manolo_bot.rag.prompting import build_rag_instructions
from manolo_bot.rag.sources import RAGSource
from manolo_bot.storage.messages.memory_storage import MemoryMessagesStorage


async def main():
    llm_config = LLMConfig(
        google_api_key="your_api_key",
        google_api_model="",
        openai_api_key="",
        openai_api_model="",
        openai_api_base_url="",
        ollama_model="",
    )
    llm = LLMBuilder(llm_config).get_llm()
    bot_config = BotConfig(
        bot_uuid="my-bot",
        bot_name="Assistant",
        bot_username="assistant_bot",
        bot_token="123456:ABC",
        user_id=0,
    )
    storage = MemoryMessagesStorage(bot_uuid="my-bot", chat_id=123)
    await storage.refresh_messages()

    # Build the RAG backend and index local documents, described with
    # RAGSource records (no ::DESC= strings needed; descriptions may contain commas).
    sources = [
        RAGSource("docs/handbook.md", "Employee handbook, covering policies and benefits"),
        RAGSource("docs/policies/", "Company policies"),
    ]
    backend = build_rag_backend("in_memory", build_embeddings(llm_config), bot_uuid="my-bot")
    await backend.build_or_load()
    await backend.ingest(sources)

    # Pass the backend explicitly; the agent appends the rag_search tool itself.
    # Append build_rag_instructions() to the system prompt so the agent knows when to use it.
    system_instructions = "You are a helpful assistant." + build_rag_instructions(sources)
    agent = LLMAgent(
        llm=llm,
        bot_config=bot_config,
        system_instructions=system_instructions,
        messages_storage=storage,
        rag_backend=backend,
    )
    response = await agent.answer_message(chat_id=123, message="What does the handbook say?")
    print(f"Agent: {response.content}")


if __name__ == "__main__":
    asyncio.run(main())
```

### Using LLMDeepAgent (Deep Agents Harness)

For the most advanced use case, `LLMDeepAgent` extends `LLMAgent` with the full Deep Agents harness: to-do list
planning, a virtual filesystem, and sub-agent support. It is ideal for complex, multi-step tasks that benefit from
planning and persistent scratch space.

```python
import asyncio
from manolo_bot.ai.llmdeepagent import LLMDeepAgent
from manolo_bot.ai.llmbot import LLMBuilder
from manolo_bot.ai.config import BotConfig, LLMConfig
from manolo_bot.storage.messages.memory_storage import MemoryMessagesStorage
from manolo_bot.storage.deep_agent_backends.memory_backend import MemoryDeepAgentBackend


async def main():
    # 1. Configure LLM (Google, OpenAI, or Ollama)
    llm_config = LLMConfig(
        google_api_key="your_api_key",
        google_api_model="",
        openai_api_key="",
        openai_api_model="",
        openai_api_base_url="",
        ollama_model="",
    )
    llm = LLMBuilder(llm_config).get_llm()

    # 2. Define Bot identity
    bot_config = BotConfig(
        bot_uuid="my-bot",
        bot_name="Assistant",
        bot_username="assistant_bot",
        bot_token="123456:ABC",
        user_id=0,
    )

    # 3. Setup Storage for a specific conversation
    storage = MemoryMessagesStorage(bot_uuid="my-bot", chat_id=123)
    await storage.refresh_messages()

    # 4. Setup the deep agent filesystem backend (in-memory or filesystem)
    backend = MemoryDeepAgentBackend(bot_uuid="my-bot", chat_id=123)
    # For a persistent filesystem workspace instead:
    # backend = FilesystemDeepAgentBackend("my-bot", 123, "/path/to/workspace")

    # 5. Initialize the Deep Agent
    agent = LLMDeepAgent(
        llm=llm,
        bot_config=bot_config,
        system_instructions="You are a helpful assistant.",
        messages_storage=storage,
        backend=backend,
    )
    await agent.initialize_async_resources()

    # 6. Interact
    response = await agent.answer_message(chat_id=123, message="Research and summarize the current state of LLM agents.")
    print(f"Agent: {response.content}")


if __name__ == "__main__":
    asyncio.run(main())
```

The deep agent virtual filesystem uses a pluggable backend, similar to how message and document storage work:

- `MemoryDeepAgentBackend`: In-memory virtual filesystem. State is scoped per `(bot_uuid, chat_id)` and shared across `LLMDeepAgent` instances in the same process; cleared on `clean_context()` or process restart.
- `FilesystemDeepAgentBackend`: Persistent virtual filesystem stored under `workspace_path/bot_uuid/chat_id`.

If no `backend` is provided, `LLMDeepAgent` falls back to an in-memory `StateBackend`.

### Custom Tools

You can provide your own tools when initializing `LLMAgent`, `LLMDeepAgent`, or `LLMBot`.
Note that `tools=` **replaces** the default tool set — passing a list does NOT append
to the defaults. MCP tools are still merged on top afterwards, and an MCP tool wins
on name conflicts.

```python
from langchain_core.tools import tool
from manolo_bot.ai.llmagent import LLMAgent


@tool
def my_tool(query: str) -> str:
    """Description of my tool."""
    return "Result"


# Full replacement: the agent only sees my_tool (+ MCP tools).
agent = LLMAgent(..., tools=[my_tool])
```

Recipes for combining custom tools with the defaults (defaults + extra is NOT automatic —
compose explicitly):

```python
from manolo_bot.ai.tools import get_tools

# Defaults + extra.
agent = LLMAgent(..., tools=get_tools(bot_config) + [my_tool])

# Drop one default: filter by name.
tools = [t for t in get_tools(bot_config) if t.name != "multiply"]
agent = LLMAgent(..., tools=tools)
```

### Using LLMBot (Simple)

For simpler use cases or when using models that don't support tool calling, you can use the basic `LLMBot`. It provides
a direct chat interface without iterative reasoning.

```python
from manolo_bot.ai.llmbot import LLMBot

# ... (same setup as above)

bot = LLMBot(
    llm=llm,
    bot_config=bot_config,
    system_instructions="You are a simple assistant.",
    messages_storage=storage
)

response = await bot.answer_message(chat_id=123, message="Hello!")
```

### Attachment downloads with header authentication

Some platforms require HTTP header authentication to fetch attachment file URLs
(images, audio, documents). `BotConfig` exposes an optional field for this:

- `attachment_headers: dict` — headers sent on every attachment download.

It defaults to empty, preserving the previous behavior (no auth headers sent).

```python
from manolo_bot.ai.config import BotConfig

bot_config = BotConfig(
    bot_uuid="my-bot",
    bot_name="Assistant",
    bot_username="assistant_bot",
    bot_token="123456:ABC",
    user_id=0,
    attachment_headers={"Authorization": "Bearer <token>"},
)
```

`LLMBot._download_headers()` returns a copy of this mapping and is used by
`LLMBot._download_file` (and by `LLMAgent.answer_image_message`'s inline
download); `LLMDeepAgent` inherits the behavior. Subclasses can override the
hook for custom schemes:

```python
from manolo_bot.ai.llmbot import LLMBot


class CustomAuthBot(LLMBot):
    def _download_headers(self) -> dict:
        return {"Authorization": "Custom scheme"}
```

For more advanced usage and full API details, please refer to
the [Full Documentation](https://manolo-bot.readthedocs.io/).

## Contributing

If you'd like to contribute to this project, feel free to submit a pull request. We're always open to new ideas or
improvements to the code.

## License

This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
