cd /news/ai-tools/albertapi-tool-calling-py Β· home β€Ί topics β€Ί ai-tools β€Ί article
[ARTICLE Β· art-15224] src=gist.github.com pub= topic=ai-tools verified=true sentiment=Β· neutral

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.

read9 min publishedMay 23, 2026

| """ | | | 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.") |

── more in #ai-tools 4 stories Β· sorted by recency
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain β€” perfect for shipping the agent you just read about.

$git push zahid main
β†’ Live at https://your-agent.zahid.host βœ“
Get free account β†’ Pricing
from €0/mo Β· no card required
LIVE [news/albertapi-tool-calli…] indexed:0 read:9min 2026-05-23 Β· β€”