"""Function-calling tool helpers (parsing / validating user-supplied JSON).""" from __future__ import annotations import json import logging from typing import Any, Optional import gradio as gr from i18n import t logger = logging.getLogger(__name__) def _normalize_tool_item(item: Any) -> Optional[dict]: """Accept either {type, function} or a bare {name, parameters} entry.""" if not isinstance(item, dict): return None if item.get("type") == "function" and "function" in item: fn = item["function"] if isinstance(fn, dict) and fn.get("name"): return fn return None if item.get("name"): return item return None def parse_functions_json(functions_json_str: Any) -> list[dict]: if functions_json_str is None: return [] if not isinstance(functions_json_str, str): functions_json_str = str(functions_json_str) if not functions_json_str.strip(): return [] try: data = json.loads(functions_json_str) except json.JSONDecodeError: return [] if isinstance(data, dict): data = [data] if not isinstance(data, list): return [] result: list[dict] = [] for item in data: fn = _normalize_tool_item(item) if fn: result.append(fn) return result def build_tools_list(functions_json_str: Any) -> Optional[list[dict]]: functions = parse_functions_json(functions_json_str) logger.debug("parsed functions count=%d", len(functions)) if not functions: return None tools: list[dict] = [] for fn in functions: params = fn.get("parameters", {}) if isinstance(params, str): try: params = json.loads(params) except (json.JSONDecodeError, TypeError): continue tools.append({ "type": "function", "function": { "name": fn["name"], "description": fn.get("description", ""), "parameters": params, }, }) return tools or None def validate_functions_json(functions_json_str: str): """Gradio handler: validate + pretty-print the function definitions textbox.""" if not functions_json_str or not functions_json_str.strip(): gr.Warning(t("warn.fn.enter_json")) return gr.update() try: data = json.loads(functions_json_str) except json.JSONDecodeError as e: gr.Warning(t("warn.fn.invalid_format", err=str(e))) return gr.update() if isinstance(data, dict): data = [data] if not isinstance(data, list): gr.Warning(t("warn.fn.must_be_array")) return gr.update() names: list[str] = [] for i, item in enumerate(data): if not isinstance(item, dict): gr.Warning(t("warn.fn.item_not_object", i=i + 1)) return gr.update() fn = _normalize_tool_item(item) if not fn: gr.Warning(t("warn.fn.item_invalid", i=i + 1)) return gr.update() if fn["name"] in names: gr.Warning(t("warn.fn.duplicate_name", name=fn["name"])) return gr.update() names.append(fn["name"]) formatted = json.dumps(data, indent=2, ensure_ascii=False) gr.Info(t("info.fn.validation_passed", n=len(names), names=", ".join(names))) return gr.update(value=formatted)