Skip to content

tdfs4ds.agent.graph

tdfs4ds.agent.graph

LangGraph-based orchestration for the tdfs4ds consumer agent.

Flow::

START -> node_classify -> node_detect_domain -> node_recognize_entity -> [conditional on intent] -> node_skill_* -> node_synthesize -> END

_mcp_trace_local module-attribute

_mcp_trace_local = local()

logger module-attribute

logger = getLogger('tdfs4ds')

_GROUP_PRONOUN_TOKENS module-attribute

_GROUP_PRONOUN_TOKENS = ('these features', 'those features', 'the features', 'the feature group', 'the ones', 'the ones i just', 'i just saw', 'the same features', 'ces features', 'ces variables', 'celles', 'celles-ci', 'celles-là', 'les features', 'le groupe', 'le même groupe')

_ALGORITHM_UNAVAILABLE_INTENT module-attribute

_ALGORITHM_UNAVAILABLE_INTENT = 'ALGORITHM_UNAVAILABLE'

_MCP_INTENT_UNAVAILABLE_INTENT module-attribute

_MCP_INTENT_UNAVAILABLE_INTENT = 'MCP_INTENT_UNAVAILABLE'

_MCP_GATED_INTENTS module-attribute

_MCP_GATED_INTENTS = ('DATA_QUERY', 'ALGORITHM')

_ALGORITHM_QUESTION_RE module-attribute

_ALGORITHM_QUESTION_RE = compile('(algorithm|algorithme|hyper.?param|model code|code du mod|python package|paquet|sklearn|scikit|machine.?learning model|mod[eè]le de machine learning|quel mod[eè]le|which model|what model)', IGNORECASE)

_EXPLAIN_SHAPE_RE module-attribute

_EXPLAIN_SHAPE_RE = compile('\\b(how\\s+is|how\\s+are|how\\s+do(?:es)?|computed|calculat|formula|logic|derived|comment\\s+est|calcul|logique|formule)\\b', IGNORECASE)

_LINEAGE_SHAPE_RE module-attribute

_LINEAGE_SHAPE_RE = compile("(where\\s+(does|do|is)|come[s]?\\s+from|depend[s]?\\s+on|upstream|downstream|consum|impact|lineage|d[' ]?o[uù]\\s+vien|d[eé]pend|en\\s+amont|en\\s+aval|lignage)", IGNORECASE)

_DEFINITION_SHAPE_RE module-attribute

_DEFINITION_SHAPE_RE = compile("(what\\s+is|what\\s+are|what\\s+does|define|meaning|signif|qu[' ]?est|quelle?\\s+est|d[eé]finition)", IGNORECASE)

_FRESHNESS_SHAPE_RE module-attribute

_FRESHNESS_SHAPE_RE = compile('(fresh|up.?to.?date|last\\s+(refresh|updat|run)|stale|à\\s+jour|fra[iî]ch|derni[eè]re?\\s+(mise|exécution))', IGNORECASE)

_DATASET_PENDING_ACTIONS module-attribute

_DATASET_PENDING_ACTIONS = {'awaiting_feature_selection', 'awaiting_feature_confirmation', 'awaiting_dataset_kind', 'awaiting_dataset_name', 'awaiting_version_selection', 'awaiting_entity_clarification'}

_BUILD_REQUEST_RE module-attribute

_BUILD_REQUEST_RE = compile('\\b(build|create|make|generate|cr[ée]e[rz]?|cr[ée]ons|construi[st]?|construire|g[ée]n[èe]re[rz]?)\\b', IGNORECASE)

_CLARIFICATION_PENDING_ACTION module-attribute

_CLARIFICATION_PENDING_ACTION = 'awaiting_clarification_choice'

_FEATURE_ENTITY_CLARIFICATION_PENDING_ACTION module-attribute

_FEATURE_ENTITY_CLARIFICATION_PENDING_ACTION = 'awaiting_feature_entity_clarification'

_NEW_QUESTION_RE module-attribute

_NEW_QUESTION_RE = compile('^\\s*(what|which|how|where|who|why|when|list|show|give|tell|is|are|can|could|do|does|quel|quelle|quels|quelles|comment|où|qui|pourquoi|quand|liste|listez|montre|montrez|donne|donnez|est-ce|peux|pouvez)\\b', IGNORECASE)

_FEATURE_INVENTORY_RE module-attribute

_FEATURE_INVENTORY_RE = compile('^\\s*(?:what|which)\\s+(?:are\\s+(?:all\\s+)?the\\s+|are\\s+)?(?:all\\s+)?(?:the\\s+)?(?:available\\s+|existing\\s+|registered\\s+)?features?\\b(?:\\s+(?:are|do\\s+we\\s+have|exist|available|registered|in\\s+the\\s+(?:feature\\s+)?store))*\\s*\\??\\s*$|^\\s*(?:list|show|give)(?:\\s+me)?\\s+(?:all\\s+)?(?:the\\s+)?(?:available\\s+)?features\\s*\\??\\s*$|^\\s*quelles?\\s+sont\\s+les\\s+(?:features|caract[ée]ristiques)(?:\\s+disponibles)?\\s*\\??\\s*$|^\\s*(?:liste[rz]?|montre[rz]?)(?:-moi)?\\s+(?:toutes\\s+)?les\\s+features\\s*\\??\\s*$', IGNORECASE)

_BARE_SAMPLE_RE module-attribute

_BARE_SAMPLE_RE = compile("^\\s*(?:(?:can\\s+you\\s+|could\\s+you\\s+|please\\s+)?(?:give|show|get)(?:\\s+me)?\\s+)?(?:a\\s+|an\\s+|some\\s+|a\\s+few\\s+|\\d{1,3}\\s+)?(?:sample|samples|rows?|lines?|records?|extract|preview|example\\s+rows?)(?:\\s+(?:of|from)\\s+(?:it|this|that|them|the\\s+data(?:set)?|this\\s+dataset|that\\s+dataset))?(?:\\s*,?\\s*please)?\\s*[?.!]?\\s*$|^\\s*(?:(?:donne|montre)[sz]?(?:[-\\s]moi)?\\s+)?(?:un\\s+|des\\s+|quelques\\s+|\\d{1,3}\\s+)?(?:[ée]chantillons?|extraits?|lignes?|exemples?\\s+de\\s+lignes?)(?:\\s+(?:de\\s+(?:ce|cet|ces)\\s+(?:dataset|donn[ée]es)|du\\s+dataset))?(?:\\s*,?\\s*s'il\\s+(?:te|vous)\\s+pla[iî]t)?\\s*[?.!]?\\s*$", IGNORECASE)

_DATASET_INVENTORY_RE module-attribute

