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.
- {databao-0.3.3.dev2.dist-info → databao-0.3.3.dev3.dist-info}/METADATA +1 -1
- {databao-0.3.3.dev2.dist-info → databao-0.3.3.dev3.dist-info}/RECORD +17 -17
- databao_cli/__main__.py +18 -1
- databao_cli/commands/app.py +2 -0
- databao_cli/ui/app.py +39 -1
- databao_cli/ui/cli.py +6 -0
- databao_cli/ui/components/chat.py +178 -17
- databao_cli/ui/components/datasource_manager.py +2 -0
- databao_cli/ui/components/results.py +40 -23
- databao_cli/ui/pages/context_settings.py +2 -6
- databao_cli/ui/pages/welcome.py +31 -26
- databao_cli/ui/services/__init__.py +2 -0
- databao_cli/ui/services/query_executor.py +94 -0
- databao_cli/ui/suggestions.py +36 -0
- {databao-0.3.3.dev2.dist-info → databao-0.3.3.dev3.dist-info}/WHEEL +0 -0
- {databao-0.3.3.dev2.dist-info → databao-0.3.3.dev3.dist-info}/entry_points.txt +0 -0
- {databao-0.3.3.dev2.dist-info → databao-0.3.3.dev3.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
databao_cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
-
databao_cli/__main__.py,sha256
|
|
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=
|
|
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=
|
|
28
|
-
databao_cli/ui/cli.py,sha256=
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
50
|
-
databao_cli/ui/services/__init__.py,sha256=
|
|
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=
|
|
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.
|
|
60
|
-
databao-0.3.3.
|
|
61
|
-
databao-0.3.3.
|
|
62
|
-
databao-0.3.3.
|
|
63
|
-
databao-0.3.3.
|
|
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
|
|
databao_cli/commands/app.py
CHANGED
|
@@ -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(
|
|
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
|
|
364
|
-
"""
|
|
365
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
311
|
+
"""Fragment that renders visualization status and action buttons.
|
|
312
312
|
|
|
313
|
-
|
|
314
|
-
|
|
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
|
-
|
|
317
|
-
|
|
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
|
|
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
|
|
365
|
+
"""Render action buttons and handle clicks.
|
|
358
366
|
|
|
359
|
-
|
|
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
|
-
|
|
390
|
+
st.session_state["pending_plot_message_index"] = message_index
|
|
391
|
+
st.rerun(scope="app")
|
|
381
392
|
|
|
382
393
|
|
|
383
|
-
def
|
|
384
|
-
"""
|
|
394
|
+
def execute_pending_plot(chat: "ChatSession") -> None:
|
|
395
|
+
"""Execute a pending manual "Generate Plot" request.
|
|
385
396
|
|
|
386
|
-
|
|
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
|
-
|
|
393
|
-
|
|
394
|
-
|
|
405
|
+
message_index: int = st.session_state.pop("pending_plot_message_index")
|
|
406
|
+
|
|
407
|
+
try:
|
|
408
|
+
thread.plot()
|
|
395
409
|
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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 ----
|
databao_cli/ui/pages/welcome.py
CHANGED
|
@@ -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
|
-
|
|
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.
|
|
129
|
-
4.
|
|
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
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
databao_cli/ui/suggestions.py
CHANGED
|
@@ -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}")
|
|
File without changes
|
|
File without changes
|
|
File without changes
|