Skip to content

tdfs4ds.agent.chatbot

tdfs4ds.agent.chatbot

Gradio-based chatbot interface for the tdfs4ds consumer agent.

Provides a simple web UI that non-technical users can interact with directly from a Jupyter notebook or as a standalone server.

Usage in a notebook::

from tdfs4ds.agent.chatbot import launch_chatbot
demo = launch_chatbot()          # opens inline + new-tab link

Standalone::

python -m tdfs4ds.agent.chatbot   # starts on port 7860

logger module-attribute

logger = getLogger(__name__)

_STEP_LABELS module-attribute

_STEP_LABELS: dict = {'classify': 'Classifying intent', 'detect_domain': 'Detecting data domain', 'recognize_entity': 'Recognizing entities', 'skill_search': 'Searching features', 'skill_definition': 'Looking up definition', 'skill_usage': 'Checking usage', 'skill_freshness': 'Checking data freshness', 'skill_summary': 'Summarizing dataset', 'skill_lineage': 'Building lineage graph', 'pre_skill_explain': 'Analyzing formula', 'skill_explain': 'Explaining process SQL', 'node_skill_dataset': 'Looking up datasets', 'skill_data_query': 'Querying data', 'synthesize': 'Synthesizing answer'}

_SKILL_NODES module-attribute

_SKILL_NODES = frozenset({'recognize_entity', 'skill_search', 'skill_definition', 'skill_usage', 'skill_freshness', 'skill_summary', 'skill_lineage', 'pre_skill_explain', 'skill_explain', 'node_skill_dataset', 'skill_data_query'})

_LIVE_HEADER module-attribute

_LIVE_HEADER = '**Live execution trace**\n\n| # | Step | Detail |\n|---|------|--------|\n'

_TRACE_PLACEHOLDER module-attribute

_TRACE_PLACEHOLDER = '_No trace yet. Send a message to see the agent steps._'

_TD_ORANGE module-attribute

_TD_ORANGE = '#FF5F00'

_TD_NAVY module-attribute

_TD_NAVY = '#00233C'

_TD_CSS module-attribute

_TD_CSS = '\n:root {\n  --td-orange: #FF5F00;\n  --td-navy: #00233C;\n  --td-surface: #F5F7FA;\n  --td-border: #E3E8EE;\n}\n.gradio-container { background: var(--td-surface) !important; color: var(--td-navy); }\n.gradio-container, .gradio-container * { font-family: system-ui, -apple-system, "Segoe UI", Roboto, Arial, sans-serif; }\n/* Hero banner */\n#td-hero { text-align:center; padding: 12px 0 4px; }\n#td-hero .td-wordmark { color: var(--td-navy); font-weight: 800; font-size: 1.4rem; letter-spacing:-.5px; }\n#td-hero .td-wordmark .dot { color: var(--td-orange); }\n#td-hero .td-sub { color:#5A6B7B; font-size:.95rem; }\n/* Left-oriented tab nav (sidebar look) */\n.td-shell .tab-nav, .td-shell [role="tablist"] {\n  flex-direction: column !important; align-items: stretch !important;\n  min-width: 210px; max-width: 230px; gap: 4px; border:none !important;\n  background:#fff; border-radius:14px; padding:10px; height:fit-content;\n  box-shadow: 0 1px 3px rgba(0,35,60,.06);\n}\n.td-shell .tab-nav button, .td-shell [role="tab"] {\n  text-align:left !important; justify-content:flex-start !important;\n  border:none !important; border-radius:10px !important; padding:10px 14px !important;\n  color: var(--td-navy) !important; font-weight:600; background:transparent !important;\n}\n.td-shell .tab-nav button.selected, .td-shell [role="tab"][aria-selected="true"] {\n  background: #EAF0F6 !important; color: var(--td-navy) !important;\n}\n.td-shell .tab-nav button.selected::before {\n  content:""; display:inline-block; width:4px; height:18px; border-radius:3px;\n  background: var(--td-orange); margin-right:10px; vertical-align:middle;\n}\n/* Cards / groups */\n.gr-group, .gr-box, .block.padded { border-radius:14px !important; }\n/* Primary buttons in Teradata orange */\nbutton.primary, .gr-button-primary {\n  background: var(--td-orange) !important; border-color: var(--td-orange) !important; color:#fff !important;\n}\n/* Chat + skills pane cards */\n#td-chat-col, #td-skills-pane { background:#fff; border-radius:14px; padding:12px; box-shadow:0 1px 3px rgba(0,35,60,.06); }\n#td-skills-pane h3 { margin-top:4px; color:var(--td-navy); }\n/* Skills pane horizontal collapse */\n#td-skills-pane { overflow:hidden; transition: max-width 0.2s ease; }\n#td-skills-pane.td-skills-collapsed { max-width: 52px !important; min-width: 0 !important; }\n#td-skills-pane.td-skills-collapsed > div > :not(#td-skills-toggle) { display:none !important; }\n/* Resizable viz iframe wrappers */\n.td-viz-resizable { resize:vertical; overflow:auto; min-height:400px; }\n.td-viz-resizable iframe { width:100%; height:100%; min-height:400px; }\n/* Full-width tabs */\n.tabitem > .gap, .tabitem { width: 100% !important; }\n'

