Skip to content

tdfs4ds.agent.consumer_agent

tdfs4ds.agent.consumer_agent

Consumer agent skills, intent classifier, and answer synthesizer.

Business consumers (data managers, study managers, regulatory affairs) interact with the feature store in natural language. This module provides: - intent_classifier — routes questions to the right skill - Five skills: search, definition, usage, freshness, summary - synthesize_answer — converts structured skill output to plain language

logger module-attribute

logger = getLogger(__name__)

_token_tracker module-attribute

_token_tracker = TokenTracker()

_TOKENS_RECORDED_TAG module-attribute

_TOKENS_RECORDED_TAG = 'tdfs4ds:tokens-recorded'

_STYLE_DIRECTIVES module-attribute

_STYLE_DIRECTIVES = {'concise': 'Style directive: Be terse. 3-6 short bullets or two compact sentences. No analogies, no preamble, no closing summary. Skip narrative framing.', 'standard': None, 'verbose': 'Style directive: Long-form is allowed. Use sectioned bullets, analogies, and worked examples when they help the reader.'}

_GROUNDING_DIRECTIVE module-attribute

_GROUNDING_DIRECTIVE = 'GROUNDING RULE: Ground all claims strictly in the documentation and catalog information provided. You MAY volunteer a helpful interpretation or idea beyond what is documented, but you MUST explicitly label it — prefix it with "Not documented — interpretation:" (in English) or "Idée non documentée :" (in French) — so the reader can tell a stored fact from an inference. NEVER present an inference as a documented fact. Omit sections (e.g. Inclusion/Exclusion Criteria, Interpretation) entirely when the provided documentation contains no content for them — do not write placeholder text like "not documented" for every structural section.'

_EXAMPLE_SLOT_RE module-attribute

_EXAMPLE_SLOT_RE = compile('<(domain2?|feature[23]?|entity|dataset|view)>')

_TURN_CACHE module-attribute

_TURN_CACHE: dict = {}

_AS_OF module-attribute

_AS_OF: Optional[str] = None

_MONTHS module-attribute

_MONTHS = {'january': 1, 'february': 2, 'march': 3, 'april': 4, 'may': 5, 'june': 6, 'july': 7, 'august': 8, 'september': 9, 'october': 10, 'november': 11, 'december': 12, 'janvier': 1, 'fevrier': 2, 'février': 2, 'mars': 3, 'avril': 4, 'mai': 5, 'juin': 6, 'juillet': 7, 'aout': 8, 'août': 8, 'septembre': 9, 'octobre': 10, 'novembre': 11, 'decembre': 12, 'décembre': 12}

_MONTH_RE module-attribute

_MONTH_RE = join(sorted(_MONTHS, key=len, reverse=True))

_NOW_RE module-attribute

_NOW_RE = compile("\\b(now|today|currently|current state|at present|right now|maintenant|aujourd'hui|actuellement|à présent|en ce moment)\\b", I)

_REL_UNITS module-attribute

_REL_UNITS = {'hour': 'hour', 'hours': 'hour', 'heure': 'hour', 'heures': 'hour', 'day': 'day', 'days': 'day', 'jour': 'day', 'jours': 'day', 'week': 'week', 'weeks': 'week', 'semaine': 'week', 'semaines': 'week', 'month': 'month', 'months': 'month', 'mois': 'month', 'year': 'year', 'years': 'year', 'an': 'year', 'ans': 'year', 'annee': 'year', 'année': 'year', 'annees': 'year', 'années': 'year'}

_REL_NUMBERS module-attribute

_REL_NUMBERS = {'a': 1, 'an': 1, 'one': 1, 'un': 1, 'une': 1, 'two': 2, 'deux': 2, 'three': 3, 'trois': 3, 'four': 4, 'quatre': 4, 'five': 5, 'cinq': 5, 'six': 6, 'seven': 7, 'sept': 7, 'eight': 8, 'huit': 8, 'nine': 9, 'neuf': 9, 'ten': 10, 'dix': 10, 'twelve': 12, 'douze': 12}

_REL_UNIT_RE module-attribute

_REL_UNIT_RE = join(sorted(_REL_UNITS, key=len, reverse=True))

_REL_NUM_RE module-attribute

_REL_NUM_RE = '\\d{1,4}|' + join(sorted(_REL_NUMBERS, key=len, reverse=True))

_REL_PATTERNS module-attribute

_REL_PATTERNS = (compile(f'\b(?:last|latest|lastest|past|previous|recent|preceding)\s+(?:({_REL_NUM_RE})\s+)?({_REL_UNIT_RE})\b', I), compile(f'(?:\b({_REL_NUM_RE})\s+)?\bderni[eè]re?s?\s+({_REL_UNIT_RE})\b', I), compile(f'\b()({_REL_UNIT_RE})\s+derni[eè]re?\b', I), compile(f'\bdepuis\s+({_REL_NUM_RE})\s+({_REL_UNIT_RE})\b', I))

_SNAPSHOT_CORE module-attribute

_SNAPSHOT_CORE = ('features', 'processes', 'datasets', 'dataset_features')

OUTSIDE_STORE_MAX_OBJECTS module-attribute

OUTSIDE_STORE_MAX_OBJECTS = 5

_SOURCE_DATA_RE module-attribute

_SOURCE_DATA_RE = compile('\\b(?:which|what|where|any|is\\s+there|are\\s+there|find|look(?:ing)?\\s+for|search)\\b[^?]*\\b(?:tables?|columns?|fields?|data|source\\s+tables?|views?)\\b|\\b(?:quelles?|quels?|o[uù]|y\\s+a-t-il|existe-t-il|trouver|cherche\\w*)\\b[^?]*\\b(?:tables?|colonnes?|champs?|donn[ée]es|vues?)\\b', IGNORECASE)

_SOURCE_TOPIC_RE module-attribute

_SOURCE_TOPIC_RE = compile('\\b(?:about|on|regarding|related|relating|concerning|deal(?:s|ing)?|contain(?:s|ing)?|hold(?:s|ing)?|has|have|having|gives?|records?|per|store[sd]?|describ\\w*|with|for|sur|concernant|li[ée]e?s?|contien\\w*|avec|pour|traitant|parlant|a|ont|donne\\w*|enregistre\\w*|par)\\b', IGNORECASE)

_FEATURE_STORE_WORDS_RE module-attribute

_FEATURE_STORE_WORDS_RE = compile('\\b(?:features?|datasets?|caract[ée]ristiques?|jeux?\\s+de\\s+donn[ée]es|feature\\s+store|data[\\s_-]?domains?|domaines?\\s+de\\s+donn[ée]es|process(?:es|us)?)\\b', IGNORECASE)

_SOURCE_WORDS_RE module-attribute

_SOURCE_WORDS_RE = compile('\\b(?:tables?|columns?|fields?|source|raw|colonnes?|champs?|brutes?|any\\s+data|des\\s+donn[ée]es)\\b', IGNORECASE)

_ATTACHED_KINDS module-attribute

_ATTACHED_KINDS = ('attached view', 'attached table')

_SEARCH_STOP_WORDS module-attribute

_SEARCH_STOP_WORDS = frozenset({'a', 'an', 'the', 'is', 'are', 'was', 'were', 'be', 'been', 'being', 'have', 'has', 'had', 'do', 'does', 'did', 'will', 'would', 'could', 'should', 'may', 'might', 'shall', 'can', 'what', 'which', 'who', 'how', 'when', 'where', 'why', 'i', 'me', 'my', 'we', 'our', 'you', 'your', 'it', 'its', 'this', 'that', 'these', 'those', 'and', 'or', 'but', 'in', 'on', 'at', 'to', 'for', 'of', 'with', 'about', 'from', 'by', 'as', 'into', 'through', 'during', 'there', 'than', 'so', 'if', 'any', 'all', 'both', 'each', 'few', 'more', 'most', 'other', 'some', 'such', 'no', 'not', 'only', 'own', 'same', 'then', 'than', 'too', 'very', 'just', 'show', 'me', 'give', 'list', 'find', 'get', 'tell', 'want', 'need', 'look', 'looking'})

_RISK_HIGHER_CLAIM_PATTERN module-attribute

_RISK_HIGHER_CLAIM_PATTERN = 'higher\\s+(?:values?\\s+)?(?:means?|=|is|indicate[sd]?|signal[s]?|suggest[s]?)?\\s*(?:a\\s+)?(?:greater|more|higher)?\\s*(?:likelihood\\s+of\\s+|risk|anomal|fraud|danger)'

_OWNER_STOP module-attribute

_OWNER_STOP = {'THE', 'OF', 'AND', 'WITH', 'FROM', 'TABLE', 'VIEW', 'COLUMN', 'SIMILAR', 'SAME', 'THAN', 'DATA', 'VALUE', 'VALUES', 'DANS', 'TABLE', 'COLONNE', 'DE', 'DU', 'LA', 'LE', 'LES', 'DES'}

_LARGE_LIST_MAX_GROUPS module-attribute

_LARGE_LIST_MAX_GROUPS = 15

_LARGE_LIST_NAMES_PER_GROUP module-attribute

_LARGE_LIST_NAMES_PER_GROUP = 5

_SEM_NAMES_PER_GROUP module-attribute

_SEM_NAMES_PER_GROUP = 12

_TOPIC_BATCH module-attribute

_TOPIC_BATCH = 150

_DATASET_FILLER_WORDS module-attribute

_DATASET_FILLER_WORDS = frozenset({'the', 'a', 'an', 'my', 'our', 'this', 'that', 'dataset', 'datasets', 'data', 'set', 'table', 'view', 'le', 'la', 'les', 'un', 'une', 'du', 'des', 'de', 'd', 'jeu', 'jeux', 'donnees', 'données'})

_DATASET_PREFIXES module-attribute

_DATASET_PREFIXES = ('ds_', 'dataset_')

_CONCEPT_FILLER_WORDS module-attribute

_CONCEPT_FILLER_WORDS = frozenset({'feature', 'features', 'indicator', 'indicators', 'variable', 'variables', 'column', 'columns', 'metric', 'metrics', 'kpi', 'kpis', 'attribute', 'attributes', 'caracteristique', 'caracteristiques', 'caractéristique', 'caractéristiques', 'indicateur', 'indicateurs', 'colonne', 'colonnes', 'métrique', 'métriques'})

_MEANING_FILLER_WORDS module-attribute

_MEANING_FILLER_WORDS = frozenset({'the', 'feature', 'features', 'column', 'variable', 'indicator', 'la', 'le', 'les', 'variable', 'indicateur', 'colonne'})

_FRENCH_MARKERS module-attribute

_FRENCH_MARKERS = {'est', 'sont', 'quoi', 'quel', 'quelle', 'quels', 'quelles', 'comment', 'pourquoi', 'les', 'des', 'une', 'cette', 'ces', 'dans', 'pour', 'avec', 'que', 'qui', 'où', 'donnée', 'données', 'derrière', 'signifie'}

_DOWNSTREAM_RE module-attribute

_DOWNSTREAM_RE = compile('\\b(depend(s|ed)?\\s+on\\s+(it|this|that|the|x|\\w+)|what\\s+(uses|consumes|depends|reads|relies)|used\\s+by|consumed\\s+by|consumers?|downstream|impact(ed|s)?\\b|affect(s|ed)?|who\\s+uses|qui\\s+(utilise|dépend)|en\\s+aval|impacte)\\b', I)

_UPSTREAM_RE module-attribute

_UPSTREAM_RE = compile("\\b(where\\s+(does|do).*(come|comes)\\s+from|comes?\\s+from|sources?|upstream|depends?\\s+on\\s+what|built\\s+from|derived\\s+from|d['e]?où|en\\s+amont|provient|feeds?|fed\\s+by|alimente)\\b", I)

_STO_MARKER_RE module-attribute

_STO_MARKER_RE = compile('TDS_STO_|\\bScript\\s*\\(|SCRIPT_COMMAND', IGNORECASE)

_STO_SCORING_UUID_RE module-attribute

_STO_SCORING_UUID_RE = compile('TDS_STO_SCORING(?:_LIVE)?_([0-9a-fA-F]{8}_[0-9a-fA-F]{4}_[0-9a-fA-F]{4}_[0-9a-fA-F]{4}_[0-9a-fA-F]{12})', IGNORECASE)

_ALGO_CODE_KEYWORDS module-attribute

_ALGO_CODE_KEYWORDS = {'code', 'source', 'package', 'packages', 'library', 'libraries', 'import', 'algorithm', 'algorithms', 'sklearn', 'scikit', 'python', 'paquet', 'paquets', 'librairie', 'bibliothèque', 'algorithme', 'modèle'}

_PY_STDLIB module-attribute

_PY_STDLIB = {'json', 'os', 'sys', 're', 'math', 'time', 'datetime', 'io', 'base64', 'pickle', 'collections', 'itertools', 'functools', 'typing', 'logging', 'warnings', 'abc', 'copy', 'random', 'string', 'csv', 'decimal'}

_NUMERIC_TYPE_TOKENS module-attribute

_NUMERIC_TYPE_TOKENS = ('FLOAT', 'DECIMAL', 'NUMERIC', 'NUMBER', 'INT', 'BYTEINT', 'SMALLINT', 'REAL', 'DOUBLE')

_BAND_NAME_TOKENS module-attribute

_BAND_NAME_TOKENS = ('band', 'flag', 'status', 'label', 'tier', 'category', 'class')

_SCORE_NAME_TOKENS module-attribute

_SCORE_NAME_TOKENS = ('combined_score', 'ml_risk_score', 'score', 'zscore', 'z_score', 'risk', 'anomaly')

_NUMERIC_NAME_HINTS module-attribute

_NUMERIC_NAME_HINTS = ('score', 'zscore', 'z_score', 'amount', 'count', 'ratio', 'rate', 'velocity', 'avg', 'stddev')

_TIME_ORDER module-attribute

_TIME_ORDER = {'current': 0, 'time_manager': 1, 'as_of': 2, 'history': 3, 'unknown': 4}

_KIND_PROBLEMS module-attribute

_KIND_PROBLEMS = {'unrecognised': ('I did not recognise the kind of dataset.', "Je n'ai pas reconnu le type de dataset."), 'as_of_needs_date': ('A snapshot at a date needs the date (and optionally the time).', "Un instantané à une date demande la date (et éventuellement l'heure)."), 'history_needs_window': ("A history needs its window: two dates, a duration, or 'all'.", 'Un historique demande sa fenêtre : deux dates, une durée, ou « tout ».'), 'ambiguous_date': ('A date like 03/04/2026 can be read two ways: please write it as YYYY-MM-DD.', 'Une date comme 03/04/2026 se lit de deux façons : écrivez-la AAAA-MM-JJ.'), 'unreadable_date': ('I could not read that date.', "Je n'ai pas pu lire cette date."), 'unreadable_duration': ('I could not read that duration.', "Je n'ai pas pu lire cette durée.")}

_KIND_TS_RE module-attribute

_KIND_TS_RE = compile('\\b(\\d{4}-\\d{2}-\\d{2})(?:[ T](\\d{1,2}:\\d{2}(?::\\d{2})?)\\s*(Z|UTC|GMT|[+-]\\d{2}:?\\d{2})?)?(?![\\d:])', IGNORECASE)

_SLASH_DATE_RE module-attribute

_SLASH_DATE_RE = compile('\\b\\d{1,2}[/.]\\d{1,2}[/.]\\d{2,4}\\b')

_DURATION_TERM_RE module-attribute

_DURATION_TERM_RE = compile('(\\d+)\\s*(?:derni[eè]re?s?\\s+|last\\s+|past\\s+)?(years?|yrs?|months?|weeks?|days?|hours?|minutes?|ann[ée]es?|ans?|mois|semaines?|jours?|heures?)\\b', IGNORECASE)

_FR_UNITS module-attribute