_DATASET_INVENTORY_RE = compile('^\\s*(?:what|which|list|show|give|tell)\\b[^?]*\\bdatasets\\b[^?]*\\??\\s*$|^\\s*(?:quels?|quelles?|liste[rz]?|montre[rz]?|donne[rz]?)\\b[^?]*\\b(?:datasets|jeux de donn[ée]es)\\b[^?]*\\??\\s*$', IGNORECASE)

_DATASET_SCOPED_RE module-attribute

_DATASET_SCOPED_RE = compile('\\b(expos\\w*|contain\\w*|includ\\w*|with|for|of|has|have|avec|pour|contien\\w*|incluan\\w*|qui ont)\\b', IGNORECASE)

_OBJECT_PRONOUN_RE module-attribute

_OBJECT_PRONOUN_RE = compile('\\b(?:this|that|the\\s+same|its?)\\s+(?:source\\s+)?(?:table|view|object|source)s?\\b|\\b(?:cette|cet|ce|la\\s+m[êe]me)\\s+(?:table|vue|objet|source)s?\\b', IGNORECASE)

_LINEAGE_WORDS_RE module-attribute

_LINEAGE_WORDS_RE = compile('\\b(?:lineage|lignage|upstream|downstream|amont|aval|depends?|dependenc\\w+|d[ée]pend\\w*|used\\s+by|uses?|feeds?|fed|comes?\\s+from|provien\\w*|alimente\\w*|utilis\\w+\\s+par|impact\\w*)\\b', IGNORECASE)

_SOURCE_DATA_REROUTE module-attribute

_SOURCE_DATA_REROUTE = {'SEARCH', 'OUT_OF_SCOPE', 'DEFINITION', 'SUMMARY', 'HELP', 'DATA_QUERY', 'COMPOUND'}

_TRAINING_RE module-attribute

_TRAINING_RE = compile('\\b(?:train\\w*|entra[iî]n\\w*|apprentissage)\\b', IGNORECASE)

_INVENTORY_FILLER module-attribute

_INVENTORY_FILLER = frozenset({'dataset', 'datasets', 'data', 'set', 'sets', 'feature', 'features', 'model', 'models', 'used', 'use', 'using', 'available', 'registered', 'exist', 'exists', 'existing', 'train', 'trained', 'training', 'build', 'built', 'store', 'jeux', 'jeu', 'donnees', 'données', 'quels', 'quelles', 'sont', 'pour', 'les', 'des', 'disponibles', 'utilises', 'utilisés', 'entrainer', 'entraîner', 'modele', 'modèle', 'modeles', 'modèles', 'ont', 'ete', 'été', 'dans', 'sur', 'une', 'qui', 'servent', 'serve', 'serves'})

_REFINE_RE module-attribute

_REFINE_RE = compile("^\\s*(?:i\\s+meant|i\\s+mean|i\\s+would\\s+like|i'?d\\s+like|i\\s+want|what\\s+about|how\\s+about|and|only|rather|just|but|for|instead|je\\s+voulais\\s+dire|je\\s+veux\\s+dire|je\\s+voudrais|plut[ôo]t|et|seulement|uniquement|pour|mais)\\b", IGNORECASE)

_LIST_ITEM_RE module-attribute

_LIST_ITEM_RE = compile('^\\s*(?:[-*]\\s+)?(?:\\*\\*)?(\\d{1,2})(?:\\*\\*)?[.)](?:\\*\\*)?\\s+(.+?)\\s*$')

_SINGLE_CHOICE_RE module-attribute

_SINGLE_CHOICE_RE = compile("^\\s*(?:the\\s+|le\\s+|la\\s+)?(?:(?:option|choice|number|num[ée]ro|n°|no\\.?|#)\\s*)?(\\d{1,2})\\s*(?:st|nd|rd|th|e|er|[eè]me)?\\s*[.)]?\\s*(?:one|please|s'il\\s+(?:te|vous)\\s+pla[iî]t)?\\s*[.!]?\\s*$", IGNORECASE)

_NUMBERED_ANSWER_RE module-attribute

_NUMBERED_ANSWER_RE = compile('(?:^|(?<=[\\s,;]))(\\d{1,2})\\s*[.):=-]?\\s+')

_ORDINALS module-attribute

_ORDINALS = {'first': 1, '1st': 1, 'premier': 1, 'première': 1, 'premiere': 1, 'second': 2, '2nd': 2, 'deuxième': 2, 'deuxieme': 2, 'seconde': 2, 'third': 3, '3rd': 3, 'troisième': 3, 'troisieme': 3, 'fourth': 4, '4th': 4, 'quatrième': 4, 'quatrieme': 4, 'fifth': 5, '5th': 5, 'cinquième': 5, 'cinquieme': 5, 'last': -1, 'dernier': -1, 'dernière': -1, 'derniere': -1}

_ORDINAL_REF_RE module-attribute

_ORDINAL_REF_RE = compile("\\b(?:the|le|la|l')\\s*(" + join(sorted(_ORDINALS, key=len, reverse=True)) + ')(?:\\s+(?:one|feature|item|dataset|column|row|entry)\\b|(?=\\s*(?:[?.!,;]|$)))', IGNORECASE)

_BACKTICK_NAME_RE module-attribute

_BACKTICK_NAME_RE = compile('`([^`]+)`')

_POINTING_WORD_RE module-attribute

_POINTING_WORD_RE = compile('\\b(?:it|its|them|they|these|those|this\\s+one|that\\s+one|the\\s+dataset|this\\s+dataset|that\\s+dataset|ce\\s+dataset|cet?\\s+objet|ces\\s+datasets|ils?|elles?|ça|cela|celui-ci|celle-ci)\\b', IGNORECASE)

_FRESHNESS_WORDS_RE module-attribute

_FRESHNESS_WORDS_RE = compile('\\b(?:up\\s+to\\s+date|up-to-date|fresh|freshness|stale|last\\s+(?:refresh\\w*|update\\w*|run)|refreshed|updated|à\\s+jour|fra[iî]che?|dernière\\s+mise\\s+à\\s+jour)\\b', IGNORECASE)

_LISTING_INTENTS module-attribute

_LISTING_INTENTS = {'DATASET', 'SUMMARY', 'SEARCH'}

_POINTING_INTENTS module-attribute

_POINTING_INTENTS = {'FRESHNESS', 'DEFINITION', 'LINEAGE', 'USAGE', 'EXPLAIN', 'SUMMARY', 'DATA_QUERY'}

_MAX_LISTED_CHOICES module-attribute

_MAX_LISTED_CHOICES = 10

_REFERENCE_WORD_RE module-attribute

