Aller au contenu

Track A — Points d'extension

Un rendu kaboum neuf démarre, passe ses tests, et ne fait rien d’utile — par conception. Le template possède la plomberie de plateforme ; vous possédez le domaine. Cette frontière est explicite : sept points d’extension que le template scaffolde une fois et que vous devez remplir. Les chemins ci-dessous utilisent booking comme domain_module d’un acteur généré avec project_name=kalendar.

# Point d’extension Chemin dans l’acteur généré
1 Client de domaine src/booking/client.py
2 Modèles de domaine src/booking/models.py
3 Outils LangChain src/agent/tools/booking_tools.py
4 Champs de configuration de domaine src/config.py, section 4
5 Migrations de domaine migrations/004_*.sql et suivantes
6 Prompts système ROLE_PLAYBOOK dans src/agent/agent.py
7 Serveurs MCP variable d’environnement MCP_SERVERS_JSON — zéro code

Une classe encapsulant le système externe auquel votre acteur parle (Google Calendar, Gmail, une API interne). Le scaffolding est délibérément minimal :

import structlog
from src.config import KalendarConfig
logger = structlog.get_logger(__name__)
class BookingClient:
"""Client for booking operations."""
def __init__(self, config: KalendarConfig):
self.config = config
logger.info("booking_client_initialized")
async def health_check(self) -> bool:
"""Check if the domain service is reachable."""
...

Initialisez-le dans le bloc de lifespan de src/main.py marqué # Domain client (non-critical), et gardez le constructeur sans effet de bord pour que les tests puissent l’instancier sans identifiants.

Des modèles Pydantic simples partagés par votre client et vos outils. Rien de spécifique au framework ici :

from datetime import UTC, datetime
from pydantic import BaseModel, Field
class Booking(BaseModel):
"""A calendar booking."""
id: str = Field(..., description="Booking identifier")
title: str = Field(..., description="Event title")
status: str = Field(default="active", description="Booking status")
created_at: datetime = Field(default_factory=lambda: datetime.now(UTC))
metadata: dict | None = Field(default_factory=dict)

3. Outils LangChain — src/agent/tools/booking_tools.py

Section intitulée « 3. Outils LangChain — src/agent/tools/booking_tools.py »

Les outils suivent le motif de classe BaseTool : un schéma d’entrée Pydantic, un name/description explicite que le modèle lit, et _run et _arun tous les deux :

from langchain_core.tools import BaseTool
from pydantic import BaseModel, Field
from src.config import KalendarConfig
class CreateBookingInput(BaseModel):
"""Input schema for create_booking."""
title: str = Field(description="Event title")
start_iso: str = Field(description="Start time, ISO 8601")
class CreateBookingTool(BaseTool):
"""Create a booking on the connected calendar."""
name: str = "create_booking"
description: str = "Create a calendar booking with a title and start time."
args_schema: type[BaseModel] = CreateBookingInput
config: KalendarConfig = None
def __init__(self, config: KalendarConfig, **kwargs):
super().__init__(**kwargs)
self.config = config
def _run(self, title: str, start_iso: str) -> str:
return self._arun(title, start_iso)
async def _arun(self, title: str, start_iso: str) -> str:
# call the domain client here
...

Les outils ne s’enregistrent jamais eux-mêmes. L’orchestrateur (src/agent/orchestrator.py) les construit et alimente un ToolRegistry — des sources nommées fusionnées avec dédoublonnage par tool.name, la première inscription gagnant. Le rendu de base enregistre deux sources, domain puis mcp ; les plugins d’intégration en ajoutent sans toucher au registre.

4. Champs de configuration de domaine — src/config.py, section 4

Section intitulée « 4. Champs de configuration de domaine — src/config.py, section 4 »

La section 4 de la classe Settings est à vous ; les sections 1–3 et 5+ appartiennent au template. Chaque champ de domaine porte des annotations x-kaoohi-* qui pilotent le formulaire de déploiement du marketplace :

# =========================================================================
# 4. BOOKING (domain-specific parameters)
# =========================================================================
calendar_id: str = Field(
default="",
description="Google Calendar ID to operate on",
json_schema_extra={
"x-kaoohi-section": "booking",
"x-kaoohi-managed-by": "user",
"x-kaoohi-ui": {"widget": "text", "order": 30},
},
)

Des conventions encodées dans les commentaires du template lui-même :

  • x-kaoohi-section doit correspondre à un id dans KAOOHI_SECTIONS (l’id de section par défaut de la section 4 est votre domain_module).
  • widget et order sont requis dans x-kaoohi-ui ; order doit être unique dans sa section.
  • Les secrets vers les services externes sont optionnels avec un défaut None — calendar_api_key: SecretStr | None = Field(default=None, ...). Des secrets requis cassent la construction des tests, et un défaut SecretStr("") fait échouer la validation de l’émetteur de configuration.

L’ensemble complet des annotations et les règles de validation derrière lui sont couverts dans ](fr/track-a/config-and-verify/).