_FR_UNITS = {'an': 'years', 'ans': 'years', 'annee': 'years', 'annees': 'years', 'année': 'years', 'années': 'years', 'mois': 'months', 'semaine': 'weeks', 'semaines': 'weeks', 'jour': 'days', 'jours': 'days', 'heure': 'hours', 'heures': 'hours'}

_HISTORY_WORDS module-attribute

_HISTORY_WORDS = compile('\\b(histor\\w*|over time|window|p[ée]riode|between|entre|from|since|depuis|last|past|derni[eè]re?s?|[ée]volution|until|jusqu)\\b', IGNORECASE)

_CURRENT_WORDS module-attribute

_CURRENT_WORDS = compile("\\b(current|now|latest|today|actuel\\w*|courant\\w*|maintenant|aujourd'hui|default|d[ée]faut)\\b", IGNORECASE)

_FULL_WORDS module-attribute

_FULL_WORDS = compile('\\b(all|full|entire|whole|complete|tout|toute|complet|compl[èe]te|enti[èe]re?)\\b', IGNORECASE)

_DATASET_NAME_RE module-attribute

_DATASET_NAME_RE = compile('^[A-Za-z][A-Za-z0-9_$#]{0,127}$')

_NAME_STOPWORDS module-attribute

_NAME_STOPWORDS = {'call', 'it', 'name', 'named', 'the', 'dataset', 'please', 'use', 'as', 'create', 'build', 'appelle', 'le', 'la', 'nom', 'nomme', 'jeu', 'de', 'données', 'donnees', 'merci', 'avec'}

_OBJECT_ARGS module-attribute

_OBJECT_ARGS = ('table_name', 'obj_name')

_DATABASE_ARGS module-attribute

_DATABASE_ARGS = ('db_name', 'database_name')

_mcp_trace_local module-attribute

_mcp_trace_local = local()

TokenTracker

Thread-safe token usage tracker for LLM calls.

calls instance-attribute

calls = []

__init__

__init__()

record

record(skill_name: str, input_tokens: int, output_tokens: int, node_name: Optional[str] = None)

Record token usage for an LLM call.

totals

totals() -> dict

Return aggregated token usage statistics for the whole session.

mark_turn

mark_turn()

Mark the start of a new conversational turn for per-turn reporting.

turn_totals

turn_totals() -> dict

Return aggregated usage for calls recorded since the last mark_turn().

position

position() -> int

Number of calls recorded so far (a cursor for :meth:totals_since).

totals_since

totals_since(position: int) -> dict

Aggregated usage of the calls recorded after position.

reset

reset()

Clear all recorded calls.

_SubIntent

Bases: BaseModel

One sub-question of a COMPOUND (multi-purpose) question.

Populated only when a single user message carries MORE THAN ONE intent over interrelated subjects (e.g. "what is provider_z_score AND where does it come from?" = DEFINITION + LINEAGE). Single-purpose questions leave the parent sub_intents field null.

intent class-attribute instance-attribute

intent: str = Field(description='Intent for this sub-question (e.g. DEFINITION, LINEAGE, FRESHNESS, EXPLAIN)')

object_name class-attribute instance-attribute

object_name: Optional[str] = Field(default=None, description='The object/feature/column this sub-question is about')

entity_name class-attribute instance-attribute

entity_name: Optional[str] = Field(default=None, description='Entity/granularity for this sub-question, if named')

depends_on_prev class-attribute instance-attribute

depends_on_prev: bool = Field(default=False, description="True when this sub-question needs the PREVIOUS sub-question's answer to resolve (sequential — e.g. resolve the owning object in DEFINITION, then LINEAGE-position it). False when it is independent (parallel).")

IntentResult

Bases: BaseModel

Structured output for intent classification (hardwired fallback — all 10 intents).

intent instance-attribute

intent: Literal['SEARCH', 'DEFINITION', 'USAGE', 'FRESHNESS', 'SUMMARY', 'DATA_DOMAINS', 'FEASIBILITY', 'LINEAGE', 'EXPLAIN', 'DATASET', 'BUILD_DATASET', 'RECAP', 'HELP', 'OUT_OF_SCOPE']

object_name class-attribute instance-attribute

object_name: Optional[str] = Field(None, description='Name of any documented item mentioned: a feature, a documented column, a table, a view, or a dataset')

domain class-attribute instance-attribute

domain: Optional[str] = Field(None, description='DATA_DOMAIN — the feature-store project/namespace that isolates features; NOT a feature theme. Copy verbatim if the user names one, else null.')

entity_name class-attribute instance-attribute

entity_name: Optional[str] = Field(None, description='Entity / granularity level if the user names one (the key a feature is computed for), copied verbatim')

user_preferences class-attribute instance-attribute

user_preferences: Optional[dict] = Field(None, description="User preferences or constraints (e.g. {'currency': 'USD', 'output_format': 'JSON'})")

sub_intents class-attribute instance-attribute

sub_intents: Optional[List['_SubIntent']] = Field(None, description='Ordered sub-questions when ONE message carries MORE THAN ONE intent over interrelated subjects; null for single-purpose questions')

as_of class-attribute instance-attribute

as_of: Optional[str] = Field(None, description="The date the question asks about the feature store AT (e.g. 'on 30 June 2023', 'as of 2023-06-30', 'at the end of June 2023'), copied as written. Null when the question is about the current state.")

_ClarificationInterpretation

Bases: BaseModel

One candidate interpretation of an ambiguous query.

label class-attribute instance-attribute

label: str = Field(description="One-line description shown to the user (e.g. 'Define <the named feature>')")

intent class-attribute instance-attribute

intent: str = Field(description='Intent that best matches this interpretation (e.g. DEFINITION, EXPLAIN)')

object_name class-attribute instance-attribute

object_name: Optional[str] = Field(default=None, description='Feature or dataset name for this interpretation')

SourceColumnRef

Bases: BaseModel

A column referenced in a feature formula and its source table/view.

column_name class-attribute instance-attribute

column_name: str = Field(description='Column or alias name as it appears in the SQL expression (bare name, no schema)')

source_table class-attribute instance-attribute

source_table: str = Field(description='Source table or view the column comes from (bare name, no schema prefix)')

ExplainOutput

Bases: BaseModel

Structured output for the EXPLAIN skill.

explanation class-attribute instance-attribute

explanation: str = Field(description='Plain business language explanation of the feature: data sources (by purpose, not table name), filters in business terms, how the value is computed, and what it represents. Maximum 6 sentences. No SQL syntax, no column names, no table names.')

calculation_sql class-attribute instance-attribute

calculation_sql: Optional[str] = Field(None, description="The SQL expression that computes this specific feature, extracted verbatim from the SELECT list. If the outer SELECT merely passes through an alias defined in a subquery, drill down into that subquery to find the real computation — repeat for every nesting level until the actual arithmetic or aggregation is found. Include only the expression itself (e.g. 'CAST(CURRENT_DATE - MAX(transaction_date) AS INT)'), never the full query. Return null only when the expression genuinely cannot be resolved at any nesting level.")

calculation_latex class-attribute instance-attribute

calculation_latex: Optional[str] = Field(None, description="LaTeX representation of the calculation in display-math notation, WITHOUT surrounding delimiters. Populate ONLY when the user explicitly requested a formula, equation, or LaTeX. Use \\text{} for business names and standard LaTeX for operators (\\max, \\sum, \\frac, \\cdot, etc.). Example: r'\\text{nb\\_days} = \\text{TODAY} - \\max(\\text{transaction\\_date})'. Return null when not requested or when the expression is too complex to render meaningfully as a single formula.")

source_columns class-attribute instance-attribute

source_columns: Optional[list[SourceColumnRef]] = Field(None, description="For every distinct column or alias referenced in the feature's calculation expression, provide its bare name and the source table or view it originates from (resolve through subquery aliases to the real base table/view wherever possible). Bare names only — no schema prefix. Omit entity-key columns (e.g. primary-key columns). Return null if the formula contains no identifiable column references.")

logger_safe

logger_safe(level, message, *args, **kwargs)

Wrapper around the global logger that only emits logs when tdfs4ds.DISPLAY_LOGS is True. level is a string like "info", "error", etc.

follow_up_report

follow_up_report(filtermanager: Optional[object] = None, process_id: Optional[str] = None)

Return a follow-up report as a tdml.DataFrame, optionally filtered by process_id and/or a filtermanager's applied filter.

Behavior by arguments
  • process_id is None and filtermanager is None: Return all rows from SCHEMA.FOLLOW_UP_NAME, sorted by START_DATETIME desc.
  • process_id is not None and filtermanager is None: Return rows for the given PROCESS_ID.
  • process_id is not None and filtermanager is not None: Return rows for the given PROCESS_ID whose APPLIED_FILTER matches the JSON_AGG of filtermanager's columns coming from its schema/view.
  • process_id is None and filtermanager is not None: Return rows whose APPLIED_FILTER matches the JSON_AGG of filtermanager (no PROCESS_ID constraint).

Parameters:

Name Type Description Default
filtermanager Optional[object]

An object exposing col_names, schema_name, and view_name. Its columns are aggregated via JSON_AGG(col1, col2, ...) to compare against A.APPLIED_FILTER.

None
process_id Optional[str]

Optional process identifier used to filter by PROCESS_ID.

None

Returns:

Type Description

tdml.DataFrame: The resulting dataframe sorted by START_DATETIME (descending).

Raises:

Type Description
ValueError

If filtermanager is provided but is missing required attributes or has an empty col_names list.

RuntimeError

If the query fails.

follow_up_report_many

follow_up_report_many(process_ids)

Return follow-up rows for several process_ids in a single query.

Batched counterpart to follow_up_report(process_id=…): issues one WHERE PROCESS_ID IN (…) query instead of one round-trip per process. Callers group the result by PROCESS_ID in pandas to reproduce the per-process aggregation. Returns a tdml.DataFrame sorted by START_DATETIME descending.

Parameters:

Name Type Description Default
process_ids

Iterable of process identifier strings. Empty/None entries are dropped; duplicates are collapsed.

required

Returns:

Type Description

tdml.DataFrame with the same columns as the follow-up table. When the

id list is empty, an empty result (WHERE 1=0) is returned so the

caller's downstream pandas logic still works.

Raises:

Type Description
RuntimeError

If the query fails.

_structured

_structured(llm, schema, **kwargs)

llm.with_structured_output(schema), resilient to a gateway that does not enforce the schema it was asked for.

The single seam every structured-output call site goes through — one in the classifier, one in EXPLAIN, seven in the query optimizer, all of which fail identically on a relaying gateway. A new call site added directly against the model would silently miss this (there is a test that forbids it).

An explicit method= from the caller always wins, and a configured STRUCTURED_OUTPUT_METHOD short-circuits the probe.

get_token_tracker

get_token_tracker() -> TokenTracker

Get the global token tracker.

_usage_from_llm_result

_usage_from_llm_result(response) -> tuple

(input, output) tokens of an LLMResult, whatever the provider's shape.

turn_token_callback

turn_token_callback()

A LangChain callback that records every LLM call of a turn in the tracker.

Only _llm_call recorded usage, so the intent classifier, EXPLAIN's structured output and the JSON calls were never counted and a turn could show 0 tokens. Put in the turn's run config (graph._run_config), this sees every model call made inside the graph, labelled by its node; the runs _llm_call already recorded carry :data:_TOKENS_RECORDED_TAG and are skipped. Never raises.

list_instruct_models

list_instruct_models() -> pd.DataFrame

List available instruct/chat models from the configured provider.

Returns:

Type Description
DataFrame

DataFrame with columns: MODEL_ID, OWNED_BY (OpenAI-compatible) or

DataFrame

MODEL_ID, PROVIDER, MODALITY (Bedrock).

Raises:

Type Description
ValueError

If the provider is not recognized or not configured.

_get_llm

_get_llm(max_tokens: Optional[int] = None, callbacks=None)

Return a LangChain chat model based on the active provider.

Injects tdfs4ds config variables into environment so that boto3, Azure, and Google Cloud clients pick up credentials and endpoints automatically.

Parameters:

Name Type Description Default
max_tokens Optional[int]

Optional cap on completion tokens. Useful when the model has a small total context window and the caller knows the expected output is short (e.g. structured candidate generation).

None
callbacks

Optional list of LangChain callback handlers (e.g. for token usage tracking). Passed through to the model constructor.

None

Returns:

Type Description

A LangChain BaseChatModel instance.

Raises:

Type Description
ValueError

If the provider is not recognized.

_style_prefix

_style_prefix() -> str

Return the style directive line(s) for the active AGENT_ANSWER_STYLE.

Returns an empty string when the style is unset, unknown, or 'standard'.

_grounding_prefix

_grounding_prefix() -> str

Grounding directive prepended to every _llm_call system prompt.

Instructs every skill to stay grounded in the provided docs while explicitly labelling non-grounded interpretations (so the judge does not penalise them as hallucinations). Also suppresses empty boilerplate sections in DEFINITION answers.

_language_directive

_language_directive(user_prompt: str) -> str

Name the answer language explicitly, decided once by _question_is_french.

"Answer in the same language as the question" was not enough: an English question was answered in French (seen live, 0.3.1.224), the prompt itself quoting French text and the documentation possibly being French.

_llm_call

_llm_call(system_prompt: str, user_prompt: str, skill_name: str = 'unknown') -> str

Execute a single LLM chat completion with token tracking.

The active tdfs4ds.AGENT_ANSWER_STYLE is prepended to the system prompt so every answer (search, definition, explain, freshness, ...) inherits the same verbosity preference. The grounding directive is also prepended to instruct the model to label non-grounded interpretations explicitly.

Parameters:

Name Type Description Default
system_prompt str

System message content.

required
user_prompt str

User message content.

required
skill_name str

Name of the skill calling this (for token tracking).

'unknown'

Returns:

Type Description
str

The assistant's response text.

_llm_call_json

_llm_call_json(system_prompt: str, user_prompt: str, *, schema=None, skill_name: str = 'unknown', default=None, expect=None)

Ask for JSON through whatever constraint this endpoint actually honours.

:func:_llm_call is a prose call — an unconstrained invoke that returns text. Five callers were nevertheless asking it for JSON and hand-stripping markdown fences afterwards, which works on an endpoint that enforces a response format and degrades silently on one that does not. Two of them wrote json.loads(x) if x.startswith("[") else [], so a model answering in prose produced an empty result with no error and no warning — on the SEARCH path, among others.

That is not hypothetical. Measured on a live LiteLLM gateway, eight of nine constraint syntaxes were accepted and ignored, and the model answered **Classification:** ... to a request for JSON. These calls also prepend the grounding and style prefixes, which push towards prose.

So the same prompt now goes through the provider dispatcher — the one the documentation generator uses, which honours STRUCTURED_OUTPUT_METHOD (tool calling included) and falls back once to an unconstrained call — and is parsed by _robust_json_parser rather than a local regex. On failure it returns default and says so in the log: a silent empty is the thing being fixed, so it must not be reintroduced here.

Parameters:

Name Type Description Default
schema

plain JSON Schema for the reply, or None to send no constraint (still parsed and logged properly).

None
expect

list/dict the result must be, else default is returned. The shape is part of the contract with the caller.

None
default

returned on any failure. Callers pass their own fallback.

None

get_token_usage_markdown

get_token_usage_markdown() -> str

Get current token usage as markdown for trace display.

Shows the usage of the current turn (since the last mark_turn()), not the cumulative session totals — per-turn numbers are what the trace panel and LLM-as-judge evaluations need. The session total is appended as a single summary line.

get_data_domain

get_data_domain() -> pd.DataFrame

List all DATA_DOMAINs available in the feature store.

Returns:

Type Description
DataFrame

DataFrame with columns DATA_DOMAIN and FEATURE_COUNT.

_canonical_data_domain

_canonical_data_domain(name: Optional[str], available=None) -> Optional[str]

Return the real DATA_DOMAIN that name designates, or None.

