Skip to content

Add follow-up suggestion chips after successful queries#1764

Draft
ghostiee-11 wants to merge 9 commits into
holoviz:mainfrom
ghostiee-11:feat/smart-followup-suggestions
Draft

Add follow-up suggestion chips after successful queries#1764
ghostiee-11 wants to merge 9 commits into
holoviz:mainfrom
ghostiee-11:feat/smart-followup-suggestions

Conversation

@ghostiee-11

@ghostiee-11 ghostiee-11 commented Mar 17, 2026

Copy link
Copy Markdown
Collaborator

While exploring data in Lumen AI, I noticed that after each query I'd stare at the results trying to figure out what to ask next. Users unfamiliar with their dataset's columns have it worse - they don't know what's possible.

This adds 2-3 suggestion chips after each successful query that reference actual column names and values from the result. Clicking one sends it as a new query. A "Follow-Up Suggestions" toggle in Settings lets users turn it off.

Demo

Screen.Recording.2026-03-17.at.7.30.46.PM.mp4

How to reproduce

  1. ExplorerUI().servable(), upload a CSV
  2. Ask "Show total revenue by product"
  3. After the result, suggestion chips appear like "Compare revenue across region" or "Filter to top products by revenue"
  4. Click one - it executes as a follow-up query
  5. Toggle off in Settings > Follow-Up Suggestions

Closes #1763

AI Disclosure

I identified the UX gap while testing Lumen AI , designed the approach (hooking into _postprocess_exploration, reusing _add_suggestions_to_footer with a group_name param), and verified through 9 new tests + manual end-to-end testing. AI helped scaffold the implementation.

@codecov

codecov Bot commented Mar 17, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 90.98361% with 11 lines in your changes missing coverage. Please review.
✅ Project coverage is 69.73%. Comparing base (45f06f7) to head (454c26d).
⚠️ Report is 40 commits behind head on main.

Files with missing lines Patch % Lines
lumen/ai/ui.py 50.00% 10 Missing ⚠️
lumen/ai/coordinator/base.py 94.44% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #1764      +/-   ##
==========================================
+ Coverage   69.63%   69.73%   +0.09%     
==========================================
  Files         190      190              
  Lines       31495    31614     +119     
==========================================
+ Hits        21933    22046     +113     
- Misses       9562     9568       +6     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@ahuang11

Copy link
Copy Markdown
Contributor

My initial impression is that this looks a bit ugly; can we use a horizontal scrolling Row instead?

image

Separately, we need to update the docs for this I think...