_SKILL_AUTHORING_GUIDE module-attribute

_SKILL_AUTHORING_GUIDE = '\nA **plugin skill** is a subdirectory inside `TDFS4DS_SKILLS_FOLDER`.\nEach subdirectory needs at minimum a `SKILL.md` file (frontmatter + description).\nOptionally add a `skill.py` with a `node(state)` function for custom logic.\n\n---\n\n### `SKILL.md` structure\n\n```\n---\nname: My skill name          # Human-readable label shown in this pane\nintent: MY_INTENT            # UPPER_CASE token the classifier emits\ndescription: >\n  One or two sentences the intent classifier reads to decide when to\n  route here. Be specific: name the question shape and list SKIP cases.\nargument-hint: [object name] # (optional) shown as a tooltip in the UI\nallowed-tools: Read, Grep    # (optional) tools this skill may call\n---\n\nLonger explanation of what the skill does (not shown to the classifier).\n\n## Queries\n...\n\n## Returns\n...\n\n## Classifier Examples\n\n- "What is the regulatory status of X?" -> MY_INTENT, object_name=X\n- "Quel est le statut réglementaire de X ?" -> MY_INTENT, object_name=X\n```\n\n---\n\n### `skill.py` (optional — for custom logic)\n\n```python\n# skill.py\ndef node(state: dict) -> dict:\n    """Called by the agent graph when intent == MY_INTENT."""\n    obj = state.get("object_name") or state.get("resolved_object_name") or ""\n    # ... your logic here ...\n    answer = f"Regulatory status of {obj}: ..."\n    # Setting \'answer\' bypasses the synthesize step (no extra LLM call).\n    return {"skill_result": {"answer": answer, "docs_sources": ["MY_SOURCE"]}}\n```\n\nKey rules:\n- `state` is the LangGraph `AgentState` dict — read `object_name`, `resolved_data_domain`, `resolved_feature_triplet`, etc.\n- Return a dict with `skill_result` key. Set `skill_result["answer"]` for a direct response (no second LLM pass). Leave it unset to let `synthesize_answer` render the result.\n- Fail-open: return `{"skill_result": {"error": "my_error_code", "details": "..."}}` on errors.\n- Access Teradata via `import tdfs4ds; tdfs4ds.feature_catalog()`, `tdfs4ds.process_catalog()`, or `import teradataml as tdml; tdml.DataFrame.from_query(...)`.\n\n---\n\n### Minimal working example\n\nDirectory layout:\n```\nTDFS4DS_SKILLS_FOLDER/\n└── ca-status/\n    ├── SKILL.md\n    └── skill.py        ← optional\n```\n\n`ca-status/SKILL.md`:\n```\n---\nname: Regulatory status\nintent: STATUS\ndescription: >\n  User asks about the regulatory or compliance status of a feature or dataset.\n  SKIP when the user asks for a definition or formula — use ca-definition or ca-explain.\n---\n\n## Classifier Examples\n\n- "Is total_amount compliant with GDPR?" -> STATUS, object_name=total_amount\n```\n\n`ca-status/skill.py`:\n```python\ndef node(state):\n    obj = state.get("object_name") or ""\n    return {"skill_result": {"answer": f"No compliance record found for \'{obj}\'.",\n                              "docs_sources": []}}\n```\n\nAfter saving, click **Refresh list** and the new skill appears. The agent will route matching questions to it automatically.\n'

_TD_STATIC_DIR module-attribute

_TD_STATIC_DIR = join(dirname(dirname(abspath(__file__))), 'serve', 'static')

_TD_AVATAR module-attribute

_TD_AVATAR = join(_TD_STATIC_DIR, 'td-avatar.svg')

_CHAT_SUGGESTIONS module-attribute

_CHAT_SUGGESTIONS = ['Which data domains exist?', 'What features are available?', 'Which datasets can I use?']

_CHAT_PLACEHOLDER module-attribute

_CHAT_PLACEHOLDER = "<div style='text-align:center'><div class='td-ph-title'>Ask the Feature Store</div><div class='td-ph-sub'>Features, datasets, how a value is computed, where it comes from, whether it is up to date, or how things stood on a past date.</div></div>"

_COPY_CHAT_JS module-attribute

