Skip to content

questions_chat

questions_chat

Questions de cadrage du chat, sans action ni attente d'inférence (19/09/2026).

Classes

Functions:

texte_questions

texte_questions(demande: dict[str, Any]) -> str

L'historique, la copie et un ancien client gardent un texte intelligible.

Source code in src/diapason/server/questions_chat.py
def texte_questions(demande: dict[str, Any]) -> str:
    """L'historique, la copie et un ancien client gardent un texte intelligible."""
    lignes = [demande["intro"]] if demande["intro"] else []
    for i, question in enumerate(demande["questions"], 1):
        lignes.append(f"{i}. {question['title']}")
        lignes.extend(f"- {choix['label']}" for choix in question["options"])
    return "\n\n".join(lignes)

cadrer_sans_outils async

cadrer_sans_outils(
    moteur: Any, modele: str, messages: list[Message]
) -> dict[str, Any] | None

Un modèle sans fonctions peut produire les données d'une carte, sans agir.

Source code in src/diapason/server/questions_chat.py
async def cadrer_sans_outils(
    moteur: Any, modele: str, messages: list[Message]
) -> dict[str, Any] | None:
    """Un modèle sans fonctions peut produire les données d'une carte, sans agir."""
    schema = schema_questions()["function"]["parameters"]
    schema["properties"]["questions"]["minItems"] = 0
    schema["properties"] = {
        "subject": {"type": "string", "description": "Sujet de la dernière demande."},
        "knownDetails": {
            "type": "array",
            "items": {"type": "string"},
            "maxItems": 12,
            "description": "Précisions explicitement fournies pour CE SUJET seulement.",
        },
        **schema["properties"],
    }
    schema["required"] = ["subject", "knownDetails", "questions"]
    consigne = (
        "Décide si la DERNIÈRE demande de l'utilisateur nécessite des précisions "
        "avant de la réaliser. Tu prépares uniquement les données d'un formulaire, "
        "sans exécuter la demande ni aucune action. Pour une création ou un "
        "programme encore vague, choisis toi-même le nombre de questions utiles, "
        "chacune avec 2 à 4 choix courts. Aucun quota : peu pour un besoin simple, "
        "davantage pour un projet complexe. Chaque question est affichée séparément. "
        "Ne répète pas une question sous une autre forme et arrête-toi dès que "
        "les informations nécessaires sont couvertes. "
        "Respecte le périmètre donné : si l'utilisateur indique qu'une seule "
        "précision manque, demande seulement celle-ci, sans ajouter des "
        "préférences facultatives. "
        "Respecte la langue de l'utilisateur. N'invente pas ses "
        "réponses et ne redemande pas des informations déjà données POUR CE SUJET. "
        "Un niveau ou un objectif donné pour une autre activité ne vaut pas "
        "automatiquement pour la nouvelle. Par exemple, un programme "
        "d'apprentissage sans niveau ni temps disponible nécessite un cadrage : "
        '{"questions":[{"title":"Quel est ton niveau dans ce domaine ?",'
        '"options":[{"label":"Débutant"},{"label":"J’ai déjà des bases"}]}]}. '
        "Pour une question factuelle, une demande déjà précise, des réponses "
        "à un questionnaire ou une demande d'avancer sans questions, renvoie "
        "une liste questions vide. Ne mets jamais de noms d’outils ou de "
        "fonctions dans les textes du formulaire. L'introduction est facultative ; "
        "ne répète "
        "pas une question dans l'introduction. Réponds uniquement avec du JSON "
        "conforme au schéma suivant :\n" + json.dumps(schema, ensure_ascii=False)
    )
    derniere_demande = next(
        (m.content or "" for m in reversed(messages) if m.role == Role.USER), ""
    )
    travail = [
        *messages,
        Message(role=Role.SYSTEM, content=consigne),
        Message(
            role=Role.USER,
            content=(
                "Évalue uniquement le besoin de précisions pour cette demande "
                "du dialogue, puis fournis le JSON du formulaire :\n"
                + json.dumps(derniere_demande, ensure_ascii=False)
                + "\nIdentifie d'abord le sujet de CETTE demande dans subject, "
                "puis les seules précisions déjà connues POUR CE SUJET dans "
                "knownDetails. Les réponses sur une autre activité ne "
                "décrivent pas automatiquement celle-ci. Complète ensuite "
                "les questions utiles pour personnaliser le résultat."
            ),
        ),
    ]
    fragments: list[str] = []
    taille = 0
    arret = None
    async with aclosing(
        moteur.stream_full(
            travail,
            model=modele,
            temperature=0.0,
            max_tokens=CADRAGE_MAX_JETONS,
            format=schema,
        )
    ) as flux:
        async for morceau in flux:
            if morceau.tool_calls:
                raise ValueError("Un formulaire ne doit pas demander d'action.")
            if morceau.content:
                taille += len(morceau.content.encode("utf-8"))
                if taille > QUESTIONNAIRE_MAX_OCTETS:
                    raise ValueError("Le questionnaire est trop long.")
                fragments.append(morceau.content)
            if morceau.finish_reason:
                arret = morceau.finish_reason
    if arret != "stop":
        raise ValueError("Le questionnaire n'a pas été terminé.")
    decision = _Decision.model_validate_json("".join(fragments))
    if not decision.questions:
        return None
    return valider_questions(
        decision.model_dump_json(exclude={"subject", "knownDetails"})
    )

