dbagent-cli 0.9.9__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.9 → dbagent_cli-0.10.0}/PKG-INFO +1 -1
- {dbagent_cli-0.9.9 → dbagent_cli-0.10.0}/dbagent/__init__.py +1 -1
- {dbagent_cli-0.9.9 → dbagent_cli-0.10.0}/dbagent/cli.py +49 -15
- dbagent_cli-0.10.0/dbagent/execution/prompt_queue.py +152 -0
- {dbagent_cli-0.9.9 → dbagent_cli-0.10.0}/dbagent_cli.egg-info/PKG-INFO +1 -1
- {dbagent_cli-0.9.9 → dbagent_cli-0.10.0}/dbagent_cli.egg-info/SOURCES.txt +2 -0
- {dbagent_cli-0.9.9 → 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.9 → dbagent_cli-0.10.0}/README.md +0 -0
- {dbagent_cli-0.9.9 → dbagent_cli-0.10.0}/dbagent/agent/confidence.py +0 -0
- {dbagent_cli-0.9.9 → dbagent_cli-0.10.0}/dbagent/agent/conversation.py +0 -0
- {dbagent_cli-0.9.9 → dbagent_cli-0.10.0}/dbagent/agent/doctor.py +0 -0
- {dbagent_cli-0.9.9 → dbagent_cli-0.10.0}/dbagent/agent/error_classifier.py +0 -0
- {dbagent_cli-0.9.9 → dbagent_cli-0.10.0}/dbagent/agent/generator.py +0 -0
- {dbagent_cli-0.9.9 → dbagent_cli-0.10.0}/dbagent/agent/pipeline.py +0 -0
- {dbagent_cli-0.9.9 → dbagent_cli-0.10.0}/dbagent/agent/recommender.py +0 -0
- {dbagent_cli-0.9.9 → dbagent_cli-0.10.0}/dbagent/agent/validator.py +0 -0
- {dbagent_cli-0.9.9 → dbagent_cli-0.10.0}/dbagent/config.py +0 -0
- {dbagent_cli-0.9.9 → dbagent_cli-0.10.0}/dbagent/connectors/base.py +0 -0
- {dbagent_cli-0.9.9 → dbagent_cli-0.10.0}/dbagent/connectors/factory.py +0 -0
- {dbagent_cli-0.9.9 → dbagent_cli-0.10.0}/dbagent/connectors/mongo.py +0 -0
- {dbagent_cli-0.9.9 → dbagent_cli-0.10.0}/dbagent/connectors/relational.py +0 -0
- {dbagent_cli-0.9.9 → dbagent_cli-0.10.0}/dbagent/execution/__init__.py +0 -0
- {dbagent_cli-0.9.9 → dbagent_cli-0.10.0}/dbagent/execution/cache.py +0 -0
- {dbagent_cli-0.9.9 → dbagent_cli-0.10.0}/dbagent/execution/changeset.py +0 -0
- {dbagent_cli-0.9.9 → dbagent_cli-0.10.0}/dbagent/execution/cost.py +0 -0
- {dbagent_cli-0.9.9 → dbagent_cli-0.10.0}/dbagent/execution/recovery.py +0 -0
- {dbagent_cli-0.9.9 → dbagent_cli-0.10.0}/dbagent/execution/sandbox.py +0 -0
- {dbagent_cli-0.9.9 → dbagent_cli-0.10.0}/dbagent/execution/streaming.py +0 -0
- {dbagent_cli-0.9.9 → dbagent_cli-0.10.0}/dbagent/llm/anthropic_provider.py +0 -0
- {dbagent_cli-0.9.9 → dbagent_cli-0.10.0}/dbagent/llm/auto_setup.py +0 -0
- {dbagent_cli-0.9.9 → dbagent_cli-0.10.0}/dbagent/llm/base.py +0 -0
- {dbagent_cli-0.9.9 → dbagent_cli-0.10.0}/dbagent/llm/deepseek_provider.py +0 -0
- {dbagent_cli-0.9.9 → dbagent_cli-0.10.0}/dbagent/llm/factory.py +0 -0
- {dbagent_cli-0.9.9 → dbagent_cli-0.10.0}/dbagent/llm/gemini_provider.py +0 -0
- {dbagent_cli-0.9.9 → dbagent_cli-0.10.0}/dbagent/llm/groq_provider.py +0 -0
- {dbagent_cli-0.9.9 → dbagent_cli-0.10.0}/dbagent/llm/mistral_provider.py +0 -0
- {dbagent_cli-0.9.9 → dbagent_cli-0.10.0}/dbagent/llm/mock_provider.py +0 -0
- {dbagent_cli-0.9.9 → dbagent_cli-0.10.0}/dbagent/llm/ollama_provider.py +0 -0
- {dbagent_cli-0.9.9 → dbagent_cli-0.10.0}/dbagent/llm/openai_provider.py +0 -0
- {dbagent_cli-0.9.9 → dbagent_cli-0.10.0}/dbagent/llm/openrouter_provider.py +0 -0
- {dbagent_cli-0.9.9 → dbagent_cli-0.10.0}/dbagent/schema/cache.py +0 -0
- {dbagent_cli-0.9.9 → dbagent_cli-0.10.0}/dbagent/schema/formatter.py +0 -0
- {dbagent_cli-0.9.9 → dbagent_cli-0.10.0}/dbagent/schema/graph.py +0 -0
- {dbagent_cli-0.9.9 → dbagent_cli-0.10.0}/dbagent/schema/ground_truth.py +0 -0
- {dbagent_cli-0.9.9 → dbagent_cli-0.10.0}/dbagent/schema/models.py +0 -0
- {dbagent_cli-0.9.9 → dbagent_cli-0.10.0}/dbagent/schema/retriever.py +0 -0
- {dbagent_cli-0.9.9 → dbagent_cli-0.10.0}/dbagent/schema/selector.py +0 -0
- {dbagent_cli-0.9.9 → dbagent_cli-0.10.0}/dbagent/security/__init__.py +0 -0
- {dbagent_cli-0.9.9 → dbagent_cli-0.10.0}/dbagent/security/audit.py +0 -0
- {dbagent_cli-0.9.9 → dbagent_cli-0.10.0}/dbagent/security/environment.py +0 -0
- {dbagent_cli-0.9.9 → dbagent_cli-0.10.0}/dbagent/security/masking.py +0 -0
- {dbagent_cli-0.9.9 → dbagent_cli-0.10.0}/dbagent/security/modes.py +0 -0
- {dbagent_cli-0.9.9 → dbagent_cli-0.10.0}/dbagent/security/rbac.py +0 -0
- {dbagent_cli-0.9.9 → dbagent_cli-0.10.0}/dbagent/semantic/__init__.py +0 -0
- {dbagent_cli-0.9.9 → dbagent_cli-0.10.0}/dbagent/semantic/dictionary.py +0 -0
- {dbagent_cli-0.9.9 → dbagent_cli-0.10.0}/dbagent/semantic/knowledge.py +0 -0
- {dbagent_cli-0.9.9 → dbagent_cli-0.10.0}/dbagent/semantic/templates.py +0 -0
- {dbagent_cli-0.9.9 → dbagent_cli-0.10.0}/dbagent/sql/__init__.py +0 -0
- {dbagent_cli-0.9.9 → dbagent_cli-0.10.0}/dbagent/sql/optimizer.py +0 -0
- {dbagent_cli-0.9.9 → dbagent_cli-0.10.0}/dbagent/sql/validator.py +0 -0
- {dbagent_cli-0.9.9 → dbagent_cli-0.10.0}/dbagent/ui/chat_screen.py +0 -0
- {dbagent_cli-0.9.9 → dbagent_cli-0.10.0}/dbagent/ui/console.py +0 -0
- {dbagent_cli-0.9.9 → dbagent_cli-0.10.0}/dbagent/ui/grid_window.py +0 -0
- {dbagent_cli-0.9.9 → dbagent_cli-0.10.0}/dbagent/ui/live_streamer.py +0 -0
- {dbagent_cli-0.9.9 → dbagent_cli-0.10.0}/dbagent/ui/viewer.py +0 -0
- {dbagent_cli-0.9.9 → dbagent_cli-0.10.0}/dbagent/ui/visualizer.py +0 -0
- {dbagent_cli-0.9.9 → dbagent_cli-0.10.0}/dbagent_cli.egg-info/dependency_links.txt +0 -0
- {dbagent_cli-0.9.9 → dbagent_cli-0.10.0}/dbagent_cli.egg-info/entry_points.txt +0 -0
- {dbagent_cli-0.9.9 → dbagent_cli-0.10.0}/dbagent_cli.egg-info/requires.txt +0 -0
- {dbagent_cli-0.9.9 → dbagent_cli-0.10.0}/dbagent_cli.egg-info/top_level.txt +0 -0
- {dbagent_cli-0.9.9 → dbagent_cli-0.10.0}/setup.cfg +0 -0
- {dbagent_cli-0.9.9 → dbagent_cli-0.10.0}/tests/test_cache.py +0 -0
- {dbagent_cli-0.9.9 → dbagent_cli-0.10.0}/tests/test_cli.py +0 -0
- {dbagent_cli-0.9.9 → dbagent_cli-0.10.0}/tests/test_connectors.py +0 -0
- {dbagent_cli-0.9.9 → dbagent_cli-0.10.0}/tests/test_direct_sql.py +0 -0
- {dbagent_cli-0.9.9 → dbagent_cli-0.10.0}/tests/test_doctor_and_viz.py +0 -0
- {dbagent_cli-0.9.9 → dbagent_cli-0.10.0}/tests/test_generator.py +0 -0
- {dbagent_cli-0.9.9 → dbagent_cli-0.10.0}/tests/test_graph.py +0 -0
- {dbagent_cli-0.9.9 → dbagent_cli-0.10.0}/tests/test_knowledge.py +0 -0
- {dbagent_cli-0.9.9 → dbagent_cli-0.10.0}/tests/test_providers.py +0 -0
- {dbagent_cli-0.9.9 → dbagent_cli-0.10.0}/tests/test_recovery.py +0 -0
- {dbagent_cli-0.9.9 → dbagent_cli-0.10.0}/tests/test_safety.py +0 -0
- {dbagent_cli-0.9.9 → dbagent_cli-0.10.0}/tests/test_sandbox.py +0 -0
- {dbagent_cli-0.9.9 → dbagent_cli-0.10.0}/tests/test_schema.py +0 -0
- {dbagent_cli-0.9.9 → dbagent_cli-0.10.0}/tests/test_security.py +0 -0
- {dbagent_cli-0.9.9 → dbagent_cli-0.10.0}/tests/test_semantic.py +0 -0
|
@@ -617,7 +617,7 @@ def chat_command(
|
|
|
617
617
|
":help", ":tables", ":schema", ":dt", ":table", ":fk", ":path", ":join",
|
|
618
618
|
":doctor", ":mode", ":changes", ":provider", ":model", ":models",
|
|
619
619
|
":page", ":card", ":wide", ":scroll", ":window", ":grid", ":web",
|
|
620
|
-
":explain", ":history", ":auto", ":noauto", ":run", ":clear", ":export",
|
|
620
|
+
":explain", ":history", ":queue", ":auto", ":noauto", ":run", ":clear", ":export",
|
|
621
621
|
":exit", ":quit", ":q",
|
|
622
622
|
], ignore_case=True)
|
|
623
623
|
chat_style = Style.from_dict({
|
|
@@ -638,6 +638,9 @@ def chat_command(
|
|
|
638
638
|
query_history: List[PipelineResult] = []
|
|
639
639
|
last_result: Optional[PipelineResult] = None
|
|
640
640
|
|
|
641
|
+
from dbagent.execution.prompt_queue import PromptQueueManager
|
|
642
|
+
prompt_queue = PromptQueueManager(console=console)
|
|
643
|
+
|
|
641
644
|
# Create pipeline ONCE and reuse across all queries (avoids re-init overhead)
|
|
642
645
|
pipeline = QueryPipeline(
|
|
643
646
|
connector=connector,
|
|
@@ -654,26 +657,37 @@ def chat_command(
|
|
|
654
657
|
curr_model = model or getattr(llm, "default_model", "default")
|
|
655
658
|
width = min(console.width or 100, 120)
|
|
656
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 ""
|
|
657
662
|
return HTML(
|
|
658
663
|
f"<ansidarkgray>{rule}</ansidarkgray>\n"
|
|
659
664
|
f" <style class='bottom-toolbar.green'>● Connected:</style> <b>{db_name}</b> <i>({dialect} • {len(table_names)} tables)</i> │ "
|
|
660
665
|
f"<style class='bottom-toolbar.cyan'>🤖 AI:</style> <b>{curr_model}</b> │ "
|
|
661
|
-
f"<style class='bottom-toolbar.yellow'>🛡️ {current_mode.value}</style> │ "
|
|
666
|
+
f"<style class='bottom-toolbar.yellow'>🛡️ {current_mode.value}</style>{queue_badge} │ "
|
|
662
667
|
f"<b>:help</b> for commands · <b>:models</b> to switch"
|
|
663
668
|
)
|
|
664
669
|
|
|
665
670
|
while True:
|
|
666
671
|
try:
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
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)
|
|
677
691
|
|
|
678
692
|
# --- REPL commands (all start with :) ---
|
|
679
693
|
if user_input.startswith(":"):
|
|
@@ -703,6 +717,7 @@ def chat_command(
|
|
|
703
717
|
cmd_table.add_row(":forget <id|topic>", "Remove a learned rule from persistent memory")
|
|
704
718
|
cmd_table.add_row(":mode <name>", "Switch safety mode (SAFE, CONFIRM, ADMIN, READ_ONLY)")
|
|
705
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)")
|
|
706
721
|
cmd_table.add_row(":page <n>, :card", "Inspect wide multi-column query results")
|
|
707
722
|
cmd_table.add_row(":window, :web", "Open interactive desktop or browser data grid")
|
|
708
723
|
cmd_table.add_row(":explain", "Explain last generated SQL query with execution plan")
|
|
@@ -1007,6 +1022,21 @@ def chat_command(
|
|
|
1007
1022
|
print_warning("No previous query to explain.")
|
|
1008
1023
|
continue
|
|
1009
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
|
+
|
|
1010
1040
|
if cmd_name == ":history":
|
|
1011
1041
|
render_session_history(query_history)
|
|
1012
1042
|
continue
|
|
@@ -1115,7 +1145,7 @@ def chat_command(
|
|
|
1115
1145
|
console.print("[bold cyan]Goodbye![/bold cyan]")
|
|
1116
1146
|
break
|
|
1117
1147
|
|
|
1118
|
-
# --- Natural language query via pipeline with live streaming ---
|
|
1148
|
+
# --- Natural language query via pipeline with live streaming & background queue listener ---
|
|
1119
1149
|
pipeline.model = model
|
|
1120
1150
|
pipeline.mode = current_mode
|
|
1121
1151
|
pipeline.auto_execute = auto_execute
|
|
@@ -1135,8 +1165,12 @@ def chat_command(
|
|
|
1135
1165
|
console.print(f"\n[bold cyan]● DB-Agent{model_display}[/bold cyan] [dim]{timing_str}[/dim]")
|
|
1136
1166
|
streamer.feed(chunk)
|
|
1137
1167
|
|
|
1138
|
-
|
|
1139
|
-
|
|
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()
|
|
1140
1174
|
|
|
1141
1175
|
if stream_started[0]:
|
|
1142
1176
|
streamer.finalize()
|
|
@@ -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
|
|
@@ -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
|
|
@@ -74,6 +75,7 @@ tests/test_doctor_and_viz.py
|
|
|
74
75
|
tests/test_generator.py
|
|
75
76
|
tests/test_graph.py
|
|
76
77
|
tests/test_knowledge.py
|
|
78
|
+
tests/test_prompt_queue.py
|
|
77
79
|
tests/test_providers.py
|
|
78
80
|
tests/test_recovery.py
|
|
79
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
|
|
File without changes
|
|
File without changes
|