{"slug": "albertapi-tool-calling-py", "title": "AlbertAPI-tool-calling.py", "summary": "Albert API has released a new tool-calling implementation that enables developers to define Python functions as callable tools for AI models using a `@tool` decorator. The system automatically generates JSON schemas from function signatures, type hints, and docstrings, supporting parameter types including strings, integers, booleans, lists, enums via `Literal`, and annotated descriptions. The implementation uses OpenAI's function calling format and includes a `ToolRegistry` class that manages registered tools and their corresponding schemas.", "body_md": "| \"\"\" | |\n| Tool Calling avec Albert API | |\n| ======================================================================== | |\n| Prérequis : pip install openai | |\n| \"\"\" | |\n| import json | |\n| import inspect | |\n| from typing import Annotated, Literal, get_type_hints, get_origin, get_args | |\n| from functools import wraps | |\n| from openai import OpenAI | |\n| import sys | |\n| sys.stdout.reconfigure(encoding=\"utf-8\") | |\n| sys.stderr.reconfigure(encoding=\"utf-8\") | |\n| # ───────────────────────────────────────────── | |\n| # 1. Le décorateur @tool et le registre | |\n| # ───────────────────────────────────────────── | |\n| class ToolRegistry: | |\n| \"\"\"Registre central des tools déclarés avec @tool.\"\"\" | |\n| def __init__(self): | |\n| self.functions: dict[str, callable] = {} | |\n| self.schemas: list[dict] = [] | |\n| def tool(self, func): | |\n| \"\"\" | |\n| Décorateur qui enregistre une fonction comme tool. | |\n| Le schéma JSON est auto-généré à partir de : | |\n| - La docstring → description de la fonction | |\n| - Les type hints → types des paramètres | |\n| - Annotated[type, \"description\"] → description des paramètres | |\n| - Literal[\"a\", \"b\"] → enum | |\n| \"\"\" | |\n| schema = self._build_schema(func) | |\n| self.functions[func.__name__] = func | |\n| self.schemas.append({\"type\": \"function\", \"function\": schema}) | |\n| @wraps(func) | |\n| def wrapper(*args, **kwargs): | |\n| return func(*args, **kwargs) | |\n| return wrapper | |\n| def _python_type_to_json(self, annotation) -> dict: | |\n| \"\"\"Convertit un type Python en type JSON Schema.\"\"\" | |\n| origin = get_origin(annotation) | |\n| args = get_args(annotation) | |\n| # Literal[\"a\", \"b\", \"c\"] → enum | |\n| if origin is Literal: | |\n| # Déduire le type à partir des valeurs | |\n| if all(isinstance(a, str) for a in args): | |\n| return {\"type\": \"string\", \"enum\": list(args)} | |\n| elif all(isinstance(a, int) for a in args): | |\n| return {\"type\": \"integer\", \"enum\": list(args)} | |\n| return {\"type\": \"string\", \"enum\": [str(a) for a in args]} | |\n| # list[str], list[int], etc. | |\n| if origin is list: | |\n| items_type = self._python_type_to_json(args[0]) if args else {\"type\": \"string\"} | |\n| return {\"type\": \"array\", \"items\": items_type} | |\n| # Types simples | |\n| type_map = { | |\n| str: {\"type\": \"string\"}, | |\n| int: {\"type\": \"integer\"}, | |\n| float: {\"type\": \"number\"}, | |\n| bool: {\"type\": \"boolean\"}, | |\n| } | |\n| return type_map.get(annotation, {\"type\": \"string\"}) | |\n| def _build_schema(self, func) -> dict: | |\n| \"\"\"Construit le schéma OpenAI function calling à partir de la fonction.\"\"\" | |\n| hints = get_type_hints(func, include_extras=True) | |\n| sig = inspect.signature(func) | |\n| docstring = inspect.getdoc(func) or \"\" | |\n| # Extraire la description principale (première ligne de la docstring) | |\n| doc_lines = docstring.strip().split(\"\\n\") | |\n| description = doc_lines[0] if doc_lines else func.__name__ | |\n| properties = {} | |\n| required = [] | |\n| for param_name, param in sig.parameters.items(): | |\n| if param_name in (\"self\", \"cls\"): | |\n| continue | |\n| annotation = hints.get(param_name, str) | |\n| param_description = None | |\n| # Gérer Annotated[type, \"description\"] | |\n| if get_origin(annotation) is Annotated: | |\n| args = get_args(annotation) | |\n| real_type = args[0] | |\n| # Chercher une string dans les métadonnées comme description | |\n| for meta in args[1:]: | |\n| if isinstance(meta, str): | |\n| param_description = meta | |\n| break | |\n| annotation = real_type | |\n| # Construire la propriété | |\n| prop = self._python_type_to_json(annotation) | |\n| if param_description: | |\n| prop[\"description\"] = param_description | |\n| properties[param_name] = prop | |\n| # Si pas de valeur par défaut → required | |\n| if param.default is inspect.Parameter.empty: | |\n| required.append(param_name) | |\n| schema = { | |\n| \"name\": func.__name__, | |\n| \"description\": description, | |\n| \"parameters\": { | |\n| \"type\": \"object\", | |\n| \"properties\": properties, | |\n| \"required\": required, | |\n| }, | |\n| } | |\n| return schema | |\n| def execute(self, function_name: str, arguments: dict): | |\n| \"\"\"Exécute une fonction enregistrée par son nom.\"\"\" | |\n| if function_name not in self.functions: | |\n| raise ValueError(f\"Fonction '{function_name}' non enregistrée\") | |\n| return self.functions[function_name](**arguments) | |\n| # ───────────────────────────────────────────── | |\n| # 2. Instanciation du registre | |\n| # ───────────────────────────────────────────── | |\n| registry = ToolRegistry() | |\n| tool = registry.tool # Raccourci pour le décorateur | |\n| # ───────────────────────────────────────────── | |\n| # 3. Déclaration des tools — C'est ici que la magie opère ✨ | |\n| # ───────────────────────────────────────────── | |\n| @tool | |\n| def get_meteo( | |\n| ville: Annotated[str, \"Le nom de la ville (ex: Paris, Lyon, Marseille)\"], | |\n| unite: Annotated[Literal[\"celsius\", \"fahrenheit\"], \"L'unité de température\"] = \"celsius\", | |\n| ) -> dict: | |\n| \"\"\"Obtenir la météo actuelle pour une ville donnée.\"\"\" | |\n| meteo_data = { | |\n| \"Paris\": {\"temperature\": 18, \"conditions\": \"Nuageux\", \"humidite\": 72}, | |\n| \"Lyon\": {\"temperature\": 22, \"conditions\": \"Ensoleillé\", \"humidite\": 45}, | |\n| \"Marseille\": {\"temperature\": 26, \"conditions\": \"Ensoleillé\", \"humidite\": 38}, | |\n| \"Lille\": {\"temperature\": 14, \"conditions\": \"Pluvieux\", \"humidite\": 85}, | |\n| } | |\n| data = meteo_data.get(ville, {\"temperature\": 20, \"conditions\": \"Inconnu\", \"humidite\": 50}) | |\n| if unite == \"fahrenheit\": | |\n| data[\"temperature\"] = round(data[\"temperature\"] * 9 / 5 + 32, 1) | |\n| data[\"ville\"] = ville | |\n| data[\"unite\"] = unite | |\n| return data | |\n| @tool | |\n| def rechercher_restaurants( | |\n| ville: Annotated[str, \"La ville où chercher des restaurants\"], | |\n| cuisine: Annotated[str, \"Le type de cuisine (française, italienne, japonaise, etc.)\"] = \"française\", | |\n| budget: Annotated[Literal[\"économique\", \"moyen\", \"haut de gamme\"], \"Le niveau de budget\"] = \"moyen\", | |\n| ) -> dict: | |\n| \"\"\"Rechercher des restaurants dans une ville selon le type de cuisine et le budget.\"\"\" | |\n| restaurants = { | |\n| (\"Paris\", \"française\"): [ | |\n| {\"nom\": \"Le Petit Cler\", \"note\": 4.5, \"prix\": \"€€\"}, | |\n| {\"nom\": \"Chez Janou\", \"note\": 4.3, \"prix\": \"€€\"}, | |\n| ], | |\n| (\"Paris\", \"italienne\"): [ | |\n| {\"nom\": \"Pink Mamma\", \"note\": 4.4, \"prix\": \"€€\"}, | |\n| {\"nom\": \"Ober Mamma\", \"note\": 4.2, \"prix\": \"€€\"}, | |\n| ], | |\n| (\"Lyon\", \"française\"): [ | |\n| {\"nom\": \"Le Bouchon des Filles\", \"note\": 4.6, \"prix\": \"€€\"}, | |\n| {\"nom\": \"Daniel et Denise\", \"note\": 4.7, \"prix\": \"€€€\"}, | |\n| ], | |\n| } | |\n| result = restaurants.get( | |\n| (ville, cuisine), | |\n| [{\"nom\": f\"Restaurant {cuisine} à {ville}\", \"note\": 4.0, \"prix\": \"€€\"}], | |\n| ) | |\n| return {\"ville\": ville, \"cuisine\": cuisine, \"budget\": budget, \"restaurants\": result} | |\n| @tool | |\n| def calculer_itineraire( | |\n| depart: Annotated[str, \"La ville de départ\"], | |\n| arrivee: Annotated[str, \"La ville d'arrivée\"], | |\n| mode: Annotated[Literal[\"voiture\", \"train\", \"avion\"], \"Le mode de transport\"] = \"voiture\", | |\n| ) -> dict: | |\n| \"\"\"Calculer un itinéraire entre deux villes avec un mode de transport.\"\"\" | |\n| itineraires = { | |\n| (\"Paris\", \"Lyon\", \"voiture\"): {\"distance_km\": 465, \"duree\": \"4h30\", \"peages\": \"35€\"}, | |\n| (\"Paris\", \"Lyon\", \"train\"): {\"distance_km\": 427, \"duree\": \"1h58\", \"prix\": \"49-89€\"}, | |\n| (\"Paris\", \"Marseille\", \"train\"): {\"distance_km\": 775, \"duree\": \"3h15\", \"prix\": \"59-109€\"}, | |\n| (\"Paris\", \"Marseille\", \"voiture\"): {\"distance_km\": 775, \"duree\": \"7h15\", \"peages\": \"65€\"}, | |\n| } | |\n| data = itineraires.get( | |\n| (depart, arrivee, mode), | |\n| {\"distance_km\": 300, \"duree\": \"3h00\", \"info\": \"Estimation approximative\"}, | |\n| ) | |\n| data[\"depart\"] = depart | |\n| data[\"arrivee\"] = arrivee | |\n| data[\"mode\"] = mode | |\n| return data | |\n| # ───────────────────────────────────────────── | |\n| # 4. Client Albert API | |\n| # ───────────────────────────────────────────── | |\n| client = OpenAI( | |\n| base_url=\"https://albert.api.etalab.gouv.fr/v1\", | |\n| api_key=\"sk-\", | |\n| ) | |\n| MODEL = \"openweight-large\" | |\n| # ───────────────────────────────────────────── | |\n| # 5. Boucle de conversation avec tool calling | |\n| # ───────────────────────────────────────────── | |\n| def chat(user_message: str, messages: list | None = None) -> str: | |\n| \"\"\"Conversation avec gestion automatique du tool calling.\"\"\" | |\n| if messages is None: | |\n| messages = [ | |\n| { | |\n| \"role\": \"system\", | |\n| \"content\": ( | |\n| \"Tu es un assistant intelligent. Utilise les outils à disposition \" | |\n| \"quand c'est pertinent. Réponds en français.\" | |\n| ), | |\n| } | |\n| ] | |\n| messages.append({\"role\": \"user\", \"content\": user_message}) | |\n| print(f\"\\n{'─'*50}\") | |\n| print(f\"👤 {user_message}\") | |\n| response = client.chat.completions.create( | |\n| model=MODEL, | |\n| messages=messages, | |\n| tools=registry.schemas, | |\n| tool_choice=\"auto\", | |\n| ) | |\n| msg = response.choices[0].message | |\n| # Boucle de résolution des tool calls | |\n| while msg.tool_calls: | |\n| messages.append(msg) | |\n| for tc in msg.tool_calls: | |\n| args = json.loads(tc.function.arguments) | |\n| print(f\" 🔧 {tc.function.name}({', '.join(f'{k}={v!r}' for k, v in args.items())})\") | |\n| result = registry.execute(tc.function.name, args) | |\n| messages.append({ | |\n| \"role\": \"tool\", | |\n| \"tool_call_id\": tc.id, | |\n| \"content\": json.dumps(result, ensure_ascii=False), | |\n| }) | |\n| response = client.chat.completions.create( | |\n| model=MODEL, | |\n| messages=messages, | |\n| tools=registry.schemas, | |\n| tool_choice=\"auto\", | |\n| ) | |\n| msg = response.choices[0].message | |\n| messages.append(msg) | |\n| print(f\"🤖 {msg.content}\") | |\n| return msg.content | |\n| # ───────────────────────────────────────────── | |\n| # 6. Debug : voir les schémas générés | |\n| # ───────────────────────────────────────────── | |\n| def show_schemas(): | |\n| \"\"\"Affiche les schémas JSON générés automatiquement.\"\"\" | |\n| print(\"\\n📋 Schémas auto-générés :\") | |\n| print(json.dumps(registry.schemas, indent=2, ensure_ascii=False)) | |\n| # ───────────────────────────────────────────── | |\n| # 7. Utilisation | |\n| # ───────────────────────────────────────────── | |\n| if __name__ == \"__main__\": | |\n| # Voir ce que le décorateur a généré automatiquement | |\n| show_schemas() | |\n| # Conversation | |\n| chat(\"Quel temps fait-il à Lyon ?\") | |\n| chat(\"Je veux aller de Paris à Marseille en train, et manger sur place.\") |", "url": "https://wpnews.pro/news/albertapi-tool-calling-py", "canonical_source": "https://gist.github.com/XenocodeRCE/9e50486be7662312d6928725c439485d", "published_at": "2026-05-23 08:30:06+00:00", "updated_at": "2026-05-27 12:13:14.135427+00:00", "lang": "en", "topics": ["ai-tools", "large-language-models", "artificial-intelligence", "ai-agents", "natural-language-processing"], "entities": ["Albert API", "OpenAI"], "alternates": {"html": "https://wpnews.pro/news/albertapi-tool-calling-py", "markdown": "https://wpnews.pro/news/albertapi-tool-calling-py.md", "text": "https://wpnews.pro/news/albertapi-tool-calling-py.txt", "jsonld": "https://wpnews.pro/news/albertapi-tool-calling-py.jsonld"}}