diff --git a/src/meshbot/main.py b/src/meshbot/main.py index 832592c..d53c955 100644 --- a/src/meshbot/main.py +++ b/src/meshbot/main.py @@ -74,9 +74,7 @@ def cli() -> None: help="Enable/disable MeshCore auto-reconnect", ) @click.option("--meshcore-timeout", type=int, help="MeshCore timeout in seconds") -@click.option( - "--data-dir", type=click.Path(path_type=Path), help="Data directory path" -) +@click.option("--data-dir", type=click.Path(path_type=Path), help="Data directory path") @click.option( "--custom-prompt", type=click.Path(exists=True, path_type=Path), @@ -260,9 +258,7 @@ async def run_agent(agent: MeshBotAgent) -> None: help="Enable/disable MeshCore auto-reconnect", ) @click.option("--meshcore-timeout", type=int, help="MeshCore timeout in seconds") -@click.option( - "--data-dir", type=click.Path(path_type=Path), help="Data directory path" -) +@click.option("--data-dir", type=click.Path(path_type=Path), help="Data directory path") @click.option( "--custom-prompt", type=click.Path(exists=True, path_type=Path), diff --git a/src/meshbot/storage.py b/src/meshbot/storage.py index df5626d..e79d36a 100644 --- a/src/meshbot/storage.py +++ b/src/meshbot/storage.py @@ -82,7 +82,9 @@ def _get_channel_dir(self, channel_number: str) -> Path: channel_dir.mkdir(parents=True, exist_ok=True) return channel_dir - def _get_messages_file(self, conversation_id: str, message_type: str = "direct") -> Path: + def _get_messages_file( + self, conversation_id: str, message_type: str = "direct" + ) -> Path: """ Get the messages file path for a conversation. @@ -245,9 +247,13 @@ async def search_messages( if conversation_id: # Determine if it's a channel or node if self._is_channel_id(conversation_id): - files_to_search = [self._get_channel_dir(conversation_id) / "messages.txt"] + files_to_search = [ + self._get_channel_dir(conversation_id) / "messages.txt" + ] else: - files_to_search = [self._get_node_dir(conversation_id) / "messages.txt"] + files_to_search = [ + self._get_node_dir(conversation_id) / "messages.txt" + ] else: # Search all message files (both nodes and channels) files_to_search = [] @@ -725,7 +731,9 @@ async def get_node(self, pubkey: str) -> Optional[Dict[str, Any]]: memory = json.load(f) return { - "pubkey": memory.get("pubkey", pubkey), # Use stored pubkey, fallback to parameter + "pubkey": memory.get( + "pubkey", pubkey + ), # Use stored pubkey, fallback to parameter "name": memory.get("name"), "is_online": memory.get("is_online", False), "first_seen": memory.get("first_seen"), @@ -778,7 +786,9 @@ async def list_nodes( nodes.append( { - "pubkey": memory.get("pubkey", pubkey_prefix), # Use stored full pubkey, fallback to prefix + "pubkey": memory.get( + "pubkey", pubkey_prefix + ), # Use stored full pubkey, fallback to prefix "name": memory.get("name"), "is_online": memory.get("is_online", False), "first_seen": memory.get("first_seen"), diff --git a/src/meshbot/tools/conversation.py b/src/meshbot/tools/conversation.py index a1f13a0..34521e9 100644 --- a/src/meshbot/tools/conversation.py +++ b/src/meshbot/tools/conversation.py @@ -5,6 +5,8 @@ from pydantic_ai import RunContext +from .logging_wrapper import create_logging_tool_decorator + logger = logging.getLogger(__name__) @@ -14,11 +16,12 @@ def register_conversation_tools(agent: Any) -> None: Args: agent: The Pydantic AI agent to register tools with """ + # Create logging tool decorator + tool = create_logging_tool_decorator(agent) - @agent.tool + @tool() async def get_user_info(ctx: RunContext[Any], user_id: str) -> str: """Get information about a user.""" - logger.info(f"🔧 TOOL CALL: get_user_info(user_id='{user_id}')") try: memory = await ctx.deps.memory.get_user_memory(user_id) @@ -27,16 +30,14 @@ async def get_user_info(ctx: RunContext[Any], user_id: str) -> str: info += f"First seen: {memory.get('first_seen', 'Never')}\n" info += f"Last seen: {memory.get('last_seen', 'Never')}\n" - logger.info(f"🔧 TOOL RESULT: get_user_info -> {len(info)} chars") return info except Exception as e: logger.error(f"Error getting user info: {e}") return "Error retrieving user information." - @agent.tool + @tool() async def status_request(ctx: RunContext[Any], destination: str) -> str: """Send a status request to a MeshCore node (similar to ping).""" - logger.info(f"🔧 TOOL CALL: status_request(destination='{destination}')") try: # Use send_statusreq instead of ping (which doesn't exist) # This will request status from the destination node @@ -44,13 +45,12 @@ async def status_request(ctx: RunContext[Any], destination: str) -> str: result = ( f"Status request to {destination}: {'Success' if success else 'Failed'}" ) - logger.info(f"🔧 TOOL RESULT: status_request -> {result}") return result except Exception as e: logger.error(f"Error sending status request: {e}") return f"Status request to {destination} failed" - @agent.tool + @tool() async def get_conversation_history( ctx: RunContext[Any], user_id: str, limit: int = 5 ) -> str: diff --git a/src/meshbot/tools/fun.py b/src/meshbot/tools/fun.py index 39e5e9b..e4f306c 100644 --- a/src/meshbot/tools/fun.py +++ b/src/meshbot/tools/fun.py @@ -5,6 +5,8 @@ from pydantic_ai import RunContext +from .logging_wrapper import create_logging_tool_decorator + logger = logging.getLogger(__name__) @@ -14,8 +16,10 @@ def register_fun_tools(agent: Any) -> None: Args: agent: The Pydantic AI agent to register tools with """ + # Create logging tool decorator + tool = create_logging_tool_decorator(agent) - @agent.tool + @tool() async def roll_dice(ctx: RunContext[Any], count: int = 1, sides: int = 6) -> str: """Roll dice and return the results. @@ -47,7 +51,7 @@ async def roll_dice(ctx: RunContext[Any], count: int = 1, sides: int = 6) -> str logger.error(f"Error rolling dice: {e}") return "Error rolling dice" - @agent.tool + @tool() async def flip_coin(ctx: RunContext[Any]) -> str: """Flip a coin and return the result. @@ -63,7 +67,7 @@ async def flip_coin(ctx: RunContext[Any]) -> str: logger.error(f"Error flipping coin: {e}") return "Error flipping coin" - @agent.tool + @tool() async def random_number( ctx: RunContext[Any], min_value: int = 1, @@ -93,7 +97,7 @@ async def random_number( logger.error(f"Error generating random number: {e}") return "Error generating random number" - @agent.tool + @tool() async def magic_8ball(ctx: RunContext[Any], question: str) -> str: """Ask the magic 8-ball a yes/no question. diff --git a/src/meshbot/tools/logging_wrapper.py b/src/meshbot/tools/logging_wrapper.py new file mode 100644 index 0000000..df2d06c --- /dev/null +++ b/src/meshbot/tools/logging_wrapper.py @@ -0,0 +1,97 @@ +"""Logging wrapper for tool calls.""" + +import functools +import logging +from typing import Any, Callable + +logger = logging.getLogger(__name__) + + +def with_tool_logging(tool_func: Callable) -> Callable: + """Decorator to add logging to tool functions. + + This wrapper logs: + - Tool name and parameters when called + - Tool result when completed + - Any errors that occur + + Args: + tool_func: The tool function to wrap + + Returns: + Wrapped function with logging + """ + + @functools.wraps(tool_func) + async def wrapper(*args, **kwargs): + tool_name = tool_func.__name__ + + # Extract meaningful parameters (skip ctx and self) + params_str = "" + if kwargs: + # Filter out context and other internal params + display_params = { + k: v + for k, v in kwargs.items() + if k not in ["ctx", "self"] and not k.startswith("_") + } + if display_params: + # Truncate long values for readability + truncated_params = {} + for k, v in display_params.items(): + if isinstance(v, str) and len(v) > 50: + truncated_params[k] = v[:47] + "..." + else: + truncated_params[k] = v + params_str = f" with {truncated_params}" + + logger.info(f"🔧 TOOL CALL: {tool_name}(){params_str}") + + try: + result = await tool_func(*args, **kwargs) + + # Truncate result for logging + result_preview = str(result) + if len(result_preview) > 100: + result_preview = result_preview[:97] + "..." + + logger.info(f"✅ TOOL RESULT: {tool_name} -> {result_preview}") + return result + + except Exception as e: + logger.error( + f"❌ TOOL ERROR: {tool_name} failed with {type(e).__name__}: {str(e)[:100]}" + ) + raise + + return wrapper + + +def create_logging_tool_decorator(agent: Any) -> Callable: + """Create a tool decorator that automatically adds logging. + + This returns a decorator that can be used like @agent.tool, + but automatically wraps the function with logging. + + Args: + agent: The Pydantic AI agent + + Returns: + A decorator function that registers tools with logging + """ + + def logging_tool(*decorator_args, **decorator_kwargs): + """Decorator that adds logging to agent tools.""" + + def decorator(func: Callable) -> Callable: + # First wrap with logging + logged_func = with_tool_logging(func) + + # Then register with agent + agent.tool(*decorator_args, **decorator_kwargs)(logged_func) + + return logged_func + + return decorator + + return logging_tool diff --git a/src/meshbot/tools/query.py b/src/meshbot/tools/query.py index 5ce4974..42a9625 100644 --- a/src/meshbot/tools/query.py +++ b/src/meshbot/tools/query.py @@ -5,6 +5,8 @@ from pydantic_ai import RunContext +from .logging_wrapper import create_logging_tool_decorator + logger = logging.getLogger(__name__) @@ -14,8 +16,10 @@ def register_query_tools(agent: Any) -> None: Args: agent: The Pydantic AI agent to register tools with """ + # Create logging tool decorator + tool = create_logging_tool_decorator(agent) - @agent.tool + @tool() async def search_messages( ctx: RunContext[Any], keyword: str, @@ -75,7 +79,7 @@ async def search_messages( logger.error(f"Error searching messages: {e}") return "Error searching messages" - @agent.tool + @tool() async def list_adverts( ctx: RunContext[Any], node_id: Optional[str] = None, @@ -141,7 +145,7 @@ async def list_adverts( logger.error(f"Error searching adverts: {e}") return "Error searching advertisements" - @agent.tool + @tool() async def get_node_info(ctx: RunContext[Any], node_id: str) -> str: """Get detailed information about a specific mesh node. @@ -191,7 +195,7 @@ async def get_node_info(ctx: RunContext[Any], node_id: str) -> str: logger.error(f"Error getting node info: {e}") return "Error retrieving node information" - @agent.tool + @tool() async def list_nodes( ctx: RunContext[Any], online_only: bool = False, diff --git a/src/meshbot/tools/utility.py b/src/meshbot/tools/utility.py index 83dbcff..c39d905 100644 --- a/src/meshbot/tools/utility.py +++ b/src/meshbot/tools/utility.py @@ -5,6 +5,8 @@ from pydantic_ai import RunContext +from .logging_wrapper import create_logging_tool_decorator + logger = logging.getLogger(__name__) @@ -14,8 +16,10 @@ def register_utility_tools(agent: Any) -> None: Args: agent: The Pydantic AI agent to register tools with """ + # Create logging tool decorator + tool = create_logging_tool_decorator(agent) - @agent.tool + @tool() async def calculate(ctx: RunContext[Any], expression: str) -> str: """Evaluate a mathematical expression safely. @@ -61,7 +65,7 @@ async def calculate(ctx: RunContext[Any], expression: str) -> str: logger.error(f"Calculation error: {e}") return f"Error calculating: {str(e)[:50]}" - @agent.tool + @tool() async def get_current_time(ctx: RunContext[Any], format: str = "human") -> str: """Get current date and time. @@ -86,7 +90,7 @@ async def get_current_time(ctx: RunContext[Any], format: str = "human") -> str: logger.error(f"Error getting time: {e}") return "Error retrieving current time" - @agent.tool + @tool() async def search_history( ctx: RunContext[Any], user_id: str, @@ -134,14 +138,13 @@ async def search_history( logger.error(f"Error searching history: {e}") return "Error searching conversation history" - @agent.tool + @tool() async def get_bot_status(ctx: RunContext[Any]) -> str: """Get current bot status and statistics. Returns: Bot status information including uptime, memory stats, and connection status """ - logger.info("📊 TOOL CALL: get_bot_status()") try: # Get memory statistics memory_stats = await ctx.deps.memory.get_statistics() @@ -161,9 +164,6 @@ async def get_bot_status(ctx: RunContext[Any]) -> str: f"Users: {memory_stats.get('total_users', 0)}" ) - logger.info( - f"📊 TOOL RESULT: get_bot_status -> {online_count}/{len(contacts)} contacts, {memory_stats.get('total_messages', 0)} messages" - ) return status except Exception as e: logger.error(f"Error getting bot status: {e}") diff --git a/src/meshbot/tools/weather.py b/src/meshbot/tools/weather.py index 52e34eb..3a64e80 100644 --- a/src/meshbot/tools/weather.py +++ b/src/meshbot/tools/weather.py @@ -5,6 +5,8 @@ from pydantic_ai import RunContext +from .logging_wrapper import create_logging_tool_decorator + logger = logging.getLogger(__name__) try: @@ -19,8 +21,10 @@ def register_weather_tool(agent: Any) -> None: Args: agent: The Pydantic AI agent to register tools with """ + # Create logging tool decorator + tool = create_logging_tool_decorator(agent) - @agent.tool + @tool() async def get_weather( ctx: RunContext[Any], latitude: Optional[float] = None, @@ -40,9 +44,6 @@ async def get_weather( Returns: Concise weather summary with forecast """ - logger.info( - f"🌤️ TOOL CALL: get_weather(lat={latitude}, lon={longitude}, days={forecast_days})" - ) try: import os @@ -133,9 +134,6 @@ async def get_weather( f"{forecast_summary.strip()}" ) - logger.info( - f"🌤️ TOOL RESULT: get_weather -> {len(result)} chars" - ) return result.strip() except Exception as http_err: logger.error(f"🌐 HTTP request failed: {http_err}")