Comment thread lumen/ai/ui.py Outdated
Comment on lines +1590 to +1602
prompt = render_template(
PROMPTS_DIR / "ExplorerUI" / "follow_up_suggestions.jinja2",
user_query=user_query,
data_summary=data_summary,
sql=sql,
output_type=output_type,
conversation_history=conversation_history,
)
result = await self.llm.invoke(
messages=[{"role": "user", "content": "Generate follow-up suggestions."}],
system=prompt,
response_model=FollowUpSuggestions,
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should use self._invoke_prompt instead, or even self._stream_prompt, so each suggestion appears as it's generated.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will refactor to use _invoke_prompt for consistency. Since UI doesn't inherit from Actor (where _invoke_prompt lives), I'll route it through self._coordinator._invoke_prompt. For streaming, structured output with a Pydantic model needs the full response to parse, so I'll start with _invoke_prompt and can explore streaming as a follow-up if needed.

Comment thread lumen/ai/models.py Outdated
Comment thread lumen/ai/models.py Outdated
@ghostiee-11

Copy link
Copy Markdown
Collaborator Author

My initial impression is that this looks a bit ugly; can we use a horizontal scrolling Row instead?

Separately, we need to update the docs for this I think...

Good call, the FlexBox wrapping does look cramped. I'll switch to a pn.Row with horizontal scrolling so the chips sit in a clean single line and scroll when they overflow. Will also add docs for the follow-up suggestions feature and the settings toggle.

@ghostiee-11

Copy link
Copy Markdown
Collaborator Author

current state :

Screenshot 2026-03-18 at 11 34 50 PM

@ahuang11 ahuang11 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Still not happy with how it looks under the message; looks really messy / imbalanced, especially with the horizontal scroll bar. It might be because the icons are above the buttons (maybe reverse them in panel-material-ui).

Instead, maybe we can have a automagic (AI) icon button added to the footer (like pencil edit in your other PR), and then on click it replaces/populates the ChatAreaInput with a suggestion instead?

Comment thread lumen/ai/ui.py Outdated
Comment thread lumen/ai/coordinator/base.py Outdated
"response_model": ThinkingYesNo,
},
"follow_up_suggestions": {
"template": PROMPTS_DIR / "ExplorerUI" / "follow_up_suggestions.jinja2",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should live in Coordinator imo

Comment on lines +13 to +30
Given data with columns: product, region, revenue, date
User asked: "Show total revenue by product"
Good suggestions:
- (timeline) "Show revenue trend over date by product"
- (compare_arrows) "Compare revenue across region"
- (filter_list) "Filter to top 5 products by revenue"

Bad suggestions (too generic):
- "Show me more data"
- "Analyze the results"
- "Create a visualization"

Given data with columns: name, age, salary, department
User asked: "Show average salary by department"
Good suggestions:
- (scatter_plot) "Plot age vs salary colored by department"
- (bar_chart) "Show employee count by department"
- (filter_list) "Filter departments where avg salary > 50000"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shorten good suggestions I think

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay sure

Comment thread lumen/ai/prompts/ExplorerUI/follow_up_suggestions.jinja2 Outdated
{% if conversation_history %}

## Previous Questions in This Exploration (do NOT repeat these)
{% for msg in conversation_history %}- {{ msg }}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not really a "conversation" if we're filtering by user

@ahuang11

ahuang11 commented Mar 18, 2026

Copy link
Copy Markdown
Contributor

Separately, can we use model_spec="ui" or create a new LLM key (and document)? UI key currently uses the smallest model

            await self.invoke(
                messages=[{'role': 'user', 'content': 'Ready? "Y" or "N"'}],
                model_spec="ui",
            )

    model_kwargs = param.Dict(default={
        "default": {"model": "gpt-5.4-mini"},  # Use standard models, not reasoning models (gpt-5, o4-mini)
        "ui": {"model": "gpt-5.4-nano"},
    })

@ghostiee-11

Copy link
Copy Markdown
Collaborator Author

Will use "ui" - follow-up suggestions are lightweight enough for the smallest model, same level as the ready-check. Adding "llm_spec": "ui" to the prompt config so it gets picked up automatically. No need for a new key since "ui" already covers all providers.

@ghostiee-11

ghostiee-11 commented Mar 18, 2026

Copy link
Copy Markdown
Collaborator Author

Pushed the latest round of changes:

  • Moved all LLM logic from UI into Coordinator.suggest_followup() - UI just calls it and renders
  • Registered the prompt on Coordinator with llm_spec="ui" so it uses the cheapest model
  • Shortened the Jinja2 template (removed redundant user_query section, trimmed examples, renamed conversation_history to previous_queries)
  • Removed unused imports from ui.py (FlexBox, render_template, PROMPTS_DIR, FollowUpSuggestions)
  • Added 4 coordinator-level tests for suggest_followup
  • Added docs for the feature in ui.md and navigating_the_ui.md

Still planning to:

  • Rework the chip layout - the horizontal Row with scrollbar doesn't look great. Thinking about switching to an AI icon button in the footer that populates the input on click instead of inline chips.
  • Update doc screenshots (results.png, revise.png) to show the new follow-up suggestions UI once the layout is finalized.

Will tackle those in a follow-up PR

Comment thread lumen/ai/coordinator/base.py Outdated
@ahuang11

ahuang11 commented Mar 20, 2026

Copy link
Copy Markdown
Contributor

I still like this idea so I'm going to hold off on merging. May require changes to panel or pmui ChatMessage to allow adding functionality to button icons (unless already implemented).

Instead, maybe we can have a automagic (AI) icon button added to the footer (like pencil edit in your other PR), and then on click it replaces/populates the ChatAreaInput with a suggestion instead?

@ahuang11
ahuang11 marked this pull request as draft March 20, 2026 18:09
@ghostiee-11

Copy link
Copy Markdown
Collaborator Author

I still like this idea so I'm going to hold off on merging. May require changes to panel or pmui ChatMessage to allow adding functionality to button icons (unless already implemented).

Thanks a lot Andrew about the review and suggestion, Just working on this, will get back with you quickly

@ahuang11

Copy link
Copy Markdown
Contributor

Np rush. It'll probably go into the next release.

@ghostiee-11

Copy link
Copy Markdown
Collaborator Author

Np rush. It'll probably go into the next release.

Okay Thankyouu!!

Comment on lines +1 to +28
You are a data exploration assistant. Based on the user's question and the resulting data, suggest 2-3 natural follow-up questions that reference actual columns and values from the dataset.

## Rules
- Each suggestion MUST reference specific column names from the data summary below
- Suggestions should be diverse: mix breakdowns, comparisons, filters, trends, distributions
- Keep suggestions short and actionable (under 100 characters)
- Do NOT repeat any previous queries listed below
- Do NOT suggest analyses the data cannot support (e.g. no time trend if no date column)
- Use appropriate icon names: bar_chart, pie_chart, timeline, map, filter_list, compare_arrows, table_chart, trending_up, scatter_plot

## Examples
- (timeline) "Show revenue trend over date by product"
- (compare_arrows) "Compare revenue across region"
- (filter_list) "Filter to top 5 products by revenue"

## Data Summary
{{ data_summary }}

## SQL Executed
{{ sql }}

## Output Type
{{ output_type }}
{% if previous_queries %}

## Previous Queries (do NOT repeat these)
{% for q in previous_queries %}- {{ q }}
{% endfor %}{% endif %}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should use the structure of Actor.jinja2

Comment thread lumen/ai/models.py Outdated
Comment on lines +33 to +48
icon: str = Field(
description="Material UI icon name for the button",
examples=["bar_chart", "timeline", "filter_list"]
)
query: str = Field(
description="The exact natural language query to send to Lumen AI",
max_length=100
)


class FollowUpSuggestions(BaseModel):
suggestions: list[FollowUpSuggestion] = Field(
description="2-3 dataset-specific follow-up suggestions",
min_length=1,
max_length=3
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I still think it's better to populate the ChatAreaInput.value with one follow up suggestion for efficiency and look.

@ahuang11 ahuang11 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry if I'm being unclear; I drew a picture to illustrate what I desire from follow up suggestions:

A light bulb/auto awesome lightbulb icon next to the edit/copy/etc icon, and on click, it would populate the chat area input with the suggested text.

Image

@ahuang11
ahuang11 requested review from ahuang11 March 24, 2026 02:12
@ahuang11
ahuang11 marked this pull request as draft March 24, 2026 02:13
@ghostiee-11
ghostiee-11 force-pushed the feat/smart-followup-suggestions branch from 440b192 to 942c7b1 Compare March 24, 2026 07:15
Add a lightbulb icon to message footer that populates the chat input
with an AI-generated follow-up suggestion on click. Uses the cheapest
'ui' LLM spec for efficient generation.

- Add FollowUpSuggestion model in models.py
- Add suggest_followup() to Coordinator with llm_spec='ui'
- Add follow_up_suggestions.jinja2 extending Actor/main.jinja2
- Add lightbulb icon to footer_actions (fallback to footer_objects)
- Add follow_up_suggestions settings toggle
- 14 tests (10 UI + 4 coordinator) + docs

Depends on panel-extensions/panel-material-ui#605 for footer_actions.
@ghostiee-11
ghostiee-11 force-pushed the feat/smart-followup-suggestions branch from 942c7b1 to 582b0e1 Compare March 24, 2026 07:19
@ghostiee-11

Copy link
Copy Markdown
Collaborator Author

Thanks for the drawing Andrew, that made it very clear! Pushed a rework:

  • Single suggestion - returns one query string, populates ChatAreaInput.value_input on click, hides button after
  • Template extends Actor/main.jinja2 - uses instructions, examples, context blocks
  • Logic in Coordinator - suggest_followup(plan) returns str | None, UI just renders
  • llm_spec="ui" - cheapest model, no chain of thought, model is just query: str
  • Lightbulb in footer_actions - inline with edit/copy icons
    (needs Add footer_actions parameter to ChatMessage panel-extensions/panel-material-ui#605), falls back to footer_objects
  • Squashed to 1 commit, docs updated

@ghostiee-11
ghostiee-11 marked this pull request as ready for review March 24, 2026 07:28

@ahuang11 ahuang11 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. I don't think there should be an auto-generate follow up suggestion; it should just be an icon button that users click when they're out of ideas
  2. Inconsistent naming: follow_up is sometimes followup
  3. Lightbulb should always show; no need for a toggle

Toggles should be reserved for anything that auto-generates, e.g. Validation agent or SQL planning.

@ahuang11
ahuang11 marked this pull request as draft March 26, 2026 18:00
@ghostiee-11

Copy link
Copy Markdown
Collaborator Author

Okay!! will fix all this..

- Follow-up suggestion is no longer auto-generated after every query.
  The lightbulb icon always appears and triggers the LLM call on click.
- Remove follow_up_suggestions param and Settings toggle. Toggles are
  reserved for auto-generating features (Validation, SQL Planning).
- Fix naming: suggest_followup -> suggest_follow_up, followup_button ->
  follow_up_button for consistency. is_followup on Plan is unchanged
  (separate exploration feature).
- Remove _background_tasks (no longer needed).
- Update tests and docs accordingly.
@ghostiee-11
ghostiee-11 marked this pull request as ready for review March 26, 2026 20:05
)
return result.query
except Exception:
return None

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe it should raise a notification perhaps through pn.state.notifications instead of failing silently.

Comment thread lumen/ai/ui.py Outdated
size="small",
icon_size="0.9em",
margin=(5, 0),
on_click=lambda _: state.execute(_generate_suggestion),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it necessary to call state.execute() vs just _generate_suggestion. Also for consistency, I think it should be called _generate_follow_up. And nested functions should be right under function signature, i.e. move

        if not plan.out_context.get("pipeline"):
            return
        if not plan.out_context.get("data"):
            return

        num_objects = len(self.interface.objects)

below

Comment thread lumen/ai/ui.py Outdated
Comment on lines +1625 to +1627
def hide_follow_up(_=None):
if len(self.interface.objects) > num_objects:
follow_up_button.visible = False

@ahuang11 ahuang11 Mar 26, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe this can be a rx one-liner, e.g. IconButton(visible=self...); see https://medium.com/@pYdeas/rx-marks-the-spot-a-prescription-for-reactive-python-bdf25e5bf727

Alternatively, there might not be a need to hide it. Maybe user wants to keep stemming from the same conversation.

@ghostiee-11

ghostiee-11 commented Mar 26, 2026

Copy link
Copy Markdown
Collaborator Author

Addressed all three inline comments + the top-level review:

  • base.py:549 - replaced silent except with state.notifications.warning() so users see why suggestion failed
  • ui.py:1635 - renamed to _generate_follow_up, moved nested function right under signature, kept state.execute() (needed for async on the event loop, same pattern as replan_button/rerun_button)
  • ui.py:1627 - removed hide logic entirely, button stays visible and re-enables after each click so users can keep generating
  • Naming - standardized on follow_up everywhere
  • Removed bare except Exception: pass in UI - coordinator already notifies on failure, UI uses try/finally to re-enable the button

- Replace silent except with state.notifications.warning in coordinator
- Rename _generate_suggestion to _generate_follow_up, move under signature
- Keep state.execute() for async on_click (matches replan/rerun pattern)
- Remove hide logic so button stays visible for repeated use
- Remove bare except Exception: pass, use try/finally instead
Comment thread lumen/ai/coordinator/base.py Outdated
"template": PROMPTS_DIR / "Coordinator" / "tool_relevance.jinja2",
"response_model": ThinkingYesNo,
},
"follow_up_suggestions": {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
"follow_up_suggestions": {
"follow_up": {

Comment thread lumen/ai/coordinator/base.py Outdated

try:
result = await self._invoke_prompt(
"follow_up_suggestions",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
"follow_up_suggestions",
"follow_up",

Comment thread lumen/ai/ui.py Outdated
Comment on lines +1612 to +1613
with edit_readonly(self._chat_input):
self._chat_input.value_input = suggestion

@ahuang11 ahuang11 Mar 27, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need edit read only? Can't we just do self._chat_input.value = suggestion

Suggested change
with edit_readonly(self._chat_input):
self._chat_input.value_input = suggestion
self._chat_input.value = suggestion

Comment thread lumen/ai/ui.py Outdated
Comment on lines +1638 to +1649
try:
existing = message.footer_actions or []
except AttributeError:
existing = message.footer_objects or []
existing = [
obj for obj in existing
if getattr(obj, 'name', '') != "FollowUp"
]
try:
message.footer_actions = existing + [follow_up_button]
except AttributeError:
message.footer_objects = existing + [follow_up_button]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's just assume footer actions is implemented, and then bump panel-material-ui pinned requirement.

- Rename prompt key follow_up_suggestions -> follow_up
- Use self._chat_input.value instead of edit_readonly + value_input
- Remove try/except fallback for footer_actions, assume it exists
- Bump panel-material-ui pin to >=0.9.0 (has footer_actions)
footer_actions is not yet in any released panel-material-ui version.
Use hasattr to detect it and fall back to footer_objects for now.
Revert pin bump to >=0.8.1.
Comment thread pixi.toml Outdated
ghostiee-11 and others added 2 commits April 2, 2026 11:35
…i pin

Removed hasattr fallback for footer_actions/footer_objects since
panel-material-ui >=0.9.0 guarantees footer_actions. Restored the
pin that was accidentally lowered to >=0.8.1.
Comment thread pixi.toml Outdated
@ghostiee-11
ghostiee-11 force-pushed the feat/smart-followup-suggestions branch from e297d4c to bd3e033 Compare April 5, 2026 04:19
@ahuang11
ahuang11 marked this pull request as draft May 6, 2026 21:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add dataset-specific follow-up suggestion chips after successful queries

2 participants