Skip to content

Commit 766b79a

Browse files
authored
Merge pull request #75 from bagofwords1/feedback-dashboard
## Version 0.0.306 (January 26, 2026) - **New Interactive Dashboards**: Dashboards are now generated as executable React/HTML code, enabling rich interactivity, custom styling, and dynamic visualizations - **Visual Feedback**: Upload screenshots or images with your prompts to show the AI exactly what you want—perfect for requesting design tweaks or pointing out issues - Dashboard validation now includes automatic screenshot capture, allowing the AI to visually verify the output before finalizing - Added vision model support for OpenAI, Anthropic, and Google Gemini LLM providers
2 parents b8d85fe + 3e7c29e commit 766b79a

File tree

27 files changed

+1032
-93
lines changed

27 files changed

+1032
-93
lines changed

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
# Release Notes
22

3+
## Version 0.0.306 (January 26, 2026)
4+
- **New Interactive Dashboards**: Dashboards are now generated as executable React/HTML code, enabling rich interactivity, custom styling, and dynamic visualizations
5+
- **Visual Feedback**: Upload screenshots or images with your prompts to show the AI exactly what you want—perfect for requesting design tweaks or pointing out issues
6+
- Dashboard validation now includes automatic screenshot capture, allowing the AI to visually verify the output before finalizing
7+
- Added vision model support for OpenAI, Anthropic, and Google Gemini LLM providers
8+
39
## Version 0.0.305 (January 24, 2026)
410
- **Rebuilt Dashboards**: Now fully AI-generated as executable code (React/HTML) with iterative refinement based on conversation history
511
- Fixed @ mention detection in prompt input (no longer triggers inside existing mentions)

Dockerfile

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,9 @@ ENV PATH="/opt/venv/bin:$PATH"
3030
RUN python3 -m pip install --no-cache-dir --upgrade pip setuptools wheel && \
3131
python3 -m pip install --no-cache-dir --prefer-binary -r requirements_versioned.txt
3232

33+
# Install Playwright browser (chromium only to save space)
34+
RUN playwright install chromium --with-deps
35+
3336
FROM ubuntu:24.04 AS frontend-builder
3437

3538
ENV DEBIAN_FRONTEND=noninteractive
@@ -93,12 +96,21 @@ COPY --from=backend-builder --chown=app:app /opt/venv /opt/venv
9396
ENV PATH="/opt/venv/bin:$PATH"
9497
COPY --from=backend-builder --chown=app:app /app/backend /app/backend
9598

99+
# Copy Playwright browser binaries from builder
100+
COPY --from=backend-builder --chown=app:app /root/.cache/ms-playwright /home/app/.cache/ms-playwright
101+
102+
# Install Playwright system dependencies (runtime libs only, no browser download)
103+
RUN playwright install-deps chromium
104+
96105
# Copy demo data sources (SQLite/DuckDB files for demo databases)
97106
COPY --chown=app:app ./backend/demo-datasources /app/backend/demo-datasources
98107

99108
# Copy only the built Nuxt output to keep the image small
100109
COPY --from=frontend-builder --chown=app:app /app/frontend/.output /app/frontend/.output
101110

111+
# Copy sandbox HTML for artifact validation (used by headless browser)
112+
COPY --from=frontend-builder --chown=app:app /app/frontend/public/artifact-sandbox.html /app/frontend/public/artifact-sandbox.html
113+
102114
# Copy runtime configs and scripts
103115
COPY --chown=app:app ./backend/requirements_versioned.txt /app/backend/
104116

VERSION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
0.0.305
1+
0.0.306
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
"""add supports_vision to llm_models
2+
3+
Revision ID: p1q2r3s4t5u6
4+
Revises: o0p1q2r3s4t5
5+
Create Date: 2025-01-25 12:00:00.000000
6+
7+
Adds supports_vision column to llm_models table to indicate whether a model accepts image inputs.
8+
"""
9+
from typing import Sequence, Union
10+
from sqlalchemy import false, true
11+
12+
from alembic import op
13+
import sqlalchemy as sa
14+
15+
16+
# revision identifiers, used by Alembic.
17+
revision: str = 'p1q2r3s4t5u6'
18+
down_revision: Union[str, None] = 'o0p1q2r3s4t5'
19+
branch_labels: Union[str, Sequence[str], None] = None
20+
depends_on: Union[str, Sequence[str], None] = None
21+
22+
23+
def upgrade() -> None:
24+
with op.batch_alter_table('llm_models', schema=None) as batch_op:
25+
batch_op.add_column(sa.Column('supports_vision', sa.Boolean(), nullable=False, server_default=true()))
26+
27+
28+
def downgrade() -> None:
29+
with op.batch_alter_table('llm_models', schema=None) as batch_op:
30+
batch_op.drop_column('supports_vision')

