LLM Client

Backend-agnostic LLM client using instructor for structured extraction.

folderbot.llm_client.trim_history_to_budget(history, max_chars)[source]

Trim history to fit within a character budget.

Drops oldest messages first, in pairs (user+assistant) to avoid orphaned messages. Always keeps at least the most recent pair.

Return type:

list[Message]

Parameters:
folderbot.llm_client.build_topic_history(history, max_chars)[source]

Build topic-aware history: recent messages + same-topic backfill.

Always keeps the last RECENCY_WINDOW messages for immediate context. Fills remaining budget with older messages matching the current topic.

Return type:

list[Message]

Parameters:
class folderbot.llm_client.ToolCallRequest[source]

Bases: BaseModel

A request from the LLM to call a tool.

name: str
arguments: dict[str, Any]
model_config: ClassVar[ConfigDict] = {'frozen': True}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

class folderbot.llm_client.AgentResponse[source]

Bases: BaseModel

Structured response from the LLM.

Either provide tool_calls (to request tool execution) or answer (final response). When tool_calls is non-empty, tools will be executed and results fed back. When answer is set and tool_calls is empty, the answer is returned to the user.

tool_calls: list[ToolCallRequest]
answer: str | None
topic: str
classmethod normalize_topic(v)[source]
Return type:

str

Parameters:

v (str)

model_config: ClassVar[ConfigDict] = {'frozen': True}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

class folderbot.llm_client.AskUserRequest[source]

Bases: BaseModel

Request to ask the user a question with interactive UI.

Used by the agent loop when the LLM calls the ask_user tool. The Telegram handler renders this as inline keyboards, location pickers, or text prompts depending on input_type.

question: str
options: list[str]
input_type: str
model_config: ClassVar[ConfigDict] = {'frozen': True}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

class folderbot.llm_client.TokenUsage[source]

Bases: object

Accumulated token usage from one chat() call.

input_tokens: int
output_tokens: int
__init__(input_tokens, output_tokens)
Parameters:
  • input_tokens (int)

  • output_tokens (int)

Return type:

None

class folderbot.llm_client.LLMClient[source]

Bases: object

Backend-agnostic LLM client using instructor for structured extraction.

Uses instructor’s from_provider() to support multiple LLM backends (Anthropic, OpenAI, etc.) through a unified interface. The agent loop uses structured extraction (AgentResponse model) to get either tool calls or a final answer from the LLM.

MAX_TOOL_ITERATIONS = 10
__init__(config)[source]
Parameters:

config (Config)

async chat(user_message, context, history, on_tool_use=None, on_ask_user=None, chat_id=0, images=None)[source]

Send a message and get a response through the agent loop.

Parameters:
  • user_message (str) – The user’s message

  • context (BotContext) – BotContext with services and user info for tool execution

  • history (list[Message]) – Conversation history

  • on_tool_use (Callable[[str], Awaitable[None]] | None) – Optional async callback when tools are being used

  • on_ask_user (Callable[[AskUserRequest], Awaitable[str]] | None) – Optional async callback for ask_user tool (returns user answer)

  • chat_id (int) – Telegram chat ID (for scheduler tools)

  • images (list[dict[str, str]] | None) – Optional list of image dicts with ‘data’ (base64) and ‘media_type’

Return type:

tuple[str, list[str], str, TokenUsage]

Returns:

Tuple of (response text, list of tools used, topic label, token usage)