_REFERENCE_WORD_RE = compile('(?i)\\b(it|its|that|this|these|those|them|they|same|above|previous|ce|cet|cette|ces|celui|celle|ceux|celles|cela|ça|il|elle|ils|elles)\\b')

_PRONOUNS module-attribute

_PRONOUNS = {'it', 'this', 'that', 'they', 'them', 'these', 'those', 'this one', 'that one', 'this feature', 'that feature', 'the feature', 'these features', 'those features', 'il', 'elle', 'ça', 'ca', 'cela', 'ceci', 'celle-ci', 'celui-ci', 'cette feature', 'ce feature', 'ces features', 'la feature', 'le feature'}

_ROUTING_STOP_WORDS module-attribute

_ROUTING_STOP_WORDS = {'what', 'is', 'the', 'a', 'an', 'does', 'mean', 'how', 'why', 'this', 'that', 'it', 'are', 'was', 'these', 'those', 'feature', 'features', 'tell', 'about', 'from', 'with', 'and', 'or', 'for', 'show', 'give', 'list', 'explain', 'define', 'qui', 'est', 'la', 'le', 'les', 'un', 'une', 'de', 'du', 'des', 'cette', 'ce', 'que', 'quel', 'quelle', 'ces', 'sont', 'quoi', 'comment'}

_COMPARE_RE module-attribute

_COMPARE_RE = compile('\\b(similar|same\\s+as|the\\s+same|identical|equivalent|equal\\s+to|compar\\w*|differen\\w*|versus|vs|match(?:es|ing)?|correspond\\w*|semblable|similaire|identique|m[eê]me|[ée]quivalent\\w*|diff[ée]ren\\w*|corresp\\w*)\\b', IGNORECASE)

_DASHBOARD_HINT_TOKENS module-attribute

_DASHBOARD_HINT_TOKENS = ('dashboard', 'report', 'select', 'top ', 'highest', 'lowest', 'query for', 'build a', 'join', 'rows', 'filter', 'list of')

_CONCEPT_INTENTS module-attribute

_CONCEPT_INTENTS = frozenset({'DATASET'})

_ACRONYM_RE module-attribute

_ACRONYM_RE = compile('\\b[A-Z][A-Z0-9]{1,9}\\b')

_AS_OF_INTENTS module-attribute

_AS_OF_INTENTS = {'SUMMARY', 'DATA_DOMAINS', 'DEFINITION', 'DATASET', 'USAGE', 'SEARCH', 'FRESHNESS', 'LINEAGE', 'COMPOUND'}

_INTENT_NODE_MAP module-attribute

_INTENT_NODE_MAP = {'SEARCH': node_skill_search, 'DEFINITION': node_skill_definition, 'USAGE': node_skill_usage, 'FRESHNESS': node_skill_freshness, 'SUMMARY': node_skill_summary, 'DATA_DOMAINS': node_skill_data_domains, 'FEASIBILITY': node_skill_feasibility, 'HELP': node_skill_help, 'RECAP': node_skill_recap, 'LINEAGE': node_skill_lineage, 'EXPLAIN': node_skill_explain, 'DATASET': node_skill_dataset, 'BUILD_DATASET': node_skill_build_dataset, 'PLOT': node_skill_plot, 'VALUES': node_skill_values, 'OUT_OF_SCOPE': node_skill_out_of_scope}

_SKILL_LABEL module-attribute

_SKILL_LABEL = {'SEARCH': 'skill_search', 'DEFINITION': 'skill_definition', 'USAGE': 'skill_usage', 'FRESHNESS': 'skill_freshness', 'SUMMARY': 'skill_summary', 'LINEAGE': 'skill_lineage', 'EXPLAIN': 'skill_explain', 'ALGORITHM': 'skill_algorithm', 'DATASET': 'skill_dataset', 'BUILD_DATASET': 'skill_build_dataset', 'DATA_QUERY': 'skill_data_query', 'MCP_TOOLS': 'skill_mcp_tools'}

_graph_instances module-attribute

_graph_instances: dict = {}

_CHECKPOINTER module-attribute

_CHECKPOINTER = None

AgentState

Bases: TypedDict

question instance-attribute

question: str

intent instance-attribute

intent: Optional[str]

object_name instance-attribute

object_name: Optional[str]

domain instance-attribute

domain: Optional[str]

skill_result instance-attribute

skill_result: Optional[dict]

answer instance-attribute

answer: Optional[str]

messages instance-attribute

messages: Annotated[list, add_messages]

resolved_data_domain instance-attribute

resolved_data_domain: Optional[str]

available_data_domains instance-attribute

available_data_domains: Optional[list]

resolved_feature_list instance-attribute

resolved_feature_list: Optional[list]

resolved_column_sources instance-attribute

resolved_column_sources: Optional[dict]

secondary_intent instance-attribute

secondary_intent: Optional[str]

recognized_entities instance-attribute

recognized_entities: Optional[dict]

sub_intents instance-attribute

sub_intents: Optional[list]

resolved_entity_name instance-attribute

resolved_entity_name: Optional[str]

pending_action instance-attribute

pending_action: Optional[str]

pending_action_context instance-attribute

pending_action_context: Optional[dict]

mcp_enabled instance-attribute

mcp_enabled: Optional[bool]

accumulated_context_graph instance-attribute

accumulated_context_graph: Optional[dict]

accumulated_preferences instance-attribute

accumulated_preferences: Optional[dict]

token_usage instance-attribute

token_usage: Optional[dict]

resolved_as_of instance-attribute

resolved_as_of: Optional[str]

as_of_unreadable instance-attribute

as_of_unreadable: Optional[str]

_features_df

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

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

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.

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.

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_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}.

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_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.

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.

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.

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.

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.

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

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.

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.

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_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).

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}).

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).

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}]}].

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.

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).

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.

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.

_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).

_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).

_looks_like_group_reference

_looks_like_group_reference(question: str) -> bool

_rejected_intent_value

_rejected_intent_value(exc) -> Optional[str]

The intent the model returned that the schema refused, if it can be read.

Pydantic renders it as input_value='DATA_QUERY', once per rejected literal. Best-effort and never raises: it only chooses a better fallback message, so a miss degrades to the generic one rather than to a wrong answer.

_safe_fallback_intent

_safe_fallback_intent(intent_descriptions) -> Optional[str]

An intent that is certainly registered, for use when classification fails.

SEARCH when available — it is the one skill that answers a vague question usefully — else any registered intent. None when nothing is known, which tells the caller to re-raise rather than invent a route.

_looks_like_algorithm_question

_looks_like_algorithm_question(question: str) -> bool

True when the question is about the ML model/algorithm/packages/hyperparameters.

_resolve_compound_sub_intents

_resolve_compound_sub_intents(result, question: str, object_name, entity_name)

