AlbertAPI-tool-calling.py 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. | """ | | | Tool Calling avec Albert API | | | ======================================================================== | | | Prérequis : pip install openai | | | """ | | | import json | | | import inspect | | | from typing import Annotated, Literal, get type hints, get origin, get args | | | from functools import wraps | | | from openai import OpenAI | | | import sys | | | sys.stdout.reconfigure encoding="utf-8" | | | sys.stderr.reconfigure encoding="utf-8" | | | ───────────────────────────────────────────── | | | 1. Le décorateur @tool et le registre | | | ───────────────────────────────────────────── | | | class ToolRegistry: | | | """Registre central des tools déclarés avec @tool.""" | | | def init self : | | | self.functions: dict str, callable = {} | | | self.schemas: list dict = | | | def tool self, func : | | | """ | | | Décorateur qui enregistre une fonction comme tool. | | | Le schéma JSON est auto-généré à partir de : | | | - La docstring → description de la fonction | | | - Les type hints → types des paramètres | | | - Annotated type, "description" → description des paramètres | | | - Literal "a", "b" → enum | | | """ | | | schema = self. build schema func | | | self.functions func. name = func | | | self.schemas.append {"type": "function", "function": schema} | | | @wraps func | | | def wrapper args, kwargs : | | | return func args, kwargs | | | return wrapper | | | def python type to json self, annotation - dict: | | | """Convertit un type Python en type JSON Schema.""" | | | origin = get origin annotation | | | args = get args annotation | | | Literal "a", "b", "c" → enum | | | if origin is Literal: | | | Déduire le type à partir des valeurs | | | if all isinstance a, str for a in args : | | | return {"type": "string", "enum": list args } | | | elif all isinstance a, int for a in args : | | | return {"type": "integer", "enum": list args } | | | return {"type": "string", "enum": str a for a in args } | | | list str , list int , etc. | | | if origin is list: | | | items type = self. python type to json args 0 if args else {"type": "string"} | | | return {"type": "array", "items": items type} | | | Types simples | | | type map = { | | | str: {"type": "string"}, | | | int: {"type": "integer"}, | | | float: {"type": "number"}, | | | bool: {"type": "boolean"}, | | | } | | | return type map.get annotation, {"type": "string"} | | | def build schema self, func - dict: | | | """Construit le schéma OpenAI function calling à partir de la fonction.""" | | | hints = get type hints func, include extras=True | | | sig = inspect.signature func | | | docstring = inspect.getdoc func or "" | | | Extraire la description principale première ligne de la docstring | | | doc lines = docstring.strip .split "\n" | | | description = doc lines 0 if doc lines else func. name | | | properties = {} | | | required = | | | for param name, param in sig.parameters.items : | | | if param name in "self", "cls" : | | | continue | | | annotation = hints.get param name, str | | | param description = None | | | Gérer Annotated type, "description" | | | if get origin annotation is Annotated: | | | args = get args annotation | | | real type = args 0 | | | Chercher une string dans les métadonnées comme description | | | for meta in args 1: : | | | if isinstance meta, str : | | | param description = meta | | | break | | | annotation = real type | | | Construire la propriété | | | prop = self. python type to json annotation | | | if param description: | | | prop "description" = param description | | | properties param name = prop | | | Si pas de valeur par défaut → required | | | if param.default is inspect.Parameter.empty: | | | required.append param name | | | schema = { | | | "name": func. name , | | | "description": description, | | | "parameters": { | | | "type": "object", | | | "properties": properties, | | | "required": required, | | | }, | | | } | | | return schema | | | def execute self, function name: str, arguments: dict : | | | """Exécute une fonction enregistrée par son nom.""" | | | if function name not in self.functions: | | | raise ValueError f"Fonction '{function name}' non enregistrée" | | | return self.functions function name arguments | | | ───────────────────────────────────────────── | | | 2. Instanciation du registre | | | ───────────────────────────────────────────── | | | registry = ToolRegistry | | | tool = registry.tool Raccourci pour le décorateur | | | ───────────────────────────────────────────── | | | 3. Déclaration des tools — C'est ici que la magie opère ✨ | | | ───────────────────────────────────────────── | | | @tool | | | def get meteo | | | ville: Annotated str, "Le nom de la ville ex: Paris, Lyon, Marseille " , | | | unite: Annotated Literal "celsius", "fahrenheit" , "L'unité de température" = "celsius", | | | - dict: | | | """Obtenir la météo actuelle pour une ville donnée.""" | | | meteo data = { | | | "Paris": {"temperature": 18, "conditions": "Nuageux", "humidite": 72}, | | | "Lyon": {"temperature": 22, "conditions": "Ensoleillé", "humidite": 45}, | | | "Marseille": {"temperature": 26, "conditions": "Ensoleillé", "humidite": 38}, | | | "Lille": {"temperature": 14, "conditions": "Pluvieux", "humidite": 85}, | | | } | | | data = meteo data.get ville, {"temperature": 20, "conditions": "Inconnu", "humidite": 50} | | | if unite == "fahrenheit": | | | data "temperature" = round data "temperature" 9 / 5 + 32, 1 | | | data "ville" = ville | | | data "unite" = unite | | | return data | | | @tool | | | def rechercher restaurants | | | ville: Annotated str, "La ville où chercher des restaurants" , | | | cuisine: Annotated str, "Le type de cuisine française, italienne, japonaise, etc. " = "française", | | | budget: Annotated Literal "économique", "moyen", "haut de gamme" , "Le niveau de budget" = "moyen", | | | - dict: | | | """Rechercher des restaurants dans une ville selon le type de cuisine et le budget.""" | | | restaurants = { | | | "Paris", "française" : | | | {"nom": "Le Petit Cler", "note": 4.5, "prix": "€€"}, | | | {"nom": "Chez Janou", "note": 4.3, "prix": "€€"}, | | | , | | | "Paris", "italienne" : | | | {"nom": "Pink Mamma", "note": 4.4, "prix": "€€"}, | | | {"nom": "Ober Mamma", "note": 4.2, "prix": "€€"}, | | | , | | | "Lyon", "française" : | | | {"nom": "Le Bouchon des Filles", "note": 4.6, "prix": "€€"}, | | | {"nom": "Daniel et Denise", "note": 4.7, "prix": "€€€"}, | | | , | | | } | | | result = restaurants.get | | | ville, cuisine , | | | {"nom": f"Restaurant {cuisine} à {ville}", "note": 4.0, "prix": "€€"} , | | | | | | return {"ville": ville, "cuisine": cuisine, "budget": budget, "restaurants": result} | | | @tool | | | def calculer itineraire | | | depart: Annotated str, "La ville de départ" , | | | arrivee: Annotated str, "La ville d'arrivée" , | | | mode: Annotated Literal "voiture", "train", "avion" , "Le mode de transport" = "voiture", | | | - dict: | | | """Calculer un itinéraire entre deux villes avec un mode de transport.""" | | | itineraires = { | | | "Paris", "Lyon", "voiture" : {"distance km": 465, "duree": "4h30", "peages": "35€"}, | | | "Paris", "Lyon", "train" : {"distance km": 427, "duree": "1h58", "prix": "49-89€"}, | | | "Paris", "Marseille", "train" : {"distance km": 775, "duree": "3h15", "prix": "59-109€"}, | | | "Paris", "Marseille", "voiture" : {"distance km": 775, "duree": "7h15", "peages": "65€"}, | | | } | | | data = itineraires.get | | | depart, arrivee, mode , | | | {"distance km": 300, "duree": "3h00", "info": "Estimation approximative"}, | | | | | | data "depart" = depart | | | data "arrivee" = arrivee | | | data "mode" = mode | | | return data | | | ───────────────────────────────────────────── | | | 4. Client Albert API | | | ───────────────────────────────────────────── | | | client = OpenAI | | | base url="https://albert.api.etalab.gouv.fr/v1", | | | api key="sk-", | | | | | | MODEL = "openweight-large" | | | ───────────────────────────────────────────── | | | 5. Boucle de conversation avec tool calling | | | ───────────────────────────────────────────── | | | def chat user message: str, messages: list | None = None - str: | | | """Conversation avec gestion automatique du tool calling.""" | | | if messages is None: | | | messages = | | | { | | | "role": "system", | | | "content": | | | "Tu es un assistant intelligent. Utilise les outils à disposition " | | | "quand c'est pertinent. Réponds en français." | | | , | | | } | | | | | | messages.append {"role": "user", "content": user message} | | | print f"\n{'─' 50}" | | | print f"👤 {user message}" | | | response = client.chat.completions.create | | | model=MODEL, | | | messages=messages, | | | tools=registry.schemas, | | | tool choice="auto", | | | | | | msg = response.choices 0 .message | | | Boucle de résolution des tool calls | | | while msg.tool calls: | | | messages.append msg | | | for tc in msg.tool calls: | | | args = json.loads tc.function.arguments | | | print f" 🔧 {tc.function.name} {', '.join f'{k}={v r}' for k, v in args.items } " | | | result = registry.execute tc.function.name, args | | | messages.append { | | | "role": "tool", | | | "tool call id": tc.id, | | | "content": json.dumps result, ensure ascii=False , | | | } | | | response = client.chat.completions.create | | | model=MODEL, | | | messages=messages, | | | tools=registry.schemas, | | | tool choice="auto", | | | | | | msg = response.choices 0 .message | | | messages.append msg | | | print f"🤖 {msg.content}" | | | return msg.content | | | ───────────────────────────────────────────── | | | 6. Debug : voir les schémas générés | | | ───────────────────────────────────────────── | | | def show schemas : | | | """Affiche les schémas JSON générés automatiquement.""" | | | print "\n📋 Schémas auto-générés :" | | | print json.dumps registry.schemas, indent=2, ensure ascii=False | | | ───────────────────────────────────────────── | | | 7. Utilisation | | | ───────────────────────────────────────────── | | | if name == " main ": | | | Voir ce que le décorateur a généré automatiquement | | | show schemas | | | Conversation | | | chat "Quel temps fait-il à Lyon ?" | | | chat "Je veux aller de Paris à Marseille en train, et manger sur place." |