databao 0.3.3.dev2__py3-none-any.whl → 0.3.3.dev3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: databao
3
- Version: 0.3.3.dev2
3
+ Version: 0.3.3.dev3
4
4
  License-File: LICENSE
5
5
  Requires-Python: >=3.11
6
6
  Requires-Dist: adbc-driver-manager>=1.10.0
@@ -1,8 +1,8 @@
1
1
  databao_cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
- databao_cli/__main__.py,sha256=BFA_15eQIiQOvT26APsazBTmnynYk7_OlipcVHiQcpU,8428
2
+ databao_cli/__main__.py,sha256=-ZySAVV7bABN_PwleKCZUx06gUEcnJvBN2kjZQYV2yE,9027
3
3
  databao_cli/executor_utils.py,sha256=WFUuUoxfHGsxp2wfjpgCeGs4v1EGKl8ZKltzKp4VFmI,1733
4
4
  databao_cli/commands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
5
- databao_cli/commands/app.py,sha256=u9odVAWyEJHFcaxWb-quCHIb8RRGEtkInU_2hW45yfs,645
5
+ databao_cli/commands/app.py,sha256=mCUN8qJlF7tj23fRNHAAzlZp50GJgwMVUxDJwaR2vcA,813
6
6
  databao_cli/commands/ask.py,sha256=NGvjJwdYbBhqyqSFGknaNmgd-O2X-HrI3acH7pJ5psA,7143
7
7
  databao_cli/commands/build.py,sha256=alsyExwwI23QyG49h5d6QL6nf4hjGHtluciepLKY2LQ,506
8
8
  databao_cli/commands/context_engine_cli.py,sha256=U88IlVS_PQpcQiNp86lCxx1uAQLyGlfcgNXNqUsg1pY,1241
@@ -24,18 +24,18 @@ databao_cli/mcp/tools/databao_ask/tool.py,sha256=-1541H8KDY18jS_qYuB1AFu_n0pRHjC
24
24
  databao_cli/project/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
25
25
  databao_cli/project/layout.py,sha256=WcIUaNBD-njKiGmFiTDKwcOk1NgRV5CQ6NjdNVTnu30,1586
26
26
  databao_cli/ui/__init__.py,sha256=5U7huKF5FMi7RcgidxcbFfCx2hKKAJj6JxD4CgvS7n0,39
27
- databao_cli/ui/app.py,sha256=Y4DQRKoijASHwrHgs_OvoA2_aoFuHaqztKtAtlgwvTE,17858
28
- databao_cli/ui/cli.py,sha256=JNuE9n8oAaYkTEMRHs588Vi5YtEVYVPqBEvWwbirOgk,1022
27
+ databao_cli/ui/app.py,sha256=qmoklnHCFPSRA2HXWGL_375f2qQwEV-Ob8twQMnx-r8,19379
28
+ databao_cli/ui/cli.py,sha256=nRzaGBHoJTzTSEQ_8yB_5G0EoGC-do-crA4DzV_RnXQ,1281
29
29
  databao_cli/ui/project_utils.py,sha256=4LG8OimVxuRa2Ke0rtn7AxE-7OuAK3qnvVNxLk11z3c,1614
30
30
  databao_cli/ui/streaming.py,sha256=aBaw8UC01lTL0M2AfvT-VRRVFamAbhP4BLrfDLC4f50,1376
31
- databao_cli/ui/suggestions.py,sha256=EuDUsG0gA2T1q-lqRas5lYOhXX4ZjZAlzkKc7EvbkEM,7217
31
+ databao_cli/ui/suggestions.py,sha256=vXkmr8dmRL0xhIemCa6oifGgpTClJsVTf-PQ9hJQaqw,8486
32
32
  databao_cli/ui/assets/bao.png,sha256=aJOABLmxPmn7pfCAeiUbXgho_SilUcdpCtKDYm0_LLs,54635
33
33
  databao_cli/ui/components/__init__.py,sha256=bcxC59NGEPVfDfb4VwSoj1mHuWVJnWd-aNEmMvZMmlI,31
34
- databao_cli/ui/components/chat.py,sha256=jQ-Q9620UuygLtHzc8wF4hjOOCRjfGJGSm7ecMusr1k,13516
34
+ databao_cli/ui/components/chat.py,sha256=ttxBhLoRH4ljm0xwQilns1c33yQTffVJNF0BTfhO6jA,19478
35
35
  databao_cli/ui/components/datasource_form.py,sha256=qaGfBakCnkHtitcMAj4rnhx7z6aYxhXI3msFGMKk65g,6306
36
- databao_cli/ui/components/datasource_manager.py,sha256=vP0Z9gISfKzdYKestYZHuGSAkMi-rMAuMiCPRpCT8cM,9495
36
+ databao_cli/ui/components/datasource_manager.py,sha256=v_djWJZ7xetvkYuFLBRsAl7o3uY7qqPFgpIMAx7ARbc,9635
37
37
  databao_cli/ui/components/icons.py,sha256=IdFBEwTXK_gBaDIoTCdGaFAV2kGhP1pcM94-ISmp9rA,1755
38
- databao_cli/ui/components/results.py,sha256=5vkCenMQUDj4Nt9r9cGHhDppfIXZiRTvHiQKazmXqPs,12488
38
+ databao_cli/ui/components/results.py,sha256=yXsiboayqH54GytO2dJB6-5piYFDugOM_JtqEXXy3wE,13246
39
39
  databao_cli/ui/components/sidebar.py,sha256=AuL02hDvmCO64yzstncUoW64byZdfy4vBZdRIjVVe4A,6027
40
40
  databao_cli/ui/components/status.py,sha256=W9RG250yef39ZTLfGW7vVfRYJ3DYPb0JjKkWgK-TPfo,3953
41
41
  databao_cli/ui/models/__init__.py,sha256=fwnl7W0XfhKPlCgHnQ33SEF4uldigo_Ndm5h6SDJDLA,279