_COPY_CHAT_JS = "\n(history) => {\n  const text = (c) => typeof c === 'string' ? c\n    : Array.isArray(c) ? c.map(p => (p && (p.text || p.content)) || '').join('\\n')\n    : (c && (c.text || c.value)) || '';\n  const parts = (history || []).map(m =>\n    (m.role === 'user' ? '**You:** ' : '**Assistant:**\\n\\n') + text(m.content));\n  const md = '# Feature Store conversation — ' + new Date().toLocaleString() + '\\n\\n'\n    + parts.join('\\n\\n---\\n\\n') + '\\n';\n  const fallback = () => {\n    const ta = document.createElement('textarea');\n    ta.value = md; ta.style.position = 'fixed'; ta.style.opacity = '0';\n    document.body.appendChild(ta); ta.select();\n    try { document.execCommand('copy'); } catch (e) {}\n    ta.remove();\n  };\n  try {\n    if (navigator.clipboard && window.isSecureContext) navigator.clipboard.writeText(md).catch(fallback);\n    else fallback();\n  } catch (e) { fallback(); }\n  return [];\n}\n"

_CHAT_CSS module-attribute

_CHAT_CSS = '\n.gradio-container { max-width: none !important; margin: 0 !important; padding: 0 12px !important;\n  font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif !important; }\nfooter { display: none !important; }\n/* The conversation is the page, not a boxed panel (as in the app). */\n#td-chatbot, #td-chatbot .bubble-wrap { border: none !important; box-shadow: none !important;\n  background: transparent !important; }\n/* Empty conversation: the heading near the top, the suggestions as pills under it. */\n#td-chatbot .placeholder-content { justify-content: flex-start !important; padding-top: 8vh; }\n#td-chatbot .placeholder { flex: 0 0 auto !important; height: auto !important; }\n#td-chatbot .examples { display: flex !important; flex-wrap: wrap; justify-content: center; gap: 8px;\n  max-width: 860px; margin: 22px auto 0 !important; grid-template-columns: none !important; }\n#td-chatbot .example { width: auto !important; min-height: 0 !important; flex: 0 1 auto !important;\n  border: 1px solid var(--border-color-primary) !important; background: var(--background-fill-primary) !important;\n  border-radius: 999px !important; padding: 8px 14px !important; font-size: .88rem !important;\n  box-shadow: none !important; }\n#td-chatbot .example:hover { border-color: #FF5F00 !important; }\n#td-chatbot .example * { font-size: .88rem !important; }\n/* Messages in a centred column; the agent\'s bubble takes its width. */\n#td-chatbot .message-wrap { max-width: 900px; width: 100%; margin: 0 auto !important; }\n#td-chatbot .message-row.bot-row { max-width: 100% !important; width: 100%; }\n#td-chatbot .bot-row .flex-wrap, #td-chatbot .message.bot { flex: 1 1 auto; max-width: 100% !important; }\n#td-chatbot .message.bot { padding: 12px 16px !important; }\n#td-chatbot .bot ul { list-style: disc !important; }\n#td-chatbot .avatar-container { border: none !important; background: transparent !important;\n  box-shadow: none !important; }\n/* The composer and its foot line are centred, 860px at most, as in the app. */\n#td-composer, #td-composer-foot { max-width: 860px; width: 100%; margin-left: auto !important;\n  margin-right: auto !important; }\n#td-composer .block, #td-composer .form, #td-composer .container { border: none !important;\n  box-shadow: none !important; background: transparent !important; padding: 0 !important; }\n.td-acc { border: none !important; background: transparent !important; box-shadow: none !important;\n  max-width: 860px; margin: 0 auto !important; }\n.td-acc .label-wrap, .td-acc button { font-size: .8rem !important; color: var(--body-text-color-subdued) !important; }\n#td-chat-head { align-items: center; gap: 8px; flex-wrap: nowrap;\n  border-bottom: 1px solid var(--border-color-primary); padding: 2px 4px 8px; }\n#td-chat-head > div:first-child { flex: 1 1 auto !important; min-width: 0 !important; }\n.td-head-title { font-weight: 700; color: var(--body-text-color); font-size: 1rem; }\n.td-beta { background: var(--background-fill-secondary); color: var(--body-text-color-subdued);\n  border-radius: 999px; padding: 2px 8px; font-size: .72rem; font-weight: 600; margin-left: 6px; }\n.td-head-btn { flex: none !important; width: auto !important; min-width: 0 !important; white-space: nowrap !important;\n  border: 1px solid var(--border-color-primary) !important; background: var(--background-fill-primary) !important;\n  color: var(--body-text-color) !important; box-shadow: none !important; font-weight: 600 !important;\n  padding: 6px 12px !important; border-radius: 8px !important; }\n.td-head-btn:hover { border-color: #FF5F00 !important; }\n#td-chat-col .message.user { background: #00233C !important;\n  border-radius: 18px 18px 4px 18px !important; border: none !important; }\n.dark #td-chat-col .message.user { background: #FF5F00 !important; }\n#td-chat-col .message.user, #td-chat-col .message.user * { color: #fff !important; }\n#td-chat-col .message.bot { background: var(--background-fill-primary) !important;\n  border: 1px solid var(--border-color-primary) !important; border-radius: 4px 18px 18px 18px !important;\n  box-shadow: 0 1px 2px rgba(0,35,60,.06), 0 4px 16px rgba(0,35,60,.06) !important; }\n#td-chat-col .bot table { border-collapse: collapse; font-size: .88rem; }\n#td-chat-col .bot th, #td-chat-col .bot td { border: 1px solid var(--border-color-primary); padding: 5px 10px;\n  text-align: left; }\n#td-chat-col .bot th, #td-chat-col .bot th * { background: #00233C !important; color: #fff !important; }\n#td-chat-col .bot tr:nth-child(even) td { background: var(--background-fill-secondary); }\n#td-chat-col .bot code { color: #FF5F00; background: var(--background-fill-secondary); border-radius: 4px;\n  padding: 1px 4px; }\n#td-chat-col .bot pre code { color: inherit; background: transparent; }\n#td-composer { border: 1px solid var(--border-color-primary); border-radius: 22px; padding: 4px 4px 4px 14px;\n  background: var(--background-fill-primary); align-items: center; flex-wrap: nowrap;\n  box-shadow: 0 1px 2px rgba(0,35,60,.06), 0 4px 16px rgba(0,35,60,.06); }\n#td-composer:focus-within { border-color: #FF5F00; }\n#td-composer textarea, #td-composer .wrap, #td-composer label { border: none !important; box-shadow: none !important;\n  background: transparent !important; }\n#td-send { flex: none !important; border-radius: 50% !important; width: 40px !important; height: 40px !important;\n  min-width: 40px !important; background: #FF5F00 !important; color: #fff !important; border: none !important;\n  padding: 0 !important; }\n#td-composer-foot { align-items: center; gap: 12px; }\n.td-foot, #td-step { color: var(--body-text-color-subdued); font-size: .78rem; }\n.td-ph-title { font-size: 1.9rem; font-weight: 300; color: var(--body-text-color); }\n.td-ph-sub { color: var(--body-text-color-subdued); margin-top: 4px; }\n#td-graph-col { border-left: 1px solid var(--border-color-primary); padding-left: 10px; }\n'

