Ctrl K
ComunidadGuías…

prompt_engine/source_scope.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
"""Narrow the knowledge in a prompt to one data source.

With several data sources configured, a question is routed to one source
first (:mod:`retrieval.source_selector`) and the prompt then describes that
source alone: its tables (:meth:`schema_data.registry.SchemaRegistry.tables_for_source`),
the relationships between them, and the few-shot examples that can run
there. This module holds the two pieces the prompt builders share:
:func:`scoped_source`, which decides whether a source narrows anything at
all, and :func:`examples_for_source`.

Business rules and metrics are not table-scoped and stay as they are.

A deployment with one data source is never narrowed: :func:`scoped_source`
returns ``None`` for it whatever it is given, so every function that takes
a ``source`` renders exactly what it did before sources existed.
"""

from __future__ import annotations

from typing import Iterable, Mapping

__all__ = ["examples_for_source", "scoped_source"]


def scoped_source(source: str | None) -> str | None:
    """*source* if it narrows the prompt, else ``None``.

    Parameters
    ----------
    source:
        A data source name, or ``None`` for no restriction.

    Returns
    -------
    str | None
        *source* when several data sources are configured; ``None`` when
        *source* is ``None`` or only one source is configured (nothing to
        narrow).

    Raises
    ------
    ValueError
        If several sources are configured and *source* is not one of them
        (a mistyped name must not silently become "every table").

    Examples
    --------
    >>> scoped_source(None) is None
    True

    With the single-source fallback, any name is ignored:

    >>> import config as cfg
    >>> with cfg.override_settings(project_config_dir="/nonexistent"):
    ...     scoped_source("default") is None
    True

    >>> from unittest.mock import patch
    >>> with patch("database.datasources.datasource_names", return_value=("sales", "inventory")):
    ...     scoped_source("inventory")
    'inventory'
    >>> with patch("database.datasources.datasource_names", return_value=("sales", "inventory")):
    ...     scoped_source("archive")
    Traceback (most recent call last):
        ...
    ValueError: unknown data source 'archive'; configured: ['inventory', 'sales']
    """
    if source is None:
        return None
    # Module attribute lookup at call time, as the registry does.
    import database.datasources as datasources

    names = datasources.datasource_names()
    if len(names) < 2:
        return None
    if source not in names:
        raise ValueError(f"unknown data source {source!r}; configured: {sorted(names)}")
    return source


def examples_for_source(
    examples: Iterable[Mapping[str, object]], source: str | None,
) -> list[dict]:
    """The few-shot *examples* whose SQL can run on data source *source*.

    An example is kept when every table its SQL reads lives in *source*
    (:func:`security.sql_guard.extract_touched_tables` finds the tables,
    :func:`database.datasources.table_datasource_sets` says where they
    live; a table in several sources counts for each). An example whose
    tables cannot be determined (SQL that does not parse, or that reads no
    table the schema lists) is kept: dropping it would take away a
    demonstration for no reason anyone could check.

    Parameters
    ----------
    examples:
        ``{"question": ..., "sql": ..., ...}`` mappings, in the order to
        keep.
    source:
        The data source, or ``None`` to keep everything.

    Returns
    -------
    list[dict]
        The kept examples, in their original order. Everything, as a new
        list, when *source* is ``None`` or only one source is configured.

    Examples
    --------
    >>> from unittest.mock import patch
    >>> exs = [
    ...     {"question": "q1", "sql": "SELECT 1"},
    ...     {"question": "q2", "sql": "SELECT * FROM sales_fact"},
    ...     {"question": "q3", "sql": "SELECT * FROM stock_dim"},
    ... ]
    >>> sets = {"sales_fact": ("sales",), "stock_dim": ("inventory",)}
    >>> def fake_touched(sql, dialect="tsql"):
    ...     return sorted(t for t in sets if t in sql)
    >>> with patch("database.datasources.datasource_names", return_value=("sales", "inventory")), \\
    ...      patch("database.datasources.table_datasource_sets", return_value=sets), \\
    ...      patch("security.sql_guard.extract_touched_tables", fake_touched):
    ...     [e["question"] for e in examples_for_source(exs, "inventory")]
    ['q1', 'q3']
    """
    kept = [dict(example) for example in examples]
    source = scoped_source(source)
    if source is None:
        return kept

    import database.datasources as datasources
    from security.sql_guard import extract_touched_tables

    table_sets = datasources.table_datasource_sets()
    default = datasources.datasource_names()[0]

    def runs_on_source(example: Mapping[str, object]) -> bool:
        tables = extract_touched_tables(str(example.get("sql", "")))
        return all(source in table_sets.get(table, (default,)) for table in tables)

    return [example for example in kept if runs_on_source(example)]