@@ -44,20 +44,20 @@ databao_cli/ui/models/settings.py,sha256=xrmtHpmhsL8H1PpxWtkA6qJRegT-95u6phPm50Y
44
44
  databao_cli/ui/pages/__init__.py,sha256=_pRh8Qxs02DzS4r9z3-LPjUQTWte0NTa0Zu4la6dHRc,60
45
45
  databao_cli/ui/pages/agent_settings.py,sha256=huBZAmM9McA_e3W6iyJPfF23y5nFqYohLqwH7h_hlC4,12639
46
46
  databao_cli/ui/pages/chat.py,sha256=yLJt3Jm_62ar2jUFB1OUAQqG5AkaOa9zciMiQaStASk,7779
47
- databao_cli/ui/pages/context_settings.py,sha256=aT_Hx5XlJj5eEfvLDqnSLtNWt5q30QuijJj1AwdRx2g,5297
47
+ databao_cli/ui/pages/context_settings.py,sha256=wI0_In_19tSLqEnXpe-vslNzl7eYLyhii_FmIt4jdlY,5085
48
48
  databao_cli/ui/pages/general_settings.py,sha256=wXtNpnvFK-BD_AYExbym0aFluDOg2J_aMWshFKfqZTg,3874
49
- databao_cli/ui/pages/welcome.py,sha256=VmDrxnTar-0y3ggVdC2lZYNGA2VlsYqi4tv4e7_3IWc,12512
50
- databao_cli/ui/services/__init__.py,sha256=K_5kJPty2sBEUpGkql-PQQouWZsl-Pywj54CLogLmXc,1334
49
+ databao_cli/ui/pages/welcome.py,sha256=bXtdJ35LG48KNEdSJHaeJpkZ-uHUPeAA1aL7a4ofeDs,12924
50
+ databao_cli/ui/services/__init__.py,sha256=lmPHHuOVdOA-c_POOMkD1044SRBb6-Jl1Gv-2g4Z63U,1368
51
51
  databao_cli/ui/services/build_service.py,sha256=St78meRjvT5BJzVhHusWTSL76NESuQGryxRD0CtnGwQ,8033
52
52
  databao_cli/ui/services/chat_persistence.py,sha256=kb7KTT2WCNFPcSz99m653cAMV8mN2hVbD1pMyAVaX40,9346
53
53
  databao_cli/ui/services/chat_title.py,sha256=cN5isa3NOp2_yHvLtTuO1ZuOII7D064KfiGxGQuxeT8,5630
54
54
  databao_cli/ui/services/dce_operations.py,sha256=mHGmLit270BC8br9wBxSGwCewdUjts8MOa40QS_rxSo,7199
55
55
  databao_cli/ui/services/llm_models.py,sha256=EQQ1ATSaDbs7bHVSwWpLHcYFbO1VA0nTl-t3HZJz14Q,2779
56
- databao_cli/ui/services/query_executor.py,sha256=9-l171v944_EhagiCPaxI-SOy1t-pYHmaeTu9-mV0Yo,7606
56
+ databao_cli/ui/services/query_executor.py,sha256=TEcHFUp7yuo7GdTtGJXJmuF8IAkctjc2B3BHbSFLz7c,10879
57
57
  databao_cli/ui/services/settings_persistence.py,sha256=LFgO21guu0CfcKSRWWwUkc7nREwSe8iLnu3GSE8JhCg,1878
58
58
  databao_cli/ui/services/storage.py,sha256=wCSAjL8WiAghX2JaZVCDxb52NCMwL010ziqo6xcwqd8,2305
59
- databao-0.3.3.dev2.dist-info/METADATA,sha256=iDyAQHee0Iu8dHn5BEo3f3p9pjXLestuTELsHr1IGz0,3374
60
- databao-0.3.3.dev2.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
61
- databao-0.3.3.dev2.dist-info/entry_points.txt,sha256=fKKysivJDgZbz5wKQXsbPALm2WTpY1RPHqZmZ_hu1f4,53
62
- databao-0.3.3.dev2.dist-info/licenses/LICENSE,sha256=_u6l_NE7cSV2FtV2hwTT9hB-AKjkQfZHQgE1P1kFs24,12353
63
- databao-0.3.3.dev2.dist-info/RECORD,,
59
+ databao-0.3.3.dev3.dist-info/METADATA,sha256=kaXR-3XFhEcbGpm_OdpVo2AvjAk4JnD4y_UmfmSKr_4,3374
60
+ databao-0.3.3.dev3.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
61
+ databao-0.3.3.dev3.dist-info/entry_points.txt,sha256=fKKysivJDgZbz5wKQXsbPALm2WTpY1RPHqZmZ_hu1f4,53
62
+ databao-0.3.3.dev3.dist-info/licenses/LICENSE,sha256=_u6l_NE7cSV2FtV2hwTT9hB-AKjkQfZHQgE1P1kFs24,12353
63
+ databao-0.3.3.dev3.dist-info/RECORD,,
databao_cli/__main__.py CHANGED
@@ -215,8 +215,23 @@ def ask(
215
215
  default=False,
216
216
  help="Disable all domain-editing operations (init, datasources, build) in the UI",
217
217
  )
218
+ @click.option(
219
+ "--hide-suggested-questions",
220
+ is_flag=True,
221
+ default=False,
222
+ help="Hide the suggested questions on the empty chat screen",
223
+ )
224
+ @click.option(
225
+ "--hide-build-context-hint",
226
+ is_flag=True,
227
+ default=False,
228
+ help=(
229
+ "Hide the 'Context isn't built yet' warning on the empty chat screen and "
230
+ "remove the Build Context step from the setup wizard"
231
+ ),
232
+ )
218
233
  @click.pass_context
