File size: 6,106 Bytes
fbd808d 76398fd fbd808d 76398fd fbd808d 76398fd fbd808d 76398fd fbd808d 76398fd 311871d 76398fd 311871d 76398fd fbd808d 76398fd fbd808d 76398fd fbd808d 76398fd fbd808d 76398fd fbd808d 76398fd fbd808d 76398fd fbd808d | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 | """
services/tool_executor.py
ββββββββββββββββββββββββββ
Tool dispatch layer β executes tool calls and normalizes assistant messages.
"""
import asyncio
import json
import re
import urllib.parse
from services.observability import setup_logger
from services.supabase_service import execute_tool_rpc
from services.tool_compressor import compress_tool_output, geocode_address_handler
from config import CONTACT_EMAIL, CONTACT_WHATSAPP
logger = setup_logger(__name__)
def normalize_assistant_message(msg) -> dict:
"""Converts a ChatCompletionMessage SDK object to a clean dict, stripping internal fields."""
if getattr(msg, "tool_calls", None):
return {
"role": "assistant",
"content": msg.content or "",
"tool_calls": [
{
"id": tc.id,
"type": "function",
"function": {
"name": tc.function.name,
"arguments": tc.function.arguments,
},
}
for tc in msg.tool_calls
],
}
return {
"role": "assistant",
"content": getattr(msg, "content", "") or "",
}
def parse_tool_args(raw_arguments) -> dict:
"""Safely parse tool call arguments β handles both str and dict."""
if isinstance(raw_arguments, str):
try:
return json.loads(raw_arguments)
except Exception:
return {}
return raw_arguments or {}
def _suggest_actions_handler(func_args: dict) -> dict:
"""Handles suggest_actions tool by registering action chips."""
actions = func_args.get("actions") or func_args.get("options") or []
return {
"status": "success",
"message": (
"Interactive action button chips have been registered and will be rendered automatically "
"by the UI below the message. Do NOT write markdown links, dummy URLs, or repetitive link lists in your text response."
),
"actions": actions,
}
def _contact_buttons_handler(func_args: dict) -> dict:
"""Generates markdown contact buttons for email and WhatsApp."""
raw_message = func_args.get("message", "Hi, I'd like to discuss a custom build.")
encoded_message = urllib.parse.quote(raw_message)
return {
"status": "success",
"email_button_markdown": f"[Get in touch via Email](mailto:{CONTACT_EMAIL})",
"whatsapp_button_markdown": f"[Chat on WhatsApp](https://wa.me/{CONTACT_WHATSAPP}?text={encoded_message})",
}
def _geocode_handler(func_args: dict) -> dict:
"""Forwards geocode requests to the geocode_address_handler."""
return geocode_address_handler(func_args.get("address", ""))
async def _data_export_handler(func_args: dict) -> dict:
"""Uploads export markdown to Appwrite storage."""
from services.appwrite_service import upload_document_to_appwrite
content = func_args.get("content", "")
return await upload_document_to_appwrite(content, "md")
async def _alert_subscription_handler(func_args: dict) -> dict:
"""Forwards alert subscription creation to alert_service."""
from services.alert_service import create_pending_subscription
return await create_pending_subscription(
email=func_args.get("email", ""),
criteria_type=func_args.get("criteria_type", ""),
criteria=func_args.get("criteria", {}),
raw_text=func_args.get("raw_request_text", ""),
)
_LOCAL_HANDLERS = {
"suggest_actions": _suggest_actions_handler,
"generate_contact_buttons": _contact_buttons_handler,
"geocode_address": _geocode_handler,
"generate_data_export": _data_export_handler,
"create_alert_subscription": _alert_subscription_handler,
}
async def execute_tool_by_name(func_name: str, func_args: dict) -> dict:
"""Dispatches a tool call by name, routing to local handlers or Supabase RPC."""
handler = _LOCAL_HANDLERS.get(func_name)
if handler:
if asyncio.iscoroutinefunction(handler):
return await handler(func_args)
return handler(func_args)
return await execute_tool_rpc(func_name, func_args)
def _parse_json_direct(failed_gen: str) -> tuple[str | None, dict | None]:
"""Attempts direct JSON parse of failed generation payload."""
try:
data = json.loads(failed_gen)
if isinstance(data, dict) and "name" in data:
args = data.get("arguments", {})
if isinstance(args, str):
try:
args = json.loads(args)
except Exception:
pass
return data["name"], args if isinstance(args, dict) else {}
except Exception:
pass
return None, None
def _parse_json_regex(failed_gen: str) -> tuple[str | None, dict | None]:
"""Attempts regex extraction of JSON name/arguments dictionary."""
pattern = r'\{\s*"name"\s*:\s*"([a-zA-Z0-9_]+)"\s*,\s*"arguments"\s*:\s*(\{.*?\})\s*\}'
match = re.search(pattern, failed_gen, re.DOTALL)
if match:
try:
return match.group(1), json.loads(match.group(2))
except Exception:
pass
return None, None
def _parse_function_tag(failed_gen: str) -> tuple[str | None, dict | None]:
"""Attempts regex extraction of legacy <function=name>{args} tag format."""
pattern = r"<function=([a-zA-Z0-9_]+)[>\s]*(\{.*?\})"
match = re.search(pattern, failed_gen, re.DOTALL)
if match:
try:
return match.group(1), json.loads(match.group(2))
except Exception:
pass
return None, None
def parse_failed_generation(failed_gen: str) -> tuple[str | None, dict | None]:
"""Extracts tool name and arguments from a Groq 400 failed_generation string."""
if not failed_gen:
return None, None
for parser in (_parse_json_direct, _parse_json_regex, _parse_function_tag):
name, args = parser(failed_gen)
if name is not None:
return name, args
return None, None
|