FinchBot provides powerful extension capabilities, allowing developers to enhance Agent capabilities by adding new Tools and writing new Skills.
- Adding New Tools
- Writing New Skills
- Custom Memory Retrieval Strategy
- Adding New LLM Providers
- Best Practices
Tools are Python code used to perform actual operations (such as calling APIs, processing data, manipulating files, etc.). All tools must inherit from finchbot.tools.base.FinchTool.
flowchart LR
classDef step fill:#e3f2fd,stroke:#1565c0,stroke-width:2px,color:#0d47a1,rx:10,ry:10;
A["1. Create Tool Class<br/>Inherit FinchTool"]:::step --> B["2. Define Parameters<br/>JSON Schema"]:::step
B --> C["3. Implement Logic<br/>_run / _arun"]:::step
C --> D["4. Register Tool<br/>Factory / Registry"]:::step
Create a new Python file (e.g., src/finchbot/tools/custom/my_tool.py) and define the tool class.
from typing import Any, ClassVar
from finchbot.tools.base import FinchTool
class WeatherTool(FinchTool):
"""Weather query tool.
Allows Agent to query weather conditions for a specified city.
"""
name: str = "get_weather"
description: str = "Get current weather for a specific city."
parameters: ClassVar[dict[str, Any]] = {
"type": "object",
"properties": {
"city": {
"type": "string",
"description": "The name of the city, e.g. Beijing, New York",
},
"unit": {
"type": "string",
"enum": ["celsius", "fahrenheit"],
"description": "Temperature unit",
"default": "celsius"
}
},
"required": ["city"],
}
def _run(self, city: str, unit: str = "celsius") -> str:
"""Synchronous execution logic."""
return f"The weather in {city} is Sunny, 25 degrees {unit}."
async def _arun(self, city: str, unit: str = "celsius") -> str:
"""Asynchronous execution logic (optional)."""
return self._run(city, unit)Method A: Modify Tool Factory (Recommended for built-in tools)
Modify create_default_tools in src/finchbot/tools/factory.py:
from finchbot.tools.custom.my_tool import WeatherTool
class ToolFactory:
def create_default_tools(self) -> list[BaseTool]:
tools: list[BaseTool] = [
# ... existing tools
WeatherTool(), # Add new tool instance
]
return toolsMethod B: Runtime Registration (Recommended for plugins)
from finchbot.tools.registry import get_global_registry
from my_plugin import WeatherTool
registry = get_global_registry()
registry.register(WeatherTool())| Principle | Description |
|---|---|
| Single Responsibility | One tool does one thing |
| Clear Description | description and parameters must be clear, this determines if LLM can call correctly |
| Error Handling | Return meaningful error messages, don't throw exceptions |
| Security Limits | Sensitive operations need permission checks |
The session_title tool embodies FinchBot's out-of-the-box philosophy:
| Method | Description | Example |
|---|---|---|
| Auto Generate | After 2-3 turns, AI automatically generates title based on content | "Python Async Programming Discussion" |
| Agent Modify | Tell Agent "Change session title to XXX" | Agent calls tool to modify automatically |
| Manual Rename | Press r key in session manager to rename |
User manually enters new title |
# Agent call example
session_title(action="set", title="New Session Title")
# Get current title
session_title(action="get")Skills are Markdown-based documents used to teach Agent how to handle specific types of tasks. They are similar to "Standard Operating Procedures (SOP)" or "In-Context Learning" examples.
FinchBot includes a built-in skill-creator skill, the ultimate expression of the out-of-the-box philosophy:
sequenceDiagram
autonumber
participant U as User
participant A as Agent
participant S as skill-creator
participant F as File System
U->>A: Help me create a translation skill
A->>S: Invoke skill-creator skill
S->>F: Create skills/translator/SKILL.md
F-->>S: Success
S-->>A: Return result
A-->>U: Translation skill created, ready to use!
Just tell the Agent what skill you want, and it will create it automatically!
User: Help me create a translation skill that can translate Chinese to English
Agent: Okay, I'll create a translation skill for you...
[Invokes skill-creator skill]
Created skills/translator/SKILL.md
You can now use the translation feature directly!
No manual file creation, no coding—extend Agent capabilities with just one sentence!
If you prefer to create skills manually, follow the steps below.
Skill files are stored in the workspace's skills/ directory (default is ~/.finchbot/workspace/skills/).
workspace/
skills/
data-analysis/
SKILL.md # Skill definition file
python-coding/
SKILL.md
Create a new directory under skills/, e.g., report-writing.
Create a SKILL.md file in the directory. The file contains YAML Frontmatter and Markdown content.
Example:
---
name: report-writing
description: Guide Agent on how to write professional analysis reports
metadata:
finchbot:
emoji:
always: false # Whether to always load this skill (true/false)
requires:
bins: [] # Required CLI tools
env: [] # Required environment variables
---
# Report Writing Guide
When user requests an analysis report, follow these structure and principles:
## 1. Structure Requirements
* **Title**: Clearly reflect the topic.
* **Executive Summary**: Briefly outline key findings (within 200 words).
* **Methodology**: Explain data sources and analysis methods.
* **Detailed Analysis**: Present points with data support.
* **Conclusions and Recommendations**: Provide actionable suggestions.
## 2. Writing Style
* Remain objective and neutral.
* Use professional terminology, but explain uncommon terms.
* Use lists and tables to present data.| Field | Type | Required | Description |
|---|---|---|---|
name |
string | Skill unique identifier | |
description |
string | Skill description, used for Agent to decide when to use | |
metadata.finchbot.emoji |
string | Skill icon | |
metadata.finchbot.always |
boolean | Whether to always load (default false) | |
metadata.finchbot.requires.bins |
list | Required CLI tools list | |
metadata.finchbot.requires.env |
list | Required environment variables list |
flowchart TD
classDef startEnd fill:#ffebee,stroke:#c62828,stroke-width:2px,color:#b71c1c;
classDef process fill:#e3f2fd,stroke:#1565c0,stroke-width:2px,color:#0d47a1;
classDef decision fill:#fff9c4,stroke:#fbc02d,stroke-width:2px,color:#f57f17;
A([Agent Startup]):::startEnd --> B[Scan skills/ directory]:::process
B --> C{always: true?}:::decision
C -->|Yes| D[Inject into System Prompt]:::process
C -->|No| E[Add to available skills list]:::process
D --> F([Skill Ready]):::startEnd
E --> F
- Auto Discovery: Agent automatically scans
skills/directory at startup - Dynamic Injection:
- If
always: true, skill content is directly appended to System Prompt - If
always: false, skill'snameanddescriptionappear in System Prompt's available skills list
- If
FinchBot's memory retrieval uses Weighted RRF strategy. You can adjust retrieval behavior by modifying QueryType or customizing RetrievalService.
Modify QueryType weight mapping in src/finchbot/memory/types.py:
QUERY_WEIGHTS = {
QueryType.KEYWORD_ONLY: (1.0, 0.0), # (keyword weight, semantic weight)
QueryType.SEMANTIC_ONLY: (0.0, 1.0),
QueryType.FACTUAL: (0.8, 0.2),
QueryType.CONCEPTUAL: (0.2, 0.8),
QueryType.COMPLEX: (0.5, 0.5),
QueryType.AMBIGUOUS: (0.3, 0.7),
}Inherit RetrievalService and override search() method:
from finchbot.memory.services.retrieval import RetrievalService
class MyRetrievalService(RetrievalService):
async def search(
self,
query: str,
query_type: QueryType,
top_k: int = 5,
**kwargs
) -> list[dict]:
# Custom retrieval logic
results = await super().search(query, query_type, top_k, **kwargs)
# Apply custom ranking
results = self._apply_custom_ranking(results)
return resultsAdd a new Provider class in src/finchbot/providers/factory.py.
from langchain_core.language_models import BaseChatModel
from langchain_openai import ChatOpenAI
def create_my_provider_model(config: ProviderConfig) -> BaseChatModel:
"""Create custom provider model instance."""
return ChatOpenAI(
model=config.model or "my-default-model",
api_key=config.api_key,
base_url=config.api_base or "https://api.my-provider.com/v1",
temperature=config.temperature or 0.7,
)
# Register in ProviderFactory
PROVIDER_FACTORIES = {
# ... existing providers
"my-provider": create_my_provider_model,
}flowchart TB
classDef tool fill:#e3f2fd,stroke:#1565c0,stroke-width:2px,color:#0d47a1;
classDef skill fill:#e8f5e9,stroke:#2e7d32,stroke-width:2px,color:#1b5e20;
subgraph Tools [Use Tools]
T1[Execute Actions<br/>Network/Files/Calculate]:::tool
T2[Call External APIs]:::tool
T3[Process Data]:::tool
end
subgraph Skills [Use Skills]
S1[Follow Processes/Styles]:::skill
S2[Teach Thinking Methods]:::skill
S3[Define Behavior Standards]:::skill
end
Need{Requirement Type?}
Need -->|Execute Operations| Tools
Need -->|Guide Behavior| Skills
| Scenario | Use Tools | Use Skills |
|---|---|---|
| Need to perform actions (network, read files, calculate) | ||
| Need to follow processes or specific styles | ||
| Need to call external APIs | ||
| Need to teach Agent how to think |
- Atomicity: Keep tool functionality single, one tool does one thing
- Documentation: Write clear
descriptionandparametersfor tools - Error Handling: Return meaningful error messages, don't throw exceptions
- Security Limits: Sensitive operations need permission checks
- Clear Scenarios: Skill description should clearly define applicable scenarios
- Provide Examples: Include specific input/output examples
- Clear Structure: Use headings, lists, tables to organize content
- Moderate Length: Skill content shouldn't be too long to avoid consuming too much context
# Complete custom tool example
from typing import Any, ClassVar
from finchbot.tools.base import FinchTool
import aiohttp
class JokeTool(FinchTool):
"""Random joke tool."""
name: str = "get_joke"
description: str = "Get a random joke to make the user happy."
parameters: ClassVar[dict[str, Any]] = {
"type": "object",
"properties": {
"category": {
"type": "string",
"enum": ["programming", "general", "dad"],
"description": "Joke category",
"default": "programming"
}
},
"required": [],
}
async def _arun(self, category: str = "programming") -> str:
async with aiohttp.ClientSession() as session:
url = f"https://official-joke-api.appspot.com/jokes/{category}/random"
async with session.get(url) as response:
if response.status == 200:
data = await response.json()
joke = data[0] if isinstance(data, list) else data
return f"{joke['setup']} - {joke['punchline']}"
return "Sorry, couldn't fetch a joke right now."
def _run(self, category: str = "programming") -> str:
import asyncio
return asyncio.run(self._arun(category))