Return an ordered list of >=2 sub-intent dicts for a COMPOUND question, else [].

Prefers the classifier's own sub_intents. Falls back to a deterministic backstop for the common pairs (DEFINITION/EXPLAIN/FRESHNESS + LINEAGE, EXPLAIN + ALGORITHM) so a small model that emits a single intent still produces the compound. Validates each sub-intent's name; collapses to [] when only one distinct sub-intent survives (single-purpose question — unchanged behaviour).

_rescue_out_of_scope

_rescue_out_of_scope(question: str)

Rescue an in-scope question the classifier wrongly sent to OUT_OF_SCOPE.

Returns (object_name, intent) when the question names a KNOWN catalog feature/object, else (None, None) (leave OUT_OF_SCOPE as-is). Intent is EXPLAIN for a "how/formula" shape, otherwise DEFINITION. Deterministic and grounded in _known_catalog_names so genuine off-topic questions still refuse.

_normalize_source_tables

_normalize_source_tables(tables, view_sql)

Drop source-table entries that are not real FROM/JOIN base tables of view_sql.

Prevents subquery aliases (e.g. flags, base_data) from being stored in the accumulated context graph as real tables. Fail-open: returns the input unchanged when view_sql is missing or the real-table set can't be derived. Handles both a list of names and a list of {name: …} dicts.

_merge_accumulated_context

_merge_accumulated_context(current: Optional[dict], updates: dict) -> dict

Non-destructive deep merge of structural relationship updates into the accumulated graph.

Each section is a flat dict keyed by an uppercase identifier. New entries overwrite existing ones so the map always reflects the latest resolution. Focus pointers (_focus, _focus_dataset, _focus_entity) and user_context are updated atomically. User preferences are merged, with new values overriding old ones. Freshness timelines and search results are merged entry-by-entry (new overwrites old).

_build_context_update

_build_context_update(skill_result: dict, domain: Optional[str], current_acc: Optional[dict] = None) -> dict

Extract structural relationships and discoverable payload from a skill result.

Reads resolved_triplet (single-feature path) and context_graph.features (multi-feature path) to populate features, processes, datasets, and entities. Also extracts and merges discoverable payload: - From EXPLAIN: calculation_sql, source_columns, explanation, docs_sources - From LINEAGE: view_sql, source_tables, node metadata (type, depth, features) - From USAGE: entity_keys, select_template - From FRESHNESS: last_successful_run, days_since_update, has_recent_failure

current_acc is the accumulated context graph before this update. When provided, existing feature-entry fields (e.g. dataset_names, business_description) are inherited so that skills which do not return those fields (e.g. EXPLAIN) do not accidentally overwrite them with empty values.

_looks_like_build_request

_looks_like_build_request(text: str) -> bool

True when the reply asks to build / create the dataset (EN/FR).

_is_new_question

_is_new_question(reply: str) -> bool

True when a reply to "which did you mean?" is really a new question.

A short reply ("2", "the summary one", "first") is an answer to the clarification. A longer one that ends with "?" or opens like a question ("what are the data domains?") is a new request: it must be classified on its own, not matched against the pending options — otherwise the agent re-offers the same clarification and the user is stuck (seen live).

_is_feature_inventory_question

_is_feature_inventory_question(question: str) -> bool

_is_bare_sample_request

_is_bare_sample_request(question: str) -> bool

_focus_dataset

_focus_dataset(state) -> Optional[str]

The registered dataset the conversation is about, as db.name, or None.

Read from the remembered object (_focus), resolved like a dataset named in a question: exactly, or by :func:_fuzzy_match_dataset (which also reads "customer RFM dataset" as ds_customer_rfm). Fail-open: None.

_is_dataset_inventory_question

_is_dataset_inventory_question(question: str) -> bool

True for "which datasets are there?" - not "which datasets expose X?".

_is_source_data_question_g

_is_source_data_question_g(question: str) -> bool

_names_documented_object

_names_documented_object(object_name) -> bool

The classifier's object is a documented table/view/column or a catalog name (then the question is about THAT object, not a search). Fail-open: True on a read error, so a misread never reroutes.

_entity_candidates

_entity_candidates(domain: Optional[str] = None) -> list

[{entity, domain, n_features}] of the entities that have features.

domain keeps that data domain's entities when it has any (else every domain). Fail-open: [] when the catalog cannot be read.

_choose_entity

_choose_entity(text: str, candidates: list, domain: Optional[str] = None)

(candidate, matches): the one candidate text names (or a numbered reply "2"), and every candidate carrying the named entity (several data domains can share a name; domain breaks the tie).

_inventory_filters

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

{"topic": [words], "purpose": "train"} of a dataset inventory question, each key only when present (so a plain inventory stays {"entity": ...}). The entity's own words are not a topic.

_dataset_inventory_request

_dataset_inventory_request(question: str, focus_entity: Optional[str] = None)

{"entity": name or None, "topic": [...], "purpose": ...} when question asks for the dataset inventory, else None.

"what are the datasets?" -> every dataset; "datasets for CustomerID" -> those on CustomerID; "what are these datasets?" -> those on the conversation's entity (focus_entity). "Which datasets expose X?" (X not an entity) is a lookup, not an inventory: None. "Which datasets are used to train the propensity models?" keeps the topic words and the training purpose; a scoped question naming no entity is an inventory only when it asks for training sets.

_scoped_follow_up

_scoped_follow_up(state: dict) -> Optional[dict]

A short follow-up that only narrows the previous request to an entity.

"I meant for CustomerID?", "what about CustomerID", "pour CustomerID" after a dataset list, a dataset build or a feature listing redo THAT request for the entity, instead of being classified on its own (seen live: answered as a definition of CustomerID). Returns the classify output, or None.

_looks_like_plot_follow_up

_looks_like_plot_follow_up(question: str) -> bool

_says_historian

_says_historian(question: str) -> bool

The reply points at a historian table ("these data are in the historian"): it completes a pending chart request, even when worded as a question.

_looks_like_plot_request

_looks_like_plot_request(question: str) -> bool

_skill_registered

_skill_registered(intent: str) -> bool

A skill of that intent is installed (its ca-*/SKILL.md is in the catalog).

_values_request

_values_request(question: str)

_plot_skill_registered

_plot_skill_registered() -> bool

The PLOT skill is installed (its ca-plot SKILL.md is in the catalog).

_numbered_items

_numbered_items(text: str) -> dict

The last numbered list of text: {number: item}, numbered from 1 on consecutive numbers; {} when there is none. Markdown emphasis removed.

_expand_numbered_reply

_expand_numbered_reply(reply: str, previous_answer: Optional[str]) -> Optional[str]