_SCORE_GREEN module-attribute

_SCORE_GREEN = '#27ae60'

_SCORE_ORANGE module-attribute

_SCORE_ORANGE = '#f39c12'

_SCORE_RED module-attribute

_SCORE_RED = '#e74c3c'

_SCORE_GREY module-attribute

_SCORE_GREY = '#95a5a6'

_TD_MD_CSS module-attribute

_TD_MD_CSS = "<style>.td-md { font-family: system-ui, -apple-system, Segoe UI, sans-serif; color:#2c3e50; }.td-md h1, .td-md h2, .td-md h3 { color:#00233C; }.td-md h2 { border-bottom:2px solid #FF5F00; padding-bottom:4px; margin-top:18px; }.td-md h3 { margin-top:14px; }.td-md p { margin:6px 0; }.td-md pre { background:#0f172a; color:#e2e8f0; padding:12px 16px; border-radius:8px; overflow-x:auto; margin:8px 0; font-size:.85rem; }.td-md code { font-family:'Courier New', monospace; font-size:.88em; color:#c0392b; }.td-md pre code { color:#e2e8f0; background:transparent; }.td-md table { border-collapse:collapse; width:100%; margin:10px 0; font-size:.9rem; }.td-md th, .td-md td { border:1px solid #E3E8EE; padding:6px 12px; text-align:left; }.td-md th { background:#00233C; color:#fff; font-weight:600; }.td-md tr:nth-child(even) { background:#f8fafc; }.td-md blockquote { border-left:4px solid #FF5F00; margin:0; padding:4px 12px; background:#f8fafc; color:#555; }.td-md strong { color:#00233C; }.td-md th * { color:#fff; }.td-md ul, .td-md ol { margin:6px 0; padding-left:24px; }.td-md li { margin:2px 0; }.td-md.inline p { display:inline; margin:0; }</style>"

_EV_METRICS module-attribute

_EV_METRICS = ['faithfulness', 'completeness', 'relevance', 'robustness', 'efficiency']

demo module-attribute

demo = launch_chatbot(inline=False)

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.

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

build_vector_index

build_vector_index(force_rebuild: bool = False, progress=None, stores=None)

Build or refresh the three vector-store collections from the business dictionary tables.

Dispatches to the configured backend (tdfs4ds.VECTOR_STORE_BACKEND): 'chroma' (default) or 'teradata'.

On Chroma, each feature store has its own three collections (chroma_collection_name). stores lists the stores to index (database names or descriptors; default: every store in tdfs4ds.read_stores(), the primary first). A store that is not the primary is read through its public FS_V_BUSINESS_DICTIONARY_* views, so read rights are enough, and nothing is written to its database. stores is ignored by the Teradata backend, which indexes the primary only.