219
- def app(ctx: click.Context, read_only_domain: bool) -> None:
234
+ def app(ctx: click.Context, read_only_domain: bool, hide_suggested_questions: bool, hide_build_context_hint: bool) -> None:
220
235
  """Launch the Databao Streamlit web interface.
221
236
 
222
237
  All additional arguments are passed directly to streamlit run.
@@ -231,6 +246,8 @@ def app(ctx: click.Context, read_only_domain: bool) -> None:
231
246
  from databao_cli.commands.app import app_impl
232
247
 
233
248
  ctx.obj["read_only_domain"] = read_only_domain
249
+ ctx.obj["hide_suggested_questions"] = hide_suggested_questions
250
+ ctx.obj["hide_build_context_hint"] = hide_build_context_hint
234
251
  app_impl(ctx)
235
252
 
236
253
 
@@ -16,6 +16,8 @@ def app_impl(ctx: click.Context) -> None:
16
16
  ctx.obj["project_dir"],
17
17
  ctx.args,
18
18
  read_only_domain=ctx.obj.get("read_only_domain", False),
19
+ hide_suggested_questions=ctx.obj.get("hide_suggested_questions", False),
20
+ hide_build_context_hint=ctx.obj.get("hide_build_context_hint", False),
19
21
  )
20
22
  except subprocess.CalledProcessError as e:
21
23
  click.echo(f"Error running Streamlit: {e}", err=True)
databao_cli/ui/app.py CHANGED
@@ -177,11 +177,28 @@ def _clear_all_chat_threads() -> None:
177
177
  chat.thread = None
178
178
 
179
179
 
180
+ def invalidate_agent(status_message: str = "Reloading project...") -> None:
181
+ """Reset the in-memory agent and project so they are re-created on the next rerun.
182
+
183
+ Call this whenever the set of datasources changes (add, remove, save)
184
+ to ensure the agent does not reference stale catalogs/schemas.
185
+ """
186
+ st.session_state.databao_project = None
187
+ st.session_state.agent = None
188
+ _clear_all_chat_threads()
189
+ set_status(AppStatus.INITIALIZING, status_message)
190
+
191
+
180
192
  def is_read_only_domain() -> bool:
181
193
  """Check whether domain-editing operations are disabled."""
182
194
  return cast(bool, st.session_state.get("_read_only_domain", False))
183
195
 
184
196
 
197
+ def is_hide_build_context_hint() -> bool:
198
+ """Check whether the build context hint and step are hidden."""
199
+ return cast(bool, st.session_state.get("_hide_build_context_hint", False))
200
+
201
+
185
202
  def _is_project_ready(project_dir: Path) -> bool:
186
203
  """Check if the Databao project is fully set up and ready for normal use."""
187
204
  project = find_project(project_dir)
@@ -503,11 +520,28 @@ def main() -> None:
503
520
  default=False,
504
521
  help="Disable all domain-editing operations (init, datasources, build)",
505
522
  )
523
+ parser.add_argument(
524
+ "--hide-suggested-questions",
525
+ action="store_true",
526
+ default=False,
527
+ help="Hide the suggested questions on the empty chat screen",
528
+ )
529
+ parser.add_argument(
530
+ "--hide-build-context-hint",
531
+ action="store_true",
532
+ default=False,
533
+ help="Hide the 'Context isn't built yet' warning on the empty chat screen and skip the Build Context step in the setup wizard",
534
+ )
506
535
  try:
507
536
  args = parser.parse_args()
508
537
  except SystemExit:
509
538
  st.warning("Failed to parse arguments. Using current directory as project directory.")
510
- args = argparse.Namespace(project_dir=None, read_only_domain=False)
539
+ args = argparse.Namespace(
540
+ project_dir=None,
541
+ read_only_domain=False,
542
+ hide_suggested_questions=False,
543
+ hide_build_context_hint=False,
544
+ )
511
545
 
512
546
  project_dir = Path(args.project_dir) if args.project_dir else Path.cwd()
513
547
 
@@ -515,6 +549,10 @@ def main() -> None:
515
549
 
516
550
  if "_read_only_domain" not in st.session_state:
517
551
  st.session_state._read_only_domain = args.read_only_domain
552
+ if "_hide_suggested_questions" not in st.session_state:
553
+ st.session_state._hide_suggested_questions = args.hide_suggested_questions
554
+ if "_hide_build_context_hint" not in st.session_state:
555
+ st.session_state._hide_build_context_hint = args.hide_build_context_hint
518
556
 
519
557
  if "_setup_mode_active" not in st.session_state:
520
558
  project = find_project(project_dir)
databao_cli/ui/cli.py CHANGED
@@ -21,6 +21,8 @@ def bootstrap_streamlit_app(
21
21
  streamlit_args: list[str] | None = None,
22
22
  *,
23
23
  read_only_domain: bool = False,
24
+ hide_suggested_questions: bool = False,
25
+ hide_build_context_hint: bool = False,
24
26
  ) -> None:
25
27
  """Bootstrap the UI."""
26
28
 
@@ -32,6 +34,10 @@ def bootstrap_streamlit_app(
32
34
  app_args = ["--project-dir", str(project_path)]
33
35
  if read_only_domain:
34
36
  app_args.append("--read-only-domain")
37
+ if hide_suggested_questions:
38
+ app_args.append("--hide-suggested-questions")
39
+ if hide_build_context_hint:
40
+ app_args.append("--hide-build-context-hint")
35
41
 
36
42
  subprocess.run(
37
43
  [sys.executable, "-m", "streamlit", "run", app_path, *streamlit_args, "--", *app_args],
@@ -13,6 +13,7 @@ from databao_cli.ui.services import (
13
13
  is_query_running,
14
14
  save_current_chat,
15
15
  start_query_execution,
16
+ stop_query,
16
17
  )
17
18
  from databao_cli.ui.suggestions import (
18
19
  check_suggestions_completion,
@@ -51,6 +52,9 @@ def render_assistant_message(
51
52
  elif message.content:
52
53
  st.error(message.content)
53
54
 
55
+ if message.metadata.get("stopped"):
56
+ st.warning("Query was stopped by user")
57
+
54
58
 
55
59
  def _truncate_question(question: str, max_len: int = 60) -> tuple[str, bool]:
56
60
  """Truncate a question for display, returning (display_text, was_truncated)."""
@@ -73,8 +77,9 @@ def render_welcome_component(chat: "ChatSession") -> None:
73
77
  - ready: shows questions with appropriate subtitle
74
78
  """