backend/app.db

Whitespace-only changes.

backend/app/ai/agent_v2.py

Lines changed: 48 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
from app.core.telemetry import telemetry
3636
from app.ai.utils.token_counter import count_tokens
3737
from app.services.instruction_usage_service import InstructionUsageService
38+
from app.ai.llm.types import ImageInput
3839

3940
INDEX_LIMIT = 1000 # Number of tables to include in the index
4041

@@ -69,11 +70,15 @@ def __init__(self, db=None, organization=None, organization_settings=None, repor
6970
# Handle case where data_sources or files might be None
7071
self.data_sources = getattr(report, 'data_sources', []) or []
7172
self.clients = clients
72-
self.files = getattr(report, 'files', []) or []
73+
all_files = getattr(report, 'files', []) or []
74+
# Split files: images go to LLM vision, everything else goes through existing flow
75+
self.image_files = [f for f in all_files if (getattr(f, 'content_type', '') or '').startswith('image/')]
76+
self.analysis_files = [f for f in all_files if not (getattr(f, 'content_type', '') or '').startswith('image/')]
7377
else:
7478
self.data_sources = []
7579
self.clients = {}
76-
self.files = []
80+
self.image_files = []
81+
self.analysis_files = []
7782

7883
self.sigkill_event = asyncio.Event()
7984
websocket_manager.add_handler(self._handle_completion_update)
@@ -152,6 +157,26 @@ def __init__(self, db=None, organization=None, organization_settings=None, repor
152157
# Initialize SuggestInstructions agent for post-analysis suggestions
153158
self.suggest_instructions = SuggestInstructions(model=self.small_model)
154159

160+
async def _load_images_as_input(self) -> list[ImageInput]:
161+
"""Load image files as base64-encoded ImageInput objects for vision models."""
162+
import base64
163+
import aiofiles
164+
165+
images: list[ImageInput] = []
166+
for f in self.image_files:
167+
try:
168+
file_path = getattr(f, 'path', None)
169+
if not file_path:
170+
continue
171+
async with aiofiles.open(file_path, 'rb') as file:
172+
content = await file.read()
173+
data = base64.b64encode(content).decode('utf-8')
174+
media_type = getattr(f, 'content_type', 'image/png') or 'image/png'
175+
images.append(ImageInput(data=data, media_type=media_type, source_type='base64'))
176+
except Exception as e:
177+
logger.warning(f"Failed to load image file {getattr(f, 'id', 'unknown')}: {e}")
178+
return images
179+
155180
async def estimate_prompt_tokens(self) -> dict:
156181
"""Approximate the total planner prompt tokens without executing tools."""
157182
try:
@@ -691,6 +716,23 @@ async def main_execution(self):
691716
# Entities context (catalog entities relevant to this turn)
692717
entities_context = (view.warm.entities.render() if getattr(view.warm, "entities", None) else "")
693718

719+
# Load user-uploaded images for vision models (only on first loop iteration)
720+
user_images = await self._load_images_as_input() if loop_index == 0 else []
721+
722+
# Extract images from observation (tool screenshots, etc.)
723+
observation_images: list[ImageInput] = []
724+
if observation and isinstance(observation, dict) and observation.get("images"):
725+
for img in observation["images"]:
726+
if isinstance(img, dict) and img.get("data"):
727+
observation_images.append(ImageInput(
728+
data=img["data"],
729+
media_type=img.get("media_type", "image/png"),
730+
source_type=img.get("source_type", "base64"),
731+
))
732+
733+
# Combine user images + observation images
734+
all_images = user_images + observation_images
735+
694736
planner_input = PlannerInput(
695737
organization_name=self.organization.name,
696738
organization_ai_analyst_name=self.ai_analyst_name,
@@ -710,7 +752,8 @@ async def main_execution(self):
710752
past_observations=self.context_hub.observation_builder.tool_observations,
711753
external_platform=getattr(self.head_completion, "external_platform", None),
712754
tool_catalog=self.planner.tool_catalog,
713-
mode=self.mode
755+
mode=self.mode,
756+
images=all_images if all_images else None,
714757
)
715758
# Kick off early scoring in background without blocking the loop (isolated DB session)
716759
asyncio.create_task(self._run_early_scoring_background(planner_input))
@@ -1122,7 +1165,7 @@ async def _next_seq():
11221165
"context_view": view,
11231166
"context_hub": self.context_hub,
11241167
"ds_clients": self.clients,
1125-
"excel_files": self.files,
1168+
"excel_files": self.analysis_files,
11261169
"training_build_id": self.training_build_id, # For training mode instruction creation
11271170
"agent_execution_id": str(self.current_execution.id) if self.current_execution else None,
11281171
"mode": self.mode, # Current agent mode (chat/training/deep) for tool access control
@@ -1344,7 +1387,7 @@ async def emit(ev: dict):
13441387
"status": "success" if observation and not observation.get("error") else "error",
13451388
"result_summary": observation.get("summary", "") if observation else "",
13461389
# Include query_id for hydration in frontend previews when available
1347-
"result_json": ({**safe_result_json, "query_id": (str(self.current_query.id) if getattr(self, "current_query", None) else None)} if isinstance(safe_result_json, dict) else safe_result_json),
1390+
"result_json": ({**safe_result_json, "query_id": (str(self.current_query.id) if getattr(self, "current_query", None) else None), "created_visualization_ids": created_visualization_ids} if isinstance(safe_result_json, dict) else safe_result_json),
13481391
"duration_ms": tool_execution.duration_ms,
13491392
"created_widget_id": created_widget_id,
13501393
"created_step_id": created_step_id,

backend/app/ai/agents/planner/planner_v2.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ async def execute(
5858
# Stream LLM tokens and build decision snapshots
5959
async for chunk in self.llm.inference_stream(
6060
prompt,
61+
images=planner_input.images,
6162
usage_scope="planner",
6263
usage_scope_ref_id=None,
6364
):

backend/app/ai/agents/planner/prompt_builder.py

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,11 @@ def build_prompt(planner_input: PlannerInput) -> str:
5656
# Determine mode label for prompt
5757
mode_label = "Deep Analytics" if planner_input.mode == "deep" else "Chat"
5858

59+
# Build images context - images can be user-uploaded or from tool observations (screenshots)
60+
images_context = ""
61+
if planner_input.images:
62+
images_context = f"<images>{len(planner_input.images)} image(s) attached to this request. These may include user-uploaded images or tool observation screenshots (see last_observation for context). Analyze them as part of your response when relevant.</images>"
63+
5964
prompt= f"""
6065
SYSTEM
6166
Time: {datetime.now().strftime("%Y-%m-%d %H:%M:%S")}; timezone: {datetime.now().astimezone().tzinfo}
@@ -116,8 +121,11 @@ def build_prompt(planner_input: PlannerInput) -> str:
116121
- Do not include sample/fabricated data in final_answer.
117122
- If the user asks (explicitly or implicitly) to create/show/list/visualize/compute a metric/table/chart, prefer the create_data tool.
118123
- A widget should represent a SINGLE piece of data or analysis (a single metric, a single table, a single chart, etc).
119-
- If the user asks for a dashboard/report/etc, create all the widgets first, then call the create_artifact tool once all queries were created.
124+
- If the user asks for a dashboard/report/etc, create all the required widgets first, then call the create_artifact tool once all queries were created.
120125
- If the user asks to build a dashboard/report/layout (or to design/arrange/present widgets), and all widgets are already created, call the create_artifact tool immediately.
126+
- When calling create_artifact, choose the appropriate mode:
127+
- Use mode="page" (default) for dashboards, reports, and interactive data displays
128+
- Use mode="slides" for presentations, slide decks, or when the user mentions PowerPoint/PPTX export
121129
- If the user is asking for a subjective metric or uses a semantic metric that is not well defined (in instructions or schema or context), output your clarifying questions in assistant_message and call the clarify tool.
122130
- If the user is asking about something that can be answered from provided context (schemas/resources/history) and your confidence is high (≥0.8) AND the user is not asking to create/visualize/persist an artifact, you may use the answer_question tool. Prefer a short reasoning_message (or null). It streams the final user-facing answer.
123131
- Prefer using data sources, tables, files, and entities explicitly listed in <mentions>. Treat them as high-confidence anchors for this turn. If you select an unmentioned source, briefly explain why.
@@ -185,6 +193,7 @@ def build_prompt(planner_input: PlannerInput) -> str:
185193
186194
INPUT ENVELOPE
187195
<user_prompt>{planner_input.user_message}</user_prompt>
196+
{images_context}
188197
<context>
189198
<platform>{planner_input.external_platform}</platform>
190199
{planner_input.instructions}
@@ -262,6 +271,11 @@ def _build_training_prompt(planner_input: PlannerInput) -> str:
262271
research_tools_json = json.dumps(research_tools, ensure_ascii=False)
263272
action_tools_json = json.dumps(action_tools, ensure_ascii=False)
264273

274+
# Build images context - images can be user-uploaded or from tool observations (screenshots)
275+
images_context = ""
276+
if planner_input.images:
277+
images_context = f"<images>{len(planner_input.images)} image(s) attached to this request. These may include user-uploaded images or tool observation screenshots (see last_observation for context). Analyze them as part of your response when relevant.</images>"
278+
265279
prompt = f"""
266280
SYSTEM
267281
Time: {datetime.now().strftime("%Y-%m-%d %H:%M:%S")}; timezone: {datetime.now().astimezone().tzinfo}
@@ -474,6 +488,7 @@ def _build_training_prompt(planner_input: PlannerInput) -> str:
474488
475489
INPUT ENVELOPE
476490
<user_prompt>{planner_input.user_message}</user_prompt>
491+
{images_context}
477492
<context>
478493
<platform>{planner_input.external_platform}</platform>
479494
{planner_input.instructions}

backend/app/ai/context/builders/message_context_builder.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,12 +194,35 @@ async def build_context(
194194
digest_parts.append(f"chart: {dm_type}")
195195
except Exception:
196196
pass
197+
# Surface visualization_id if available (added by orchestrator)
198+
try:
199+
viz_ids = rj.get('created_visualization_ids') or []
200+
if viz_ids:
201+
digest_parts.append(f"viz_id: {viz_ids[0]}")
202+
except Exception:
203+
pass
197204
if sample_row:
198205
try:
199206
digest_parts.append(f"top row: {json.dumps(sample_row)}")
200207
except Exception:
201208
pass
202209
tool_info += " - " + "; ".join(digest_parts)
210+
# Digest for describe_entity results
211+
elif tool_execution.tool_name == 'describe_entity' and tool_execution.result_json:
212+
rj = tool_execution.result_json or {}
213+
digest_parts = []
214+
entity_title = rj.get('title')
215+
if entity_title:
216+
digest_parts.append(f"entity: {entity_title}")
217+
# Surface visualization_id if created
218+
try:
219+
viz_ids = rj.get('created_visualization_ids') or []
220+
if viz_ids:
221+
digest_parts.append(f"viz_id: {viz_ids[0]}")
222+
except Exception:
223+
pass
224+
if digest_parts:
225+
tool_info += " - " + "; ".join(digest_parts)
203226
elif tool_execution.tool_name == 'describe_tables' and tool_execution.result_json:
204227
# Show table names extracted from schemas excerpt; fallback to query/arguments
205228
rj = tool_execution.result_json or {}
@@ -556,7 +579,30 @@ async def build(
556579
digest_parts.append(f"chart: {dm_type}")
557580
except Exception:
558581
pass
582+
# Surface visualization_id if available (added by orchestrator)
583+
try:
584+
viz_ids = rj.get('created_visualization_ids') or []
585+
if viz_ids:
586+
digest_parts.append(f"viz_id: {viz_ids[0]}")
587+
except Exception:
588+
pass
559589
tool_info += " - " + "; ".join(digest_parts)
590+
elif tool_execution.status == 'success' and tool_execution.tool_name == 'describe_entity' and tool_execution.result_json:
591+
# Digest for describe_entity results
592+
rj = tool_execution.result_json or {}
593+
digest_parts = []
594+
entity_title = rj.get('title')
595+
if entity_title:
596+
digest_parts.append(f"entity: {entity_title}")
597+
# Surface visualization_id if created
598+
try:
599+
viz_ids = rj.get('created_visualization_ids') or []
600+
if viz_ids:
601+
digest_parts.append(f"viz_id: {viz_ids[0]}")
602+
except Exception:
603+
pass
604+
if digest_parts:
605+
tool_info += " - " + "; ".join(digest_parts)
560606
elif tool_execution.status == 'success' and tool_execution.tool_name == 'describe_tables' and tool_execution.result_json:
561607
# Show table names extracted from schemas excerpt; fallback to query/arguments
562608
rj = tool_execution.result_json or {}

0 commit comments

Comments
 (0)