A reply made of the numbers of a list the previous answer offered, written out with the items it points at; None when it is anything else.

"2" -> the second item. "1 monthly, 2 last year" -> "<item 1>: monthly; <item 2>: last year" (answers to numbered questions). Every number must be an item of the list, and the reply must start with one.

_listed_items

_listed_items(text: str) -> list

The names the previous answer listed, in order: its last numbered list (the item's first name or name, else its text before ':'), else the first name of each table row.

_resolve_ordinal_reference

_resolve_ordinal_reference(question: str, previous_answer: Optional[str]) -> Optional[str]

question with "the second one" replaced by the second item the previous answer listed; None when there is no ordinal reference or no such item.

_with_name

_with_name(question: str, name: str) -> str

question with its first reference word replaced by name; the question with name appended when it has none ("give me a sample" -> "... of name").

_listed_follow_up

_listed_follow_up(question: str, previous_intent: Optional[str], previous_answer: Optional[str], intent: Optional[str], object_name: Optional[str]) -> Optional[dict]

The object a follow-up points at, from the list the previous answer gave.

After an inventory ("what datasets are available?"), "is it up to date?" or "give me a sample" names nothing; the previous answer's items are what "it" means. One item -> {"intent", "object_name", "question"}; several -> {"intent", "choices": [{"label", "intent", "object_name"}]} (asked as a numbered question). None when the question names something, the previous answer listed nothing, or the question is not about one object. Pure.

_pointing_update

_pointing_update(state, intent, object_name, mcp_enabled) -> Optional[dict]

The classify result for a follow-up pointing at the previous answer's list, or None: {"intent", "object_name", "question"} for one item, {"intent", "object_name": None, "pending_action", "pending_action_context"} to ask among several. Used after classification and when classification failed.

_resolve_clarification_choice

_resolve_clarification_choice(reply: str, interpretations: list[dict]) -> Optional[dict]

Match the user's reply against the stored clarification options.

Accepts: - A 1-based index ("1", "2", "option 2", "the second one") - A fuzzy label/object match (substring or difflib similarity)

Returns the matched interpretation dict, or None when unresolvable.

_ambiguous_feature_pending

_ambiguous_feature_pending(result: dict, intent: str) -> dict

Build the state-update dict for an ambiguous-feature entity clarification.

Called by node_skill_definition when skill_definition returns error='ambiguous_feature'. Stores the entity list, original object name, and origin intent so that the next turn's classify can resolve the reply deterministically without an LLM round-trip.

Factored into a helper so other skills (USAGE, FRESHNESS, …) can trigger the same pending flow with a single call when they encounter the same error.

_fuzzy_resolve_from_column_sources

_fuzzy_resolve_from_column_sources(question: str, col_sources: dict) -> Optional[str]

Try to resolve a vague column/table reference from a prior EXPLAIN result.

Splits the question into tokens and checks whether any token appears as a substring in a known column name or source table name. Returns the matched name only when exactly one candidate matches (to avoid false positives on common words).

Parameters:

Name Type Description Default
question str

Raw user question.

required
col_sources dict

resolved_column_sources dict (COLUMN_UPPER → source_table).

required

Returns:

Type Description
Optional[str]

The matched column name or source table name, or None.

_carried_object_suspect

_carried_object_suspect(question, object_name, context_hint, remembered) -> bool

True when the classifier's object is the remembered one, not the question's.

All must hold: a context hint was given; the object equals a remembered object (case-insensitive); the question does not contain it; the question has no reference word. Otherwise the classifier's answer stands.

_clean_object_name

_clean_object_name(name)

Strip markdown/quote wrappers and a trailing annotation around a name.

The intent classifier (or user) sometimes wraps the object name in markdown backticks/quotes (`provider_z_score`) or appends a parenthetical annotation (provider_z_score (from flags)). Catalog lookups are exact, case-insensitive matches, so either form matches nothing and DEFINITION wrongly falls through to "feature not found / did you mean …".

Strips, in order: surrounding backticks/quotes/whitespace, then ONE trailing (...) group, then surrounding wrappers again. Only the surrounding wrappers and a trailing parenthetical are removed — interior characters are never touched (feature names contain _ and digits; composite entities use commas like claim_id,region; names never contain parentheses). Falls back to the wrapper-only result if stripping the parenthetical would empty the name.

Returns the input unchanged when falsy (None / "").

_split_object_names

_split_object_names(obj)

Split a multi-object string into a clean list of names.

Handles the "A and B" / "A & B" / "A, B" forms the classifier emits for multi-feature/-dataset/-domain questions, normalising the conjunctions to commas, then cleaning each element with :func:_clean_object_name (strips wrappers + a trailing parenthetical per element — the classify-node clean runs on the whole comma-joined string and cannot reach per-element annotations). Mirrors the inline idiom used in node_skill_definition so multi-name parsing is consistent across skills.

Returns [] for a falsy input.

_comparison_names

_comparison_names(question: str, known_names, column_lookup=None) -> list

The documented items a comparison question names, in order (>= 2), else [].

"is the MCE_NUMBER of HCF_ACCESS.HCF_FIBER_VW similar to the MACHINE_NR of the Historian?" -> ['MCE_NUMBER', 'MACHINE_NR']. An item is a catalog name (known_names: features, datasets, processes) or, for identifier-shaped words (underscore, digit or all capitals), a documented column: column_lookup(words) returns the subset that are documented columns (one read). Qualified names (DB.TABLE) are owners, not compared items. Pure when column_lookup is None.

_documented_column_names

_documented_column_names(words: list) -> list

The words that are documented column names (one business-dictionary read).

_extract_feature_list_for_routing

_extract_feature_list_for_routing(question, known_names, fuzzy_fn=None)

Extract the catalog feature names a question lists, for RECAP→DEFINITION re-routing.

Tokenizes the raw question (commas / newlines / whitespace), cleans each token (_clean_object_name strips wrappers + a trailing (from flags) style annotation), drops stop-words and tokens shorter than 3 chars, and keeps a token when it EXACTLY matches a known catalog name OR fuzzy-matches one with high confidence (>= tdfs4ds.FUZZY_NAME_MATCH_CUTOFF, default 0.90 — a real typo, not a coincidental word).

Typo'd tokens are returned as the user typed them (after cleaning), so the downstream skill_definition_multi per-element fuzzy path emits the "did you mean …?" correction rather than the guard auto-correcting silently.

Parameters:

Name Type Description Default
question

Raw user question text.

required
known_names

Iterable of catalog names (any case).

required
fuzzy_fn

Optional tokens, names -> {token: (score, name)} (injected for tests). Defaults to the size-adaptive _best_name_similarity.

None

Returns:

Type Description

(matched_tokens, had_colon) — matched_tokens is the list of name-like

tokens in question order (deduped, as-typed); had_colon is True when the

question contains a ':' before the list (e.g. "what are these features: …").

Empty list when nothing matches (caller leaves intent unchanged).

_node_classify_impl

_node_classify_impl(state: AgentState, intent_model, intent_descriptions: dict, mcp_enabled: Optional[bool] = None) -> dict

Classify, then settle the turn's valid time (0.3.1.240).

The date the question names (or the conversation's, or none) is set before classification - domain detection already reads the catalogs - and settled after it with the classifier's as_of. It lives in resolved_as_of and, for the turn, in consumer_agent._AS_OF (reset by node_synthesize).

_node_classify_core

_node_classify_core(state: AgentState, intent_model, intent_descriptions: dict, mcp_enabled: Optional[bool] = None) -> dict

Core classify logic — extracted so it can be closed over a dynamic model.

Short-circuits the LLM classifier for all dataset-creation pending states so that the user's reply is routed directly to the dataset skill without being misclassified.

When the intent is DEFINITION but no object_name was extracted, attempts a fuzzy match against resolved_column_sources (populated by a prior EXPLAIN turn) so that vague follow-ups like "what does the date mean?" resolve to the correct column or source table without requiring the user to quote the exact name.

Extracts user preferences (e.g., currency, output format) and merges them into accumulated_preferences for all downstream skills to access.

node_classify

node_classify(state: AgentState) -> dict

Run intent classification using the hardwired full-intent model (legacy path).

node_detect_domain

node_detect_domain(state: AgentState) -> dict

Detect the DATA_DOMAIN for the requested object and list all available domains.

DATA_DOMAIN is sticky: once resolved for the thread, it is only changed when the current turn extracts an explicit object_name AND detect_data_domain_for_object returns a domain that actually owns it. This prevents silent context switches on vague follow-up questions.

_with_domain

_with_domain(domain: Optional[str])

Context manager that temporarily sets tdfs4ds.DATA_DOMAIN.

_extract_triplet

_extract_triplet(skill_result: Optional[dict]) -> Optional[dict]

Pull resolved_triplet from a skill result if present and non-empty.

_skill_with_triplet

_skill_with_triplet(skill_result: dict, current_acc: Optional[dict] = None) -> dict

Build state-update dict from a skill result, propagating triplet and dataset context when resolved.

Focus pointers (_focus, _focus_entity, _focus_dataset) are written into the accumulated_context_graph rather than as top-level state fields.

_hint_view

_hint_view(state: AgentState) -> Optional[str]

Return the remembered view_name only when this turn has no explicit object.

When the user names something explicitly, we re-resolve rather than reuse the stale hint so that a new feature mentioned in the same thread is handled correctly.

_effective_object

_effective_object(state: AgentState) -> str

Return the object name: explicit this turn > last focus in accumulated graph.

_get_entity_name_from_state

_get_entity_name_from_state(state: AgentState) -> Optional[str]

Extract entity name from state for fast-path queries (skills can use this for disambiguation).

_looks_like_dashboard_request

_looks_like_dashboard_request(question: str) -> bool
_capture_feature_group_from_search(updates: dict) -> dict

Populate resolved_feature_list / resolved_entity_name from a SEARCH or compose-dashboard skill result.

Walks the skill result for feature_names (composition path) or matches (regular search) and lifts the feature names into resolved_feature_list. When all matched features share the same entity in the catalog, also remembers resolved_entity_name. This keeps multi-turn group-pronoun questions ("are these features fresh?") in lockstep with the SUMMARY path.

Parameters:

Name Type Description Default
updates dict

Dict containing the skill_result key.

required

Returns:

Type Description
dict

The same updates dict, augmented with state keys when applicable.

_make_feature_group

_make_feature_group(entity: Optional[str], feature_names: list, question: str) -> Optional[dict]

Build a _recent_feature_group record for the focus history.

Only returns a record for genuine multi-feature groups (>1 feature); a single-feature turn is not a group and must never evict the history. The label_tokens carry words from the question that qualify the group (e.g. "pharmacy", "provider") so a later group-pronoun reference can match on the qualifier even when no entity was resolved.

_entity_group_from_question

_entity_group_from_question(question: str) -> Optional[tuple]

When the question names a known entity ("...for pharmacies", "provider behaviour"), return (entity_name, [all feature names for that entity]) from the live catalog.

SEARCH match sets vary with the LLM, so a group reference like "the pharmacy features I just saw" is far more robustly grounded against the full catalog feature set for the matched entity than against the LLM's selected subset. Returns None when no single entity is clearly named.

node_skill_search(state: AgentState) -> dict

Execute the search skill.

When the question looks like a dashboard-composition request (multiple feature concepts + dashboard/SELECT/top-N tokens), runs :func:skill_compose_dashboard first to find a dataset exposing the whole set. If decomposition yields fewer than 2 features, falls back to the regular SEARCH path.

Also captures the resulting feature names into resolved_feature_list so follow-up group-pronoun questions ("are these features fresh?") can aggregate freshness without the user having to re-state the names.

_apply_definition_enrichment

_apply_definition_enrichment(result: dict, question: str) -> None

Mutate result in-place: extend explanation with approved plugin knowledge.

Scans $TDFS4DS_SKILLS_FOLDER for SKILL.md files that carry an enrichment-for: DEFINITION field and whose enrichment-match keywords appear in the base explanation. When matches are found, makes a second LLM call that presents the grounded base answer followed by the approved domain knowledge. The enriched text replaces result["explanation"] and "PLUGIN_ENRICHMENT" is added to result["docs_sources"].

This function is a no-op when: - $TDFS4DS_SKILLS_FOLDER is not set or contains no enrichment skills. - The result has no "explanation" key or is a cached answer. - Any exception occurs (always non-fatal — the base answer is preserved).

_column_owner_from_definition

_column_owner_from_definition(result: dict, obj: str) -> Optional[str]

The table a DEFINITION answer placed a non-feature column in, or None.

Only a column answer counts (no resolved feature), and only when every documented row of that column names the same table: two tables would be a guess. Returns the bare table name, the form resolved_column_sources holds.

node_skill_definition

node_skill_definition(state: AgentState) -> dict

Execute the definition skill.

For comma-separated object names (multi-object question — features, datasets, or a mix), calls skill_definition once per name and combines the explanations into a single answer with per-object section headers. The last successfully resolved triplet is propagated to state for follow-up turns.

node_skill_usage

node_skill_usage(state: AgentState) -> dict

Execute the usage skill with fast-path caching.

_resolve_group_from_history

_resolve_group_from_history(question: str, acc: Optional[dict]) -> Optional[dict]

Resolve a qualified group reference against the focus history.

For "the pharmacy-level features I just saw", the live _focus_feature_list may have been overwritten by an intervening single-feature turn. This scans _recent_feature_groups (most-recent first) and returns the group whose entity or label tokens match a qualifier word in the question. Returns {"features": [...], "entity": ...} on a unique match, else None.

_question_has_group_qualifier

_question_has_group_qualifier(question: str, acc: Optional[dict]) -> bool

True when a group reference is scoped by an entity/qualifier word.

"the features I just saw" is unqualified; "the pharmacy-level features I just saw" is qualified. A qualifier is a token from a known entity name (from the focus history or accumulated entities). Qualified-but-unresolvable group references must NOT fall back to a sticky single feature.

node_skill_freshness

node_skill_freshness(state: AgentState) -> dict

Execute the freshness skill.

When the user's question is a group pronoun ("are these features up to date", "the pharmacy features I just saw") and a feature list is remembered from a previous turn, aggregates freshness across the whole group instead of trying to resolve a single object.

node_skill_summary

node_skill_summary(state: AgentState) -> dict

Execute the summary skill.

node_skill_feasibility

node_skill_feasibility(state: AgentState) -> dict

Execute the FEASIBILITY skill — read-only use-case capability assessment.

Identifies relevant existing features and missing-but-derivable capabilities for a described use case, WITHOUT creating a dataset. Direct-answer skill.

node_skill_data_domains

node_skill_data_domains(state: AgentState) -> dict

Execute the DATA_DOMAINS skill — list/describe feature-store data domains.

No domain named → inventory all DATA_DOMAINs. One or more named (comma/and separated) → per-domain feature count + entity breakdown, typo-tolerant. Direct-answer skill (the skill builds its own answer).

node_skill_help

node_skill_help(state: AgentState) -> dict

Execute the HELP skill — explain the feature store's conceptual model.

Direct-answer (no Teradata query); the skill builds its own answer.

node_skill_values

node_skill_values(state: AgentState) -> dict

Execute the VALUES skill: an entity's values or a feature's extremes, read in the database. Direct-answer skill (the answer is written by code).

node_skill_plot

node_skill_plot(state: AgentState) -> dict

Execute the PLOT skill - a chart drawn in the database by TD_PLOT.

Direct-answer skill: the answer is written by code and the chart travels in skill_result["charts"] like an MCP chart. Data domain and entity are passed explicitly (no _with_domain): the feature is resolved by triplet.

_complete_plot_request

_complete_plot_request(pending: dict, reply: str) -> str

The pending chart request with the user's reply added. A number names one of the periods offered ("period 1"): it is replaced by that feature's name, which the plot skill then finds in the text.

node_skill_compound

node_skill_compound(state: AgentState, intent_node_map: dict) -> dict

Orchestrate a COMPOUND (multi-purpose) question — run each sub-intent in order.

Generalises the hardcoded EXPLAIN+ALGORITHM dual-intent pattern: for every sub-intent the classifier emitted, dispatch to the SAME skill node it would normally route to, synthesize that sub-answer to text, and join the parts with a --- divider. Sequential sub-intents (depends_on_prev) thread the prior sub-question's resolved owning object forward (column→object→lineage), reusing skill_lineage's hint_table_name via resolved_column_sources. Accumulated context is merged from every sub-result so both subjects stay pronoun-referenceable.

node_skill_recap

node_skill_recap(state: AgentState) -> dict

Execute the RECAP skill — recap features/datasets from conversation context.

Reads directly from accumulated_context_graph; no Teradata queries.

node_skill_out_of_scope

node_skill_out_of_scope(state: AgentState) -> dict

Execute the OUT_OF_SCOPE skill — polite refusal for off-topic questions.

No-op node; the answer is rendered directly in synthesize_answer.

node_skill_lineage

node_skill_lineage(state: AgentState) -> dict

Execute the lineage skill with fast-path caching.

Supports multi-feature comma-separated lineage via context_graph consolidation.

node_skill_explain

node_skill_explain(state: AgentState) -> dict

Execute the explain skill and persist column→source-table mapping for follow-up DEFINITION turns.

node_pre_skill_explain

node_pre_skill_explain(state: AgentState) -> dict

No-op node emitted before node_skill_explain to show a live trace step immediately.

node_skill_algorithm

node_skill_algorithm(state: AgentState) -> dict

Execute the ALGORITHM skill (explain the ML model behind a feature).

Mirrors node_skill_explain: resolves the effective object, runs the skill in the active domain, and merges the resolved feature triplet + context update. The skill returns its final answer directly, so no second LLM pass.

Dual-intent (EXPLAIN + ALGORITHM): when the question asks both how the formula works ("how is X computed?") AND about the ML model ("what algorithm is behind it?"), this node runs EXPLAIN first and prepends its answer. This handles compound questions without changing the graph structure.

node_skill_dataset

node_skill_dataset(state: AgentState) -> dict

Execute the dataset availability skill — handles a 4-step creation flow.

Step 0 (no pending): look up datasets that expose the feature. Step 1 (awaiting_feature_selection): user described their use case → LLM proposes top-20 relevant features (or all if ≤ 20). Step 2 (awaiting_feature_confirmation): user replied 'ok' or 'all' → record selection and ask for a dataset name. Step 3 (awaiting_dataset_name): user provided a name → validate it.

node_skill_build_dataset

node_skill_build_dataset(state: AgentState) -> dict

Execute the BUILD_DATASET creation flow: the dataset is created, registered and (when a model is configured) documented - nobody else has to do it.

Step 0 (no pending): locate the entity, propose features. Step 0a (awaiting_feature_selection, from a DATASET lookup that found none): propose features of the feature's own (domain, entity). Step 1 (awaiting_feature_confirmation): 'ok' / 'all' → resolve versions. Step 2 (awaiting_version_selection): pick a version per conflicting feature. Step 3 (awaiting_dataset_kind): current snapshot, snapshot at a date, or history over a window - asked with the parameters it needs. Step 4 (awaiting_dataset_name): check the name (catalog and database) and build. The database refuses what the user may not create; that refusal is reported whole.

_ask_entity

_ask_entity(use_case: str, candidates: list, hint: str = '', unmatched: bool = False) -> dict

Ask on which entity to build the dataset (the reply is read by step 0b).

_propose_for_entity

_propose_for_entity(state: AgentState, use_case: str, cand: dict) -> dict

Propose the features of cand (an entity of a data domain); the entity becomes the conversation's (_focus_entity) and its domain the active one.

node_recognize_entity

node_recognize_entity(state: AgentState) -> dict

Identify the catalog type of each named object before skill dispatch.

Populates recognized_entities (feature/dataset/process/unknown per name) and sets secondary_intent = "DATASET" when a DEFINITION question also asks about dataset availability.

_search_concept

_search_concept(state) -> Optional[tuple]

(phrase, recognition) of a SEARCH turn that found nothing, when the phrase is a concept the catalog resolves to a dataset; else None.

The phrase is the classified object, else each acronym of the question. Only a dataset counts here: features matched by their description are what SEARCH itself looks for, so its own answer stands.

_recognized_concept

_recognized_concept(state, name: str) -> Optional[dict]

The recognition of name when it is a concept (matched by words or documentation, not by an exact catalog name), else None.

_render_clarification_prompt

_render_clarification_prompt(question: str, interpretations: list[dict], re_offer: bool = False, listed: bool = False) -> str

Build the numbered clarification answer shown to the user.

A fixed EN/FR template, not an LLM call: the options are already written, and asking a model to restate them made the language drift (an English question answered in French, seen live) for no gain. Same choice as the ambiguous-feature entity clarification.

node_synthesize

node_synthesize(state: AgentState) -> dict

Synthesize, then open the answer with the valid time it was read at.

_node_synthesize_core

_node_synthesize_core(state: AgentState) -> dict

Synthesize a natural language answer from the skill result.

When a secondary DATASET intent was detected by node_recognize_entity, also runs skill_dataset and appends a dataset-availability section to the answer. When the classifier flagged low confidence with multiple interpretations, renders a clarification prompt instead and sets the pending_action for the next turn.

build_graph

build_graph(skills=None, mcp_enabled: bool = False)

Build and compile the LangGraph StateGraph.

The skill set is loaded from the ca-* SKILL.md files bundled with the package (via :func:tdfs4ds.consumer_agent_skill_catalog). Only skills whose intent is in _INTENT_NODE_MAP are registered as graph nodes.

Parameters:

Name Type Description Default
skills

Optional list of intent names (e.g. ['SEARCH', 'DEFINITION']) to restrict the agent to a subset of skills. When None, all installed consumer-agent skills are included.

None
mcp_enabled bool

When True and tdfs4ds.MCP_SERVER_URL is set, adds an MCP_TOOLS intent node that delegates to the configured MCP server.

False

Returns:

Type Description

A compiled LangGraph with MemorySaver checkpointer for multi-turn support.

Raises:

Type Description
ValueError

When no consumer-agent skills are found in the package.

_build_trace_md

_build_trace_md(state: dict) -> str

Build a markdown trace of the agent execution from the final state.

Parameters:

Name Type Description Default
state dict

Final state dict returned by the graph after invocation.

required

Returns:

Type Description
str

Markdown-formatted string describing intent, skill, and result.

_build_context_graph_pyvis

_build_context_graph_pyvis(state: dict) -> Optional[str]

Build an interactive Pyvis network graph from the agent's context state.

Visualises the accumulated context as a knowledge graph with: - Draggable nodes with physics-based layout - Rich hover tooltips for nodes (type + name) and edges (relationship description) - HTML legend showing all 6 node types

Parameters:

Name Type Description Default
state dict

Final state dict from the graph.

required

Returns:

Type Description
Optional[str]

HTML string wrapping an iframe with the graph + legend, or None if no context/pyvis unavailable.

_shared_checkpointer

_shared_checkpointer()

_get_graph

_get_graph(skills=None, mcp_enabled: bool = False)

Return the shared compiled graph, building it once per unique skill subset.

The graph is cached by (frozenset(skills), plugin_key, mcp_enabled) so that different subsets and MCP states each get their own compiled graph, but all of them share one checkpointer (:func:_shared_checkpointer), so a conversation keeps its memory when the MCP toggle or the skill set changes.

reset_consumer_agent

reset_consumer_agent()

Clear the graph singleton cache.

Call this after adding or removing consumer-agent SKILL.md files so that the next :func:consumer_agent call picks up the new skill set.

_run_config

_run_config(graph, thread_id: str, mcp_enabled: bool) -> dict

LangGraph run config of one turn: the thread, the token counter, and a tracer when tdfs4ds.TRACING asks for one (:mod:tdfs4ds.utils.tracing).

The token counter (consumer_agent.turn_token_callback) records every model call of the turn, the classifier included; it only counts.

_merge_token_usage

_merge_token_usage(prev_usage: Optional[dict], turn: dict) -> dict

Conversation token usage = the previous turns' plus this turn's.

turn is a :meth:TokenTracker._aggregate result for the calls of this turn only (total_input / total_output / by_skill). The deltas used to be read as input_tokens / output_tokens, keys totals() never had, so every turn counted 0, while by_skill added the whole session's figures.

consumer_agent

consumer_agent(question: str, thread_id: str = 'default', return_trace: bool = False, return_context: bool = False, skills: Optional[List[str]] = None, mcp_enabled: bool = False) -> Union[str, tuple]

Run the LangGraph agent for a single conversational turn.

Parameters:

Name Type Description Default
question str

User question in natural language (French or English).

required
thread_id str

Conversation thread ID for multi-turn memory.

'default'
return_trace bool

When True, includes trace_md in the return tuple.

False
return_context bool

When True, includes accumulated_context_graph in the return tuple.

False
skills Optional[List[str]]

Optional list of intent names (e.g. ['SEARCH', 'DEFINITION']) to restrict the agent to a subset of skills. When None (default), all installed consumer-agent skills are used. Threads are isolated per skill subset — mixing skills values in the same thread_id is not recommended.

None
mcp_enabled bool

When True and tdfs4ds.MCP_SERVER_URL is set, the agent can route questions to the MCP server's tools. The toggle changes the graph variant (different cache key) so it is safe to flip mid-conversation — but note that conversation history is not shared between the MCP-on and MCP-off graph instances.

False

Returns:

Type Description
Union[str, tuple]
  • answer: str when both return_trace and return_context are False (default).
Union[str, tuple]
  • (answer, trace_md) when return_trace=True and return_context=False.
Union[str, tuple]
  • (answer, accumulated_context_graph) when return_trace=False and return_context=True.
Union[str, tuple]
  • (answer, trace_md, accumulated_context_graph) when both are True.

consumer_agent_stream

consumer_agent_stream(question: str, thread_id: str = 'default', skills: Optional[List[str]] = None, mcp_enabled: bool = False)

Generator variant of consumer_agent for real-time step streaming.

Yields:

Type Description

("step", node_name, partial_state) after each graph node completes,

where partial_state is the state accumulated so far (all fields set by

nodes that have already run).

("done", answer, trace_md) as the final event with the complete result.

The MemorySaver checkpointer behaves identically with .stream() as with .invoke() — multi-turn memory is fully preserved.