75
79
  status = st.session_state.get("suggestions_status", "not_started")
80
+ hide_suggested_questions = bool(st.session_state.get("_hide_suggested_questions"))
76
81
 
77
- if status == "not_started":
82
+ if status == "not_started" and not hide_suggested_questions:
78
83
  agent: Agent | None = st.session_state.get("agent")
79
84
  if agent is not None:
80
85
  start_suggestions_generation(agent)
@@ -94,7 +99,7 @@ def render_welcome_component(chat: "ChatSession") -> None:
94
99
  unsafe_allow_html=True,
95
100
  )
96
101
 
97
- if status == "loading":
102
+ if status == "loading" and not hide_suggested_questions:
98
103
  st.markdown(
99
104
  "<p style='text-align: center; color: #888; font-size: 0.9em; margin-top: 1em;'>"
100
105
  "🔄 Analyzing your data to suggest questions..."
@@ -106,7 +111,7 @@ def render_welcome_component(chat: "ChatSession") -> None:
106
111
  questions: list[str] = st.session_state.get("suggested_questions", [])
107
112
  is_llm_generated: bool = st.session_state.get("suggestions_are_llm_generated", False)
108
113
 
109
- if questions:
114
+ if questions and not hide_suggested_questions:
110
115
  if is_llm_generated:
111
116
  st.markdown(
112
117
  "<p style='text-align: center; color: #888; font-size: 0.9em; margin-top: 1em;'>"
@@ -360,14 +365,170 @@ def _should_show_welcome(chat: "ChatSession") -> bool:
360
365
  return not has_messages and not query_running
361
366
 
362
367
 
363
- def render_chat_interface(chat: "ChatSession") -> None:
364
- """Render the complete chat interface."""
365
- query_running = is_query_running(chat)
368
+ def _has_stopped_exchange(chat: "ChatSession") -> bool:
369
+ """Check if the last exchange was stopped *without* producing results.
370
+
371
+ Returns False when the stop happened during the visualization phase,
372
+ because the data result is already available and the conversation can
373
+ continue normally.
374
+ """
375
+ if not chat.messages:
376
+ return False
377
+ last = chat.messages[-1]
378
+ return last.role == "assistant" and bool(last.metadata.get("stopped")) and last.result is None
379
+
380
+
381
+ def _remove_stopped_exchange(chat: "ChatSession") -> None:
382
+ """Remove the trailing user + stopped-assistant message pair."""
383
+ if not chat.messages:
384
+ return
385
+ last = chat.messages[-1]
386
+ if last.role == "assistant" and last.metadata.get("stopped"):
387
+ chat.messages.pop()
388
+ if chat.messages and chat.messages[-1].role == "user":
389
+ chat.messages.pop()
390
+
391
+
392
+ @st.dialog("Overwrite previous request?")
393
+ def _confirm_overwrite_dialog() -> None:
394
+ """Modal dialog asking whether to discard the stopped exchange."""
395
+ st.markdown("The previous request was stopped. Sending a new request will remove it.")
396
+ col1, col2 = st.columns(2)
397
+ with col1:
398
+ if st.button("OK", use_container_width=True, type="primary"):
399
+ st.session_state["overwrite_confirmed"] = True
400
+ st.rerun()
401
+ with col2:
402
+ if st.button("Cancel", use_container_width=True):
403
+ st.session_state.pop("pending_query", None)
404
+ st.rerun()
405
+
406
+
407
+ def _handle_stop_click(chat: "ChatSession") -> None:
408
+ """Stop the running query and record the partial result.
409
+
410
+ If the data phase already produced a message (``viz_pending``), marks
411
+ it as stopped. Otherwise creates a new assistant message with
412
+ whatever thinking text was captured so far.
413
+ """
414
+ thinking_text = stop_query(chat)
415
+ if thinking_text is None:
416
+ return
366
417
 
367
- user_input = st.chat_input("Ask a question about your data...", disabled=query_running)
418
+ pending = _find_pending_viz_message(chat)
419
+ if pending is not None:
420
+ pending.viz_pending = False
421
+ pending.metadata["stopped"] = True
422
+ else:
423
+ chat.messages.append(
424
+ ChatMessage(
425
+ role="assistant",
426
+ thinking=thinking_text or None,
427
+ content="",
428
+ metadata={"stopped": True},
429
+ )
430
+ )
431
+ save_current_chat()
432
+
433
+
434
+ def _render_chat_input_bar(chat: "ChatSession", query_running: bool) -> None:
435
+ """Render the chat input bar.
436
+
437
+ When ``query_running`` is False, shows an enabled text input inside a
438
+ form (so Enter submits) and a send button.
439
+
440
+ When ``query_running`` is True (background query **or** manual plot
441
+ in progress), the input is disabled and a stop button is shown
442
+ instead.
443
+ """
444
+ if query_running:
445
+ col1, col2 = st.columns([12, 1], vertical_alignment="bottom")
446
+ with col1:
447
+ st.text_input(
448
+ "Message",
449
+ placeholder="Query in progress...",
450
+ disabled=True,
451
+ label_visibility="collapsed",
452
+ key="chat_input_disabled",
453
+ )
454
+ with col2:
455
+ if st.button(
456
+ ":material/stop_circle:",
457
+ key="stop_btn",
458
+ use_container_width=True,
459
+ type="primary",
460
+ help="Stop the running query",
461
+ ):
462
+ _handle_stop_click(chat)
463
+ st.rerun()
464
+ else:
465
+ with st.form("chat_input_form", clear_on_submit=True, border=False):
466
+ col1, col2 = st.columns([12, 1], vertical_alignment="bottom")
467
+ with col1:
468
+ user_input = st.text_input(
469
+ "Message",
470
+ placeholder="Ask a question about your data...",
471
+ label_visibility="collapsed",
472
+ key="chat_input",
473
+ )
474
+ with col2:
475
+ submitted = st.form_submit_button(
476
+ ":material/send:",
477
+ use_container_width=True,
478
+ )
479
+
480
+ if submitted and user_input:
481
+ if _has_stopped_exchange(chat):
482
+ st.session_state["pending_query"] = user_input
483
+ _confirm_overwrite_dialog()
484
+ else:
485
+ user_message = ChatMessage(role="user", content=user_input)
486
+ chat.messages.append(user_message)
487
+ save_current_chat()
488
+ start_background_query(chat, user_input)
489
+ st.rerun()
490
+
491
+
492
+ def _process_pending_overwrite(chat: "ChatSession") -> None:
493
+ """Process a confirmed overwrite of a stopped exchange.
494
+
495
+ Called at the top of ``render_chat_interface`` (before any widgets are
496
+ rendered), so no ``st.rerun()`` is needed — the updated state is
497
+ picked up by the rendering that follows in the same script run.
498
+ """
499
+ if not st.session_state.pop("overwrite_confirmed", False):
500
+ return
501
+
502
+ pending = st.session_state.pop("pending_query", None)
503
+ if not pending:
504
+ return
505
+
506
+ _remove_stopped_exchange(chat)
507
+ user_message = ChatMessage(role="user", content=pending)
508
+ chat.messages.append(user_message)
509
+ save_current_chat()
510
+ start_background_query(chat, pending)
511
+
512
+
513
+ def render_chat_interface(chat: "ChatSession") -> None:
514
+ """Render the complete chat interface.
515
+
516
+ Orchestrates, in order:
517
+ 1. Processing a confirmed overwrite of a stopped exchange.
518
+ 2. Chat history, thinking section, and polling fragments.
519
+ 3. The input bar (enabled or disabled).
520
+ 4. A pending manual "Generate Plot" execution (blocking, runs last
521
+ so the disabled input bar is already visible).
522
+ """
523
+ _process_pending_overwrite(chat)
368
524
 
369
525
  agent: Agent | None = st.session_state.get("agent")
370
- if agent is not None and not agent.domain.is_context_built() and not chat.messages:
526
+ if (
527
+ agent is not None
528
+ and not agent.domain.is_context_built()
529
+ and not chat.messages
530
+ and not st.session_state.get("_hide_build_context_hint")
531
+ ):
371
532
  st.markdown(
372
533
  "⚠️ Context isn't built yet. "
373
534
  '<a href="/context-settings#build-context" target="_self">Build context</a> '
@@ -375,15 +536,7 @@ def render_chat_interface(chat: "ChatSession") -> None:
375
536
  unsafe_allow_html=True,
376
537
  )
377
538
 
378
- if user_input:
379
- user_message = ChatMessage(role="user", content=user_input)
380
- chat.messages.append(user_message)
381
-
382
- save_current_chat()
383
-
384
- start_background_query(chat, user_input)
385
-
386
- st.rerun()
539
+ query_running = is_query_running(chat) or "pending_plot_message_index" in st.session_state
387
540
 
388
541
  if handle_query_completion(chat):
389
542
  st.rerun()
@@ -403,3 +556,11 @@ def render_chat_interface(chat: "ChatSession") -> None:
403
556
 
404
557
  if query_running:
405
558
  _query_polling_fragment()
559
+
560
+ st.markdown("<div style='height: 2em'></div>", unsafe_allow_html=True)
561
+ _render_chat_input_bar(chat, query_running)
562
+
563
+ if "pending_plot_message_index" in st.session_state:
564
+ from databao_cli.ui.components.results import execute_pending_plot
565
+
566
+ execute_pending_plot(chat)
@@ -11,6 +11,7 @@ from typing import Any, cast
11
11
  import streamlit as st
12
12
  from databao_context_engine import ConfiguredDatasource, DatasourceConnectionStatus
13
13
 
14
+ from databao_cli.ui.app import invalidate_agent
14
15
  from databao_cli.ui.components.datasource_form import render_datasource_config_form
15
16
  from databao_cli.ui.services.dce_operations import (
16
17
  add_datasource,
@@ -216,6 +217,7 @@ def _render_existing_datasource(project_dir: Path, ds: ConfiguredDatasource, idx
216
217
  try:
217
218
  remove_datasource(project_dir, ds_id)
218
219
  st.session_state.pop(f"_confirm_remove_{idx}", None)
220
+ invalidate_agent("Datasource removed, reinitializing agent...")
219
221
  st.rerun()
220
222
  except Exception as e:
221
223
  st.error(f"Remove failed: {e}")
@@ -308,13 +308,17 @@ def render_visualization_and_actions(
308
308
  *,
309
309
  is_latest: bool = False,
310
310
  ) -> None:
311
- """Fragment that renders visualization and action buttons together.
311
+ """Fragment that renders visualization status and action buttons.
312
312
 
313
- This is a fragment so that when action buttons trigger updates (e.g., Generate Plot),
314
- only this fragment reruns, showing the new visualization without a full app rerun.
313
+ Re-reads ``has_visualization``, ``visualization_data``, ``viz_pending``
314
+ and ``viz_error`` from ``chat.messages[message_index]`` on every run so
315
+ it reflects the latest state.
315
316
 
316
- The fragment reads has_visualization and visualization_data from chat.messages
317
- so it can see updates made by button click handlers on fragment rerun.
317
+ When a manual "Generate Plot" is pending (flagged via session state),
318
+ this fragment only renders a placeholder; the actual blocking
319
+ ``thread.plot()`` call is executed by ``execute_pending_plot`` in the
320
+ main script — after the input bar has been rendered — to keep the UI
321
+ responsive and the input visibly disabled.
318
322
  """
319
323
  current_chat = _get_current_chat()
320
324
  if current_chat is None:
@@ -337,11 +341,15 @@ def render_visualization_and_actions(
337
341
  viz_pending = False
338
342
  viz_error = None
339
343
 
344
+ if st.session_state.get("pending_plot_message_index") == message_index:
345
+ st.info("Generating visualization...", icon="📈")
346
+ return
347
+
340
348
  if viz_pending:
341
349
  st.info("Generating visualization...", icon="📈")
342
350
  elif viz_error:
343
351
  st.error(f"Failed to generate visualization: {viz_error}")
344
- elif has_visualization or thread._visualization_result is not None or visualization_data is not None:
352
+ elif has_visualization or visualization_data is not None or (is_latest and thread._visualization_result is not None):
345
353
  render_visualization_section(thread, visualization_data)
346
354
 
347
355
  if is_latest and not viz_pending and not viz_error:
@@ -354,9 +362,11 @@ def _render_and_handle_action_buttons(
354
362
  message_index: int,
355
363
  has_visualization: bool,
356
364
  ) -> None:
357
- """Render action buttons and handle clicks inline.
365
+ """Render action buttons and handle clicks.
358
366
 
359
- Called from within the fragment, so button clicks can trigger fragment-scoped reruns.
367
+ "Generate Plot" sets a session-state flag and triggers a full app
368
+ rerun so that the input bar is rendered as disabled before the
369
+ blocking ``thread.plot()`` call begins.
360
370
  """
361
371
  from databao_cli.ui.services import is_query_running
362
372
 
@@ -377,31 +387,38 @@ def _render_and_handle_action_buttons(
377
387
  button_key = f"action_generate_plot_{message_index}"
378
388
  clicked = st.button("📈 Generate Plot", key=button_key, width="stretch", disabled=is_processing)
379
389
  if clicked and not is_processing:
380
- _handle_generate_plot(chat, message_index)
390
+ st.session_state["pending_plot_message_index"] = message_index
391
+ st.rerun(scope="app")
381
392
 
382
393
 
383
- def _handle_generate_plot(chat: "ChatSession", message_index: int) -> None:
384
- """Handle Generate Plot button click.
394
+ def execute_pending_plot(chat: "ChatSession") -> None:
395
+ """Execute a pending manual "Generate Plot" request.
385
396
 
386
- Called from within a fragment, so st.rerun() will only rerun the fragment.
397
+ Must be called **after** the input bar has been rendered (disabled)
398
+ so the user sees a locked UI while the blocking ``thread.plot()``
399
+ runs. On completion (or error) triggers a full app rerun.
387
400
  """
388
401
  thread = chat.thread
389
402
  if thread is None:
390
403
  return
391
404
 
392
- with st.spinner("Generating visualization..."):
393
- try:
394
- thread.plot()
405
+ message_index: int = st.session_state.pop("pending_plot_message_index")
406
+
407
+ try:
408
+ thread.plot()
395
409
 
396
- messages = chat.messages
397
- if message_index < len(messages):
398
- messages[message_index].has_visualization = True
399
- messages[message_index].visualization_data = _extract_visualization_data(thread)
400
- save_current_chat()
410
+ messages = chat.messages
411
+ if message_index < len(messages):
412
+ messages[message_index].has_visualization = True
413
+ messages[message_index].visualization_data = _extract_visualization_data(thread)
414
+ save_current_chat()
415
+ except Exception as e:
416
+ logger.exception("Failed to generate visualization")
417
+ if message_index < len(chat.messages):
418
+ chat.messages[message_index].metadata["viz_error"] = str(e)
419
+ save_current_chat()
401
420
 
402
- st.rerun()
403
- except Exception as e:
404
- st.error(f"Failed to generate visualization: {e}")
421
+ st.rerun(scope="app")
405
422
 
406
423
 
407
424
  def render_execution_result(
@@ -6,10 +6,9 @@ import streamlit as st
6
6
  from databao.agent import Agent
7
7
 
8
8
  from databao_cli.project.layout import ProjectLayout
9
- from databao_cli.ui.app import _clear_all_chat_threads, is_read_only_domain
9
+ from databao_cli.ui.app import invalidate_agent, is_read_only_domain
10
10
  from databao_cli.ui.components.datasource_manager import render_datasource_manager
11
11
  from databao_cli.ui.components.icons import get_db_type_and_icon
12
- from databao_cli.ui.components.status import AppStatus, set_status
13
12
  from databao_cli.ui.project_utils import DatabaoProjectStatus, databao_project_status
14
13
  from databao_cli.ui.services.build_service import render_build_section
15
14
  from databao_cli.ui.services.dce_operations import get_status_info
@@ -50,10 +49,7 @@ def render_context_settings_page() -> None:
50
49
  st.info("No Databao project detected. Initialize one from the Setup page.")
51
50
 
52
51
  if reload_clicked:
53
- st.session_state.databao_project = None
54
- st.session_state.agent = None
55
- _clear_all_chat_threads()
56
- set_status(AppStatus.INITIALIZING, "Reloading project...")
52
+ invalidate_agent("Reloading project...")
57
53
  st.rerun()
58
54
 
59
55
  # ---- Datasource Management section ----
@@ -122,16 +122,18 @@ def render_welcome_page() -> None:
122
122
  def render_setup_wizard_page() -> None:
123
123
  """Render the setup wizard for first-time project configuration.
124
124
 
125
- Four sections, all visible, disabled based on prerequisites:
125
+ The wizard is organized into up to five sections, which are disabled
126
+ based on prerequisites:
126
127
  1. Initialize Project
127
128
  2. Configure Datasources
128
- 3. Build Context
129
- 4. Ready
129
+ 3. Configure Agent
130
+ 4. Build Context (optional; can be hidden via feature flag)
131
+ 5. Ready
130
132
 
131
133
  When read-only-domain mode is active, editing sections are disabled with
132
134
  an explanation banner.
133
135
  """
134
- from databao_cli.ui.app import _create_new_chat, is_read_only_domain
136
+ from databao_cli.ui.app import _create_new_chat, is_hide_build_context_hint, is_read_only_domain
135
137
  from databao_cli.ui.components.datasource_manager import render_datasource_manager
136
138
  from databao_cli.ui.services.build_service import (
137
139
  get_build_status,
@@ -141,6 +143,7 @@ def render_setup_wizard_page() -> None:
141
143
 
142
144
  project_dir: Path = st.session_state.get("_project_dir", Path.cwd())
143
145
  read_only = is_read_only_domain()
146
+ hide_build_context = is_hide_build_context_hint()
144
147
 
145
148
  _col1, col2, _col3 = st.columns([1, 3, 1])
146
149
 
@@ -253,31 +256,33 @@ def render_setup_wizard_page() -> None:
253
256
  st.markdown("---")
254
257
 
255
258
  # ---- Section 4: Build Context ----
256
- _render_section_header(
257
- "4",
258
- "Build Context (Optional)",
259
- completed=build_started_or_done,
260
- enabled=has_datasources,
261
- )
262
-
263
- if not has_datasources:
264
- st.caption("Complete the previous steps first.")
265
- elif project is None:
266
- st.caption("Add at least one datasource first.")
267
- elif read_only:
268
- render_build_section(project.root_domain_dir, read_only=True)
269
- else:
270
- st.markdown(
271
- "Building the context indexes your datasources so Databao can better understand "
272
- "your data structure and provide higher-quality answers."
259
+ if not hide_build_context:
260
+ _render_section_header(
261
+ "4",
262
+ "Build Context (Optional)",
263
+ completed=build_started_or_done,
264
+ enabled=has_datasources,
273
265
  )
274
- render_build_section(project.root_domain_dir)
275
266
 
276
- st.markdown("---")
267
+ if not has_datasources:
268
+ st.caption("Complete the previous steps first.")
269
+ elif project is None:
270
+ st.caption("Add at least one datasource first.")
271
+ elif read_only:
272
+ render_build_section(project.root_domain_dir, read_only=True)
273
+ else:
274
+ st.markdown(
275
+ "Building the context indexes your datasources so Databao can better understand "
276
+ "your data structure and provide higher-quality answers."
277
+ )
278
+ render_build_section(project.root_domain_dir)
279
+
280
+ st.markdown("---")
277
281
 
278
282
  # ---- Final Section: Start Using Databao ----
283
+ final_step = "4" if hide_build_context else "5"
279
284
  _render_section_header(
280
- "5",
285
+ final_step,
281
286
  "Start Using Databao",
282
287
  completed=False,
283
288
  enabled=has_datasources,
@@ -286,12 +291,12 @@ def render_setup_wizard_page() -> None:
286
291
  if not has_datasources:
287
292
  st.caption("Add at least one datasource first.")
288
293
  else:
289
- if build_status == "running":
294
+ if not hide_build_context and build_status == "running":
290
295
  st.info(
291
296
  "The build is still in progress, but you can start exploring Databao. "
292
297
  "Some features may not work until the build completes."
293
298
  )
294
- elif not build_started_or_done:
299
+ elif not hide_build_context and not build_started_or_done:
295
300
  st.markdown(
296
301
  "You're ready to start using Databao! Consider building the context above for the best experience."
297
302
  )
@@ -18,6 +18,7 @@ from databao_cli.ui.services.query_executor import (
18
18
  get_query_phase,
19
19
  is_query_running,
20
20
  start_query_execution,
21
+ stop_query,
21
22
  )
22
23
  from databao_cli.ui.services.settings_persistence import (
23
24
  delete_settings,
@@ -55,5 +56,6 @@ __all__ = [
55
56
  "save_current_chat",
56
57
  "save_settings",
57
58
  "start_query_execution",
59
+ "stop_query",
58
60
  "trigger_title_generation",
59
61
  ]
@@ -4,6 +4,7 @@ This module provides background execution of queries so that they continue
4
4
  running when users switch between chats. The pattern follows suggestions.py.
5
5
  """
6
6
 
7
+ import ctypes
7
8
  import logging
8
9
  import threading
9
10
  from dataclasses import dataclass
@@ -21,6 +22,8 @@ if TYPE_CHECKING:
21
22
 
22
23
  logger = logging.getLogger(__name__)
23
24
 
25
+ _STOP_TIMEOUT_SECONDS = 5.0
26
+
24
27
 
25
28
  @dataclass
26
29
  class QueryResult:
@@ -100,6 +103,8 @@ class QueryThread(threading.Thread):
100
103
  visualization_data=visualization_data,
101
104
  error=None,
102
105
  )
106
+ except (KeyboardInterrupt, SystemExit):
107
+ logger.debug("Query thread stopped")
103
108
  except Exception as e:
104
109
  logger.exception("Query execution failed")
105
110
  thinking_text = self.writer.getvalue() if self.writer else ""
@@ -202,6 +207,95 @@ def check_query_completion(chat: "ChatSession") -> QueryResult | None:
202
207
  return None
203
208
 
204
209
 
210
+ def _raise_in_thread(thread: threading.Thread, exc_type: type) -> bool:
211
+ """Raise an exception asynchronously in a running thread.
212
+
213
+ Uses ``PyThreadState_SetAsyncExc`` to inject an exception into the
214
+ target thread. The exception will be delivered the next time the
215
+ thread executes Python bytecode (it may not interrupt C-level
216
+ blocking calls immediately).
217
+
218
+ Returns True if the exception was successfully scheduled.
219
+ """
220
+ if not thread.is_alive():
221
+ return False
222
+ thread_id = thread.ident
223
+ if thread_id is None:
224
+ return False
225
+ res = ctypes.pythonapi.PyThreadState_SetAsyncExc(
226
+ ctypes.c_ulong(thread_id),
227
+ ctypes.py_object(exc_type),
228
+ )
229
+ if res == 0:
230
+ logger.warning("Thread %s not found for async exception", thread_id)
231
+ return False
232
+ if res > 1:
233
+ ctypes.pythonapi.PyThreadState_SetAsyncExc(ctypes.c_ulong(thread_id), None)
234
+ logger.error("Multiple thread states modified — reverted")
235
+ return False
236
+ return True
237
+
238
+
239
+ def _reap_thread(thread: threading.Thread, timeout: float = _STOP_TIMEOUT_SECONDS) -> None:
240
+ """Wait for *thread* to finish, then force-kill it if necessary.
241
+
242
+ Intended to run in a short-lived daemon reaper thread so the UI is
243
+ not blocked.
244
+ """
245
+ thread.join(timeout=timeout)
246
+ if thread.is_alive():
247
+ logger.warning("Query thread did not stop after %.1fs — sending SystemExit", timeout)
248
+ _raise_in_thread(thread, SystemExit)
249
+
250
+
251
+ def stop_query(chat: "ChatSession") -> str | None:
252
+ """Stop a running background query for a chat.
253
+
254
+ Termination strategy:
255
+ 1. Capture partial thinking text and disconnect the writer so old
256
+ output cannot leak into the next query.
257
+ 2. Discard ``chat.thread`` — the databao ``Thread`` object's internal
258
+ state (``_lazy_mode``, ``_opas_processed_count``) becomes
259
+ inconsistent after an interrupted ``ask()`` call and cannot be
260
+ reused without hitting ``AssertionError``.
261
+ 3. Raise ``KeyboardInterrupt`` in the query thread (graceful stop).
262
+ 4. Spawn a short-lived daemon *reaper* that waits up to
263
+ ``_STOP_TIMEOUT_SECONDS`` and, if the thread is still alive,
264
+ force-kills it with ``SystemExit``.
265
+
266
+ Returns:
267
+ The captured thinking text (possibly empty ``""``) if a query
268
+ was stopped, or ``None`` if nothing was running.
269
+ """
270
+ if chat.query_status not in ("running", "visualizing"):
271
+ return None
272
+
273
+ thinking_text = ""
274
+ if chat.writer:
275
+ thinking_text = chat.writer.getvalue()
276
+ chat.writer._on_write = None
277
+
278
+ query_thread = chat.query_thread
279
+
280
+ chat.query_thread = None
281
+ chat.query_status = "idle"
282
+ chat.thread = None
283
+ chat.writer = None
284
+
285
+ if query_thread is not None and query_thread.is_alive():
286
+ _raise_in_thread(query_thread, KeyboardInterrupt)
287
+ reaper = threading.Thread(
288
+ target=_reap_thread,
289
+ args=(query_thread,),
290
+ name="query_reaper",
291
+ daemon=True,
292
+ )
293
+ reaper.start()
294
+
295
+ logger.info(f"Stopped query for chat {chat.id}")
296
+ return thinking_text
297
+
298
+
205
299
  def is_query_running(chat: "ChatSession") -> bool:
206
300
  """Check if a chat has a query currently running (data or visualization phase).
207
301
 
@@ -6,6 +6,7 @@ from concurrent.futures import TimeoutError as FuturesTimeoutError
6
6
  from typing import TYPE_CHECKING
7
7
 
8
8
  import streamlit as st
9
+ from databao.agent.duckdb import describe_duckdb_schema
9
10
  from langchain_core.messages import HumanMessage, SystemMessage
10
11
  from pydantic import BaseModel, Field
11
12
 
@@ -40,10 +41,45 @@ class SuggestedQuestions(BaseModel):
40
41
  )
41
42
 
42
43
 
44
+ def _ensure_sources_initialized(agent: "Agent") -> None:
45
+ """Ensure the executor has data sources attached to DuckDB.
46
+
47
+ The executor lazily attaches data sources on first query execution.
48
+ We need them earlier for schema introspection during suggestions.
49
+ """
50
+ executor = agent.executor
51
+ init_fn = getattr(executor, "_init_sources_from_domain", None)
52
+ if init_fn is None:
53
+ return
54
+ try:
55
+ init_fn(agent.domain)
56
+ except Exception:
57
+ logger.debug("Failed to initialize sources for suggestions", exc_info=True)
58
+
59
+
60
+ def _get_duckdb_schema(agent: "Agent") -> str | None:
61
+ """Try to extract the DuckDB schema from the agent's executor."""
62
+ conn = getattr(agent.executor, "_duckdb_connection", None)
63
+ if conn is None:
64
+ return None
65
+ _ensure_sources_initialized(agent)
66
+ try:
67
+ schema: str = describe_duckdb_schema(conn)
68
+ if schema and schema != "(no tables found)":
69
+ return schema
70
+ except Exception:
71
+ logger.debug("Failed to describe DuckDB schema for suggestions", exc_info=True)
72
+ return None
73
+
74
+
43
75
  def _build_context_from_sources(agent: "Agent") -> str:
44
76
  """Extract schema/context information from agent sources."""
45
77
  context_parts: list[str] = []
46
78
 
79
+ db_schema = _get_duckdb_schema(agent)
80
+ if db_schema:
81
+ context_parts.append(f"Database schema:\n{db_schema}")
82
+
47
83
  for name, db_source in agent.dbs.items():
48
84
  if db_source.description:
49
85
  context_parts.append(f"Database '{name}':\n{db_source.description}")