progress, when given, is called with one dict per step: store, source, collection (1-based), n_collections, done, total (documents to embed in that collection, None before it is known) and detail. A failing callback never stops the build.

Returns, on Chroma, {'stores': [{'store', 'collections': {source: {'embedded', 'skipped', 'error'}}}]}.

Parameters:

Name Type Description Default
force_rebuild bool

If True, delete existing collections and re-index from scratch. On Chroma this needs no special privileges. On Teradata it destroys and recreates each collection, which requires admin privileges (Collection.destroy() is admin-only) — when the destroy is refused the call falls back to the ordinary refresh path with a warning rather than failing. (Before 0.3.1.167 this flag was a no-op on the Teradata backend.) If False: on Chroma, embed only rows that are missing or changed (same text, metadata and embedding model = skipped); on Teradata, refresh an existing collection with update(data_refresh=True), falling back to destroy + recreate, and create a missing one (since 0.3.1.169; see vector_store_teradata.build_teradata_index).

False

charts_to_panel_html

charts_to_panel_html(charts: list) -> str

Render a list of {"title","html"} charts as stacked sandboxed iframes.

wrap_iframe

wrap_iframe(chart_html: str, height: int = 440) -> str

Wrap a full HTML document in a sandboxed iframe for safe embedding.

Uses sandbox="allow-scripts allow-downloads" (Plotly needs JS; the chart toolbar downloads its PNG / SVG) and deliberately omits allow-same-origin so the embedded content runs in a unique, isolated origin. The HTML is escaped for the srcdoc attribute.

_format_step

_format_step(node_name: str, state: dict) -> str

Return a human-readable progress label for the given graph node.

_format_live_trace_line

_format_live_trace_line(step_num: int, node_name: str, state: dict) -> str

Return one markdown table row for the live accumulating trace panel.

_chat_fn

_chat_fn(message: str, history: list[dict]) -> tuple[list[dict], str]

Synchronous chat function compatible with Gradio Chatbot (messages format).

Parameters:

Name Type Description Default
message str

User message text.

required
history list[dict]

List of message dicts with 'role' and 'content' keys.

required

Returns:

Type Description
tuple[list[dict], str]

Tuple of (updated history, empty string to clear the input box).

_new_chatbot_thread_id

_new_chatbot_thread_id() -> str

Generate a new session thread ID.

_list_plugin_skills

_list_plugin_skills() -> list

List plugin skills found in TDFS4DS_SKILLS_FOLDER.

Returns a list of {name, intent, description, path} dicts (one per skill directory holding a SKILL.md). Empty when the folder is unset/missing.

_safe_skill_slug

_safe_skill_slug(name: str) -> str

Normalise a skill name to a safe directory slug (no traversal).

_import_plugin_skill_md

_import_plugin_skill_md(name: str, md_text: str) -> str

Write md_text as ${TDFS4DS_SKILLS_FOLDER}/<name>/SKILL.md.

Returns a status string. Requires TDFS4DS_SKILLS_FOLDER to be set to an existing directory and name to yield a non-empty safe slug.

_suggestion_facts

_suggestion_facts() -> dict

What the starter questions name, read from the connected store now.

Three small reads of the primary store, never the agent's per-turn cache (a cache read outside a turn can belong to the store used before a switch):

  • the current processes (domain, view, feature names);
  • the last completed run of each process (FS_FOLLOW_UP);
  • the documented columns of those process views (business dictionary).

The feature named is a documented one whose process ran most recently, so "How is X computed?" and "Is X up to date?" both have an answer; its domain is the one suggested. Returns {} when no store is connected or a read fails.

chat_suggestions

chat_suggestions(limit: int = 4) -> list

Starter questions, named after real objects of the connected store.

Shared by the admin app's chat (/ui/chat/suggestions) and the Gradio chat, and computed on every call (a few small reads, see :func:_suggestion_facts) so a store switch shows at once. Never a hard-coded store name (0.3.1.237). Fail-open to generic questions.

_chat_theme

_chat_theme()

The Teradata look for the Gradio chat (orange primary, slate neutral).

The font is set by the CSS, not here: with plain strings in font=, Gradio 6.11 once failed serving the page while comparing the theme with its built-in ones ('str' object has no attribute 'name').

_accepts

_accepts(fn, name: str) -> bool

chat_style_kwargs

chat_style_kwargs(target) -> dict