5. Migrations de domaine — migrations/004_*.sql et suivantes

Section intitulée « 5. Migrations de domaine — migrations/004_*.sql et suivantes »

Les migrations 001–003 sont celles du template (conversations, messages, stats, jetons OAuth) — ne les renumérotez ni ne les éditez jamais. Les vôtres commencent à 004 et suivent le style maison : CREATE TABLE IF NOT EXISTS, noms d’index préfixés idx_{project_name}_, et un en-tête de commentaire disant ce que la migration crée. Le runner suit les fichiers appliqués dans {project_name}_migrations :

-- ============================================================
-- 004: booking slots cache
-- ============================================================
CREATE TABLE IF NOT EXISTS kalendar_booking_slots (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
kompanion_id UUID NOT NULL,
slot_start TIMESTAMPTZ NOT NULL,
slot_end TIMESTAMPTZ NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_kalendar_booking_slots_kompanion
ON kalendar_booking_slots(kompanion_id, slot_start);

Un test embarqué (test_schema_sensitive_columns.py) échoue si une migration ajoute une colonne sensible sans la garde attendue — gardez-le vert.

6. Prompts système — ROLE_PLAYBOOK dans src/agent/agent.py

Section intitulée « 6. Prompts système — ROLE_PLAYBOOK dans src/agent/agent.py »

La connaissance du domaine de votre acteur vit dans exactement une constante. Les deux constantes SYSTEM_PROMPT_* au-dessus appartiennent au template : copier update les réécrit, et une modification là entre en conflit à chaque version du template. ROLE_PLAYBOOK est ajouté au prompt que le rôle sélectionne, donc un seul texte couvre les surfaces équipe et client :

ROLE_PLAYBOOK: str = (
"Refuse bookings outside opening hours (10:00–18:00, Mon–Fri).\n"
"Always confirm the customer's phone number before rescheduling.\n"
"Answer in French."
)

Écrivez les refus, des scénarios numérotés, le vocabulaire du domaine et les règles d’escalade — la frontière que le code applique, dite à voix haute. Puis épinglez les phrases porteuses dans tests/test_system_prompt.py (PINNED_PHRASES associe une description à la sous-chaîne exacte), pour qu’une régression de prompt échoue bruyamment au lieu de produire silencieusement un modèle qui a arrêté de refuser quelque chose.

Les outils MCP externes se chargent au démarrage sans changement de code. Définissez MCP_SERVERS_JSON avec un objet JSON indexé par nom de serveur ; main.py construit un MultiServerMCPClient (langchain-mcp-adapters) et enregistre les outils chargés comme source mcp du registre aux côtés de vos outils de domaine :

Fenêtre de terminal
MCP_SERVERS_JSON='{
"google-calendar": {
"transport": "streamable_http",
"url": "https://mcp.example/calendar",
"headers": {"Authorization": "Bearer ${CAL_MCP_KEY}"}
}
}'

Un serveur mal formé ou injoignable journalise un avertissement et est ignoré — MCP est non critique par conception.

Métadonnées marketplace : [tool.kaoohi] dans pyproject.toml

Section intitulée « Métadonnées marketplace : [tool.kaoohi] dans pyproject.toml »

Aux côtés des sept points de code, l’émetteur lit un bloc [tool.kaoohi] que vos réponses copier kaoohi_* ont semé une fois : slug, nom, description, catégorie, connecteurs, image et ressources de déploiement, déclarations de sécurité et de sortie réseau. Vous l’éditez à la main après génération — il appartient à l’acteur. Avec vos champs Settings il produit src/kaoohi/config.json, le fichier que le marketplace consomme. Ce pipeline, et comment le vérifier, c’est la page suivante.

  • Avertir, ne pas crasher. Chaque service non critique — Redis, client d’auth, MCP, client de domaine, mesh — s’initialise dans un try/except et journalise un avertissement en cas d’échec. PostgreSQL est le seul service critique : pas de base, pas de démarrage. Faites de même dans votre propre câblage.
  • Un seul rédacteur par fichier. Chaque fichier a un propriétaire : les fichiers du template sont réécrits par copier update, pas les fichiers de l’acteur. Les trappes de sortie (ROLE_PLAYBOOK, src/config_actor.py via include_actor_config_mixin) existent pour que votre contenu vive à côté de la structure possédée par le template sans conflit.
  • Événements structlog, snake_case. Les événements de log sont des identifiants style booking_client_initialized avec des champs structurés — suivez le motif dans chaque module du template.

Au-delà des sept canoniques, le template scaffolde des points d’extension optionnels quand vous les activez : src/{domain}/runtime.py (disponibilité), jobs.py (scheduler), coordination.py (mesh), src/api/panels.py (panneau propriétaire), src/config_actor.py (configuration possédée par l’acteur). Les modes opératoires pour tous ceux-ci vivent dans le guide de développement et d’extension de kaboum.