Add Tool to Existing Agent in guv3
Overview
Tools in guv3 are decorated Python functions that agents invoke via LLM tool-calling. Adding a tool requires changes in 2-3 files depending on scope.
File Locations
| Scope | Location | When to use |
|---|---|---|
| Agent-specific tool | gu/agents/{agent_name}/nodes/tools.py | Tool only used by one agent |
| Global/shared tool | gu/tools/global_tools.py | Tool reused across multiple agents |
| Arg schemas | gu/tools/schemas/schemas.py | When tool has structured args |
| Tool helpers | gu/tools/tool_helpers.py | Pure helper functions (no @tool decorator) |
Step 1: Define the Arg Schema (if needed)
File: gu/tools/schemas/schemas.py
from typing import List, Optional
from pydantic import BaseModel, Field
from enum import Enum
# For tools with no args
class DefaultEmptyArgs(BaseModel):
"Default empty args"
# For tools with structured args
class YourToolArgs(BaseModel):
param1: Optional[str] = Field(
default=None,
description="Description the LLM sees to understand this param"
)
param2: Optional[int] = Field(
default=0,
description="Another parameter"
)
param3: Optional[List[str]] = Field(
default=None,
description="List parameter"
)
# For restricted values, use Enum
class OperationType(str, Enum):
VENTA = "Venta"
RENTA = "Renta"
Rules:
- Every Field MUST have a
description— this is what the LLM reads to understand the parameter - Use
Optional[type] = Field(default=...)for all parameters - Descriptions can be multi-line with explicit allowed values
Step 2: Create the Tool Function
File: gu/agents/{agent_name}/nodes/tools.py
from langchain_core.tools import tool
from langchain_core.runnables import RunnableConfig
from loguru import logger
from gu.helpers.helpers import get_user, get_bot, update_user
from gu.db.connections.mongo import find_one, find, update_one, insert_one
from gu.core.notification_functions import (
send_whatsapp_message,
send_notification_to_owner,
save_chat,
)
from gu.tools.schemas.schemas import YourToolArgs
@tool(args_schema=YourToolArgs)
def your_tool_name(
param1: str = None,
param2: int = 0,
*,
config: RunnableConfig,
):
"""Clear description of what this tool does.
Use this when [specific scenario].
Args:
param1: Description
param2: Description
"""
try:
# 1. Get user and bot context
user = get_user(config)
if not user:
logger.error("[your_tool_name] No user found")
return ""
bot = get_bot(config)
lead_id = user.get("lead_id")
phone_number = user.get("phone_number")
owner_firebase_id = user.get("owner_firebase_id")
# 2. Your business logic here
# 3. Database operations (if needed)
result = find_one("collection_name", {"field": value})
update_one(
"collection_name",
{"field": value},
{"$set": {"updated_field": new_value}}
)
# 4. Send notifications (if needed)
save_chat(lead_id, "Message text", "Gu")
# 5. Return user-facing message
return "Response the LLM will use to reply to the user"
except Exception as e:
logger.error(f"Error in your_tool_name: {e}")
return "User-friendly error message"
Critical Rules
Config parameter
# CORRECT — keyword-only after *
def my_tool(param1: str, *, config: RunnableConfig):
# WRONG — positional parameter
def my_tool(param1: str, config: RunnableConfig):
Return values
# CORRECT — always return strings
return "Tu cita ha sido actualizada"
return "" # empty string on error (LLM handles gracefully)
# WRONG — raising exceptions
raise ValueError("Bad input") # crashes the tool node
Accessing user data
user = get_user(config)
# Common fields:
user.get("lead_id") # User identifier
user.get("phone_number") # WhatsApp number
user.get("owner_firebase_id") # Owner ID
user.get("last_property_info") # Current property dict
user.get("country_iso") # "MEX", "PER", "DOM", etc.
user.get("is_agent") # True/False
user.get("name") # User name
user.get("email") # User email
bot = get_bot(config)
bot.get("bot_number") # Bot WhatsApp number
bot.get("bot_phone_number") # Bot phone number
Database operations
from gu.db.connections.mongo import find_one, find, update_one, insert_one
from gu.config.config import MONGO_DB_NAME_V1
# Query single document
doc = find_one("appointments", {"appointment_id": appt_id})
# Query multiple documents
docs = find("appointments", {"lead_id": lead_id, "status": {"$ne": "cancelled"}})
# Update document
update_one("appointments", {"appointment_id": appt_id}, {"$set": {"status": "confirmed"}})
# Query different database
doc = find_one("property_data", {"firebase_id": prop_id}, MONGO_DB_NAME_V1)
WhatsApp notifications
from gu.core.notification_functions import (
send_whatsapp_message, # Send text message
send_whatsapp_message_audio, # Send audio message
send_contact_of_owner, # Send owner contact card
send_notification_to_owner, # Notify the property owner
save_chat, # Save to chat history
)
# Send text
send_whatsapp_message(bot, user, "Your message")
# Send audio
from gu.helpers.helpers import text_to_audio
audio = text_to_audio("Message text", config)
send_whatsapp_message_audio(bot, user, audio)
# Notify owner
send_notification_to_owner({
"user": user,
"question": question,
"template": TEMPLATE_NAME,
"bot_phone_number": bot.get("bot_number"),
"property_id": property_id,
})
# Save to chat history
save_chat(lead_id, "Message", "Gu") # "Gu" or "User"
Step 3: Add to the Tools List
At the bottom of the same tools.py file:
tools = [
existing_tool_1,
existing_tool_2,
your_tool_name, # ADD HERE
]
That's it for agent-specific tools. No other files need changes.
Step 4: Global Tools (if sharing across agents)
If the tool needs to be used by multiple agents, define it in gu/tools/global_tools.py instead, then import it in each agent's tools.py:
# In gu/agents/{agent_name}/nodes/tools.py
from gu.tools.global_tools import your_shared_tool
tools = [
your_shared_tool,
# ... other tools
]
Tool Helpers (non-decorated functions)
For reusable logic that isn't a tool itself, add to gu/tools/tool_helpers.py:
# In tool_helpers.py — no @tool decorator
def calculate_something(user, property_info):
"""Helper function used by multiple tools."""
# Logic here
return result
# In tools.py — import and use inside a tool
from gu.tools.tool_helpers import calculate_something
@tool(args_schema=SomeArgs)
def my_tool(*, config: RunnableConfig):
user = get_user(config)
result = calculate_something(user, user.get("last_property_info"))
return str(result)
Checklist
- Schema defined in
gu/tools/schemas/schemas.py(if needed) - Tool function created with
@tool(args_schema=...)decorator -
config: RunnableConfigis keyword-only (after*) - Tool returns strings only
- User validation with
get_user(config) - Error handling with try/except returning user-friendly message
- Tool added to
tools = [...]list - Tool docstring describes when the LLM should use it