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.
Files changed (87) hide show
  1. {dbagent_cli-0.9.8 → dbagent_cli-0.10.0}/PKG-INFO +1 -1
  2. {dbagent_cli-0.9.8 → dbagent_cli-0.10.0}/dbagent/__init__.py +1 -1
  3. {dbagent_cli-0.9.8 → dbagent_cli-0.10.0}/dbagent/cli.py +61 -25
  4. dbagent_cli-0.10.0/dbagent/execution/prompt_queue.py +152 -0
  5. {dbagent_cli-0.9.8 → dbagent_cli-0.10.0}/dbagent/schema/ground_truth.py +5 -3
  6. dbagent_cli-0.10.0/dbagent/ui/live_streamer.py +117 -0
  7. {dbagent_cli-0.9.8 → dbagent_cli-0.10.0}/dbagent_cli.egg-info/PKG-INFO +1 -1
  8. {dbagent_cli-0.9.8 → dbagent_cli-0.10.0}/dbagent_cli.egg-info/SOURCES.txt +3 -0
  9. {dbagent_cli-0.9.8 → dbagent_cli-0.10.0}/pyproject.toml +1 -1
  10. dbagent_cli-0.10.0/tests/test_prompt_queue.py +80 -0
  11. {dbagent_cli-0.9.8 → dbagent_cli-0.10.0}/README.md +0 -0
  12. {dbagent_cli-0.9.8 → dbagent_cli-0.10.0}/dbagent/agent/confidence.py +0 -0
  13. {dbagent_cli-0.9.8 → dbagent_cli-0.10.0}/dbagent/agent/conversation.py +0 -0
  14. {dbagent_cli-0.9.8 → dbagent_cli-0.10.0}/dbagent/agent/doctor.py +0 -0
  15. {dbagent_cli-0.9.8 → dbagent_cli-0.10.0}/dbagent/agent/error_classifier.py +0 -0
  16. {dbagent_cli-0.9.8 → dbagent_cli-0.10.0}/dbagent/agent/generator.py +0 -0
  17. {dbagent_cli-0.9.8 → dbagent_cli-0.10.0}/dbagent/agent/pipeline.py +0 -0
  18. {dbagent_cli-0.9.8 → dbagent_cli-0.10.0}/dbagent/agent/recommender.py +0 -0
  19. {dbagent_cli-0.9.8 → dbagent_cli-0.10.0}/dbagent/agent/validator.py +0 -0
  20. {dbagent_cli-0.9.8 → dbagent_cli-0.10.0}/dbagent/config.py +0 -0
  21. {dbagent_cli-0.9.8 → dbagent_cli-0.10.0}/dbagent/connectors/base.py +0 -0
  22. {dbagent_cli-0.9.8 → dbagent_cli-0.10.0}/dbagent/connectors/factory.py +0 -0
  23. {dbagent_cli-0.9.8 → dbagent_cli-0.10.0}/dbagent/connectors/mongo.py +0 -0
  24. {dbagent_cli-0.9.8 → dbagent_cli-0.10.0}/dbagent/connectors/relational.py +0 -0
  25. {dbagent_cli-0.9.8 → dbagent_cli-0.10.0}/dbagent/execution/__init__.py +0 -0
  26. {dbagent_cli-0.9.8 → dbagent_cli-0.10.0}/dbagent/execution/cache.py +0 -0
  27. {dbagent_cli-0.9.8 → dbagent_cli-0.10.0}/dbagent/execution/changeset.py +0 -0
  28. {dbagent_cli-0.9.8 → dbagent_cli-0.10.0}/dbagent/execution/cost.py +0 -0
  29. {dbagent_cli-0.9.8 → dbagent_cli-0.10.0}/dbagent/execution/recovery.py +0 -0
  30. {dbagent_cli-0.9.8 → dbagent_cli-0.10.0}/dbagent/execution/sandbox.py +0 -0
  31. {dbagent_cli-0.9.8 → dbagent_cli-0.10.0}/dbagent/execution/streaming.py +0 -0
  32. {dbagent_cli-0.9.8 → dbagent_cli-0.10.0}/dbagent/llm/anthropic_provider.py +0 -0
  33. {dbagent_cli-0.9.8 → dbagent_cli-0.10.0}/dbagent/llm/auto_setup.py +0 -0
  34. {dbagent_cli-0.9.8 → dbagent_cli-0.10.0}/dbagent/llm/base.py +0 -0
  35. {dbagent_cli-0.9.8 → dbagent_cli-0.10.0}/dbagent/llm/deepseek_provider.py +0 -0
  36. {dbagent_cli-0.9.8 → dbagent_cli-0.10.0}/dbagent/llm/factory.py +0 -0
  37. {dbagent_cli-0.9.8 → dbagent_cli-0.10.0}/dbagent/llm/gemini_provider.py +0 -0
  38. {dbagent_cli-0.9.8 → dbagent_cli-0.10.0}/dbagent/llm/groq_provider.py +0 -0
  39. {dbagent_cli-0.9.8 → dbagent_cli-0.10.0}/dbagent/llm/mistral_provider.py +0 -0
  40. {dbagent_cli-0.9.8 → dbagent_cli-0.10.0}/dbagent/llm/mock_provider.py +0 -0
  41. {dbagent_cli-0.9.8 → dbagent_cli-0.10.0}/dbagent/llm/ollama_provider.py +0 -0
  42. {dbagent_cli-0.9.8 → dbagent_cli-0.10.0}/dbagent/llm/openai_provider.py +0 -0
  43. {dbagent_cli-0.9.8 → dbagent_cli-0.10.0}/dbagent/llm/openrouter_provider.py +0 -0
  44. {dbagent_cli-0.9.8 → dbagent_cli-0.10.0}/dbagent/schema/cache.py +0 -0
  45. {dbagent_cli-0.9.8 → dbagent_cli-0.10.0}/dbagent/schema/formatter.py +0 -0
  46. {dbagent_cli-0.9.8 → dbagent_cli-0.10.0}/dbagent/schema/graph.py +0 -0
  47. {dbagent_cli-0.9.8 → dbagent_cli-0.10.0}/dbagent/schema/models.py +0 -0
  48. {dbagent_cli-0.9.8 → dbagent_cli-0.10.0}/dbagent/schema/retriever.py +0 -0
  49. {dbagent_cli-0.9.8 → dbagent_cli-0.10.0}/dbagent/schema/selector.py +0 -0
  50. {dbagent_cli-0.9.8 → dbagent_cli-0.10.0}/dbagent/security/__init__.py +0 -0
  51. {dbagent_cli-0.9.8 → dbagent_cli-0.10.0}/dbagent/security/audit.py +0 -0
  52. {dbagent_cli-0.9.8 → dbagent_cli-0.10.0}/dbagent/security/environment.py +0 -0
  53. {dbagent_cli-0.9.8 → dbagent_cli-0.10.0}/dbagent/security/masking.py +0 -0
  54. {dbagent_cli-0.9.8 → dbagent_cli-0.10.0}/dbagent/security/modes.py +0 -0
  55. {dbagent_cli-0.9.8 → dbagent_cli-0.10.0}/dbagent/security/rbac.py +0 -0
  56. {dbagent_cli-0.9.8 → dbagent_cli-0.10.0}/dbagent/semantic/__init__.py +0 -0
  57. {dbagent_cli-0.9.8 → dbagent_cli-0.10.0}/dbagent/semantic/dictionary.py +0 -0
  58. {dbagent_cli-0.9.8 → dbagent_cli-0.10.0}/dbagent/semantic/knowledge.py +0 -0
  59. {dbagent_cli-0.9.8 → dbagent_cli-0.10.0}/dbagent/semantic/templates.py +0 -0
  60. {dbagent_cli-0.9.8 → dbagent_cli-0.10.0}/dbagent/sql/__init__.py +0 -0
  61. {dbagent_cli-0.9.8 → dbagent_cli-0.10.0}/dbagent/sql/optimizer.py +0 -0
  62. {dbagent_cli-0.9.8 → dbagent_cli-0.10.0}/dbagent/sql/validator.py +0 -0
  63. {dbagent_cli-0.9.8 → dbagent_cli-0.10.0}/dbagent/ui/chat_screen.py +0 -0
  64. {dbagent_cli-0.9.8 → dbagent_cli-0.10.0}/dbagent/ui/console.py +0 -0
  65. {dbagent_cli-0.9.8 → dbagent_cli-0.10.0}/dbagent/ui/grid_window.py +0 -0
  66. {dbagent_cli-0.9.8 → dbagent_cli-0.10.0}/dbagent/ui/viewer.py +0 -0
  67. {dbagent_cli-0.9.8 → dbagent_cli-0.10.0}/dbagent/ui/visualizer.py +0 -0
  68. {dbagent_cli-0.9.8 → dbagent_cli-0.10.0}/dbagent_cli.egg-info/dependency_links.txt +0 -0
  69. {dbagent_cli-0.9.8 → dbagent_cli-0.10.0}/dbagent_cli.egg-info/entry_points.txt +0 -0
  70. {dbagent_cli-0.9.8 → dbagent_cli-0.10.0}/dbagent_cli.egg-info/requires.txt +0 -0
  71. {dbagent_cli-0.9.8 → dbagent_cli-0.10.0}/dbagent_cli.egg-info/top_level.txt +0 -0
  72. {dbagent_cli-0.9.8 → dbagent_cli-0.10.0}/setup.cfg +0 -0
  73. {dbagent_cli-0.9.8 → dbagent_cli-0.10.0}/tests/test_cache.py +0 -0
  74. {dbagent_cli-0.9.8 → dbagent_cli-0.10.0}/tests/test_cli.py +0 -0
  75. {dbagent_cli-0.9.8 → dbagent_cli-0.10.0}/tests/test_connectors.py +0 -0
  76. {dbagent_cli-0.9.8 → dbagent_cli-0.10.0}/tests/test_direct_sql.py +0 -0
  77. {dbagent_cli-0.9.8 → dbagent_cli-0.10.0}/tests/test_doctor_and_viz.py +0 -0
  78. {dbagent_cli-0.9.8 → dbagent_cli-0.10.0}/tests/test_generator.py +0 -0
  79. {dbagent_cli-0.9.8 → dbagent_cli-0.10.0}/tests/test_graph.py +0 -0
  80. {dbagent_cli-0.9.8 → dbagent_cli-0.10.0}/tests/test_knowledge.py +0 -0
  81. {dbagent_cli-0.9.8 → dbagent_cli-0.10.0}/tests/test_providers.py +0 -0
  82. {dbagent_cli-0.9.8 → dbagent_cli-0.10.0}/tests/test_recovery.py +0 -0
  83. {dbagent_cli-0.9.8 → dbagent_cli-0.10.0}/tests/test_safety.py +0 -0
  84. {dbagent_cli-0.9.8 → dbagent_cli-0.10.0}/tests/test_sandbox.py +0 -0
  85. {dbagent_cli-0.9.8 → dbagent_cli-0.10.0}/tests/test_schema.py +0 -0
  86. {dbagent_cli-0.9.8 → dbagent_cli-0.10.0}/tests/test_security.py +0 -0
  87. {dbagent_cli-0.9.8 → dbagent_cli-0.10.0}/tests/test_semantic.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dbagent-cli
