Ctrl K
ComunidadGuías…

core/start_examples.py

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
# SPDX-License-Identifier: BUSL-1.1
# Copyright (c) 2024-2026 Ali Sadeghi Aghili
"""Which ``examples.yaml`` questions the analyst page offers to start with.

An empty conversation showed one line, «برای شروع، پرسش خود را در پایین
بنویسید», and nothing to ask. ``examples.yaml`` already holds the questions an
operator curated for this deployment (with their SQL, as few-shot examples for
the model); ``GET /v2/examples`` offers the *questions* as one-click starts.

Only the question text leaves the server, never the SQL or the tags. A question
is offered to a principal only when its SQL passes the same guard an answer to
it would have to pass for that principal
(:func:`security.sql_guard.validate_sql` with the principal's
``denied_columns``): an example over a column the analyst may not read is not
suggested to them, and an example whose SQL the guard refuses for any other
reason is not suggested to anyone. Any failure while checking an example
leaves that example out (fail closed).

File reads only; nothing here touches the database.

Public API
----------
offered_questions(examples, *, denied_columns, limit, is_allowed=...) -> list[str]
"""

from __future__ import annotations

from collections.abc import Callable, Iterable, Mapping
from typing import Any

MAX_LIMIT = 12
"""The most questions one response carries: a start, not a catalogue."""

MAX_QUESTION_LENGTH = 300
"""A longer ``question`` is not a one-click start; it is left out."""


def _guard_allows(sql: str, denied_columns: list[str]) -> bool:
    """True when *sql* passes :func:`security.sql_guard.validate_sql` for a
    principal with *denied_columns*; False on a refusal or any other error."""
    from security.sql_guard import validate_sql

    try:
        validate_sql(sql, denied_columns=denied_columns)
    except Exception:  # noqa: BLE001 -- fail closed: an example that cannot be checked is not offered
        return False
    return True


def offered_questions(
    examples: Iterable[Mapping[str, Any]],
    *,
    denied_columns: Iterable[str],
    limit: int,
    is_allowed: Callable[[str, list[str]], bool] = _guard_allows,
) -> list[str]:
    """The questions of *examples* to offer, in file order.

    Args:
        examples: ``examples.yaml``'s entries as mappings with ``question``
            and ``sql`` (:data:`knowledge.examples.EXAMPLES`).
        denied_columns: the principal's ``denied_columns``.
        limit: how many to return at most; clamped to 1..:data:`MAX_LIMIT`.
        is_allowed: ``(sql, denied_columns) -> bool``; the guard by default,
            replaceable in tests.

    Returns:
        Distinct, stripped, non-empty questions of at most
        :data:`MAX_QUESTION_LENGTH` characters whose SQL *is_allowed*, in
        the order the file lists them, at most *limit* of them.

    Raises:
        Nothing: an entry that is malformed or cannot be checked is skipped.

    Examples:
        >>> rows = [
        ...     {"question": "How many orders?", "sql": "SELECT 1"},
        ...     {"question": "How many orders?", "sql": "SELECT 2"},
        ...     {"question": "  ", "sql": "SELECT 3"},
        ...     {"question": "Orders by secret id", "sql": "SELECT SecretId"},
        ...     {"question": "Top customers", "sql": "SELECT 4"},
        ... ]
        >>> allow = lambda sql, denied: not any(c in sql for c in denied)
        >>> offered_questions(rows, denied_columns=["SecretId"], limit=5, is_allowed=allow)
        ['How many orders?', 'Top customers']
        >>> offered_questions(rows, denied_columns=[], limit=1, is_allowed=allow)
        ['How many orders?']
    """
    cap = max(1, min(int(limit), MAX_LIMIT))
    denied = [str(c) for c in denied_columns]
    seen: set[str] = set()
    out: list[str] = []
    for entry in examples:
        if len(out) >= cap:
            break
        if not isinstance(entry, Mapping):
            continue
        question = entry.get("question")
        sql = entry.get("sql")
        if not isinstance(question, str) or not isinstance(sql, str):
            continue
        text = " ".join(question.split())
        if not text or len(text) > MAX_QUESTION_LENGTH or text in seen:
            continue
        try:
            allowed = bool(is_allowed(sql, denied))
        except Exception:  # noqa: BLE001 -- fail closed, as _guard_allows does
            allowed = False
        if not allowed:
            continue
        seen.add(text)
        out.append(text)
    return out