css/theme (and the avatar's folder) for target, when it takes them.

Gradio 6 reads the style from launch() / mount_gradio_app() and ignores it on gr.Blocks (with a warning); older versions read it from gr.Blocks.

_blocks_style_kwargs

_blocks_style_kwargs() -> dict

Style for gr.Blocks only where launch cannot take it (Gradio < 6).

_chatbot_kwargs

_chatbot_kwargs(height, suggestions=None) -> dict

gr.Chatbot arguments, each passed only when this Gradio has it.

_build_chat_ui

_build_chat_ui(initial_thread_id: str, with_skills_pane: bool = True, chat_height=620, suggestions=None) -> dict

Build the chat components + wiring inside the current gr.Blocks context.

When with_skills_pane is False the right-hand "Additional skills" column is omitted (used by :func:build_chat_blocks, where skills are managed by the surrounding FastAPI shell). The returned dict then carries None for every skills_* key.

Must be called while a gr.Blocks (or layout) context is active. Returns a dict of the components an embedding app may need to drive from the outside (the chatbot, trace, viz panel, step indicator, thread-id state, input box).

The behaviour here is identical to the pre-refactor inline body of :func:launch_chatbot — extracted verbatim so both launch_chatbot and launch_chatbot_app share one implementation.

launch_chatbot

launch_chatbot(title: str = 'Feature Store Assistant', description: str = 'Ask questions about available features, datasets, definitions, data freshness, or usage guidance — in French or English.', port: int = 7860, public_base_url: Optional[str] = None, inline: bool = True, share: bool = False, thread_id: Optional[str] = None) -> gr.Blocks

Launch a Gradio chatbot interface for the consumer agent.

Parameters:

Name Type Description Default
title str

Title displayed at the top of the UI.

'Feature Store Assistant'
description str

Subtitle / description text.

'Ask questions about available features, datasets, definitions, data freshness, or usage guidance — in French or English.'
port int

HTTP port for the Gradio server.

7860
public_base_url Optional[str]

If running behind JupyterHub, the public base URL (e.g. https://dmproject.myddns.me). Enables proxy-aware URLs.

None
inline bool

Whether to display an inline iframe in Jupyter.

True
share bool

Whether to create a public Gradio share link.

False
thread_id Optional[str]

Fixed conversation thread ID. If None, a new UUID is generated per session.

None

Returns:

Type Description
Blocks

The Gradio Blocks demo instance.

launch_chatbot_with_index

launch_chatbot_with_index(port: int = 7860, force_rebuild: bool = False, title: str = None, description: str = None, public_base_url: Optional[str] = None, inline: bool = True, share: bool = False, thread_id: Optional[str] = None) -> gr.Blocks

Build the vector index then launch the Gradio chatbot in one call.

Parameters:

Name Type Description Default
port int

HTTP port for the Gradio server.

7860
force_rebuild bool

If True, wipe and rebuild the Chroma index from scratch. If False (default), only index new rows (incremental).

False
title str

Title displayed at the top of the UI.

None
description str

Subtitle / description text.

None
public_base_url Optional[str]

JupyterHub public base URL for proxy-aware links.

None
inline bool

Whether to display an inline iframe in Jupyter.

True
share bool

Whether to create a public Gradio share link.

False
thread_id Optional[str]

Fixed conversation thread ID (None = new UUID per session).

None

Returns:

Type Description
Blocks

The Gradio Blocks demo instance.

build_chat_blocks

build_chat_blocks(thread_id: Optional[str] = None, *, title: str = 'Feature Store Assistant', subtitle: Optional[str] = None, chat_height='calc(100vh - 260px)') -> 'gr.Blocks'

Build the chat-only gr.Blocks with the admin app's chat look.

Used at /chatbot (mounted under the FastAPI shell via :func:gradio.mount_gradio_app) and by :func:launch_chatbot in notebooks. The skills pane and Parameters are handled by the admin shell, so they are omitted. Gradio 6 takes the style from launch() / mount_gradio_app(): pass :func:chat_style_kwargs there.

_score_badge

_score_badge(score, label='')

Return an inline HTML badge span coloured by score (1-5 int or None).

_score_colour

_score_colour(score)

_qopt_load_frame

_qopt_load_frame()

Return a pandas DataFrame of processes with stored EXPLAIN scores.

Columns: DATA_DOMAIN, VIEW_NAME, PROCESS_ID, USER_SCORE, OPTIMIZATION_SCORE, N_STEPS, N_SPOOL_OBJECTS. Fail-open: returns an empty frame on any error.

_qopt_score_fig

_qopt_score_fig(df)

Return a Plotly horizontal bar figure coloured by USER_SCORE.

_md_to_html

_md_to_html(md: str, *, inline: bool = False, with_css: bool = True) -> str

Render a Markdown string to styled HTML using the real markdown package.

Handles GFM pipe tables, fenced code, inline code, bold/italic and lists — unlike the various hand-rolled regex renderers it replaces. Scoped via the .td-md class. inline=True keeps short single-line content (e.g. a warning item or a judge rationale cell) from forcing a block paragraph. with_css=False omits the <style> block when the caller emits it once for many fragments. Fail-open: returns an escaped <pre> on any error.

_qopt_detail_md

_qopt_detail_md(process_id: str) -> str

Kept for backwards compatibility — delegates to _qopt_detail_html.

_qopt_detail_html

_qopt_detail_html(process_id: str) -> str

Return a styled HTML fragment with query detail, scores, warnings and recs.

_qopt_render_report

_qopt_render_report(result: dict) -> str

Render the optimizer full-markdown report as styled HTML.

Structure: 1. Hero banner (score delta, strategy, token count) 2. Side-by-side SQL comparison (current left, optimized right) 3. Summary paragraph (inline, not collapsible) 4. Collapsible

for all other ## sections

_qopt_report_html_export

_qopt_report_html_export(result: dict) -> str

Build a self-contained styled HTML page for the optimizer report.

_dash_inventory_html

_dash_inventory_html(feat_df, proc_df, ds_df)

Return KPI card HTML from the catalog DataFrames.

_entity_key_html

_entity_key_html(entity, roles) -> str

Entity columns, each tagged PI (primary index) / P (partitioning column) of the entity's storage tables. PI* / P*: only some tables agree.

_dash_tree_html

_dash_tree_html(feat_df, key_roles=None) -> str

Return a pure-HTML/CSS expandable tree: DATA_DOMAIN → ENTITY → features.

key_roles: {entity_key: {"tables": n, "pi": {COL: k}, "part": {COL: k}}} (store_consistency.entity_key keys) tags each entity column as primary index / partitioning column of its storage tables. None = no tags (the output is then unchanged).

_dash_bar_figs

_dash_bar_figs(feat_df, proc_df, ds_df)

Return list of Plotly bar figures for per-domain counts.

_dash_lineage_counts_fig

_dash_lineage_counts_fig(proc_df, ds_df)

Return a Plotly table figure with upstream/downstream lineage counts per object.

Reads the persisted FS_LINEAGE_GRAPH once, then counts ancestors/descendants for each registered process view and dataset. Fail-open: returns an empty figure when the graph is unavailable.

_dash_tech_tables_frame

_dash_tech_tables_frame(feat_df)

Return a pandas DataFrame summarising the feature tables (catalog only).

_dash_tech_tables_fig

_dash_tech_tables_fig(grp_df, counts_df=None)

Return a Plotly table figure for the feature-tables summary.

_dash_run_history_figs

_dash_run_history_figs(proc_ids, pid_to_view=None)

Return (last5_fig, duration_fig, cadence_fig, reliability_fig) Plotly figures.

Reads the follow-up log for all processes (bounded window: most recent 50 runs per process or last 90 days), then computes stats. Fail-open: returns None for figures when data is missing.

pid_to_view is an optional {PROCESS_ID: VIEW_NAME} dict; when provided the x-axis labels use the human-readable view name instead of the raw UUID.

_dash_storage_counts

_dash_storage_counts(feat_df, *, domains=None, processes=None, pid_to_tables=None, emit=None)

Per-table CURRENT VALIDTIME + NONSEQUENCED VALIDTIME row counts (+ DBC size).

Refactored core of the live row-count pass (formerly inlined in _dash_compute_counts). Fast on a healthy store (stats up to date, PI + partitions). Optionally scoped to a list of domains and/or to the feature tables of a list of processes (pid_to_tables maps PROCESS_ID → set of "DB.TABLE" the process writes to). emit is a callable(str) per-table progress hook; structured PROGRESS k/N lines drive the SSE bar.

Returns a pandas DataFrame with columns: Database, Table, Domains, Entities, # Features, CurrentRows, NonseqRows, HistoryRows, SizeBytes. Fail-open per table (missing counts → None).

_fmt_bytes

_fmt_bytes(v)

Human-readable byte size.

_dash_storage_treemap_fig

_dash_storage_treemap_fig(counts_df, feat_df)

Treemap: data domain → entity → table → feature, sized by stored rows.

A single FS_T_* table physically stores several features in one table, so the table's NonseqRows is split evenly across its features for the leaf size (noted in the hover). Colour encodes the history ratio (HistoryRows/NonseqRows).

_dash_storage_stacked_fig

_dash_storage_stacked_fig(counts_df)

Per-table stacked bar: current-valid rows + history rows (= nonsequenced).

_dash_orphans

_dash_orphans(feat_df, *, emit=None)

Detect FEATURE_IDs present in an FS_T_* table but absent from the catalog.

For each storage table, the catalog assigns a set of FEATURE_IDs; any DISTINCT FEATURE_ID physically in the table but not in that set is an orphan (a feature that was de-registered without purging its stored rows). Read-only. Returns (status_msg, plotly_table_fig_or_None, orphan_rows_list).

_dash_load_followup

_dash_load_followup(proc_ids, pid_to_view=None)

Load + normalise the follow-up log into a pandas frame (shared by the run-history figures). Returns an empty frame on any failure (fail-open).

_run_status_color

_run_status_color(status)

_dash_run_timeline_fig

_dash_run_timeline_fig(log_df)

Per-run timeline (Gantt): each run a bar START→END, coloured by status.

_dash_run_calendar_fig

_dash_run_calendar_fig(log_df)

Day × process status heatmap (1 = all ok, 0 = any failure that day).

_dash_run_detail_fig

_dash_run_detail_fig(process_id, view_name=None)

Full run log for one process as a Plotly table (incl. APPLIED_FILTER).

_execution_starts

_execution_starts(g)

Start times of a process's runs, sorted: one rule for the whole package, :func:tdfs4ds.process_store.process_followup.execution_starts (a FilterManager run writes one row per segment, so gaps between rows say nothing about how often the process runs).

_dash_insights

_dash_insights(feat_df, proc_df, ds_df, counts_df=None, log_df=None, dsf=None)

Compute 'remarkable fact' callouts. Returns a list of {icon,title,detail}.

dsf: the dataset-features frame already read (at the report's valid time); read from the current catalog when not given.

_dash_insights_html

_dash_insights_html(insights)

Render insight callouts as KPI-style tiles.

_dash_compute_counts_standalone

_dash_compute_counts_standalone(domains=None, processes=None, pid_to_tables=None, emit=None)

Module-level live storage-count pass (used by the serve panel / SSE route).

Returns (status, counts_df, treemap_html, stacked_html, orphans_html, counts_table_html, orphan_rows) — counts_df (pandas) and orphan_rows (list of dicts) are the raw data the exportable report consumes.

_fig_to_iframe

_fig_to_iframe(fig, height=400)

Convert a Plotly figure to a sandboxed iframe HTML string.

_ev_scenarios_summary_html_standalone

_ev_scenarios_summary_html_standalone(scenarios: dict) -> str

Compact styled HTML table summarising a scenario set (module-level twin of the launch_chatbot_app closure _ev_scenarios_summary_html).

_ev_summary_chart_standalone

_ev_summary_chart_standalone(results: dict) -> str

Plotly per-skill bar chart from evaluation results, as iframe HTML.

Module-level twin of the launch_chatbot_app closure _ev_summary_chart so :mod:tdfs4ds.serve.panels can reuse it without Gradio.

_ev_aggregate_by_skill

_ev_aggregate_by_skill(results: dict) -> dict

Average the 5 metrics per skill/category across scenarios → {label: {metric: avg}}.

_ev_radar_chart_standalone

_ev_radar_chart_standalone(results: dict) -> str

Plotly radar (Scatterpolar) over the 5 metrics, one trace per skill — as iframe HTML.

Clearer than the grouped bar for comparing the 5 evaluation dimensions at a glance.

_ev_score_table_standalone

_ev_score_table_standalone(results: dict) -> str

Color-coded per-skill score table (5 metrics + average) as HTML.

_reset_agent

_reset_agent()

Drop the cached consumer-agent graph + memory so config changes take effect.

launch_chatbot_app

launch_chatbot_app(port: int = 7860, server_name: str = '0.0.0.0', title: str = 'Feature Store Assistant', description: str = None, inline: bool = False, share: bool = False, thread_id: Optional[str] = None) -> gr.Blocks

Launch the consumer-agent chatbot with a Teradata-styled tabbed interface.

The layout uses a left-oriented tab nav (sidebar look) + a hero header, themed via :data:_TD_CSS (Teradata orange/navy palette). Tabs:

  • 💬 Chatbot — the conversational feature-store assistant (left) plus a right Additional skills pane that lists the plugin skills in TDFS4DS_SKILLS_FOLDER and can import/export a skill's SKILL.md.
  • 🔗 Lineage — interactive pyvis dependency graph of the full persisted lineage (FS_LINEAGE_GRAPH). Click Refresh lineage graph to load.
  • 🧪 Evaluation — generate / import / run evaluation scenarios + report.
  • ⚙️ Parameters — live admin controls:

  • Feature store database — switch the active feature store. The target is validated with tdfs4ds.connect(database=..., create_if_missing=False); a database that does not contain a feature store is rejected (no switch).

  • Vector store — backend (chroma/teradata) + Chroma path, and a Refresh/Rebuild button (build_vector_index(force_rebuild=True)).
  • Instruct model / Embedding model — provider, model, URL, API key, proxy (and embedding dimension), applied to the tdfs4ds globals.

Applying any change that affects the data or the model resets the agent (reset_consumer_agent()), rebuilds the index when needed, rotates the conversation thread, and clears the chat — so the session starts clean.

The existing :func:launch_chatbot / :func:launch_chatbot_with_index are unchanged; this is an additive wrapper.

Returns:

Type Description
Blocks

The Gradio Blocks demo instance.