3
- Version: 0.9.8
3
+ Version: 0.10.0
4
4
  Summary: Enterprise AI Database Copilot, Schema Graph Introspector & Autonomous Recovery Agent (CLI)
5
5
  Author: Santhosh Gupta
6
6
  Requires-Python: >=3.9
@@ -2,5 +2,5 @@
2
2
  DB-Agent: Universal Database Introspection and Script Generation AI Agent (CLI).
3
3
  """
4
4
 
5
- __version__ = "0.9.8"
5
+ __version__ = "0.10.0"
6
6
  __author__ = "Santhosh Gupta"
@@ -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
- try:
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
- width = min(console.width or 100, 120)
667
- console.print(f"[dim cyan]{'─' * (width - 1)}[/dim cyan]")
668
- user_input = session.prompt(
669
- HTML("<ansicyan><b>❯</b></ansicyan> "),
670
- bottom_toolbar=_get_bottom_toolbar,
671
- rprompt=HTML(f"<ansidarkgray>db-agent v{__version__}</ansidarkgray>"),
672
- placeholder=HTML("<ansidarkgray><i>Ask in plain English or paste direct SQL...</i></ansidarkgray>"),
673
- ).strip()
674
- if not user_input:
675
- continue
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
- try:
1134
- sys.stdout.write(chunk)
1135
- sys.stdout.flush()
1136
- except Exception:
1137
- pass
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
- with spinner:
1140
- result = pipeline.run(user_prompt=user_input, stream_callback=_live_stream_callback)
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)?\s*([\s\S]*?)\s*```', response_text, re.IGNORECASE)
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
- sql_referenced_tables.add(m_clean)
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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dbagent-cli
3
- Version: 0.9.8
3
+ Version: 0.10.0
4
4
  Summary: Enterprise AI Database Copilot, Schema Graph Introspector & Autonomous Recovery Agent (CLI)
5
5
  Author: Santhosh Gupta
6
6
  Requires-Python: >=3.9
@@ -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.9.8"
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