The classifier's domain field carries what the user typed ("the "governed demand forecasting m5" data domain"): quoted, re-cased, sometimes with a typo -- or a theme ("pharmacy") that is not a DATA_DOMAIN at all. The catalog filters on the exact stored value, so the name must be mapped to it: exact match ignoring case/quotes first, then a high-cutoff fuzzy match (FUZZY_NAME_MATCH_CUTOFF). A theme maps to None.

detect_data_domain_for_object

detect_data_domain_for_object(object_name: str, prefer_domain: Optional[str] = None) -> Optional[str]

Return the DATA_DOMAIN that contains object_name as a feature name.

Searches the full feature catalog (all data domains). Falls back to the process catalog (VIEW_NAME) if the feature is not found by name.

Parameters:

Name Type Description Default
object_name str

Feature or view name to locate.

required
prefer_domain Optional[str]

When the object exists in multiple domains, return this one if it is among the matches. Used to prevent silent context switches on ambiguous feature names (e.g. "Claims" appearing in multiple feature stores).

None

Returns:

Type Description
Optional[str]

Matching DATA_DOMAIN string, or None if not found.

Cross-store federation limit (v0.3.1.148): the catalog scanned here is the federated UNION across the primary + read-only stores, but resolution returns a bare DATA_DOMAIN with NO store tag (and _with_domain only switches tdfs4ds.DATA_DOMAIN, not the active store). With DISJOINT stores a domain uniquely identifies its store, so this is correct. If two stores share a domain name, _pick returns the first match (primary rows sort first → primary wins); making resolution store-aware is the documented follow-up.

skill_list_data_domains

skill_list_data_domains(object_name: Optional[str] = None) -> dict

Answer questions about the feature store's DATA_DOMAINs.

Two modes:

  • list (object_name empty) — inventory every registered DATA_DOMAIN with its distinct feature count.
  • detail (one or more names in object_name, comma-separated) — for each named domain report its feature count and per-entity breakdown. Names are matched case-insensitively against the registered domains, with typo-tolerant fuzzy fallback (_best_name_similarity gated by tdfs4ds.FUZZY_NAME_MATCH_CUTOFF) mirroring the DEFINITION/FRESHNESS did_you_mean contract.

Returns a dict carrying its own answer (direct-answer skill — no second LLM pass), or a did_you_mean / not_found error payload.

_data_domain_entity_breakdown

_data_domain_entity_breakdown(domain: str) -> dict

Return {entity_name: distinct_feature_count} for one DATA_DOMAIN.

_format_domain_not_found

_format_domain_not_found(not_found: list, known: list, fr: bool) -> str

Render the domain_not_found_did_you_mean answer string.

_build_intent_model

_build_intent_model(intents: list)

Build a dynamic Pydantic model for intent classification from a list of intent names.

Uses Union[Literal["A"], Literal["B"], ...] which equals Literal["A", "B", ...] per PEP 586, constructed at runtime via pydantic.create_model.

Parameters:

Name Type Description Default
intents list

List of uppercase intent names (e.g. ["SEARCH", "DEFINITION"]).

required

Returns:

Type Description

A Pydantic BaseModel subclass with an intent field constrained to

the provided values, plus object_name, domain, confidence,

and interpretations optional fields.

_literal_values

_literal_values(annotation) -> list

Collect the string values out of a Literal/Union[Literal, ...].

_build_intent_model builds Union[Literal["A"], Literal["B"], ...], so the values are one level down from the union. Recursive and total: anything unrecognised yields nothing rather than raising, because the only caller uses this to narrow a prompt and must degrade to "no filtering".

_available_intents_for_examples

_available_intents_for_examples(intent_model, intent_descriptions) -> Optional[list]

The intents the classifier is actually allowed to return, or None.

Read from the model first: that is the authority that will reject a value, so narrowing the prompt from any other source could drift from it. Falls back to the description keys, then to None (= no filtering).

_example_slots

_example_slots() -> dict

Real names from the connected store for the classifier examples.

Read from the turn's catalog snapshot (already paid for by the turn), so it costs nothing extra; deterministic (sorted) so the prompt is stable between turns. Never raises: an unreadable catalog gives {} and every example with a placeholder is dropped.

_render_example

_render_example(text: str, slots: dict) -> Optional[str]

Fill the placeholders of one example, or None when one cannot be filled.

Braces in a store name are doubled: the examples go through a LangChain prompt template.

_build_classifier_examples

_build_classifier_examples(available_intents=None, slots=None) -> str

Build the few-shot classifier examples string from ca-* SKILL.md files.

Examples are read from the '## Classifier Examples' section of each skill's SKILL.md file, making the SKILL.md files the single source of truth. Plugin skills loaded from TDFS4DS_SKILLS_FOLDER are included automatically.

Also injects a low-confidence counter-example to teach the LLM to assign below-0.5 confidence and populate interpretations for bare topic words.

available_intents restricts the examples to intents the classifier can actually return. This is correctness, not tidiness. build_graph drops an unavailable intent from the descriptions and from the dynamic Pydantic Literal, but this function used to walk the catalog unfiltered — so with MCP disabled the prompt still carried five -> DATA_QUERY examples, one of them literally "show me a sample", while the schema forbade that value. The model obeyed the examples, Pydantic rejected the answer, and the turn died with 14 literal_errors. The same leak existed for ALGORITHM, masked only by the deterministic _looks_like_algorithm_question pre-guard.

None (the default) keeps every example, so an existing caller that passes nothing is unchanged.

intent_classifier

intent_classifier(question: str, intent_model=None, intent_descriptions: dict = None, context_hint: Optional[str] = None)

Classify a user question into one of the available intents.

Parameters:

Name Type Description Default
question str

User question in natural language (French or English).

required
intent_model

Pydantic model for structured output. When None, falls back to the hardwired :class:IntentResult covering all 8 intents.

None
intent_descriptions dict

{intent_name: description} dict used to build the system-prompt intent list. When None, falls back to the hardwired descriptions.

None
context_hint Optional[str]

Optional context about recently mentioned features/datasets (e.g., "Last feature: provider_z_score in entity CLAIM_ID"). Added to the system prompt to help resolve vague follow-up questions.

None

Returns:

Type Description

An instance of intent_model (or :class:IntentResult) with

intent, object_name, and domain fields.

_as_of

_as_of() -> Optional[str]

_vt

_vt() -> str

The temporal qualifier of every metadata read of the agent.

_date_in_text

_date_in_text(text: str) -> Optional[str]

First date written in text, as YYYY-MM-DD[ HH:MM:SS], or None.

ISO (2023-06-30, with an optional time), 30 June 2023 / 30 juin 2023, June 30, 2023, and end of June 2023 / fin juin 2023 (last day). A bare year or month is not a date: it is ignored.

_relative_period

_relative_period(text: str)

A span back from now written in text: (n, unit) with unit one of hour, day, week, month, year, else None.

"the latest week" -> (1, 'week'), "last 7 days" -> (7, 'day'), "les 3 derniers mois" -> (3, 'month'), "depuis 2 jours" -> (2, 'day'). A unit word alone ("weekly", "per day") is not a span.

_resolve_as_of

_resolve_as_of(question: str, classifier_value: Optional[str] = None)

The valid time a turn asks for: (literal, clear, unreadable).

