Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 2 additions & 6 deletions src/meshbot/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down Expand Up @@ -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),
Expand Down
20 changes: 15 additions & 5 deletions src/meshbot/storage.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down Expand Up @@ -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 = []
Expand Down Expand Up @@ -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"),
Expand Down Expand Up @@ -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"),
Expand Down
14 changes: 7 additions & 7 deletions src/meshbot/tools/conversation.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@

from pydantic_ai import RunContext

from .logging_wrapper import create_logging_tool_decorator

logger = logging.getLogger(__name__)


Expand All @@ -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)

Expand All @@ -27,30 +30,27 @@ 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
success = await ctx.deps.meshcore.ping_node(destination)
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:
Expand Down
12 changes: 8 additions & 4 deletions src/meshbot/tools/fun.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@

from pydantic_ai import RunContext

from .logging_wrapper import create_logging_tool_decorator

logger = logging.getLogger(__name__)


Expand All @@ -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.

Expand Down Expand Up @@ -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.

Expand All @@ -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,
Expand Down Expand Up @@ -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.

Expand Down
97 changes: 97 additions & 0 deletions src/meshbot/tools/logging_wrapper.py
Original file line number Diff line number Diff line change
@@ -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("_")
}
Comment on lines +29 to +37
Copy link

Copilot AI Nov 23, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The logging wrapper only extracts parameters from kwargs but ignores positional args. In the tools being wrapped, parameters are passed as keyword arguments, but if tools were called with positional arguments, those would not be logged. Consider also processing args or documenting that tools must use keyword arguments only.

Copilot uses AI. Check for mistakes.
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}")
Copy link

Copilot AI Nov 23, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] The logging format uses emoji symbols (🔧, ✅, ❌) which may not display correctly in all logging environments or terminals. Consider using plain text prefixes like '[TOOL_CALL]', '[TOOL_RESULT]', '[TOOL_ERROR]' for better compatibility, or make the emoji usage configurable.

Copilot uses AI. Check for mistakes.

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
12 changes: 8 additions & 4 deletions src/meshbot/tools/query.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@

from pydantic_ai import RunContext

from .logging_wrapper import create_logging_tool_decorator

logger = logging.getLogger(__name__)


Expand All @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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.

Expand Down Expand Up @@ -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,
Expand Down
16 changes: 8 additions & 8 deletions src/meshbot/tools/utility.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@

from pydantic_ai import RunContext

from .logging_wrapper import create_logging_tool_decorator

logger = logging.getLogger(__name__)


Expand All @@ -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.

Expand Down Expand Up @@ -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.

Expand All @@ -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,
Expand Down Expand Up @@ -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()
Expand All @@ -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}")
Expand Down
Loading
Loading