est_un_cadrage_textuel

est_un_cadrage_textuel(texte: str) -> bool

Reconnaît une demande de précisions, pas une question citée dans un cours.

Source code in src/diapason/server/questions_chat.py
def est_un_cadrage_textuel(texte: str) -> bool:
    """Reconnaît une demande de précisions, pas une question citée dans un cours."""
    # 19/09/2026 : le 14b demandait simplement « Quel langage de programmation
    # veux-tu apprendre ? ». Exiger « j'ai besoin » ET une liste numérotée
    # empêchait cette vraie question de recevoir ses boutons.
    courte = texte.strip()
    question_courte = (
        len(courte) <= 500
        and courte.endswith("?")
        and not re.search(r"[.!:\n]", courte)
        and re.match(
            r"^(?:quel(?:le)?s?\b|combien\b|(?:est.ce que )?"
            r"(?:veux|souhaites|préfères|peux|pourrais|as|es).tu\b)",
            courte,
            re.I,
        )
        and re.search(r"\b(?:tu|ton|ta|tes|votre|vos|vous)\b", courte, re.I)
    )
    return bool(question_courte) or bool(
        len(texte) < 6000
        and re.search(
            r"(?:j['’]ai besoin|quelques précisions|pourrais.tu me dire|"
            r"peux.tu (?:me dire|préciser)|I need|could you (?:tell|clarify))",
            texte[:500],
            re.I,
        )
        and re.search(r"^\s*1[.)]\s+[^\n]*\?", texte, re.M)
    )

instruire_questions_texte

instruire_questions_texte(
    messages: list[Message],
) -> list[Message]

Les routes de fournisseurs qui ne transmettent que du texte gardent les choix.

Source code in src/diapason/server/questions_chat.py
def instruire_questions_texte(messages: list[Message]) -> list[Message]:
    """Les routes de fournisseurs qui ne transmettent que du texte gardent les choix."""
    consigne = CONSIGNE.replace(
        "utilise diapason_ask_questions", "pose un questionnaire"
    )
    consigne += (
        "\nPour un questionnaire seulement, réponds avec un unique bloc "
        "```diapason-questions suivi du JSON "
        '{"intro":"...","questions":[{"title":"...",'
        '"options":[{"label":"...","description":"..."},{"label":"..."}]}]}'
        " puis ferme le bloc par ```. Aucun autre texte autour. "
        "Pour une réponse ordinaire, écris normalement sans ce bloc."
    )
    # Certains fournisseurs ne gardent que le dernier SYSTEM : prolonger le
    # cadrage existant préserve aussi l'identité, au lieu de la remplacer.
    return ajouter_consigne(messages, consigne)

filtrer_questions_texte async

filtrer_questions_texte(
    source: AsyncIterator[str],
) -> AsyncIterator[tuple[str, Any]]

Ne retient que le bloc balisé ; le texte ordinaire reste en flux.

Source code in src/diapason/server/questions_chat.py
async def filtrer_questions_texte(
    source: AsyncIterator[str],
) -> AsyncIterator[tuple[str, Any]]:
    """Ne retient que le bloc balisé ; le texte ordinaire reste en flux."""
    tampon = ""
    direct = False
    async for token in source:
        if direct:
            yield "token", token
            continue
        tampon += token
        debut = tampon.lstrip()
        if len(tampon.encode("utf-8")) > QUESTIONNAIRE_MAX_OCTETS or not (
            BALISE.startswith(debut) or debut.startswith(BALISE)
        ):
            direct = True
            yield "token", tampon
            tampon = ""
    if not tampon:
        return
    contenu = tampon.strip()
    if contenu.startswith(BALISE) and contenu.endswith("```"):
        try:
            demande = valider_questions(contenu[len(BALISE) : -3].strip())
        except ValueError:
            pass
        else:
            yield "questions", demande
            yield "token", texte_questions(demande)
            return
    yield "token", tampon