literal: a normalised TIMESTAMP literal (store_consistency.normalise_as_of) when the question (or the classifier's as_of) names a date; clear: the question asks about now ("now", "today", "actuellement"), which ends a date set on an earlier turn; unreadable: the classifier named a date nobody could read (the answer says so). Neither: the turn keeps the conversation's date.

_as_of_line

_as_of_line(question: str) -> str

The first line of an answer read at a past valid time ("" when current).

_with_as_of

_with_as_of(as_of: Optional[str])

Context manager: read the metadata at as_of (a normalised literal or None).

_turn_cache_reset

_turn_cache_reset() -> None

Clear the per-turn memo. Call once at the start of each user turn.

_turn_cached

_turn_cached(key, fn)

Return the cached materialized frame for key or compute+store it.

fn must return the already-materialized object (e.g. a pandas DataFrame); caching the lazy tdml.DataFrame would save nothing because re-materializing re-runs the query. key must encode the request scope (id/name set + the active DATA_DOMAIN) so a domain switch mid-graph cannot return a wrong slice. On failure the exception propagates and nothing is cached.

_esc_sql_literal

_esc_sql_literal(s: str) -> str

Escape a single value for use inside a SQL string literal (double quotes).

_in_clause

_in_clause(col: str, values) -> Optional[str]

Build an UPPER(col) IN ('A','B',…) predicate from a str or list.

Returns None when values is falsy / empty so callers can skip the clause. De-duplicates (case-insensitively) and escapes single quotes. A single string still produces a one-element IN list, which is equivalent to =.

_split_object_values

_split_object_values(values) -> tuple

(bare names, {BARE: {DB, ...}} | None per bare name) for business dictionary lookups: DB_SOURCE.MARKET_PRICES is stored as OBJECT_NAME MARKET_PRICES in database DB_SOURCE, and a qualified name matched nothing (0.3.3.11: "tell me about DB_SOURCE.MARKET_PRICES" read as undocumented). A bare name keeps every database (None).

_keep_requested_databases

_keep_requested_databases(df: DataFrame, name_col: str, dbs: dict) -> pd.DataFrame

Rows of df whose (database, name) was asked for (see _split_object_values).

_query_bd_sections

_query_bd_sections(section_names: Optional[list[str]] = None, object_name: Optional[str] = None, object_names: Optional[list[str]] = None) -> pd.DataFrame

Query FS_BUSINESS_DICTIONARY_SECTIONS with optional filters.

Uses execute_sql (not from_query) because CURRENT VALIDTIME must be at the outermost level — from_query wraps in a subquery which breaks it.

object_name (singular, back-compat) and object_names (list, batched) are merged into a single UPPER(OBJECT_NAME) IN (…) predicate so callers can fetch many objects in one round-trip instead of looping.

_query_bd_objects

_query_bd_objects(object_name: Optional[str] = None, object_names: Optional[list[str]] = None) -> pd.DataFrame

Query FS_BUSINESS_DICTIONARY_OBJECTS with optional filter.

object_name (singular, back-compat) and object_names (list, batched) merge into one UPPER(OBJECT_NAME) IN (…) predicate.

_query_bd_columns

_query_bd_columns(table_name: Optional[str] = None, column_name: Optional[str] = None, table_names: Optional[list[str]] = None, column_names: Optional[list[str]] = None) -> pd.DataFrame

Query FS_BUSINESS_DICTIONARY_COLUMNS with optional filter.

Singular kwargs (table_name / column_name, back-compat) and list kwargs (table_names / column_names, batched) merge into UPPER(TABLE_NAME) IN (…) AND UPPER(COLUMN_NAME) IN (…) so all features for a set of views can be fetched in one round-trip; the caller slices the result per (view, column) in pandas.

_snapshot_statements

_snapshot_statements() -> dict

_run_snapshot_request

_run_snapshot_request(statements: dict) -> dict

All statements in one request (one round trip); one by one when the request fails (an optional table missing on an older store).

_store_snapshot

_store_snapshot() -> Optional[dict]

The turn's catalog snapshot, or None (read federation attached, no SCHEMA, or the core catalogs unreadable) - callers then use the accessors.

_accessor

_accessor(name: str)

The public catalog accessor tdfs4ds.<name> (the snapshot's fallback), looked up at call time so tests can stub it.

_snap

_snap(name: str) -> Optional[pd.DataFrame]

_features_df

_features_df(all_data_domains: bool = False) -> pd.DataFrame

feature_catalog(all_data_domains).to_pandas(), from the turn snapshot.

_processes_df

_processes_df(all_data_domains: bool = False) -> pd.DataFrame

process_catalog(all_data_domains).to_pandas(), from the turn snapshot.

_ds_catalog_df

_ds_catalog_df() -> pd.DataFrame

dataset_catalog().to_pandas(), from the turn snapshot.

_ds_features_df

_ds_features_df(dataset_id=None) -> pd.DataFrame

get_dataset_features(dataset_id).to_pandas(), from the turn snapshot.

_ds_entity_df

_ds_entity_df(dataset_id=None) -> pd.DataFrame

get_dataset_entity(dataset_id).to_pandas(), from the turn snapshot.

_followup_at

_followup_at(df: DataFrame) -> pd.DataFrame

At a past valid time, only the runs started by then ("what was known then"). FS_FOLLOW_UP is an event log, not a temporal table: filtered on its run time.

_followup_df

_followup_df(process_ids) -> pd.DataFrame

Run history (FS_FOLLOW_UP) of the given processes, newest first, as one raw-SQL read memoized per turn. follow_up_report_many(...).to_pandas() returned the same rows but cost ~3 s of teradataml metadata calls.

_all_processes_cached

_all_processes_cached() -> pd.DataFrame

Every process of every domain, for whole-store scans (find-object-by- name, the allow/forbid set). Served from the turn snapshot.

_all_features_cached_now

_all_features_cached_now() -> pd.DataFrame

Every feature of the current state, whatever the turn's valid time (to name a domain the store has today in an answer about a past date).

_all_features_cached

_all_features_cached(all_data_domains: bool = True) -> pd.DataFrame

The feature catalog for whole-store scans, served from the turn snapshot.

_processes_by_id

_processes_by_id(process_ids) -> pd.DataFrame

Return PROCESS_ID → VIEW_NAME (+ ENTITY_ID, FEATURE_NAMES) for given ids.

Scoped, memoized replacement for process_catalog(all_data_domains=True) .to_pandas() used only to map a handful of process_ids to their view name. Issues one WHERE PROCESS_ID IN (…) query against the public process catalog view (which already applies VALIDTIME), so it materializes only the turn's rows. Cached per-turn on the id-set. Fail-open: returns an empty frame on error (callers already tolerate a missing view name).

_distinct_feature_count

_distinct_feature_count(df, group_by: Optional[str] = None)

Count distinct (DATA_DOMAIN, ENTITY_NAME, FEATURE_NAME) feature identities.

A feature's identity in tdfs4ds is that triplet: the same name registered under two entities, or in two data domains, is two distinct features (the domain is used when the frame carries a DATA_DOMAIN column). This helper is robust to duplicate rows (e.g. a feature appearing in several datasets) and to frames that expose ENTITY_ID instead of ENTITY_NAME.

Cross-store federation limit (v0.3.1.148): the identity key does NOT include a store dimension. With read-only stores holding DISJOINT domains/entities/names (the common case) this is exact. Only when two stores independently reuse the SAME (DATA_DOMAIN, ENTITY_NAME, FEATURE_NAME) do their rows collapse to one — an undercount. Threading the FEATURE_STORE provenance column into the key here (and into domain resolution / the context-graph keys) is the documented follow-up for full collision-proof multi-store counting.

Parameters:

Name Type Description Default
df

pandas DataFrame containing at least FEATURE_NAME and an entity column (ENTITY_NAME preferred, ENTITY_ID accepted).

required
group_by Optional[str]

When given, return a {group_value: count} dict keyed by that column.

None

Returns:

Type Description

int when group_by is None, else a dict.

_feature_rows_from_snapshot

_feature_rows_from_snapshot(domain, entity_filter, feature_filter, out_cols)

_get_feature_catalog_with_docs from the turn snapshot, or None.

Same resolution as the per-entity path: a feature's process is a current process of the same data domain whose ENTITY_ID equals the feature's ENTITY_NAME and whose FEATURE_NAMES lists it (get_feature_versions's join, case-insensitive like the NOT CASESPECIFIC columns, entity columns in any order), picked deterministically when several (versions); its description is the business-dictionary column of that view.

_get_feature_catalog_with_docs

_get_feature_catalog_with_docs(feature_filter: Optional[str] = None, entity_filter: Optional[str] = None) -> pd.DataFrame

Return a DataFrame of features enriched with their process-level documentation.

Resolution chain (batched — see below): 1. Feature catalog (active domain) → all (ENTITY_NAME, FEATURE_NAME) pairs in one materialization; filters applied in pandas. 2. get_feature_versions(entity, features) once per entity (each call already batches all the entity's features via FEATURE_NAME IN (…)) → process_id. 3. _processes_by_id(all_process_ids) — one scoped PROCESS_ID IN (…) query → VIEW_NAME (replaces the old full all-domains process_catalog load). 4. _query_bd_columns(table_names=all_views, column_names=all_features) — one scoped query → BUSINESS_DESCRIPTION, sliced per (VIEW_NAME, FEATURE_NAME) in pandas.

The previous implementation issued one get_list_features per entity and one _query_bd_columns per feature (plus a whole all-domains process_catalog materialization), i.e. tens of round-trips for a multi-entity domain. This version is ~3 scoped round-trips total.

Parameters:

Name Type Description Default
feature_filter Optional[str]

If given, only return rows for this exact feature name (case-insensitive).

None
entity_filter Optional[str]

If given, only process this exact entity name (case-insensitive). Speeds up entity-scoped lookups.

None

Returns:

Type Description
DataFrame

DataFrame with columns:

DataFrame

FEATURE_NAME, ENTITY_NAME, PROCESS_ID, VIEW_NAME, BUSINESS_DESCRIPTION

_build_multi_object_context

_build_multi_object_context(feature_names: list, hint_entity_name: Optional[str] = None, accumulated_context: Optional[dict] = None) -> dict

Build a shared context graph for multiple features in one pass.

Calls _get_feature_catalog_with_docs() once (no filter) then: - Resolves each name to (entity, process_id, view_name, description) - Fetches BD_SECTIONS + all BD_COLUMNS for each unique VIEW_NAME (once each) - Fetches dataset membership for each feature via get_dataset_features() - Fetches BD_OBJECTS + BD_SECTIONS for each unique dataset (once each)

Returns:

Type Description
dict

{ "features": [{name, entity_name, process_id, view_name, description, found_in_catalog, has_description, dataset_names}], "shared_views": {view_name: {"sections": [...], "columns": [...]}}, "shared_datasets": {ds_name: {"database", "bd_description", "sections"}}, "entity_names": [str],

dict

}

_process_for_view_name

_process_for_view_name(name: str) -> Optional[dict]

{process_id, view_name, entity, domain, feature_names} of the current process whose view is name (bare or db.view, quotes ignored), else None.

_recognize_object_type

_recognize_object_type(name: str, concepts: bool = False) -> dict

Identify what type of object name is by querying catalogs.

Returns a dict with keys: type ("feature"|"dataset"|"entity"|"data_domain"|"process"|"unknown"), entity_name, view_name, process_id.

concepts: when no catalog name matches exactly, also try what the phrase refers to (:func:_recognize_concept: a dataset by its name's words or its documentation, or features by their description). concepts="datasets": datasets only, no feature-documentation step (no vector search). Exact names always win.

_known_catalog_names

_known_catalog_names() -> set

Return the union of registered feature, dataset and BD object names (uppercase).

Used to filter vector-search hits so that fabricated names (embedding-similar but not actually registered) never reach the LLM context. Empty set on failure means no filtering is applied (fail-open).

_label_match_domains

_label_match_domains(matches, feature_cat) -> None

Set data_domain on each feature match whose name belongs to one data domain (in place).

0.3.5.14: features of two domains were listed together, as if one use case (seen live). A dataset's lag / lead column (N_EVENTS_LAG_1MO) takes its base feature's domain; an ambiguous or unknown name stays unlabelled.

_dataset_offset_column_names

_dataset_offset_column_names() -> set

Upper-case names of the datasets' offset columns (N_EVENTS_LAG_1MO); empty when unreadable.

_vector_search_context

_vector_search_context(query: str, top_k: int = 5, results=None) -> tuple[str, Optional[str]]

Build LLM context from vector search results when SQL exact match fails.

Vector hits whose OBJECT_NAME is not present in the feature / dataset / process catalogs are dropped — embedding similarity alone is not enough proof that an object exists, and surfacing unverified names leads to fabricated feature suggestions. Documented data outside the feature store is reported separately (:func:_outside_store_hits), never mixed in here.

Parameters:

Name Type Description Default
query str

Free-text search query (object name or natural language question).

required
top_k int

Maximum results per collection.

5
results

Hits already read by search_vector_index (one search per turn).

None

Returns:

Type Description
tuple[str, Optional[str]]

Tuple of (context_string, resolved_object_name). Both may be empty/None.

_is_source_data_question

_is_source_data_question(question: str) -> bool

A discovery question about documented DATA ("which table holds X?"), with no feature-store word in it. Deterministic; used to route such a question to SEARCH whatever the classifier said.

_asks_feature_store_only

_asks_feature_store_only(question: str) -> bool

The question names feature-store content (features, datasets) and no source data.

_fs_scope_index

_fs_scope_index() -> dict

documentation_export.feature_store_index over the turn's catalogs (fail-open: empty).

_attached_index_cached

_attached_index_cached() -> dict

{(DB, OBJ): 'V' | 'T'} of the attached objects, once per turn (0.3.5.2). Fail-open.

_object_scope

_object_scope(db, name, object_type=None) -> tuple

(scope, kind): 'feature store' or 'outside the feature store', and the kind.

_is_outside_store

_is_outside_store(db, name, object_type=None, index: Optional[dict] = None) -> bool

_feature_processes_reading

_feature_processes_reading(db, name) -> list

Process views reading db.name (lineage), [] when unknown (fail-open).

_outside_store_hits

_outside_store_hits(results, max_objects: int = OUTSIDE_STORE_MAX_OBJECTS) -> list

Vector hits on documented objects OUTSIDE the feature store, grouped by object.

[{database, object, kind, description, columns: [{name, description}], sections: [{name, content}], read_by: [...], store}], best match first. The object's own description is read from the business dictionary when only a column or a section matched. Fail-open: [].

_token_outside_hits

_token_outside_hits(question: str, max_objects: int = OUTSIDE_STORE_MAX_OBJECTS) -> list

Without a vector index: documented objects outside the store whose name, description or column descriptions share a word with the question.

_outside_store_answer

_outside_store_answer(hits: list, question: str, *, inside_found: bool) -> str

The fixed EN/FR block naming documented data outside the feature store.

_attached_hits_block

_attached_hits_block(hits: list, fr: bool) -> str

The fixed EN/FR block for attached views / tables found by a search (0.3.5.2).

_outside_store_hint

_outside_store_hint(hits: list, question: str) -> str

One line naming outside objects, for a question about the feature store's content.

_scope_line

_scope_line(db, name, question: str, *, column: Optional[str] = None) -> str

A fixed sentence for an object (or one of its columns) OUTSIDE the store, else ''.

'' too when the catalogs could not be read (inside and outside cannot be told apart).

_graph_neighbours

_graph_neighbours(db, name, direction: str) -> list

['DB.OBJ'] read by (out) or reading (in) db.name in the lineage graph (edges go consumer -> source). Fail-open: [].

_attached_line

_attached_line(db, name, kind, fr: bool, column=None) -> str

The fixed sentence for an object INSIDE the store (0.3.5.2): an attached object names what it reads (feature-store objects marked); a dataset or process view read by attached views names them. '' otherwise.

_feature_dataset_names_map

_feature_dataset_names_map() -> tuple

Return (by_entity, by_name) feature→dataset maps for the current domain.

Reverse of the dataset catalog: maps every registered feature to the datasets that expose it, so the knowledge graph can draw feature→dataset edges (otherwise features render as isolated nodes after a SUMMARY).

A feature's identity is the pair (FEATURE_NAME, ENTITY_NAME) — the same name registered under two entities is two distinct features. The dataset _FEATURES table carries FEATURE_ID (the feature's identity), which the feature catalog resolves to the authoritative ENTITY_NAME. We therefore key the primary map on (ENTITY_UPPER, FEATURE_NAME_UPPER) so a dataset is linked only to the entity it actually exposes — never to a same-named feature under a different entity.

Returns a tuple
  • by_entity — {(ENTITY_UPPER, FEATURE_NAME_UPPER): [dataset_name, ...]}
  • by_name — {FEATURE_NAME_UPPER: [dataset_name, ...]} fallback, used only for features whose FEATURE_ID has no catalog match (entity-less / cross-domain).

Three bounded materializations (dataset features, feature catalog, dataset catalog), joined on FEATURE_ID in pandas (no per-row DB query); memoized per turn; fail-open to empty maps (no dataset linkage).

_dataset_time_info

_dataset_time_info(dataset_id: str, database: str = '', name: str = '') -> dict

The data time of one dataset: {kind, as_of, tm_view, business_date, window_text, window_start, window_end, source}. kind is current, as_of, time_manager, history or unknown. Memoized per turn, never raises.

Sources, in order: the build/window rows read straight from their PUBLIC views (never through DatasetCatalog, whose constructor creates missing objects - a legacy store has no build table and must stay untouched); then the view's own DDL (parse_dataset_ddl), which is how a dataset registered before the build table existed is read; else unknown.

_dataset_time_line

_dataset_time_line(info: Optional[dict], question: str = '') -> str

One sentence stating a dataset's data time; '' for a current snapshot.

_dataset_time_line_core

_dataset_time_line_core(info: dict, fr: bool) -> str

_dataset_time_tag

_dataset_time_tag(info: Optional[dict]) -> str

Short tag for dataset listings: current / as of / TimeManager / history.

_resolve_dataset

_resolve_dataset(object_name: str) -> Optional[dict]

Look up object_name in the dataset catalog.

Accepts "database.name" or "name" (case-insensitive).

Returns {dataset_id, dataset_name, dataset_database} or None.

_dataset_features_by_id

_dataset_features_by_id(dataset_id: str, ds_features: DataFrame) -> Optional[list]

A dataset's features resolved by identity, or None to use the name fallback.

A dataset feature row carries its FEATURE_ID, which identifies the feature (data domain, entity, name) in the catalog, whatever the active domain. Matching on the name alone took the first feature of that name in the active domain: a dataset of another domain whose feature names also exist elsewhere got the wrong entity, process and description (seen live: a one-entity dataset reported as two entities, with processes of another domain).

The process is the one of that domain and entity whose feature list names the feature; when several do, the one the dataset view names (FEATURE_VERSION).

_get_dataset_with_features

_get_dataset_with_features(dataset_id: str) -> list

Return a list of feature dicts for dataset_id, enriched with process view and docs.

Each item: {feature_name, entity_name, view_name, feature_type, description}.

_tokenize_question

_tokenize_question(question: str) -> set[str]

Return lowercase non-stop-word tokens from the question (used to pre-filter catalog).

_depluralize

_depluralize(token: str) -> str

Cheap, dependency-free English depluralization for catalog token matching.

'pharmacies'→'pharmacy', 'providers'→'provider', 'boxes'→'box', 'claims'→'claim'. Leaves 'ss' words (e.g. 'class') and short words unchanged.

_normalize_tokens

_normalize_tokens(tokens: 'set[str]') -> 'set[str]'

Return the tokens plus their depluralized singular forms (both kept).

_row_token_set

_row_token_set(feature_name: str, domain: str = '', entity: str = '') -> 'set[str]'

Split a catalog row's name/domain/entity into lowercase word tokens.

Names use _ separators (pharmacy_z_score → {pharmacy, z, score}); the full joined name is kept too so substring/prefix checks still work.

_question_matches_row

_question_matches_row(question_tokens_norm: 'set[str]', row_tokens: 'set[str]') -> int

Number of distinct normalized question tokens that match a row's tokens.

A token matches when it equals a row token, or is a prefix of one / has one as a prefix (bidirectional, e.g. provider ↔ provider_id). Tokens of length ≤2 only count on exact equality to avoid noise.

_truncate_on_word

_truncate_on_word(text: str, max_chars: int) -> str

Truncate text to ~max_chars, cutting on a word boundary and appending '…'.

Returns the text unchanged when it already fits or max_chars <= 0 (disabled).

skill_search(question: str) -> dict

Find features or datasets matching a business need expressed in natural language.

Parameters:

Name Type Description Default
question str

User question describing the business need.

required

Returns:

Type Description
dict

Dict with key matches: list of {name, type, justification}.

_description_claims_higher_is_riskier

_description_claims_higher_is_riskier(business_description: str) -> bool

True when the description asserts that higher values mean more risk.

Deterministic regex over the stored business description; used to decide whether an inverted-score formula contradicts the prose.

_deep_tag_framing

_deep_tag_framing(name: str, db: str, table: str, question: str = '') -> Optional[str]

One sentence saying name is a tag of a deep (historian) table, or None.

None when the table has no deep layout or name is one of its physical columns, so every other column answer is unchanged.

_direction_correction_note

_direction_correction_note(business_description: str, calculation_sql: Optional[str]) -> Optional[str]

Return a clarification note when the stored description's risk direction contradicts an inverted calculation formula, else None.

The DEFINITION skill trusts the stored business_description; when the actual SQL inverts the score (1 - (...), so a lower value means higher risk) but the prose claims "higher = riskier", we append an authoritative correction rather than silently surfacing the contradiction (which EXPLAIN, reading the live SQL, would expose).

skill_definition

skill_definition(question: str, object_name: str, hint_entity_name: Optional[str] = None, hint_table_name: Optional[str] = None, accumulated_context: Optional[dict] = None, _fuzzy_corrected: bool = False) -> dict

Explain what a feature, column, or dataset is in plain business language.

Parameters:

Name Type Description Default
question str

User question.

required
object_name str

Name of the feature, column, or dataset to define.

required
hint_entity_name Optional[str]

Entity name remembered from a previous turn — used to auto-disambiguate when the same feature name exists across entities.

None
hint_table_name Optional[str]

Source table/view remembered from a prior EXPLAIN turn — used to narrow column-level BD lookups to the right table when the column is not a registered feature.

None

Returns:

Type Description
dict

Dict with object_name, explanation, and columns.

_owner_words

_owner_words(name: str) -> set

Meaningful words of a table name: HCF_FIBER_VW -> {HCF, FIBER}.

_pick_column_owners

_pick_column_owners(rows: list, question: str, hint_table: Optional[str] = None, asked: Optional[list] = None) -> list

The documented owners of a column the question means (pure).

rows: [{db, table, description}] of one column. An owner named in the question (HCF_ACCESS.HCF_FIBER_VW, or a word of it: "the Historian") wins, then the table remembered from an earlier turn, else every owner. Words of the qualified names and of the asked column names do not count as owner words (HCF of HCF_ACCESS.HCF_FIBER_VW is not a hint for another HCF table). Owners with the same description are kept once (a view exposing its table repeats it).

_join_predicate_between

_join_predicate_between(sql: str, col_a: str, col_b: str) -> Optional[str]

The SQL text comparing col_a and col_b (x.A = y.B or the reverse), else None (pure).

_documented_columns_context

_documented_columns_context(names: list, question: str, column_sources: Optional[dict] = None) -> dict

Documentation of names that are columns of documented tables / views (not features).

Returns {"found": {NAME_UPPER: [owner rows]}, "lines": [...], "scope": [...], "joins": [...], "columns": [...]}. One business-dictionary read for all names, one for the owners' descriptions; join evidence reads the SQL of the processes reading both owners. Fail-open: an empty result when nothing can be read.

skill_definition_multi

skill_definition_multi(question: str, feature_names: list, hint_entity_name: Optional[str] = None, accumulated_context: Optional[dict] = None, column_sources: Optional[dict] = None) -> dict

Unified multi-feature definition using a shared context graph.

Calls _build_multi_object_context() once to resolve all features and fetch shared documentation (process views, datasets, entities), then makes a single LLM call with the full unified context. No consolidation pass needed.

skill_usage

skill_usage(question: str, object_name: str, accumulated_context: Optional[dict] = None, entity_name: Optional[str] = None) -> dict

Advise a consumer on how to safely use a feature in their work.

Enumerates ALL datasets that expose the feature (not just one) and emits a paste-ready SELECT snippet for each so the consumer can pick the right view for their dashboard.

Implements fast-path caching: queries accumulated_context_graph first (cache hit → return immediately), falls back to catalog scans (cache miss).

Parameters:

Name Type Description Default
question str

User question about usage (Tableau, Excel, regulatory, etc.).

required
object_name str

Name of the feature or dataset.

required
accumulated_context Optional[dict]

The accumulated_context_graph from state (used for cache).

None
entity_name Optional[str]

Entity name for disambiguation (used in cache lookups).

None

Returns:

Type Description
dict

Dict with object_name, guidance, datasets (list of

dict

{dataset_name, database, dataset_id, select_sql, entity_keys}),

dict

_from_cache (bool), and docs_sources.

_dataset_freshness_note

_dataset_freshness_note(time_info: Optional[dict], followup, question: str = '') -> str

What 'up to date' means for this dataset's own time ('' for a current one).

A dataset frozen at a date does not change when its processes run again; one following a TimeManager is stale when its producers have not completed a run for the manager's current business date; a history ends at its window end.

_usage_select

_usage_select(entity_keys: list, feature: str, fq: str, time_info: Optional[dict]) -> str

Paste-ready SELECT on a dataset. A historical dataset returns one row per entity and period, so its snippet shows the period and how to read one instant.

skill_freshness_for_feature_list

skill_freshness_for_feature_list(question: str, feature_names: list, entity_name: Optional[str] = None) -> dict

Aggregate freshness across a list of feature names referenced by a group pronoun.

Used when the user says "are these features up to date?" without naming any of them explicitly. Looks each feature up in the catalog, collects its process_id, pulls follow_up_report for each, and returns a combined last-successful-run / days-since / failure verdict.

_tm_position

_tm_position(process_id: str, last_business_date) -> Optional[dict]

Where a process stands against its TimeManager (0.3.1.240).

{"manager": "db.view", "business_date": "YYYY-MM-DD", "behind": n} - behind is the number of the manager's dates after the last one the process completed, up to the current one (0 = up to date). None when the process has no recorded TimeManager or it cannot be read. Two small reads, memoized per turn.

_process_view_names

_process_view_names(process_ids) -> dict

{PROCESS_ID: bare view name} from the turn's process catalog (never raises).

_business_date_lines

_business_date_lines(process_ids, followup, question: str = '', views: Optional[dict] = None)

Deterministic freshness lines, one per process, in business dates.

Returns (lines, facts); facts maps PROCESS_ID to {"last_business_date", "last_run_at", "last_failure_at", "time_manager"}.

_freshness_segments

_freshness_segments(followup) -> list

Per-segment freshness when a process runs segmented (FilterManager).

Groups the follow-up rows by their APPLIED_FILTER segment and returns one freshness record per distinct segment. Business dates (VALIDTIME_DATE) are not segments: they are reported by _business_date_lines (0.3.1.240). Returns: [{segment, last_successful_run, days_since_update, has_recent_failure}], sorted stalest-first. Returns [] when there is a single segment / no FilterManager (caller keeps the existing global aggregation unchanged).

skill_freshness

skill_freshness(question: str, object_name: str, hint_entity_name: Optional[str] = None, accumulated_context: Optional[dict] = None, _fuzzy_corrected: bool = False) -> dict

Tell the consumer whether a feature or dataset is up to date and reliable.

Parameters:

Name Type Description Default
question str

User question about freshness / reliability.

required
object_name str

Name of the feature or dataset.

required
hint_entity_name Optional[str]

Entity name remembered from a previous turn — used to auto-disambiguate when the same feature name exists across entities.

None

Returns:

Type Description
dict

Dict with object_name, last_successful_run, days_since_update,

dict

has_recent_failure, and reliability_assessment.

_summary_cache_hit

_summary_cache_hit(acc: Optional[dict], domain: Optional[str], entity_filter: Optional[str], object_name: Optional[str]) -> Optional[dict]

Return the prior turn's cached summary payload when it covers THIS request.

A hit requires: no explicit object named this turn (a named dataset/entity must re-resolve), and the prior _last_summary was taken for the same (domain, entity_filter) scope. Returns the cached payload dict or None.

_list_max_items

_list_max_items() -> int

tdfs4ds.AGENT_LIST_MAX_ITEMS as a non-negative int (0 = no cap).

_is_large_listing

_is_large_listing(total) -> bool

_first_sentence

_first_sentence(text, limit: int = 200) -> str

First sentence of text, cut on a word boundary at limit characters.

_process_docs

_process_docs(process_ids) -> dict

{PROCESS_ID: {"logic": ..., "entity": ...}} from the stored process documentation (business logic + entity description), one batched read, memoized per turn. Fail-open: {}.

_select_topic_features

_select_topic_features(question: str, frame: DataFrame, scope_label: str = '') -> dict

Does the question restrict the list to a subject? If so, which features of frame match it? {"topic", "selected": [names], "dropped": [names]}.

The LLM sees every feature of the scope (name, entity, description) in batches of _TOPIC_BATCH; the first batch decides whether there is a subject at all (one call when there is none). Names it returns that are not in the frame are dropped and reported, never shown. Fail-open: no subject.

_fr_de

_fr_de(label: str) -> str

French 'de' + label with the contraction: de le -> du, de les -> des.

_process_groups

_process_groups(frame: DataFrame) -> list

[{view, process_id, entity, names}] largest first (ties by view name).

Features with no producer are not a process: they form the group whose view is None (rendered "without a producer", never counted).

_scope_counts

_scope_counts(domain=None, entity=None, process_ids=None) -> Optional[dict]

The Report's counts (store_consistency.catalog_counts) for a scope, from the turn snapshot. process_ids restricts the processes (a dataset scope). None when the catalogs cannot be read: the caller counts from its frame.

_md_cell

_md_cell(text) -> str

_large_listing_answer

_large_listing_answer(question: str, scope_label: str, frame: DataFrame, *, full_list: bool = False, documented_themes: str = '', scope_is_process: bool = False, counts: Optional[dict] = None, group_scope: Optional[tuple] = None) -> dict

The feature inventory of a scope, rendered by code (0.3.1.240).

Same store, same question: the same answer. The LLM never writes the list nor a number; it only (a) decides whether the question narrows the list to a subject (_select_topic_features, names checked) and (b) proposes semantic groups, once per feature set, stored and reused (feature_groups).

  • up to AGENT_LIST_MAX_ITEMS features (or full_list): a table per entity, business meaning (first sentence of the documented description) and technical name;
  • above: semantic groups (label, one sentence, technical names) for the scope group_scope = (domain, entity); without them, the producing processes.

counts are catalog_counts for the scope (the Report's numbers); without them the processes are counted from frame.

Returns {"answer", "topic", "topic_dropped", "groups", "selected_frame"}.

_apply_listing

_apply_listing(result: dict, listing: dict) -> dict

Merge a _large_listing_answer into a skill_summary result: the answer, and - after a subject narrowed the list - the selected features as the turn's feature list (so a follow-up 'are these up to date?' is about them).

_entity_domains

_entity_domains(entity: str) -> tuple

(entity as stored, [data domains having features for it]). Fail-open: (entity, []).

_entity_named_in

_entity_named_in(question: str) -> Optional[str]

The one entity of the store the question names (_best_entity_match), or None.

_summary_entity_across_domains

_summary_entity_across_domains(question: str, entity: str, accumulated_context: dict = None) -> Optional[dict]

The feature list of entity in each data domain that has it, or None.

One domain: that domain's entity summary. Several: one section per domain (largest first), since the same entity in two domains is two sets of features. None when no domain has the entity (the caller keeps its path).

skill_summary

skill_summary(question: str, domain: str = None, object_name: str = None, entity_filter: str = None, accumulated_context: dict = None, _entity_fanned: bool = False) -> dict

Give a high-level summary of available features for a domain or a specific dataset.

When object_name resolves to a registered dataset, the response is scoped to that dataset's feature list and the resolved dataset context is returned in the result so the caller can persist it in agent state for follow-up EXPLAIN turns.

When entity_filter is given (or object_name resolves to an entity type), the feature list is scoped to that single entity — answering "what features belong to X?".

Parameters:

Name Type Description Default
question str

User question.

required
domain str

Therapeutic domain to filter by (optional).

None
object_name str

Dataset, entity, or feature name mentioned by the user (optional).

None
entity_filter str

Entity name to scope the feature list (optional).

None

Returns:

Type Description
dict

Dict with domain, total_features, by_granularity, themes_summary.

_entity_in_session_domain

_entity_in_session_domain(entity: str, domain: Optional[str] = None) -> bool

domain (default: the session's DATA_DOMAIN) holds features of entity (its exact key set).

_domain_scope_label

_domain_scope_label(domain, question) -> str

_dataset_frame

_dataset_frame(features: list) -> pd.DataFrame

A dataset's features as the listing frame. _get_dataset_with_features has no PROCESS_ID: it is taken from the domain catalog by (entity, name).

_summary_for_process

_summary_for_process(question: str, view_name: str, domain=None) -> dict

Every feature a process computes, listed deterministically (a process is the finest group, so the whole list is shown even above the cap). A subject in the question narrows it, as for any scope.

skill_help

skill_help(question: str = '') -> dict

Explain the feature store's own conceptual model (direct-answer, no DB query).

For a confused user asking "what is a feature vs a non-feature?", "what is a data domain?", "how is the feature store organized?", "what can I ask you?". Returns a fixed, grounded explanation (EN/FR) anchored on the certainty distinction between a feature (rich, governed column with a known entity key) and a non-feature column (free; relationships only documentation-inferable, never certain).

skill_recap

skill_recap(accumulated_context: dict) -> dict

Recap the features, entities, and datasets from the current conversation.

Reads directly from the accumulated_context_graph (MemorySaver-persisted state) instead of querying Teradata. Returns a structured dict grouping features by domain and entity for synthesis into markdown.

Parameters:

Name Type Description Default
accumulated_context dict

The accumulated_context_graph dict from agent state.

required

Returns:

Type Description
dict

Dict with by_domain, dataset_names, total_features, etc.

_get_process_view

_get_process_view(object_name: str)

Return the process VIEW_NAME registered for object_name (feature or view).

Resolution order: 1. _get_feature_catalog_with_docs chain (join feature → process catalog) → VIEW_NAME 2. process_catalog (all domains) → FEATURE_NAMES comma list match → VIEW_NAME 3. process_catalog (all domains) → exact VIEW_NAME match

Parameters:

Name Type Description Default
object_name str

Feature name or view name.

required

Returns:

Type Description
  • str VIEW_NAME on a unique match.
  • list[str] of ENTITY_NAME values when the feature matches multiple entities (caller must handle as an ambiguous-feature error).
  • None if not found at all.

_extract_formula_identifiers

_extract_formula_identifiers(formula_sql: str) -> list

Extract distinct column/alias identifiers from a SQL expression.

Strips literals, SQL keywords, function names and numeric tokens to return only identifiers that look like column or alias references. Used to cross-check the LLM's source_columns list for completeness.

Parameters:

Name Type Description Default
formula_sql str

SQL expression string (e.g. the body of a CASE WHEN).

required

Returns:

Type Description
list

Sorted list of distinct identifier strings (case-preserved as in formula).

_extract_ddl_tables

_extract_ddl_tables(ddl: str) -> set

Extract all base table/view identifiers referenced from FROM/JOIN clauses in ddl.

Returns a set of bare (unquoted, uppercase) identifiers — used as a cheap sanity check that the LLM's source_table claims correspond to a real relation the view actually reads from.

_extract_ddl_table_aliases

_extract_ddl_table_aliases(ddl: str) -> dict

Map {alias_lower: REAL_TABLE_UPPER} for FROM/JOIN <table> <alias> clauses.

ONLY records an alias when the relation after FROM/JOIN is a real (schema-qualified or bare) table — NOT a subquery. A subquery wrapper (FROM ( … ) alias) has a ( after FROM/JOIN, so it is skipped and its alias (e.g. ps, phs, and the outer base_data/flags wrappers) is deliberately NOT mapped to any table. Used to resolve an LLM-claimed source_table that is actually a table alias back to the real underlying table, and to detect subquery aliases (absent here) so a bogus "(from )" annotation can be dropped.

_resolve_source_table_alias

_resolve_source_table_alias(alias: Optional[str], ddl: Optional[str]) -> Optional[str]

Resolve an LLM-claimed source_table to the real base table, or signal "drop".

Returns:

Type Description
Optional[str]
  • the input unchanged when there is no DDL (fail-open — can't prove it bogus);
Optional[str]
  • the same name (uppercased bare) when it IS a real FROM/JOIN base table;
Optional[str]
  • the mapped real table when it is a table alias (FROM claims_raw c → c);
Optional[str]
  • None when it is a subquery/wrapper alias (e.g. flags, base_data, ps) — the caller should drop the "(from )" annotation.

_extract_ddl_columns

_extract_ddl_columns(ddl: str) -> set

Extract identifiers that look like real columns referenced in ddl.

Keeps the column in alias.col (and drops the bare alias), then returns the set of remaining whole-word identifiers (uppercased). Used to decide whether a formula identifier missing from the LLM's source_columns is a genuine base/output column (worth surfacing) or a subquery-derived temporary / alias that only exists in an intermediate scope (drop it).

_validate_source_columns

_validate_source_columns(source_columns: list, formula_sql: Optional[str], ddl: Optional[str]) -> tuple[list, list]

Validate LLM-produced source_columns against the formula and DDL.

Returns (augmented, missing_identifiers) where: * augmented = source_columns annotated with source_confidence ('high' when the source_table appears in the DDL's FROM/JOIN set, 'low' otherwise) * missing_identifiers = formula identifiers that are NOT present in source_columns AND plausibly are real columns (so the caller can surface them as UNKNOWN). Single/double-character tokens (SQL aliases like p, s) and subquery-derived temporaries that never appear in the DDL body (e.g. mn/mx computed inside an inner SELECT) are dropped so they are not mis-reported as drillable source variables.

_split_select_items

_split_select_items(select_body: str) -> list

Split a SELECT list into items at top-level commas (paren-depth aware).

Nested CASE/window/function expressions contain commas; only commas at paren depth 0 separate select items. Used to build the <expr> AS <alias> map for the formula-column closure.

_governing_clause_identifiers

_governing_clause_identifiers(expr: str) -> list

Identifiers from the columns that select WHICH ROWS / ORDERING a derived feature is computed over — i.e. window PARTITION BY / ORDER BY keys and GROUP BY keys.

These are the inputs that are NOT visible from the feature's arithmetic yet materially change its value (e.g. a duplicate-detection flag defined via ROW_NUMBER() OVER (PARTITION BY patient_id, drug_code ORDER BY claim_date) — the grain keys decide what counts as a duplicate). It deliberately does NOT pull CASE/WHERE threshold operands: those re-reference the same arithmetic as the derived feature itself (already listed as a variable), so surfacing them would re-introduce the over-listing this prune exists to remove.

_build_alias_expr_map

_build_alias_expr_map(ddl: str) -> dict

Map {alias_lower: defining_expression} for every <expr> AS <alias> in ddl.

Scans all SELECT segments (paren-aware) so derived columns at any nesting level are captured. Used by the formula-column closure to walk from a derived feature to the governing columns of its definition.

_formula_column_closure

_formula_column_closure(calculation_sql: Optional[str], ddl: Optional[str])

Transitive closure of the columns that materially feed calculation_sql.

Starts from the identifiers in the formula, then for each identifier that is a DERIVED column (<expr> AS <alias> in the DDL) pulls in the identifiers of its GOVERNING clauses (window/CASE/WHERE/GROUP BY/HAVING) — iterating to a fixpoint. This keeps columns that legitimately drive the formula through clauses (e.g. patient_id, drug_code, claim_date behind a ROW_NUMBER() OVER (PARTITION BY patient_id, drug_code ORDER BY claim_date) flag) while NOT pulling in the plain arithmetic operands of an already-listed derived feature.

Returns a set of lowercased column names, or None (= "do not prune") when either input is falsy — fail-open so a missing formula/DDL never hides variables.

_prune_source_columns

_prune_source_columns(source_columns: list, closure) -> list

Keep only source_columns whose name is in closure (None ⇒ keep all).

Fail-open: when closure is None (no formula/DDL) the list is returned unchanged, so pruning never removes a legitimately-listed variable.

_best_name_similarity

_best_name_similarity(tokens: list, known_names) -> dict

Return {token: (best_score, best_name)} — the closest catalog name and its similarity score for each token.

Size-adaptive backend (the candidate set drives the choice, per design): - small set (len(known_names) <= tdfs4ds.FUZZY_INDB_THRESHOLD): Python difflib.SequenceMatcher ratio — no DB round-trip, ideal for the small per-turn-cached catalogs the consumer agent uses (e.g. 52 names); - large set: Teradata in-database StringSimilarity (jaro_winkler), scored set-based in one query — scales to thousands of names.

known_names is an iterable of catalog names (any case). Matching is case-insensitive. Fail-open: on any error returns {} (caller treats as no fuzzy match, i.e. exact-only).

_best_name_similarity_difflib

_best_name_similarity_difflib(tokens: list, names: list) -> dict

Python-difflib backend for _best_name_similarity (small candidate sets).

_best_name_similarity_indb

_best_name_similarity_indb(tokens: list, names: list) -> dict

Teradata StringSimilarity (jaro_winkler) backend for large candidate sets.

Builds an inline (token, name) cross-product as the table-operator input and keeps the max jaro_winkler score per token. Falls back to difflib on any SQL error (fail-open via the caller's try/except).

_entity_name_pool

_entity_name_pool() -> list

Return the distinct ENTITY_NAME values across all data domains (cache-backed).

Reads the per-turn-cached feature catalog (_all_features_cached) and returns the unique non-empty ENTITY_NAME values. Fail-open: empty list on any error.

_canonicalize_name

_canonicalize_name(name: str, kind: str) -> str

Return the canonical catalog spelling of name, correcting a single typo.

Conservative, single-high-confidence typo correction used at the classify seam so a misspelled name never propagates into the context graph (mints a duplicate node). The name pool depends on kind:

  • "domain" → registered DATA_DOMAIN names (get_data_domain())
  • "entity" → distinct ENTITY_NAME values (_entity_name_pool())
  • "object" / anything else → the union of feature / dataset / process-view names (_known_catalog_names()) PLUS domain names (an object_name can itself be a typo'd domain).

Resolution order: blank → returned as-is; exact case-insensitive hit → the canonical-cased pool name; else a single fuzzy match with score >= tdfs4ds.FUZZY_NAME_MATCH_CUTOFF (default 0.90) → the matched name; otherwise name is returned unchanged so the skills' did_you_mean clarification still owns genuine multi-candidate ambiguity.

Fail-open: never raises; any error returns name unchanged. Memoized per turn.

_fuzzy_match_feature

_fuzzy_match_feature(object_name: str) -> list

Return catalog feature names that are close (fuzzy) matches for object_name.

Uses :func:difflib.get_close_matches against the full feature catalog (all data domains). Only names with a similarity score >= 0.75 are returned, which catches single-character differences (missing/extra letter, transposition) without being too permissive.

Parameters:

Name Type Description Default
object_name str

The name as typed by the user (may contain a typo).

required

Returns:

Type Description
list

List of up to 3 feature names from the catalog in their original case,

list

ordered by similarity. Empty list when no close match is found.

_fuzzy_match_dataset

_fuzzy_match_dataset(object_name: str) -> list

Return dataset names that are close (fuzzy) matches for object_name.

Mirrors :func:_fuzzy_match_feature against the dataset catalog so a near-miss like 'ds_investigation_que' resolves instead of silently failing.

Returns:

Type Description
list

List of up to 3 dataset names in their original case, ordered by

list

similarity. Empty list when no close match is found.

_dataset_words

_dataset_words(name: str) -> tuple

The words of a dataset name or phrase, lower case, filler and ds_ prefix removed.

_dataset_named_in_words

_dataset_named_in_words(phrase: str, names) -> list

The one dataset whose name is phrase's words, in order ([] otherwise).

"customer RFM dataset" -> ds_customer_rfm. Deterministic, and only a unique match counts: two datasets with the same words is a question to ask, not a guess to make.

_concept_words

_concept_words(phrase: str) -> tuple

The words of phrase that carry its meaning: dataset and feature filler removed.

_dataset_named_exactly

_dataset_named_exactly(name: str) -> Optional[dict]

{"type": "dataset", "dataset_name", "matched_by": "name"} when name is a registered dataset's own name (any case, with or without its database).

The classifier sometimes returns the dataset's name itself for "which dataset gives me the RFM features?" (DS_CUSTOMER_RFM, seen live); without this the dataset skill looked it up as a feature and answered that no feature has it.

_datasets_named_by_concept

_datasets_named_by_concept(phrase: str, names) -> list

The one dataset whose name holds every word of phrase ("RFM features" -> ds_customer_rfm), [] otherwise. Only a unique match counts.

_datasets_documented_as

_datasets_documented_as(phrase: str, names) -> list

The one dataset whose own documentation (description, sections) holds every word of phrase as a whole word, [] otherwise. Fail-open.

_recognize_concept

_recognize_concept(phrase: str, features: bool = True) -> dict

What a phrase that names no catalog object refers to.

In order: the one dataset whose name holds its words, the one dataset whose documentation does, then the features whose own description matches it (:func:_features_matching_meaning). type is "dataset" (with dataset_name), "concept" (with features) or "unknown"; matched_by says how. Reads the turn's cached catalogs; no LLM call.

_features_matching_meaning

_features_matching_meaning(phrase: str, top_k: int = 8, limit: int = 3) -> list

Registered features whose documentation matches phrase: [(name, description)].

Only for a name that matched nothing ("recency" for DAYS_SINCE_LAST_TXN). A feature is offered only when its own description contains the stem of a word of the phrase; the vector index (columns) only orders them, the features it did not return come after. At most limit. Fail-open: [] when the catalog cannot be read.

_meaning_suggestion_answer

_meaning_suggestion_answer(obj: str, suggestions, question: str = '') -> str

'No feature is named X; these documented features match its meaning', EN/FR.

_not_registered_answer

_not_registered_answer(obj: str, datasets, question: str = '') -> str

' is not registered', with the registered datasets, in the question's language.

_available_dataset_names

_available_dataset_names() -> list

Return all dataset names registered in the dataset catalog (original case).

_question_is_french

_question_is_french(question: str) -> bool

Cheap token-based French detection for deterministic (non-LLM) replies.

Only used where the answer is templated rather than LLM-generated — the LLM paths handle language matching natively via the system prompt.

_entity_from_question

_entity_from_question(question: str, entities: list) -> Optional[str]

Deterministically resolve an ambiguous feature's entity from the question text.

When a feature exists under several entities the user may have already named the one they mean ("total_amount for pharmacy_id") — asking a clarification in that case is a dead turn. Scans the question for the candidate entity names (case-insensitive, _/space-normalised; multi-key entities like claim_id,region match on any key component) and returns the candidate when exactly ONE matches. Zero or multiple matches return None so the normal clarification flow runs.

_best_entity_match

_best_entity_match(text: str, entities: list) -> Optional[str]

The one entity text names, or None.

Among several matches, the entity all of whose keys are named and that has the most keys wins ("for Category and CustomerID" -> Category,CustomerID; "for CustomerID" -> CustomerID, not Category,CustomerID). A tie is None.

_entity_spellings

_entity_spellings(part: str) -> set

The ways a key column is written in a question: CustomerID is also "customer id", "customer_id" and "customerid" (camel case split, _ as space).

_entity_words

_entity_words(entity) -> set

Every word an entity is written with (each key column, camel case split), plural forms too.

_entities_named_in

_entities_named_in(text: str, entities: list) -> list

The entities of entities the text names, in their order, without repeats.

An entity matches when one of its key columns appears as whole words, in any of its spellings (:func:_entity_spellings); a key spelt compact ("customerid") matches a word of the text. A multi-key entity matches on any of its keys, so "CustomerID" names both CustomerID and Category,CustomerID: callers prefer the exact entity (:func:_best_entity_match).

_fetch_view_ddl

_fetch_view_ddl(view_name: str) -> Optional[str]

Fetch the DDL of a view via SHOW VIEW.

Parameters:

Name Type Description Default
view_name str

Bare view name (no schema prefix needed — uses tdfs4ds.SCHEMA).

required

Returns:

Type Description
Optional[str]

DDL text, or None on failure.

_feat_cache_key

_feat_cache_key(feature_name: str, entity_name: str = '', data_domain: Optional[str] = None) -> str

Return the composite cache key used in accumulated_context_graph.features.

A feature's identity is the TRIPLET (DATA_DOMAIN, ENTITY_NAME, FEATURE_NAME). Because the active domain can change mid-conversation, the domain is part of the key so a cached feature from domain A is never served for domain B.

Format: DOMAIN::ENTITY::FEATURE_NAME (all uppercased). When entity/domain are unknown/blank those segments are empty so the key never collides with a concrete entry and stays findable by suffix scan. data_domain defaults to the active tdfs4ds.DATA_DOMAIN.

Cross-store federation limit (v0.3.1.148): the triplet has NO store segment, so two read-only stores sharing a (domain, entity, feature) triplet would map to the same key (last-write-wins in accumulated_context_graph.features). This is a no-op for stores with disjoint domains/names (the common case); adding a store segment here is the documented follow-up for collision-proof multi-store memory.

_feat_cache_lookup

_feat_cache_lookup(features_dict: dict, feature_name: str, entity_name: str = '', data_domain: Optional[str] = None) -> Optional[dict]

Look up a feature entry in accumulated_context_graph.features.

Triplet-aware (DOMAIN::ENTITY::NAME) with graceful fallback to older snapshots that used NAME::ENTITY or a plain NAME key. Tries in order: 1. Exact triplet key (when domain+entity known) → exact. 2. Active-domain + entity, or active-domain + any entity (suffix scan). 3. Any key ending in ::NAME (legacy DOMAIN::ENTITY::NAME or new) — first match (ambiguous; callers needing precision pass entity+domain). 4. Legacy NAME::ENTITY / NAME:: prefix and plain NAME keys.

_query_accumulated_context_for_lineage

_query_accumulated_context_for_lineage(feature_name: str, entity_name: str, accumulated_context: Optional[dict]) -> Optional[dict]

Extract lineage graph from accumulated_context without DDL scans.

Returns the edges and nodes found in the graph for this feature, or None if the feature is not cached (cache miss). On cache hit, includes depth estimate.

Parameters:

Name Type Description Default
feature_name str

Feature name (will be uppercased for lookup).

required
entity_name str

Entity name for disambiguation.

required
accumulated_context Optional[dict]

The accumulated_context_graph from state.

required

Returns:

Name Type Description
Optional[dict]

Dict with keys: {"edges": [...], "nodes": {...}, "depth": int} or None (cache miss).

Edges Optional[dict]

[{"source": "...", "target": "...", "relation": "depends_on"}]

Nodes Optional[dict]

{"name": {"type": "feature|view|table|dataset", ...}}

_query_accumulated_context_for_usage

_query_accumulated_context_for_usage(feature_name: str, entity_name: str, accumulated_context: Optional[dict]) -> Optional[dict]

Extract dataset memberships from accumulated_context without catalog scans.

Returns the datasets that expose this feature, or None if the feature is not cached or has no dataset membership (cache miss).

Parameters:

Name Type Description Default
feature_name str

Feature name (will be uppercased for lookup).

required
entity_name str

Entity name for disambiguation.

required
accumulated_context Optional[dict]

The accumulated_context_graph from state.

required

Returns:

Type Description
Optional[dict]

Dict with key: {"datasets": [{"name": str, "database": str}]} or None (cache miss).

_query_accumulated_context_for_source_tables

_query_accumulated_context_for_source_tables(feature_name: str, entity_name: str, accumulated_context: Optional[dict]) -> Optional[dict]

Extract direct source tables from accumulated_context without DDL scans.

Returns the immediate predecessors (tables/views) that feed into the feature's process view, or None if the feature is not cached (cache miss).

Parameters:

Name Type Description Default
feature_name str

Feature name (will be uppercased for lookup).

required
entity_name str

Entity name for disambiguation.

required
accumulated_context Optional[dict]

The accumulated_context_graph from state.

required

Returns:

Type Description
Optional[dict]

Dict with keys: {"source_tables": [str], "view_name": str} or None (cache miss).

_query_accumulated_context_for_freshness

_query_accumulated_context_for_freshness(feature_name: str, entity_name: str, accumulated_context: Optional[dict]) -> Optional[dict]

Extract freshness timeline from accumulated_context without catalog queries.

Returns the freshness metrics (last_successful_run, days_since_update, status) for the feature's process, or None if the feature is not cached (cache miss).

Parameters:

Name Type Description Default
feature_name str

Feature name (will be uppercased for lookup).

required
entity_name str

Entity name for disambiguation.

required
accumulated_context Optional[dict]

The accumulated_context_graph from state.

required

Returns:

Type Description
Optional[dict]

Dict with keys: {"last_successful_run": str, "days_since_update": int,

Optional[dict]

"has_recent_failure": bool, "last_status": str, "_from_cache": True}

Optional[dict]

or None (cache miss).

_query_accumulated_context_for_calculation

_query_accumulated_context_for_calculation(feature_name: str, entity_name: str, accumulated_context: Optional[dict]) -> Optional[dict]

Extract calculation formula and source columns from accumulated_context.

Returns the calculation SQL and source columns for a feature, or None if the feature is not cached or has no calculation data (cache miss).

Parameters:

Name Type Description Default
feature_name str

Feature name (will be uppercased for lookup).

required
entity_name str

Entity name for disambiguation.

required
accumulated_context Optional[dict]

The accumulated_context_graph from state.

required

Returns:

Type Description
Optional[dict]

Dict with keys: {"calculation_sql": str, "source_columns": list,

Optional[dict]

"explanation": str, "_from_cache": True} or None (cache miss).

_query_accumulated_context_for_explanation

_query_accumulated_context_for_explanation(feature_name: str, entity_name: str, accumulated_context: Optional[dict]) -> Optional[dict]

Extract explanation and documentation sources from accumulated_context.

Returns the explanation text and documentation sources for a feature, or None if the feature is not cached (cache miss).

Parameters:

Name Type Description Default
feature_name str

Feature name (will be uppercased for lookup).

required
entity_name str

Entity name for disambiguation.

required
accumulated_context Optional[dict]

The accumulated_context_graph from state.

required

Returns:

Type Description
Optional[dict]

Dict with keys: {"explanation": str, "docs_sources": list,

Optional[dict]

"_from_cache": True} or None (cache miss).

_resolve_owning_object_for_column

_resolve_owning_object_for_column(column_name: str, hint_table_name: Optional[str] = None) -> Optional[dict]

Resolve a non-feature column to its owning object via FS_BUSINESS_DICTIONARY_COLUMNS.

Returns {"column": COL, "object": TABLE_NAME, "database": DB, "description": ...} or None when the name is not a documented column. When hint_table_name is supplied and the column appears under several objects, that object is preferred.

_lineage_direction

_lineage_direction(question: str) -> str

Infer the asked lineage direction: 'downstream' | 'upstream' | 'both'.

_lineage_graph

_lineage_graph()

The persisted lineage graph at the turn's valid time, read once per turn.

At a past date (_AS_OF) the graph is read VALIDTIME AS OF from the primary store, with no on-the-fly fallback: today's DDL would describe today.

_lineage_node_type

_lineage_node_type(G, n: str) -> str

'time manager' / 'filter manager' / 'manager table' (tagged at load), else view / table / dataset from the stored OBJECT_TYPE.

_position_object_from_graph

_position_object_from_graph(question: str, object_name: str, *, column_note: Optional[str] = None, correction_note: Optional[str] = None, direction: Optional[str] = None) -> Optional[dict]

Position ANY documented object in the persisted lineage graph (FS_LINEAGE_GRAPH).

Used as the fallback when object_name is not a feature/dataset/process view but is a documented table/plain-view/dataset node — so the user can ask "where does the RAW_CLAIMS table sit / what depends on it?" for non-process objects too.

Returns a lineage result dict (same shape as skill_lineage's slow path), or None when the object is not a node in the graph (caller then falls through to the normal not-found path). When the node exists but has no edges, returns a lineage_empty result (no confabulation), with the object's BD description if any.

skill_lineage

skill_lineage(question: str, object_name: str, hint_view_name: Optional[str] = None, accumulated_context: Optional[dict] = None, entity_name: Optional[str] = None, hint_table_name: Optional[str] = None) -> dict

Return the upstream data dependencies of a feature, object, or column.

Walks the dependency graph starting from the registered process view, and returns a structured list of upstream nodes (tables and views) with their types. Does not require LLM — the graph is deterministic.

When object_name is not a feature/dataset/process view (e.g. a plain documented table, view, or a non-feature column), it falls back to the persisted lineage graph (FS_LINEAGE_GRAPH) to position ANY documented object. A column is first resolved to its owning object (TABLE_NAME) and that object is positioned instead — see _position_object_from_graph.

Implements fast-path caching: queries accumulated_context_graph first (cache hit → return immediately), falls back to expensive DDL scans (cache miss → rebuild and merge results for next turn).

Parameters:

Name Type Description Default
question str

User question (used to determine response language).

required
object_name str

Feature, object (table/view/dataset), or column name.

required
hint_view_name Optional[str]

Pre-resolved view name from a prior turn (skips catalog lookup).

None
accumulated_context Optional[dict]

The accumulated_context_graph from state (used for cache).

None
entity_name Optional[str]

Entity name for disambiguation (used in cache lookups).

None
hint_table_name Optional[str]

Owning object for a column (from a prior EXPLAIN/DEFINITION turn via resolved_column_sources) — positions that object directly.

None

Returns:

Type Description
dict

Dict with object_name, process_view, dependencies (list of

dict

{name, type, depth}), plain_summary (LLM plain-language answer),

dict

_from_cache (bool), and resolved_triplet when resolution succeeded.

_skill_explain_multi

_skill_explain_multi(question: str, features_list: list) -> dict

Explain how multiple features are calculated in a single LLM call.

Parameters:

Name Type Description Default
question str

User question.

required
features_list list

List of {feature_name, entity_name, view_name, process_id, description} dicts.

required

Returns:

Type Description
dict

Dict with explanation (plain-text multi-feature answer), features_explained,

dict

context_graph (for context accumulation), and optionally resolved_triplet.

_hint_violation_note

_hint_violation_note(hints: dict) -> Optional[str]

Authoritative one-line clarification(s) for the active interpretation hints.

Deterministic flag→sentence map, used as the EXPLAIN backstop when the LLM explanation contradicts a hint (see :func:_find_hint_violations). Generalises DEFINITION's inverted-only _direction_correction_note to every flag.

_explain_input_column_context

_explain_input_column_context(identifiers: list, max_cols: int = 6, max_chars: int = 300) -> str

Resolve business descriptions for a formula's input columns (bounded).

Grounds the EXPLAIN LLM in what the inputs mean (so it can derive the output's direction/meaning instead of guessing from the top-level arithmetic). Deterministic, capped, and fail-open: any lookup failure for a column is skipped silently.

Parameters:

Name Type Description Default
identifiers list

candidate real column names (already alias/temporary-filtered).

required
max_cols int

cap on the number of columns resolved (prompt-size / round-trip bound).

6
max_chars int

per-description truncation.

300

Returns:

Type Description
str

A ## Input column meanings prompt section, or "" when nothing resolves.

_is_sto_backed

_is_sto_backed(ddl: Optional[str]) -> bool

True when the view DDL references a Script Table Operator / tdstone2 STO view.

_parse_sto_mapper_uuid

_parse_sto_mapper_uuid(ddl: Optional[str]) -> Optional[str]

Extract the tdstone2 scoring-mapper UUID from a process view's DDL.

The process view of an ML feature selects FROM TDS_STO_SCORING_LIVE_<uuid> (or TDS_STO_SCORING_<uuid>); the <uuid> is the tdstone2 ID_MAPPER_SCORING. Teradata table names use _ in place of the UUID's -, so we normalise back to hyphenated canonical form.

Returns None when no STO scoring reference is present (i.e. not an ML feature).

_extract_packages_from_code

_extract_packages_from_code(code_text: Optional[str]) -> list

Return the distinct top-level Python packages imported by code_text.

Scans import X / from X import … lines, keeps the top-level package, drops standard-library modules, dedupes preserving first-seen order, and surfaces sklearn first when present (it is the headline ML library).

_is_sentinel_timestamp

_is_sentinel_timestamp(value) -> bool

True when value is the open-end sentinel (year >= 9999) or unparseable-as-real.

tdstone2's trained-models table can carry an open-period sentinel (9999-01-01) in TD_TIMECODE instead of a real training time. Treat such a value as "training date not recorded". A None/blank value is also treated as a sentinel (no usable date).

_build_distinct_model_join_sql

_build_distinct_model_join_sql(trained_models_table: str, schema: str, cap: int = 20) -> str

Build the capped DISTINCT trained-model→code resolution query.

From the trained-models table, take DISTINCT ID_TRAINED_MODEL and join TDS_MODEL_REPOSITORY to resolve ID_CODE + ARGUMENTS in one round trip, capped at cap rows. In practice all partitions share one ID_MODEL/ID_CODE so the result collapses to a single row.

_read_code_blob

_read_code_blob(schema: str, code_id: str, max_chars: int) -> str

Read the Python source of a tdstone2 code repository entry.

The CODE column is a BLOB; CAST(CODE AS VARCHAR) errors ("Conversion between BYTE data and other types is illegal"), so it must be read via FROM_BYTES(CODE,'ascii'), chunked with SUBSTR for long sources. Returns the decoded text truncated to max_chars (a marker is appended on truncation).

_read_trained_models_summary

_read_trained_models_summary(schema: str, trained_models_table: str) -> dict

Summarise a tdstone2 trained-models table: partitions, status, freshness.

Returns {n_partitions, n_successful, model_type, training_time_available}. training_time_available is False when the max TD_TIMECODE is the 9999 open-end sentinel (or unreadable). Fail-open: returns zeros on query failure.

_algo_question_wants_code

_algo_question_wants_code(question: str) -> bool

True when the question is about the code / packages / algorithm internals.

skill_algorithm

skill_algorithm(question: str, object_name: str, hint_view_name: Optional[str] = None) -> dict

Explain the machine-learning model behind a feature.

Walks the tdfs4ds → tdstone2 chain: resolve the feature's process view, detect the Script-Table-Operator scoring view in its DDL, parse the scoring-mapper UUID, resolve the trained model → model arguments → Python code, and report the algorithm, packages, hyperparameters, model inputs and training-partition status in business language.

Distinct from :func:skill_explain (which explains the SQL formula): this skill explains the ML model (algorithm / code / packages / hyperparameters).

Returns a dict carrying answer (so the synthesiser returns it directly) plus structured fields (algorithm, packages, hyperparameters, model_inputs, n_trained_partitions, training_time_available …) and error on the failure paths.

_parse_sto_invocation

_parse_sto_invocation(ddl: Optional[str]) -> dict

Parse a tdstone2 Script-Table-Operator view DDL into its run-time invocation.

Extracts the reproduction-relevant pieces of the FROM Script(...) clause: partition_by / order_by columns, the script_command string, the returns output signature (list), and charset. Regex-based; fail-open — returns {} (or partial) when a piece is absent.

_algorithm_system_prompt

_algorithm_system_prompt(facts: dict, model_code: Optional[str], wants_code: bool, input_meanings: str = '') -> str

Assemble the ALGORITHM skill's LLM system prompt (DB-free testable seam).

Injects the deterministic facts (algorithm/packages/hyperparameters/STO config/ structure/invocation), optional input-column business meanings, and the model source (only when the user asked for code). Frames the answer as a tdstone2 reproduction recipe without inventing API the package doesn't have.

The raw STO training view DDL (_raw_ddl inside sto_invocation) is extracted and injected as a separate authoritative block so the LLM can quote it verbatim instead of reconstructing SQL syntax.

_infer_algorithm

_infer_algorithm(hyperparameters: dict, code_text: Optional[str]) -> Optional[str]

Best-effort algorithm name from hyperparameter key prefixes or code text.

_strip_and_parse_json

_strip_and_parse_json(raw)

Parse a JSON value that may arrive as a dict, str, or fenced str.

tdstone2's ARGUMENTS/METADATA columns are JSON; depending on the driver they come back as a dict already or as a (possibly markdown-fenced) string.

skill_explain

skill_explain(question: str, object_name: str, hint_view_name: Optional[str] = None, hint_dataset_features: Optional[list] = None) -> dict

Explain how a feature is calculated by reading its process view DDL.

Fetches the SQL definition of the registered process view and asks the LLM to translate the logic into plain business language — no SQL jargon.

When hint_dataset_features is provided and object_name is empty, explains all features from the current dataset context (up to 10) in a single LLM call.

When object_name contains comma-separated feature names, explains each one.

Parameters:

Name Type Description Default
question str

User question.

required
object_name str

Feature or view name. May be comma-separated for multiple features.

required
hint_view_name Optional[str]

Pre-resolved view name from a prior turn (skips catalog lookup).

None
hint_dataset_features Optional[list]

Feature list from a prior SUMMARY turn on a dataset.

None

Returns:

Type Description
dict

Dict with object_name, process_view, explanation, and resolved_triplet.

_feature_homes

_feature_homes(name: str) -> list

[(domain, entity, has_process)] of every catalog feature called name at the turn's valid time; has_process: a current process lists it.

_feature_elsewhere_answer

_feature_elsewhere_answer(name: str, homes: list, question: str) -> str

Fixed answer: the feature exists, here is where (and whether a process produced it at that date). No LLM.

_explain_computation_context

_explain_computation_context(ddl: str, feature_name: str) -> Optional[dict]

sql_context.computation_context with the store's manager objects. Fail-open.

_extract_feature_list_from_question

_extract_feature_list_from_question(question: str, available_features: list, available_entities: Optional[list] = None) -> tuple

Ask the LLM to extract feature names AND entity-key concepts from a multi-feature request.

Used when the user asks something like "show top 50 highest-risk claims with risk band, top driver, provider and pharmacy" — the question references several feature concepts plus entity keys (provider, pharmacy) needed for the SELECT projection.

Parameters:

Name Type Description Default
question str

User question text.

required
available_features list

List of registered feature names in the active domain.

required
available_entities Optional[list]

List of entity-key tokens (e.g. provider_id, pharmacy_id) parsed from the feature catalog. Optional.

None

Returns:

Type Description
tuple

(feature_names, entity_keys) — both subsets of the inputs.

tuple

Returns ([], []) when the LLM call fails.

_is_numeric_feature_type

_is_numeric_feature_type(feature_type: 'str | None') -> 'bool | None'

Best-effort: True/False from a SQL type string, None when unknown.

_choose_ranking_metric

_choose_ranking_metric(feature_cols: list, feature_meta: dict, inverted_features: 'set | None' = None) -> dict

Pick a sortable ranking column for a dashboard top-N query.

Never orders by a categorical band with a bare DESC. Prefers a numeric risk/score column; falls back to a deterministic CASE over a band label.

Parameters:

Name Type Description Default
feature_cols list

requested feature/column names.

required
feature_meta dict

{NAME_UPPER: {"feature_type": str, "is_numeric": bool|None}}.

required
inverted_features 'set | None'

names (any case) whose formula inverts the score (lower = higher risk) → ranked ASC.

None

Returns {"metric", "direction", "order_expr", "note"} (order_expr may be a CASE expression; metric/order_expr are None when no sortable column exists).

_cover_features_with_datasets

_cover_features_with_datasets(feature_names: list, per_feature: list, dataset_entity_keys: dict, entity_targets: 'list | None' = None) -> dict

Choose the minimal set of datasets covering the requested features (and the requested entity columns) and the join keys to combine them — deterministic, no LLM, no fabrication.

Parameters:

Name Type Description Default
feature_names list

requested feature names.

required
per_feature list

skill_dataset per-feature list (each has datasets: [{dataset_name, database, dataset_id}]).

required
dataset_entity_keys dict

{fq_dataset_name: [entity_col, ...]}.

required
entity_targets 'list | None'

entity columns the user asked to project (e.g. provider_id, pharmacy_id). When a chosen dataset does not already expose one, an additional dataset whose entity-key list includes it is pulled in so the JOIN brings it along.

None

Returns {"datasets": [fq, ...], "join_keys": [col, ...], "uncovered": [...]}. uncovered lists requested features AND entity targets that no registered dataset exposes.

_select_dashboard_datasets

_select_dashboard_datasets(dataset_model: dict, feature_cols: list, entity_keys: list) -> dict

Pick the dataset(s) for a dashboard query from the authoritative dataset model — deterministic, no LLM, no fabrication.

A dataset projects exactly its ENTITY key columns + its registered FEATURE columns. The primary dataset projects the most requested columns; a second dataset is JOINed only when it shares ≥1 entity key AND adds projectable columns. Columns no chosen dataset can project are returned as unavailable.

Parameters:

Name Type Description Default
dataset_model dict

{fq: {"entity_keys": [...], "features": {UP: {...}}}}.

required
feature_cols list

requested feature names.

required
entity_keys list

requested entity-id columns (provider_id, …).

required

Returns {"datasets", "join_keys", "proj_entities", "proj_features", "unavailable"}.

_build_dataset_model

_build_dataset_model() -> dict

Build the authoritative dataset model from the three dataset-catalog views (CATALOG / ENTITY / FEATURES), per the tdfs4ds data model.

Returns {fq_dataset_name: {"dataset_id", "entity_keys": [...], "features": {FEATURE_NAME_UPPER: {"name", "feature_type", "is_numeric"}}}}.

This is the single source of truth for the dashboard composition: what each dataset actually exposes (its ENTITY key columns + its registered FEATURE columns, with types) — not reconstructed from the global feature catalog. Best-effort: returns {} on failure.

_build_dataset_entity_keys

_build_dataset_entity_keys() -> dict

Return {fq_dataset_name: [entity_col, ...]} for all registered datasets.

Entity columns come from get_dataset_entity (parsing the ENTITY_ID / column form). Used to derive join keys for multi-dataset dashboard queries. Best-effort: returns {} on failure.

_detect_inverted_features

_detect_inverted_features(feature_names: list) -> set

Return the subset of feature_names whose process-view formula inverts the score (1 - …, so lower = higher risk). Best-effort; bounded to the small requested set. Failures are swallowed (feature simply treated as non-inverted).

skill_compose_dashboard

skill_compose_dashboard(question: str) -> dict

Multi-feature dataset composition for dashboard-style requests.

Decomposes the question into a feature list, then re-uses :func:skill_dataset multi-feature logic to find datasets exposing all of them (or report a JOIN gap when none does).

Returns the same shape as multi-feature skill_dataset plus the extracted feature_names so the synthesiser can confirm what was matched.

skill_dataset

skill_dataset(question: str, object_name: str, entity_name: Optional[str] = None) -> dict

Find datasets that expose one or more features.

When object_name contains comma-separated feature names, returns per-feature dataset lists plus an intersection of datasets that contain all features.

Parameters:

Name Type Description Default
question str

User question.

required
object_name str

Feature name(s) to look up, comma-separated for multi-feature.

required

Returns:

Type Description
dict

Single-feature: {status, object_name, datasets} as before.

dict

Multi-feature: {status, object_names, per_feature, intersection}.

dict

Status values: 'found', 'partial' (some features have no dataset),

dict

'no_dataset'.

skill_dataset_for_concept

skill_dataset_for_concept(question: str, phrase: str, recognized: dict) -> dict

The datasets a phrase that names no feature refers to (status='concept_match').

recognized comes from :func:_recognize_concept: a dataset (by its name or its documentation) is answered with its features; features matched by their description are answered with the datasets exposing them. The answer is written by code and says how the phrase was matched.

_skill_dataset_single

_skill_dataset_single(object_name: str, entity_name: Optional[str] = None) -> dict

Single-feature dataset lookup (original logic).

Feature identity is the TRIPLET, so when the dataset-features frame carries an ENTITY_NAME and the feature name exists under multiple entities, constrain by entity_name so "which datasets expose ?" doesn't merge two entities' datasets (returns ambiguous_feature when the entity is needed but unknown).

_norm_id

_norm_id(v)

A FEATURE_ID as an int, whatever form it is read in (' 6', 6.0, Decimal); None if not one.

_feature_identities

_feature_identities(feature_name: str, entity_name: Optional[str] = None) -> list

Every registered feature called feature_name: [{domain, entity, feature_id, database}].

A feature is the triplet (data domain, entity, name): the same name can exist under several entities and domains. Restricted to the active data domain when it holds the name, and to entity_name when given and matching.

_entity_key

_entity_key(entity) -> str

Order-insensitive entity key (catalog and process lists differ in order).

_datasets_for_identities

_datasets_for_identities(identities: list) -> dict

{index of identity: [DATASET_ID]} of the datasets exposing each feature.

Matched on (FEATURE_DATABASE, FEATURE_ID), never on the name: an id is unique within one store only, and a dataset may read another store's tables (seen live: DB_LAB.DS_CUSTOMER_FROM_PRODUCTION reads DB_PROD ids 9-12, and DB_LAB's id 9 is another feature). Keys of the result are the identity dicts' positions.

_with_dataset_times

_with_dataset_times(datasets: list) -> list

Add each dataset's data time (time, time_tag) and list current snapshots first: a user asking which dataset exposes a feature usually wants today's values, and a dated or historical dataset must not be mistaken for it.

_kind_menu

_kind_menu(fr: bool) -> str

_build_dataset_answer

_build_dataset_answer(status: str, r: dict, fr: bool) -> Optional[str]

The BUILD_DATASET answers written by code (None = let the generic path answer).

skill_list_datasets

skill_list_datasets(entity: Optional[str] = None, data_domain: Optional[str] = None, topic=None, purpose: Optional[str] = None) -> dict

Every registered dataset: database-qualified name, kind, features and their entity.

status: dataset_list, datasets: [{name, kind_tag, feature_count, entities}]. Features are identified by (FEATURE_DATABASE, FEATURE_ID); a dataset reading another store's features shows no entity for them.

entity (and optionally data_domain) keeps the datasets built on exactly that entity: the conversation's "these datasets" / "datasets for CustomerID". scope echoes them and total counts every dataset.

topic (words of the question) keeps the datasets whose name, entities, data domain or description share a word with it, and purpose 'train' the datasets that carry a target (a LEAD offset column) or "train" in their name (0.3.5.14: "which datasets are used to train the propensity models?" listed every dataset of the store). Each narrows only when something matches: the list is otherwise unchanged and scope says the filter matched nothing.

_datasets_about

_datasets_about(datasets: list, words: set) -> list

The datasets whose name, entities, data domains or description share a word with words.

_training_datasets

_training_datasets(datasets: list) -> list

The datasets that carry a target: a LEAD offset column (its catalog), or "train" in the name.

_dataset_kind_tags

_dataset_kind_tags(dataset_ids: list) -> dict

{DATASET_ID: tag} from one read of the build table ({} when it cannot be read).

_dataset_list_answer

_dataset_list_answer(r: dict, fr: bool) -> str

_no_dataset_answer

_no_dataset_answer(result: dict, obj: str, fr: bool = False) -> str

The 'no dataset exposes this feature' answer, by code (EN/FR).

_skill_dataset_no_match

_skill_dataset_no_match(object_name: str, identities: Optional[list] = None) -> dict

Build the 'no_dataset' result: the feature's identity and its sibling features.

Siblings are the features of the SAME data domain and entity (a triplet), never every feature of an entity name across domains (an answer once counted "8 features of Category" including a CustomerID feature).

skill_propose_features

skill_propose_features(use_case: str, entity_name: str, all_features: list, top_n: int = 20) -> dict

Propose the most relevant features for a given use case.

When all_features has more than top_n entries the LLM selects the most relevant ones. When there are top_n or fewer, all are returned directly without an LLM call.

Parameters:

Name Type Description Default
use_case str

Free-text description of the business need (user's message).

required
entity_name str

Entity the features belong to (context for the LLM).

required
all_features list

Full list of {name, description} dicts.

required
top_n int

Maximum features to propose.

20

Returns:

Type Description
dict

Dict with status='features_proposed', proposed_features,

dict

feature_count (total available), and include_all_offered (bool).

_rank_documented_objects

_rank_documented_objects(use_case: str, documented_objects: list, documented_columns: list, diagnostic: list) -> tuple

Pick the use-case-relevant documented datasets / objects / columns.

One bounded LLM pass over the documented (EFS-governed) objects + non-feature columns. Returns (relevant_datasets, relevant_objects, relevant_columns) where datasets are the kind=='dataset' subset. Fail-open: on any error, returns small head slices so the feasibility answer still lists something.

skill_feasibility

skill_feasibility(use_case: str) -> dict

Assess a use case WITHOUT creating a dataset (read-only feasibility check).

Scoped strictly to the data governed and documented by the feature store — the registered feature catalog (+ column business descriptions) and the business-dictionary documentation tables (objects + columns + sections). It does NOT inspect physical source tables / external schemas.

Two halves:

  1. Relevant existing assets — ranks the registered features (:func:skill_propose_features) AND the other documented objects: datasets (BD_OBJECTS type 'D'), tables/views ('T'/'V'), and non-feature documented columns (BD_COLUMNS), via :func:_rank_documented_objects. Features are the finest grain but any documented object that helps the use case is surfaced.
  2. Missing capabilities — given the use case, the documented feature coverage (feature names + descriptions, grouped by entity) and the domain business-dictionary themes, the LLM names up to 6 capabilities a complete solution would need that the feature store does NOT yet document. Each is framed against the existing coverage (e.g. "no temporal trend feature at the provider grain"). No dataset is built; nothing outside the EFS is read.

Returns a direct-answer dict (its own answer) — no second LLM pass.

skill_check_dataset_name

skill_check_dataset_name(proposed_name: str, feature_name: str) -> dict

Check whether a proposed dataset name is already taken in the catalog.

Parameters:

Name Type Description Default
proposed_name str

Dataset name proposed by the user.

required
feature_name str

Feature for which the dataset is to be created (context only).

required

Returns:

Type Description
dict

Dict with status ('name_available' or 'name_taken'),

dict

proposed_name, and feature_name. When taken, also includes

dict

existing (list of {database, name}).

resolve_features_for_dataset

resolve_features_for_dataset(entity_name: str, feature_list: list) -> tuple

Resolve a list of {name, description} feature dicts to the selected_features format.

Calls get_feature_versions() and splits results into clean (single-version) and conflict (multi-version) groups.

Parameters:

Name Type Description Default
entity_name str

Entity the features belong to.

required
feature_list list

List of {name, description} dicts (as produced by skill_propose_features).

required

Returns:

Name Type Description
tuple

Tuple of (selected_features_dict, conflicts_list).

selected_features_dict tuple

{feature_name: uuid_str} ready for build_dataset().

conflicts_list tuple

[{feature_name, versions: [{process_id, process_view_name}]}].

_timestamps_in_text

_timestamps_in_text(text: str) -> list

Every date / timestamp written in text, normalised as TIMESTAMP literals.

ISO dates with an optional time and offset (2024-03-31, 2024-03-31 14:30, 2024-03-31T14:30:00Z, ... +02:00), and dates in words (31 March 2024, 31 mars 2024, end of June 2024). A value without an offset is UTC. Raises ValueError on a date that cannot be read.

_duration_in_text

_duration_in_text(text: str) -> Optional[str]

'90 days' / '1 years 2 months' from "last 90 days", "les 6 derniers mois"; None if none.

parse_dataset_kind

parse_dataset_kind(text: str) -> dict

Read the kind of dataset the user asks for, and its parameters. Pure.

Returns {"kind": "current" | "as_of" | "history" | None, "as_of", "start", "end", "duration", "full", "problem"}. problem names what is missing or unreadable (unrecognised, as_of_needs_date, history_needs_window, ambiguous_date, unreadable_date, unreadable_duration); the kind is complete when it is None. Menu numbers 1 / 2 / 3 are accepted.

_ts_text

_ts_text(literal: Optional[str]) -> str

A TIMESTAMP literal for people: 2024-03-31 14:30:00 UTC / ... +02:00.

describe_dataset_kind

describe_dataset_kind(kind: dict, fr: bool = False) -> str

One line saying what will be built.

extract_dataset_name

extract_dataset_name(reply: str) -> Optional[str]

The dataset name in a reply ("my_ds", "call it my_ds", "my_ds"); None when unclear.

check_new_dataset_name

check_new_dataset_name(reply: str) -> dict

May a new dataset be created under the name in reply?

status: name_available, invalid_name (not a plain identifier), name_taken (a registered dataset), object_exists (a table or view of that name already exists in the view database: creating would replace it) or name_check_failed (the database could not be read - never treated as free).

skill_build_dataset

skill_build_dataset(entity_name: str, selected_features: dict, dataset_name: str, schema_name: Optional[str] = None, return_query: bool = False, kind: Optional[dict] = None, join_type: Optional[str] = None, document: Optional[bool] = None, feature_offsets: Optional[dict] = None) -> dict

Create, register and (when a model is configured) document a dataset.

kind comes from :func:parse_dataset_kind: current (default) builds a snapshot valid now, as_of a snapshot at that instant, history one row per entity and period of constant values (build_historical_dataset, window start / end / duration / delay, or the full history). time_manager (kind["time_manager"] = 'db.view') is a snapshot that follows that TimeManager's business date. The view is created in the view database; the database refuses it when the user may not create it, and that refusal is returned whole (error).

join_type ('INNER' / 'FULL OUTER', None = the builders' default) and document (None = when an instruct model is configured) are for callers that let the user choose (the admin app's dataset builder), as is feature_offsets (lags and leads, see build_dataset).

Returns status (dataset_created, creation_failed, no_features_resolved or query_returned), dataset_name, schema, feature_count, kind, dataset_id (when registered), registered and doc_status (success, failed, skipped).

_render_sources_footer(intent: str, skill_result: dict) -> str

Render a small provenance footer for FRESHNESS and LINEAGE answers.

Returns an empty string when no sources dict is present or the intent is not provenance-eligible. The footer cites the underlying catalogs / methods so the user can verify (or escalate) the claim.

synthesize_answer

synthesize_answer(question: str, skill_result: dict, intent: str, resolved_feature_list: Optional[list] = None, accumulated_context: Optional[dict] = None) -> str

Convert structured skill output into a plain-language response.

Parameters:

Name Type Description Default
question str

Original user question.

required
skill_result dict

Dict returned by the skill function.

required
intent str

The classified intent (SEARCH, DEFINITION, etc.).

required
resolved_feature_list Optional[list]

Optional list of feature names from current/prior turns (for group context).

None
accumulated_context Optional[dict]

Optional accumulated context graph with cached metadata.

None

Returns:

Type Description
str

A natural language answer suitable for a non-technical business user.

_get_forbidden_objects

_get_forbidden_objects() -> tuple

Return (forbidden_set, dataset_list, feat_names) from the live feature store catalogs.

forbidden_set contains uppercase bare names and DATABASE.TABLE forms for: - all registered process views - all feature storage tables

dataset_list contains dataset names the MCP sub-agent is allowed to query. feat_names is a sorted list of all known feature names (reused by the async skill to avoid a second feature_catalog round-trip). Returns (set(), [], []) on any catalog error.

_dataset_feature_lines

_dataset_feature_lines(limit: int = 80) -> list

["DB.DS: f1, f2 (entity E, data domain D)", ...] - which dataset exposes which feature.

Matched on (FEATURE_DATABASE, FEATURE_ID), the identity of a feature inside a store, never on the name alone. A dataset reading another store's features lists them by the name it stores, without entity.

_tool_error_text

_tool_error_text(exc) -> str

A tool failure as text for the model: the database message, without driver frames.

_rewrite_select_top20

_rewrite_select_top20(sql: str) -> str

Rewrite SELECT/SEL to SELECT TOP 20 / SEL TOP 20 when no TOP N is present.

_split_after_unknown_object

_split_after_unknown_object(kwargs: dict, tool, result: str) -> Optional[dict]

The arguments to retry with when the server did not find a qualified name, else None.

The agent names datasets database-qualified (the MCP session's default database is not the store's). Some servers take the database apart and look the whole table_name='DB.T' up as one object name (Enterprise base_tablePreview: Error 3807 "Object 'DB.T' does not exist", seen live on AI Studio); others build DB.T themselves and answer (Community). So the call is first sent as the model wrote it, and only a 3807 naming that qualified value gives a retry with the database in the tool's own database argument (db_name or database_name). A database the model gave is kept unless it is empty or the same one; anything else returns None.

_make_guarded_tools

_make_guarded_tools(tools: list, forbidden_objects: set) -> list

Wrap MCP tools with feature-store query guards and a TOP 20 row cap.

Returns new StructuredTool objects — StructuredTool is a Pydantic model so monkey-patching invoke/ainvoke is not possible; we create proper wrappers.

Guards applied on every tool invocation: 1. Reject inputs that reference forbidden feature store objects. 2. Rewrite SELECT queries to SELECT TOP 20 when not already limited. 3. Append a notice when the result appears to hit the 20-row limit.

_extract_chart_from_tool_output

_extract_chart_from_tool_output(tool_name: str, output) -> Optional[dict]

Recover a renderable chart from an MCP tool's output, if present.

teradata-mcp-server's plot_* tools return Chart.js JSON, which langchain-mcp-adapters surfaces as the ToolMessage .artifact ({"structured_content": {...}}). Returns {"title", "html"} for the chatbot to embed, or None when the output carries no chart.

_skill_mcp_tools_async async

_skill_mcp_tools_async(question: str, server_url: str, forbidden_set: set, dataset_list: list, known_features: list = None, transport: str = 'streamable_http') -> dict

skill_mcp_tools

skill_mcp_tools(question: str, server_url: str, transport: str = 'streamable_http') -> dict

Delegate a question to external MCP server tools.

Enforces feature-store query guards: - process views and feature storage tables are forbidden - SELECT queries are automatically capped to TOP 20 - dataset catalog is offered as the approved data source

Parameters:

Name Type Description Default
question str

User question.

required
server_url str

MCP server endpoint URL.

required
transport str

MCP transport protocol — 'streamable_http' (default, modern) or 'sse'.

'streamable_http'

Returns:

Type Description
dict

Dict with answer (str) and error_type (None or 'mcp_error').

_mcp_question

_mcp_question(state: dict) -> str

The question handed to the MCP tools, carrying the object routing resolved (0.3.5.18): "give me a sample" about the conversation's dataset reached the MCP agent without the dataset, which then sampled others (seen live). Unchanged when no object was resolved or the question already names it.

node_skill_mcp_tools

node_skill_mcp_tools(state: dict) -> dict

LangGraph node: delegate to MCP server tools.

Dashboard-shaped questions are short-circuited to the grounded :func:skill_compose_dashboard path FIRST — even with MCP enabled — so the answer is composed from verified datasets/columns instead of free-form MCP SQL that can fabricate column names. Falls through to MCP when the question does not decompose into a feature composition.