Source code for folderbot.tools.folder_tools
"""FolderTools class for interacting with the configured folder."""
from __future__ import annotations
import logging
import time
from typing import TYPE_CHECKING, Any
from ..bot import BotContext
from ..config import Config
from ..calendar_store import CalendarStore
from ..todo_store import TodoStore
from .activity_log import ActivityLogger
from .base import ToolResult
from .loader import load_custom_tools
from .registry import FolderServices, folder_bot
from .scheduler_tools import SchedulerServices
from .upload_tools import UploadServices
# Import tools to register them with folder_bot
from . import list_files as _list_files # noqa: F401
from . import read_file as _read_file # noqa: F401
from . import read_files as _read_files # noqa: F401
from . import search_files as _search_files # noqa: F401
from . import write_file as _write_file # noqa: F401
from . import activity_log as _activity_log # noqa: F401
from . import file_notifications as _file_notifications # noqa: F401
from . import web_search as _web_search # noqa: F401
from . import web_fetch as _web_fetch # noqa: F401
from . import utils as _utils # noqa: F401
from . import scheduler_tools as _scheduler_tools # noqa: F401
from . import upload_tools as _upload_tools # noqa: F401
from . import token_usage as _token_usage # noqa: F401
from . import token_stats as _token_stats # noqa: F401
from . import topic_tools as _topic_tools # noqa: F401
from . import todo as _todo # noqa: F401
from . import plot as _plot # noqa: F401
from . import calendar as _calendar # noqa: F401
if TYPE_CHECKING:
from ..scheduler.scheduler import TaskScheduler
from ..session_manager import SessionManager
logger = logging.getLogger(__name__)
# Tools that require user confirmation before use
TOOLS_REQUIRING_CONFIRMATION = ["write_file"]
[docs]
class FolderTools:
"""Tools for interacting with the configured folder.
This class serves as the main coordinator for tool execution. All tools
(file tools, utility tools, web tools, scheduler tools) are registered
on the single folder_bot instance and executed through this class.
"""
[docs]
def __init__(self, config: Config):
self.config = config
self._root = config.root_folder.resolve()
self._scheduler: TaskScheduler | None = None
self._session_manager: SessionManager | None = None
self._upload_services: UploadServices | None = None
self._custom_tools = load_custom_tools(self._root, tools_config=config.tools)
self._activity_logger = ActivityLogger(self._root)
self._todo_store = TodoStore(config.todo_path)
self._calendar_store = CalendarStore(config.db_path)
# Create folder services for the new pattern
self._folder_services = FolderServices(
root=self._root,
config=config,
activity_logger=self._activity_logger,
session_manager=None, # Set later
)
if self._custom_tools:
logger.info("Custom tools loaded from .folderbot/tools")
# Check if web tools are available
from .web_search import is_available as web_search_available
from .web_fetch import is_available as web_fetch_available
if web_search_available() or web_fetch_available():
logger.info("Web tools available")
[docs]
def set_scheduler(self, scheduler: TaskScheduler) -> None:
"""Set the scheduler reference for tool execution."""
self._scheduler = scheduler
[docs]
def set_session_manager(self, session_manager: SessionManager) -> None:
"""Set the session manager reference for user preferences."""
self._session_manager = session_manager
# Update folder services with session manager
self._folder_services = FolderServices(
root=self._root,
config=self.config,
activity_logger=self._activity_logger,
session_manager=session_manager,
)
[docs]
def set_upload_services(self, upload_services: UploadServices) -> None:
"""Set the upload services for upload tools."""
self._upload_services = upload_services
[docs]
def create_context(self, user_id: int = 0, chat_id: int = 0) -> BotContext:
"""Create a BotContext with services configured.
This is the preferred way to create a context for tool execution.
The context includes FolderServices and SchedulerServices which
provide access to folder operations and task scheduling.
"""
context = BotContext(query="", user_id=user_id)
context.services["folder"] = self._folder_services
if self._scheduler is not None:
context.services["scheduler"] = SchedulerServices(
scheduler=self._scheduler,
chat_id=chat_id,
)
if self._upload_services is not None:
context.services["uploads"] = self._upload_services
if self._session_manager is not None:
context.services["session"] = self._session_manager
context.services["todo"] = self._todo_store
context.services["calendar"] = self._calendar_store
return context
[docs]
def get_tool_definitions(self) -> list[dict[str, Any]]:
"""Return tool definitions for the Claude API."""
# All tools (sync and async) are registered on folder_bot
definitions = folder_bot.get_tools_schema()
# Add custom tool definitions if available
if self._custom_tools:
custom_defs = self._custom_tools.get_tool_definitions()
definitions.extend(custom_defs)
return definitions
[docs]
def get_tools_requiring_confirmation(self) -> list[str]:
"""Return names of tools that require user confirmation before use."""
return TOOLS_REQUIRING_CONFIRMATION
[docs]
def execute(
self, tool_name: str, tool_input: dict[str, Any], user_id: int = 0
) -> ToolResult:
"""Execute a tool synchronously (wraps async execute for compatibility)."""
import asyncio
context = self.create_context(user_id)
try:
loop = asyncio.get_running_loop()
except RuntimeError:
loop = None
if loop and loop.is_running():
# We're already in an async context, can't use asyncio.run()
# Create a new task and run it
import concurrent.futures
with concurrent.futures.ThreadPoolExecutor() as pool:
future = pool.submit(
asyncio.run,
self._execute_tool_internal(tool_name, tool_input, context),
)
return future.result()
else:
return asyncio.run(
self._execute_tool_internal(tool_name, tool_input, context)
)
[docs]
def execute_direct(
self, tool_name: str, tool_input: dict[str, Any], user_id: int = 0
) -> ToolResult:
"""Execute a tool directly (alias for execute)."""
return self.execute(tool_name, tool_input, user_id)
async def _execute_tool_internal(
self,
tool_name: str,
tool_input: dict[str, Any],
context: BotContext,
) -> ToolResult:
"""Internal async tool execution."""
start_time = time.time()
user_id = context.user_id
# Try folder_bot registry (all tools are async)
if folder_bot.has_tool(tool_name):
try:
result = await folder_bot.execute_tool(tool_name, tool_input, context)
if not isinstance(result, ToolResult):
result = ToolResult(
content=result.model_dump_json()
if hasattr(result, "model_dump_json")
else str(result)
)
self._log_tool_call(tool_name, tool_input, result, start_time, user_id)
return result
except Exception as e:
result = ToolResult(content=f"Tool execution error: {e}", is_error=True)
self._log_tool_call(tool_name, tool_input, result, start_time, user_id)
return result
# Try custom tools if available
if self._custom_tools:
try:
custom_result = self._custom_tools.execute(tool_name, tool_input)
self._log_tool_call(
tool_name, tool_input, custom_result, start_time, user_id
)
return custom_result
except Exception as e:
error_result = ToolResult(
content=f"Custom tool execution error: {e}", is_error=True
)
self._log_tool_call(
tool_name, tool_input, error_result, start_time, user_id
)
return error_result
return ToolResult(content=f"Unknown tool: {tool_name}", is_error=True)
def _log_tool_call(
self,
tool_name: str,
tool_input: dict[str, Any],
result: ToolResult,
start_time: float,
user_id: int,
) -> None:
"""Log a tool call to the activity log."""
duration_ms = int((time.time() - start_time) * 1000)
self._activity_logger.log_tool_call(
tool_name=tool_name,
tool_input=tool_input,
result=result.content,
is_error=result.is_error,
user_id=user_id,
duration_ms=duration_ms,
)
[docs]
async def execute_async(
self,
tool_name: str,
tool_input: dict[str, Any],
context: BotContext,
chat_id: int = 0,
) -> ToolResult:
"""Execute a tool asynchronously with the given context.
Args:
tool_name: Name of the tool to execute
tool_input: Input parameters for the tool
context: BotContext with user info and services
chat_id: Telegram chat ID (for scheduler tools)
Returns:
ToolResult with the execution result
"""
start_time = time.time()
user_id = context.user_id
# Ensure services have the chat_id
if "scheduler" in context.services:
context.services["scheduler"].chat_id = chat_id
if "uploads" in context.services:
context.services["uploads"].chat_id = chat_id
# Try folder_bot registry (all tools are async)
if folder_bot.has_tool(tool_name):
try:
logger.debug(f"Executing tool {tool_name} with input: {tool_input}")
result = await folder_bot.execute_tool(tool_name, tool_input, context)
# Ensure we return a ToolResult
if not isinstance(result, ToolResult):
result = ToolResult(
content=result.model_dump_json()
if hasattr(result, "model_dump_json")
else str(result)
)
logger.debug(
f"Tool {tool_name} returned: {len(result.content)} chars, "
f"is_error={result.is_error}"
)
self._log_tool_call(tool_name, tool_input, result, start_time, user_id)
return result
except Exception as e:
logger.exception(f"Tool {tool_name} execution error")
result = ToolResult(content=f"Tool execution error: {e}", is_error=True)
self._log_tool_call(tool_name, tool_input, result, start_time, user_id)
return result
# Try custom tools if available
if self._custom_tools:
try:
custom_result = self._custom_tools.execute(tool_name, tool_input)
self._log_tool_call(
tool_name, tool_input, custom_result, start_time, user_id
)
return custom_result
except Exception as e:
error_result = ToolResult(
content=f"Custom tool execution error: {e}", is_error=True
)
self._log_tool_call(
tool_name, tool_input, error_result, start_time, user_id
)
return error_result
return ToolResult(content=f"Unknown tool: {tool_name}", is_error=True)
@property
def activity_logger(self) -> ActivityLogger:
"""Expose activity logger for external use."""
return self._activity_logger