dbagent-cli 0.9.8__tar.gz → 0.10.0__tar.gz
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.
- {dbagent_cli-0.9.8 → dbagent_cli-0.10.0}/PKG-INFO +1 -1
- {dbagent_cli-0.9.8 → dbagent_cli-0.10.0}/dbagent/__init__.py +1 -1
- {dbagent_cli-0.9.8 → dbagent_cli-0.10.0}/dbagent/cli.py +61 -25
- dbagent_cli-0.10.0/dbagent/execution/prompt_queue.py +152 -0
- {dbagent_cli-0.9.8 → dbagent_cli-0.10.0}/dbagent/schema/ground_truth.py +5 -3
- dbagent_cli-0.10.0/dbagent/ui/live_streamer.py +117 -0
- {dbagent_cli-0.9.8 → dbagent_cli-0.10.0}/dbagent_cli.egg-info/PKG-INFO +1 -1
- {dbagent_cli-0.9.8 → dbagent_cli-0.10.0}/dbagent_cli.egg-info/SOURCES.txt +3 -0
- {dbagent_cli-0.9.8 → dbagent_cli-0.10.0}/pyproject.toml +1 -1
- dbagent_cli-0.10.0/tests/test_prompt_queue.py +80 -0
- {dbagent_cli-0.9.8 → dbagent_cli-0.10.0}/README.md +0 -0
- {dbagent_cli-0.9.8 → dbagent_cli-0.10.0}/dbagent/agent/confidence.py +0 -0
- {dbagent_cli-0.9.8 → dbagent_cli-0.10.0}/dbagent/agent/conversation.py +0 -0
- {dbagent_cli-0.9.8 → dbagent_cli-0.10.0}/dbagent/agent/doctor.py +0 -0
- {dbagent_cli-0.9.8 → dbagent_cli-0.10.0}/dbagent/agent/error_classifier.py +0 -0
- {dbagent_cli-0.9.8 → dbagent_cli-0.10.0}/dbagent/agent/generator.py +0 -0
- {dbagent_cli-0.9.8 → dbagent_cli-0.10.0}/dbagent/agent/pipeline.py +0 -0
- {dbagent_cli-0.9.8 → dbagent_cli-0.10.0}/dbagent/agent/recommender.py +0 -0
- {dbagent_cli-0.9.8 → dbagent_cli-0.10.0}/dbagent/agent/validator.py +0 -0
- {dbagent_cli-0.9.8 → dbagent_cli-0.10.0}/dbagent/config.py +0 -0
- {dbagent_cli-0.9.8 → dbagent_cli-0.10.0}/dbagent/connectors/base.py +0 -0
- {dbagent_cli-0.9.8 → dbagent_cli-0.10.0}/dbagent/connectors/factory.py +0 -0
- {dbagent_cli-0.9.8 → dbagent_cli-0.10.0}/dbagent/connectors/mongo.py +0 -0
- {dbagent_cli-0.9.8 → dbagent_cli-0.10.0}/dbagent/connectors/relational.py +0 -0
- {dbagent_cli-0.9.8 → dbagent_cli-0.10.0}/dbagent/execution/__init__.py +0 -0
- {dbagent_cli-0.9.8 → dbagent_cli-0.10.0}/dbagent/execution/cache.py +0 -0
- {dbagent_cli-0.9.8 → dbagent_cli-0.10.0}/dbagent/execution/changeset.py +0 -0
- {dbagent_cli-0.9.8 → dbagent_cli-0.10.0}/dbagent/execution/cost.py +0 -0
- {dbagent_cli-0.9.8 → dbagent_cli-0.10.0}/dbagent/execution/recovery.py +0 -0
- {dbagent_cli-0.9.8 → dbagent_cli-0.10.0}/dbagent/execution/sandbox.py +0 -0
- {dbagent_cli-0.9.8 → dbagent_cli-0.10.0}/dbagent/execution/streaming.py +0 -0
- {dbagent_cli-0.9.8 → dbagent_cli-0.10.0}/dbagent/llm/anthropic_provider.py +0 -0
- {dbagent_cli-0.9.8 → dbagent_cli-0.10.0}/dbagent/llm/auto_setup.py +0 -0
- {dbagent_cli-0.9.8 → dbagent_cli-0.10.0}/dbagent/llm/base.py +0 -0
- {dbagent_cli-0.9.8 → dbagent_cli-0.10.0}/dbagent/llm/deepseek_provider.py +0 -0
- {dbagent_cli-0.9.8 → dbagent_cli-0.10.0}/dbagent/llm/factory.py +0 -0
- {dbagent_cli-0.9.8 → dbagent_cli-0.10.0}/dbagent/llm/gemini_provider.py +0 -0
- {dbagent_cli-0.9.8 → dbagent_cli-0.10.0}/dbagent/llm/groq_provider.py +0 -0
- {dbagent_cli-0.9.8 → dbagent_cli-0.10.0}/dbagent/llm/mistral_provider.py +0 -0
- {dbagent_cli-0.9.8 → dbagent_cli-0.10.0}/dbagent/llm/mock_provider.py +0 -0
- {dbagent_cli-0.9.8 → dbagent_cli-0.10.0}/dbagent/llm/ollama_provider.py +0 -0
- {dbagent_cli-0.9.8 → dbagent_cli-0.10.0}/dbagent/llm/openai_provider.py +0 -0
- {dbagent_cli-0.9.8 → dbagent_cli-0.10.0}/dbagent/llm/openrouter_provider.py +0 -0
- {dbagent_cli-0.9.8 → dbagent_cli-0.10.0}/dbagent/schema/cache.py +0 -0
- {dbagent_cli-0.9.8 → dbagent_cli-0.10.0}/dbagent/schema/formatter.py +0 -0
- {dbagent_cli-0.9.8 → dbagent_cli-0.10.0}/dbagent/schema/graph.py +0 -0
- {dbagent_cli-0.9.8 → dbagent_cli-0.10.0}/dbagent/schema/models.py +0 -0
- {dbagent_cli-0.9.8 → dbagent_cli-0.10.0}/dbagent/schema/retriever.py +0 -0
- {dbagent_cli-0.9.8 → dbagent_cli-0.10.0}/dbagent/schema/selector.py +0 -0
- {dbagent_cli-0.9.8 → dbagent_cli-0.10.0}/dbagent/security/__init__.py +0 -0
- {dbagent_cli-0.9.8 → dbagent_cli-0.10.0}/dbagent/security/audit.py +0 -0
- {dbagent_cli-0.9.8 → dbagent_cli-0.10.0}/dbagent/security/environment.py +0 -0
- {dbagent_cli-0.9.8 → dbagent_cli-0.10.0}/dbagent/security/masking.py +0 -0
- {dbagent_cli-0.9.8 → dbagent_cli-0.10.0}/dbagent/security/modes.py +0 -0
- {dbagent_cli-0.9.8 → dbagent_cli-0.10.0}/dbagent/security/rbac.py +0 -0
- {dbagent_cli-0.9.8 → dbagent_cli-0.10.0}/dbagent/semantic/__init__.py +0 -0
- {dbagent_cli-0.9.8 → dbagent_cli-0.10.0}/dbagent/semantic/dictionary.py +0 -0
- {dbagent_cli-0.9.8 → dbagent_cli-0.10.0}/dbagent/semantic/knowledge.py +0 -0
- {dbagent_cli-0.9.8 → dbagent_cli-0.10.0}/dbagent/semantic/templates.py +0 -0
- {dbagent_cli-0.9.8 → dbagent_cli-0.10.0}/dbagent/sql/__init__.py +0 -0
- {dbagent_cli-0.9.8 → dbagent_cli-0.10.0}/dbagent/sql/optimizer.py +0 -0
- {dbagent_cli-0.9.8 → dbagent_cli-0.10.0}/dbagent/sql/validator.py +0 -0
- {dbagent_cli-0.9.8 → dbagent_cli-0.10.0}/dbagent/ui/chat_screen.py +0 -0
- {dbagent_cli-0.9.8 → dbagent_cli-0.10.0}/dbagent/ui/console.py +0 -0
- {dbagent_cli-0.9.8 → dbagent_cli-0.10.0}/dbagent/ui/grid_window.py +0 -0
- {dbagent_cli-0.9.8 → dbagent_cli-0.10.0}/dbagent/ui/viewer.py +0 -0
- {dbagent_cli-0.9.8 → dbagent_cli-0.10.0}/dbagent/ui/visualizer.py +0 -0
- {dbagent_cli-0.9.8 → dbagent_cli-0.10.0}/dbagent_cli.egg-info/dependency_links.txt +0 -0
- {dbagent_cli-0.9.8 → dbagent_cli-0.10.0}/dbagent_cli.egg-info/entry_points.txt +0 -0
- {dbagent_cli-0.9.8 → dbagent_cli-0.10.0}/dbagent_cli.egg-info/requires.txt +0 -0
- {dbagent_cli-0.9.8 → dbagent_cli-0.10.0}/dbagent_cli.egg-info/top_level.txt +0 -0
- {dbagent_cli-0.9.8 → dbagent_cli-0.10.0}/setup.cfg +0 -0
- {dbagent_cli-0.9.8 → dbagent_cli-0.10.0}/tests/test_cache.py +0 -0
- {dbagent_cli-0.9.8 → dbagent_cli-0.10.0}/tests/test_cli.py +0 -0
- {dbagent_cli-0.9.8 → dbagent_cli-0.10.0}/tests/test_connectors.py +0 -0
- {dbagent_cli-0.9.8 → dbagent_cli-0.10.0}/tests/test_direct_sql.py +0 -0
- {dbagent_cli-0.9.8 → dbagent_cli-0.10.0}/tests/test_doctor_and_viz.py +0 -0
- {dbagent_cli-0.9.8 → dbagent_cli-0.10.0}/tests/test_generator.py +0 -0
- {dbagent_cli-0.9.8 → dbagent_cli-0.10.0}/tests/test_graph.py +0 -0
- {dbagent_cli-0.9.8 → dbagent_cli-0.10.0}/tests/test_knowledge.py +0 -0
- {dbagent_cli-0.9.8 → dbagent_cli-0.10.0}/tests/test_providers.py +0 -0
- {dbagent_cli-0.9.8 → dbagent_cli-0.10.0}/tests/test_recovery.py +0 -0
- {dbagent_cli-0.9.8 → dbagent_cli-0.10.0}/tests/test_safety.py +0 -0
- {dbagent_cli-0.9.8 → dbagent_cli-0.10.0}/tests/test_sandbox.py +0 -0
- {dbagent_cli-0.9.8 → dbagent_cli-0.10.0}/tests/test_schema.py +0 -0
- {dbagent_cli-0.9.8 → dbagent_cli-0.10.0}/tests/test_security.py +0 -0
- {dbagent_cli-0.9.8 → dbagent_cli-0.10.0}/tests/test_semantic.py +0 -0
|
@@ -530,6 +530,8 @@ def ask_command(
|
|
|
530
530
|
choice_callback=_choice_callback,
|
|
531
531
|
)
|
|
532
532
|
|
|
533
|
+
from dbagent.ui.live_streamer import LiveMarkdownStreamer
|
|
534
|
+
streamer = LiveMarkdownStreamer(console)
|
|
533
535
|
spinner = DynamicSpinner.for_prompt(prompt, model_name=model or getattr(llm, "default_model", None), connector=connector)
|
|
534
536
|
stream_started = [False]
|
|
535
537
|
|
|
@@ -541,15 +543,14 @@ def ask_command(
|
|
|
541
543
|
active_model = model or getattr(llm, "default_model", None)
|
|
542
544
|
model_display = f" ({active_model})" if active_model else ""
|
|
543
545
|
console.print(f"\n[bold cyan]● DB-Agent{model_display}[/bold cyan] [dim]{timing_str}[/dim]")
|
|
544
|
-
|
|
545
|
-
sys.stdout.write(chunk)
|
|
546
|
-
sys.stdout.flush()
|
|
547
|
-
except Exception:
|
|
548
|
-
pass
|
|
546
|
+
streamer.feed(chunk)
|
|
549
547
|
|
|
550
548
|
with spinner:
|
|
551
549
|
result = pipeline.run(user_prompt=prompt, force=force, bypass_cache=no_cache, stream_callback=_query_stream_callback)
|
|
552
550
|
|
|
551
|
+
if stream_started[0]:
|
|
552
|
+
streamer.finalize()
|
|
553
|
+
|
|
553
554
|
_display_pipeline_result(result, show_sql=True)
|
|
554
555
|
connector.close()
|
|
555
556
|
|
|
@@ -616,7 +617,7 @@ def chat_command(
|
|
|
616
617
|
":help", ":tables", ":schema", ":dt", ":table", ":fk", ":path", ":join",
|
|
617
618
|
":doctor", ":mode", ":changes", ":provider", ":model", ":models",
|
|
618
619
|
":page", ":card", ":wide", ":scroll", ":window", ":grid", ":web",
|
|
619
|
-
":explain", ":history", ":auto", ":noauto", ":run", ":clear", ":export",
|
|
620
|
+
":explain", ":history", ":queue", ":auto", ":noauto", ":run", ":clear", ":export",
|
|
620
621
|
":exit", ":quit", ":q",
|
|
621
622
|
], ignore_case=True)
|
|
622
623
|
chat_style = Style.from_dict({
|
|
@@ -637,6 +638,9 @@ def chat_command(
|
|
|
637
638
|
query_history: List[PipelineResult] = []
|
|
638
639
|
last_result: Optional[PipelineResult] = None
|
|
639
640
|
|
|
641
|
+
from dbagent.execution.prompt_queue import PromptQueueManager
|
|
642
|
+
prompt_queue = PromptQueueManager(console=console)
|
|
643
|
+
|
|
640
644
|
# Create pipeline ONCE and reuse across all queries (avoids re-init overhead)
|
|
641
645
|
pipeline = QueryPipeline(
|
|
642
646
|
connector=connector,
|
|
@@ -653,26 +657,37 @@ def chat_command(
|
|
|
653
657
|
curr_model = model or getattr(llm, "default_model", "default")
|
|
654
658
|
width = min(console.width or 100, 120)
|
|
655
659
|
rule = "─" * (width - 1)
|
|
660
|
+
queue_count = prompt_queue.count()
|
|
661
|
+
queue_badge = f" │ <style class='bottom-toolbar.cyan'>📥 Queued: {queue_count}</style>" if queue_count > 0 else ""
|
|
656
662
|
return HTML(
|
|
657
663
|
f"<ansidarkgray>{rule}</ansidarkgray>\n"
|
|
658
664
|
f" <style class='bottom-toolbar.green'>● Connected:</style> <b>{db_name}</b> <i>({dialect} • {len(table_names)} tables)</i> │ "
|
|
659
665
|
f"<style class='bottom-toolbar.cyan'>🤖 AI:</style> <b>{curr_model}</b> │ "
|
|
660
|
-
f"<style class='bottom-toolbar.yellow'>🛡️ {current_mode.value}</style> │ "
|
|
666
|
+
f"<style class='bottom-toolbar.yellow'>🛡️ {current_mode.value}</style>{queue_badge} │ "
|
|
661
667
|
f"<b>:help</b> for commands · <b>:models</b> to switch"
|
|
662
668
|
)
|
|
663
669
|
|
|
664
670
|
while True:
|
|
665
671
|
try:
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
672
|
+
if prompt_queue.has_next():
|
|
673
|
+
user_input = prompt_queue.pop_next()
|
|
674
|
+
rem = prompt_queue.count()
|
|
675
|
+
rem_str = f" [dim cyan]({rem} more in queue)[/dim cyan]" if rem > 0 else ""
|
|
676
|
+
console.print(f"\n[bold cyan]❯[/bold cyan] [bold white]{user_input}[/bold white] [bold yellow](Executing queued prompt)[/bold yellow]{rem_str}")
|
|
677
|
+
else:
|
|
678
|
+
width = min(console.width or 100, 120)
|
|
679
|
+
console.print(f"[dim cyan]{'─' * (width - 1)}[/dim cyan]")
|
|
680
|
+
user_input = session.prompt(
|
|
681
|
+
HTML("<ansicyan><b>❯</b></ansicyan> "),
|
|
682
|
+
bottom_toolbar=_get_bottom_toolbar,
|
|
683
|
+
rprompt=HTML(f"<ansidarkgray>db-agent v{__version__}</ansidarkgray>"),
|
|
684
|
+
placeholder=HTML("<ansidarkgray><i>Ask in plain English or paste direct SQL...</i></ansidarkgray>"),
|
|
685
|
+
).strip()
|
|
686
|
+
if not user_input:
|
|
687
|
+
continue
|
|
688
|
+
|
|
689
|
+
# Split chained prompts if user entered multiple commands like "prompt 1 ; prompt 2"
|
|
690
|
+
user_input = prompt_queue.split_and_enqueue_chained(user_input)
|
|
676
691
|
|
|
677
692
|
# --- REPL commands (all start with :) ---
|
|
678
693
|
if user_input.startswith(":"):
|
|
@@ -702,6 +717,7 @@ def chat_command(
|
|
|
702
717
|
cmd_table.add_row(":forget <id|topic>", "Remove a learned rule from persistent memory")
|
|
703
718
|
cmd_table.add_row(":mode <name>", "Switch safety mode (SAFE, CONFIRM, ADMIN, READ_ONLY)")
|
|
704
719
|
cmd_table.add_row(":history", "View session query history, execution timing & row counts")
|
|
720
|
+
cmd_table.add_row(":queue, :q-list", "View queued in-line prompts (:queue clear to reset)")
|
|
705
721
|
cmd_table.add_row(":page <n>, :card", "Inspect wide multi-column query results")
|
|
706
722
|
cmd_table.add_row(":window, :web", "Open interactive desktop or browser data grid")
|
|
707
723
|
cmd_table.add_row(":explain", "Explain last generated SQL query with execution plan")
|
|
@@ -1006,6 +1022,21 @@ def chat_command(
|
|
|
1006
1022
|
print_warning("No previous query to explain.")
|
|
1007
1023
|
continue
|
|
1008
1024
|
|
|
1025
|
+
if cmd_name in [":queue", ":q-list"]:
|
|
1026
|
+
if cmd_arg.lower() in ["clear", "cls", "reset"]:
|
|
1027
|
+
cleared_count = prompt_queue.clear()
|
|
1028
|
+
print_success(f"Cleared {cleared_count} queued prompt(s).")
|
|
1029
|
+
else:
|
|
1030
|
+
queued_items = prompt_queue.list_all()
|
|
1031
|
+
if not queued_items:
|
|
1032
|
+
console.print("[dim]No queued prompts in line. Chain prompts with ';' or type while streaming.[/dim]\n")
|
|
1033
|
+
else:
|
|
1034
|
+
console.print(f"\n[bold cyan]📥 Pending Prompts in Line ({len(queued_items)}):[/bold cyan]")
|
|
1035
|
+
for idx, q_item in enumerate(queued_items, 1):
|
|
1036
|
+
console.print(f" [{idx}] [bold white]{q_item}[/bold white]")
|
|
1037
|
+
console.print("\n[dim]To clear queue: [bold]:queue clear[/bold][/dim]\n")
|
|
1038
|
+
continue
|
|
1039
|
+
|
|
1009
1040
|
if cmd_name == ":history":
|
|
1010
1041
|
render_session_history(query_history)
|
|
1011
1042
|
continue
|
|
@@ -1114,11 +1145,13 @@ def chat_command(
|
|
|
1114
1145
|
console.print("[bold cyan]Goodbye![/bold cyan]")
|
|
1115
1146
|
break
|
|
1116
1147
|
|
|
1117
|
-
# --- Natural language query via pipeline with live streaming ---
|
|
1148
|
+
# --- Natural language query via pipeline with live streaming & background queue listener ---
|
|
1118
1149
|
pipeline.model = model
|
|
1119
1150
|
pipeline.mode = current_mode
|
|
1120
1151
|
pipeline.auto_execute = auto_execute
|
|
1121
1152
|
|
|
1153
|
+
from dbagent.ui.live_streamer import LiveMarkdownStreamer
|
|
1154
|
+
streamer = LiveMarkdownStreamer(console)
|
|
1122
1155
|
spinner = DynamicSpinner.for_prompt(user_input, model_name=model or getattr(llm, "default_model", None), connector=connector)
|
|
1123
1156
|
stream_started = [False]
|
|
1124
1157
|
|
|
@@ -1130,14 +1163,17 @@ def chat_command(
|
|
|
1130
1163
|
active_model = model or getattr(llm, "default_model", None)
|
|
1131
1164
|
model_display = f" ({active_model})" if active_model else ""
|
|
1132
1165
|
console.print(f"\n[bold cyan]● DB-Agent{model_display}[/bold cyan] [dim]{timing_str}[/dim]")
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1166
|
+
streamer.feed(chunk)
|
|
1167
|
+
|
|
1168
|
+
prompt_queue.start_background_listener()
|
|
1169
|
+
try:
|
|
1170
|
+
with spinner:
|
|
1171
|
+
result = pipeline.run(user_prompt=user_input, stream_callback=_live_stream_callback)
|
|
1172
|
+
finally:
|
|
1173
|
+
prompt_queue.stop_background_listener()
|
|
1138
1174
|
|
|
1139
|
-
|
|
1140
|
-
|
|
1175
|
+
if stream_started[0]:
|
|
1176
|
+
streamer.finalize()
|
|
1141
1177
|
|
|
1142
1178
|
render_assistant_turn(
|
|
1143
1179
|
result,
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
"""
|
|
2
|
+
In-Line Prompt Queue Manager for DB-Agent CLI.
|
|
3
|
+
Allows users to queue follow-up prompts while a query is running or stream is active,
|
|
4
|
+
and supports multi-prompt chaining using ';' or '&&'.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import collections
|
|
8
|
+
import os
|
|
9
|
+
import re
|
|
10
|
+
import sys
|
|
11
|
+
import threading
|
|
12
|
+
import time
|
|
13
|
+
from typing import List, Optional, Callable
|
|
14
|
+
from rich.console import Console
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class PromptQueueManager:
|
|
18
|
+
"""
|
|
19
|
+
Thread-safe FIFO prompt queue with cross-platform background input listener.
|
|
20
|
+
Supports asynchronous queuing during active execution and multi-prompt chaining.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
def __init__(self, console: Optional[Console] = None):
|
|
24
|
+
self.console = console or Console()
|
|
25
|
+
self._queue = collections.deque()
|
|
26
|
+
self._lock = threading.Lock()
|
|
27
|
+
self._listener_thread: Optional[threading.Thread] = None
|
|
28
|
+
self._stop_listener = threading.Event()
|
|
29
|
+
self._current_line: List[str] = []
|
|
30
|
+
|
|
31
|
+
def enqueue(self, prompt: str, notify: bool = True) -> None:
|
|
32
|
+
"""Add a prompt to the back of the queue."""
|
|
33
|
+
p = prompt.strip()
|
|
34
|
+
if not p:
|
|
35
|
+
return
|
|
36
|
+
with self._lock:
|
|
37
|
+
self._queue.append(p)
|
|
38
|
+
if notify:
|
|
39
|
+
self.console.print(f"\n[bold yellow]📥 Queued next in line:[/bold yellow] [bold white]\"{p}\"[/bold white]")
|
|
40
|
+
|
|
41
|
+
def has_next(self) -> bool:
|
|
42
|
+
"""Check if there are any queued prompts waiting."""
|
|
43
|
+
with self._lock:
|
|
44
|
+
return len(self._queue) > 0
|
|
45
|
+
|
|
46
|
+
def pop_next(self) -> Optional[str]:
|
|
47
|
+
"""Pop the next prompt from the front of the queue."""
|
|
48
|
+
with self._lock:
|
|
49
|
+
if self._queue:
|
|
50
|
+
return self._queue.popleft()
|
|
51
|
+
return None
|
|
52
|
+
|
|
53
|
+
def peek_next(self) -> Optional[str]:
|
|
54
|
+
"""Peek at the next prompt without removing it."""
|
|
55
|
+
with self._lock:
|
|
56
|
+
if self._queue:
|
|
57
|
+
return self._queue[0]
|
|
58
|
+
return None
|
|
59
|
+
|
|
60
|
+
def list_all(self) -> List[str]:
|
|
61
|
+
"""List all pending queued prompts."""
|
|
62
|
+
with self._lock:
|
|
63
|
+
return list(self._queue)
|
|
64
|
+
|
|
65
|
+
def count(self) -> int:
|
|
66
|
+
"""Return the number of queued prompts."""
|
|
67
|
+
with self._lock:
|
|
68
|
+
return len(self._queue)
|
|
69
|
+
|
|
70
|
+
def clear(self) -> int:
|
|
71
|
+
"""Clear all pending prompts from the queue and return count cleared."""
|
|
72
|
+
with self._lock:
|
|
73
|
+
cleared = len(self._queue)
|
|
74
|
+
self._queue.clear()
|
|
75
|
+
return cleared
|
|
76
|
+
|
|
77
|
+
def split_and_enqueue_chained(self, raw_input: str) -> str:
|
|
78
|
+
"""
|
|
79
|
+
Split prompts separated by ';' or '&&' if not a raw SQL statement or inside quotes.
|
|
80
|
+
Returns the first prompt to execute immediately and enqueues the rest.
|
|
81
|
+
"""
|
|
82
|
+
raw_clean = raw_input.strip()
|
|
83
|
+
if not raw_clean:
|
|
84
|
+
return raw_clean
|
|
85
|
+
|
|
86
|
+
# Do not split direct SQL statements like "SELECT 1; SELECT 2;"
|
|
87
|
+
if raw_clean.lower().startswith(("select ", "insert ", "update ", "delete ", "create ", "alter ", "drop ", "with ")):
|
|
88
|
+
return raw_clean
|
|
89
|
+
|
|
90
|
+
# Do not split special REPL commands
|
|
91
|
+
if raw_clean.startswith(":"):
|
|
92
|
+
return raw_clean
|
|
93
|
+
|
|
94
|
+
# Split by ';' or '&&'
|
|
95
|
+
delimiter_pattern = r'\s*(?:;|&&)\s*'
|
|
96
|
+
parts = [p.strip() for p in re.split(delimiter_pattern, raw_clean) if p.strip()]
|
|
97
|
+
|
|
98
|
+
if len(parts) > 1:
|
|
99
|
+
first_prompt = parts[0]
|
|
100
|
+
for remaining_prompt in parts[1:]:
|
|
101
|
+
self.enqueue(remaining_prompt, notify=True)
|
|
102
|
+
return first_prompt
|
|
103
|
+
|
|
104
|
+
return raw_clean
|
|
105
|
+
|
|
106
|
+
def start_background_listener(self) -> None:
|
|
107
|
+
"""Start listening for keyboard input in the background while a task/stream runs."""
|
|
108
|
+
self._stop_listener.clear()
|
|
109
|
+
self._current_line = []
|
|
110
|
+
self._listener_thread = threading.Thread(target=self._listen_loop, daemon=True)
|
|
111
|
+
self._listener_thread.start()
|
|
112
|
+
|
|
113
|
+
def stop_background_listener(self) -> None:
|
|
114
|
+
"""Stop the background listener thread."""
|
|
115
|
+
self._stop_listener.set()
|
|
116
|
+
if self._listener_thread:
|
|
117
|
+
self._listener_thread.join(timeout=0.15)
|
|
118
|
+
self._listener_thread = None
|
|
119
|
+
|
|
120
|
+
def _listen_loop(self) -> None:
|
|
121
|
+
"""Cross-platform non-blocking input listener loop."""
|
|
122
|
+
is_windows = os.name == "nt"
|
|
123
|
+
if is_windows:
|
|
124
|
+
try:
|
|
125
|
+
import msvcrt
|
|
126
|
+
while not self._stop_listener.is_set():
|
|
127
|
+
if msvcrt.kbhit():
|
|
128
|
+
ch = msvcrt.getwch()
|
|
129
|
+
if ch in ("\r", "\n"):
|
|
130
|
+
line = "".join(self._current_line).strip()
|
|
131
|
+
self._current_line = []
|
|
132
|
+
if line:
|
|
133
|
+
self.enqueue(line, notify=True)
|
|
134
|
+
elif ch == "\b":
|
|
135
|
+
if self._current_line:
|
|
136
|
+
self._current_line.pop()
|
|
137
|
+
elif ord(ch) >= 32:
|
|
138
|
+
self._current_line.append(ch)
|
|
139
|
+
time.sleep(0.02)
|
|
140
|
+
except Exception:
|
|
141
|
+
pass
|
|
142
|
+
else:
|
|
143
|
+
try:
|
|
144
|
+
import select
|
|
145
|
+
while not self._stop_listener.is_set():
|
|
146
|
+
r, _, _ = select.select([sys.stdin], [], [], 0.05)
|
|
147
|
+
if r:
|
|
148
|
+
line = sys.stdin.readline().strip()
|
|
149
|
+
if line:
|
|
150
|
+
self.enqueue(line, notify=True)
|
|
151
|
+
except Exception:
|
|
152
|
+
pass
|
|
@@ -85,17 +85,19 @@ class SchemaGroundTruthValidator:
|
|
|
85
85
|
if fk_desc not in result.verified_fks:
|
|
86
86
|
result.verified_fks.append(fk_desc)
|
|
87
87
|
|
|
88
|
-
# 2. Extract and validate SQL queries against physical schema
|
|
89
|
-
sql_blocks = re.findall(r'```(?:sql)
|
|
88
|
+
# 2. Extract and validate only actual SQL queries against physical schema
|
|
89
|
+
sql_blocks = re.findall(r'```(?:sql)\s*([\s\S]*?)\s*```', response_text, re.IGNORECASE)
|
|
90
90
|
result.sql_queries_checked = len(sql_blocks)
|
|
91
91
|
sql_referenced_tables = set()
|
|
92
|
+
sql_ignore_keywords = {"via", "lateral", "on", "using", "where", "select", "table", "as", "and", "or", "values"}
|
|
92
93
|
|
|
93
94
|
for block in sql_blocks:
|
|
94
95
|
# Extract FROM and JOIN table references
|
|
95
96
|
matches = re.findall(r'\b(?:FROM|JOIN)\s+([a-zA-Z0-9_]+)', block, re.IGNORECASE)
|
|
96
97
|
for m in matches:
|
|
97
98
|
m_clean = m.strip().lower()
|
|
98
|
-
|
|
99
|
+
if m_clean not in sql_ignore_keywords:
|
|
100
|
+
sql_referenced_tables.add(m_clean)
|
|
99
101
|
|
|
100
102
|
for sql_tbl in sql_referenced_tables:
|
|
101
103
|
if sql_tbl in schema_table_map:
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Real-Time Line-Buffered Rich Markdown Streamer for DB-Agent CLI.
|
|
3
|
+
Renders markdown headers, formatted diagram panels, syntax-highlighted SQL blocks,
|
|
4
|
+
and sleek bulleted lists dynamically as tokens stream in from LLM providers.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import re
|
|
8
|
+
import sys
|
|
9
|
+
from typing import List, Optional
|
|
10
|
+
from rich.console import Console
|
|
11
|
+
from rich.panel import Panel
|
|
12
|
+
from rich.syntax import Syntax
|
|
13
|
+
from rich.rule import Rule
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class LiveMarkdownStreamer:
|
|
17
|
+
"""
|
|
18
|
+
Line-buffered dynamic terminal renderer for streaming LLM responses.
|
|
19
|
+
Renders syntax-highlighted code blocks, bordered ASCII diagrams,
|
|
20
|
+
styled markdown headers, and clean bullet lists live as tokens arrive.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
def __init__(self, console: Optional[Console] = None):
|
|
24
|
+
self.console = console or Console()
|
|
25
|
+
self.buffer = ""
|
|
26
|
+
self.in_code_block = False
|
|
27
|
+
self.code_lang = ""
|
|
28
|
+
self.code_lines: List[str] = []
|
|
29
|
+
self._has_started = False
|
|
30
|
+
|
|
31
|
+
def feed(self, chunk: str) -> None:
|
|
32
|
+
"""Feed a token chunk from the LLM stream."""
|
|
33
|
+
self._has_started = True
|
|
34
|
+
self.buffer += chunk
|
|
35
|
+
while "\n" in self.buffer:
|
|
36
|
+
line, self.buffer = self.buffer.split("\n", 1)
|
|
37
|
+
self._process_line(line)
|
|
38
|
+
|
|
39
|
+
def _process_line(self, line: str) -> None:
|
|
40
|
+
line_strip = line.strip()
|
|
41
|
+
|
|
42
|
+
# Code block fence (```sql, ```text, ```)
|
|
43
|
+
if line_strip.startswith("```"):
|
|
44
|
+
if self.in_code_block:
|
|
45
|
+
# Close code block
|
|
46
|
+
self.in_code_block = False
|
|
47
|
+
code_text = "\n".join(self.code_lines)
|
|
48
|
+
lang = self.code_lang or "sql"
|
|
49
|
+
if lang in ["sql", "postgres", "postgresql", "mysql", "sqlite", "tsql"]:
|
|
50
|
+
syntax = Syntax(code_text, "sql", theme="monokai", line_numbers=False)
|
|
51
|
+
self.console.print(Panel(syntax, title="[dim]SQL Statement[/dim]", title_align="left", border_style="dim", padding=(0, 1)))
|
|
52
|
+
else:
|
|
53
|
+
syntax = Syntax(code_text, "text", theme="monokai", line_numbers=False)
|
|
54
|
+
self.console.print(Panel(syntax, title="[dim]Architecture Flow Diagram[/dim]", title_align="left", border_style="cyan", padding=(0, 1)))
|
|
55
|
+
self.code_lines = []
|
|
56
|
+
self.code_lang = ""
|
|
57
|
+
else:
|
|
58
|
+
# Open code block
|
|
59
|
+
self.in_code_block = True
|
|
60
|
+
self.code_lang = line_strip[3:].strip().lower()
|
|
61
|
+
self.code_lines = []
|
|
62
|
+
return
|
|
63
|
+
|
|
64
|
+
if self.in_code_block:
|
|
65
|
+
self.code_lines.append(line)
|
|
66
|
+
return
|
|
67
|
+
|
|
68
|
+
# Markdown Section Headers (#, ##, ###)
|
|
69
|
+
if line_strip.startswith("#"):
|
|
70
|
+
header_text = line_strip.lstrip("#").strip()
|
|
71
|
+
self.console.print("")
|
|
72
|
+
self.console.print(f"[bold cyan]{header_text}[/bold cyan]")
|
|
73
|
+
return
|
|
74
|
+
|
|
75
|
+
# Markdown Horizontal Rules (---, ***, ___)
|
|
76
|
+
if line_strip in ["---", "***", "___"]:
|
|
77
|
+
self.console.print(Rule(style="dim cyan"))
|
|
78
|
+
return
|
|
79
|
+
|
|
80
|
+
# Markdown Bullet Points (- , * , • )
|
|
81
|
+
if line_strip.startswith(("- ", "* ", "• ")):
|
|
82
|
+
content = line_strip[2:].strip()
|
|
83
|
+
styled = re.sub(r'\*\*(.*?)\*\*', r'[bold cyan]\1[/bold cyan]', content)
|
|
84
|
+
styled = re.sub(r'`(.*?)`', r'[bold white]\1[/bold white]', styled)
|
|
85
|
+
self.console.print(f" [dim]•[/dim] {styled}")
|
|
86
|
+
return
|
|
87
|
+
|
|
88
|
+
# Numbered List items (1. , 2. )
|
|
89
|
+
num_match = re.match(r'^(\d+[\.\)])\s+(.*)', line_strip)
|
|
90
|
+
if num_match:
|
|
91
|
+
num_prefix, content = num_match.groups()
|
|
92
|
+
styled = re.sub(r'\*\*(.*?)\*\*', r'[bold cyan]\1[/bold cyan]', content)
|
|
93
|
+
styled = re.sub(r'`(.*?)`', r'[bold white]\1[/bold white]', styled)
|
|
94
|
+
self.console.print(f" [bold yellow]{num_prefix}[/bold yellow] {styled}")
|
|
95
|
+
return
|
|
96
|
+
|
|
97
|
+
# Blank Line
|
|
98
|
+
if not line_strip:
|
|
99
|
+
self.console.print("")
|
|
100
|
+
return
|
|
101
|
+
|
|
102
|
+
# Regular Markdown paragraph text with bold and inline code styling
|
|
103
|
+
styled_line = re.sub(r'\*\*(.*?)\*\*', r'[bold cyan]\1[/bold cyan]', line)
|
|
104
|
+
styled_line = re.sub(r'`(.*?)`', r'[bold white]\1[/bold white]', styled_line)
|
|
105
|
+
self.console.print(styled_line)
|
|
106
|
+
|
|
107
|
+
def finalize(self) -> None:
|
|
108
|
+
"""Flush remaining buffer and close any dangling open blocks."""
|
|
109
|
+
if self.buffer.strip():
|
|
110
|
+
self._process_line(self.buffer)
|
|
111
|
+
if self.in_code_block and self.code_lines:
|
|
112
|
+
code_text = "\n".join(self.code_lines)
|
|
113
|
+
syntax = Syntax(code_text, self.code_lang or "sql", theme="monokai", line_numbers=False)
|
|
114
|
+
self.console.print(Panel(syntax, border_style="dim", padding=(0, 1)))
|
|
115
|
+
self.buffer = ""
|
|
116
|
+
self.code_lines = []
|
|
117
|
+
self.in_code_block = False
|
|
@@ -19,6 +19,7 @@ dbagent/execution/__init__.py
|
|
|
19
19
|
dbagent/execution/cache.py
|
|
20
20
|
dbagent/execution/changeset.py
|
|
21
21
|
dbagent/execution/cost.py
|
|
22
|
+
dbagent/execution/prompt_queue.py
|
|
22
23
|
dbagent/execution/recovery.py
|
|
23
24
|
dbagent/execution/sandbox.py
|
|
24
25
|
dbagent/execution/streaming.py
|
|
@@ -57,6 +58,7 @@ dbagent/sql/validator.py
|
|
|
57
58
|
dbagent/ui/chat_screen.py
|
|
58
59
|
dbagent/ui/console.py
|
|
59
60
|
dbagent/ui/grid_window.py
|
|
61
|
+
dbagent/ui/live_streamer.py
|
|
60
62
|
dbagent/ui/viewer.py
|
|
61
63
|
dbagent/ui/visualizer.py
|
|
62
64
|
dbagent_cli.egg-info/PKG-INFO
|
|
@@ -73,6 +75,7 @@ tests/test_doctor_and_viz.py
|
|
|
73
75
|
tests/test_generator.py
|
|
74
76
|
tests/test_graph.py
|
|
75
77
|
tests/test_knowledge.py
|
|
78
|
+
tests/test_prompt_queue.py
|
|
76
79
|
tests/test_providers.py
|
|
77
80
|
tests/test_recovery.py
|
|
78
81
|
tests/test_safety.py
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "dbagent-cli"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.10.0"
|
|
8
8
|
description = "Enterprise AI Database Copilot, Schema Graph Introspector & Autonomous Recovery Agent (CLI)"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.9"
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Unit tests for In-Line Prompt Queue Manager.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
from dbagent.execution.prompt_queue import PromptQueueManager
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def test_prompt_queue_fifo():
|
|
10
|
+
qm = PromptQueueManager()
|
|
11
|
+
assert qm.count() == 0
|
|
12
|
+
assert not qm.has_next()
|
|
13
|
+
|
|
14
|
+
qm.enqueue("first prompt", notify=False)
|
|
15
|
+
qm.enqueue("second prompt", notify=False)
|
|
16
|
+
|
|
17
|
+
assert qm.count() == 2
|
|
18
|
+
assert qm.has_next()
|
|
19
|
+
assert qm.peek_next() == "first prompt"
|
|
20
|
+
assert qm.pop_next() == "first prompt"
|
|
21
|
+
assert qm.count() == 1
|
|
22
|
+
assert qm.pop_next() == "second prompt"
|
|
23
|
+
assert qm.count() == 0
|
|
24
|
+
assert not qm.has_next()
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def test_split_and_enqueue_chained():
|
|
28
|
+
qm = PromptQueueManager()
|
|
29
|
+
raw = "how ivis_user and ivis_site were linked ; give the flow diagram ; count total users"
|
|
30
|
+
first = qm.split_and_enqueue_chained(raw)
|
|
31
|
+
|
|
32
|
+
assert first == "how ivis_user and ivis_site were linked"
|
|
33
|
+
assert qm.count() == 2
|
|
34
|
+
assert qm.list_all() == ["give the flow diagram", "count total users"]
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def test_split_and_enqueue_double_ampersand():
|
|
38
|
+
qm = PromptQueueManager()
|
|
39
|
+
raw = "show active tenants && list users in tenant 1"
|
|
40
|
+
first = qm.split_and_enqueue_chained(raw)
|
|
41
|
+
|
|
42
|
+
assert first == "show active tenants"
|
|
43
|
+
assert qm.count() == 1
|
|
44
|
+
assert qm.pop_next() == "list users in tenant 1"
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def test_do_not_split_sql():
|
|
48
|
+
qm = PromptQueueManager()
|
|
49
|
+
sql_prompt = "SELECT * FROM users WHERE id = 1; SELECT * FROM sites;"
|
|
50
|
+
res = qm.split_and_enqueue_chained(sql_prompt)
|
|
51
|
+
assert res == sql_prompt
|
|
52
|
+
assert qm.count() == 0
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def test_do_not_split_repl_commands():
|
|
56
|
+
qm = PromptQueueManager()
|
|
57
|
+
cmd = ":learn tenant_rule = all users belong to tenants"
|
|
58
|
+
res = qm.split_and_enqueue_chained(cmd)
|
|
59
|
+
assert res == cmd
|
|
60
|
+
assert qm.count() == 0
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def test_prompt_queue_clear():
|
|
64
|
+
qm = PromptQueueManager()
|
|
65
|
+
qm.enqueue("item 1", notify=False)
|
|
66
|
+
qm.enqueue("item 2", notify=False)
|
|
67
|
+
assert qm.count() == 2
|
|
68
|
+
|
|
69
|
+
cleared = qm.clear()
|
|
70
|
+
assert cleared == 2
|
|
71
|
+
assert qm.count() == 0
|
|
72
|
+
assert not qm.has_next()
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def test_background_listener_lifecycle():
|
|
76
|
+
qm = PromptQueueManager()
|
|
77
|
+
qm.start_background_listener()
|
|
78
|
+
assert qm._listener_thread is not None
|
|
79
|
+
qm.stop_background_listener()
|
|
80
|
+
assert qm._listener_thread is None
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|