dbagent-cli 0.9.0__tar.gz → 0.9.1__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 (84) hide show
  1. {dbagent_cli-0.9.0 → dbagent_cli-0.9.1}/PKG-INFO +1 -1
  2. {dbagent_cli-0.9.0 → dbagent_cli-0.9.1}/dbagent/__init__.py +1 -1
  3. {dbagent_cli-0.9.0 → dbagent_cli-0.9.1}/dbagent/agent/pipeline.py +22 -2
  4. {dbagent_cli-0.9.0 → dbagent_cli-0.9.1}/dbagent/cli.py +49 -0
  5. {dbagent_cli-0.9.0 → dbagent_cli-0.9.1}/dbagent/schema/graph.py +30 -0
  6. dbagent_cli-0.9.1/dbagent/semantic/knowledge.py +146 -0
  7. {dbagent_cli-0.9.0 → dbagent_cli-0.9.1}/dbagent_cli.egg-info/PKG-INFO +1 -1
  8. {dbagent_cli-0.9.0 → dbagent_cli-0.9.1}/dbagent_cli.egg-info/SOURCES.txt +2 -0
  9. {dbagent_cli-0.9.0 → dbagent_cli-0.9.1}/pyproject.toml +1 -1
  10. dbagent_cli-0.9.1/tests/test_knowledge.py +80 -0
  11. {dbagent_cli-0.9.0 → dbagent_cli-0.9.1}/README.md +0 -0
  12. {dbagent_cli-0.9.0 → dbagent_cli-0.9.1}/dbagent/agent/confidence.py +0 -0
  13. {dbagent_cli-0.9.0 → dbagent_cli-0.9.1}/dbagent/agent/conversation.py +0 -0
  14. {dbagent_cli-0.9.0 → dbagent_cli-0.9.1}/dbagent/agent/doctor.py +0 -0
  15. {dbagent_cli-0.9.0 → dbagent_cli-0.9.1}/dbagent/agent/error_classifier.py +0 -0
  16. {dbagent_cli-0.9.0 → dbagent_cli-0.9.1}/dbagent/agent/generator.py +0 -0
  17. {dbagent_cli-0.9.0 → dbagent_cli-0.9.1}/dbagent/agent/recommender.py +0 -0
  18. {dbagent_cli-0.9.0 → dbagent_cli-0.9.1}/dbagent/agent/validator.py +0 -0
  19. {dbagent_cli-0.9.0 → dbagent_cli-0.9.1}/dbagent/config.py +0 -0
  20. {dbagent_cli-0.9.0 → dbagent_cli-0.9.1}/dbagent/connectors/base.py +0 -0
  21. {dbagent_cli-0.9.0 → dbagent_cli-0.9.1}/dbagent/connectors/factory.py +0 -0
  22. {dbagent_cli-0.9.0 → dbagent_cli-0.9.1}/dbagent/connectors/mongo.py +0 -0
  23. {dbagent_cli-0.9.0 → dbagent_cli-0.9.1}/dbagent/connectors/relational.py +0 -0
  24. {dbagent_cli-0.9.0 → dbagent_cli-0.9.1}/dbagent/execution/__init__.py +0 -0
  25. {dbagent_cli-0.9.0 → dbagent_cli-0.9.1}/dbagent/execution/cache.py +0 -0
  26. {dbagent_cli-0.9.0 → dbagent_cli-0.9.1}/dbagent/execution/changeset.py +0 -0
  27. {dbagent_cli-0.9.0 → dbagent_cli-0.9.1}/dbagent/execution/cost.py +0 -0
  28. {dbagent_cli-0.9.0 → dbagent_cli-0.9.1}/dbagent/execution/recovery.py +0 -0
  29. {dbagent_cli-0.9.0 → dbagent_cli-0.9.1}/dbagent/execution/sandbox.py +0 -0
  30. {dbagent_cli-0.9.0 → dbagent_cli-0.9.1}/dbagent/execution/streaming.py +0 -0
  31. {dbagent_cli-0.9.0 → dbagent_cli-0.9.1}/dbagent/llm/anthropic_provider.py +0 -0
  32. {dbagent_cli-0.9.0 → dbagent_cli-0.9.1}/dbagent/llm/auto_setup.py +0 -0
  33. {dbagent_cli-0.9.0 → dbagent_cli-0.9.1}/dbagent/llm/base.py +0 -0
  34. {dbagent_cli-0.9.0 → dbagent_cli-0.9.1}/dbagent/llm/deepseek_provider.py +0 -0
  35. {dbagent_cli-0.9.0 → dbagent_cli-0.9.1}/dbagent/llm/factory.py +0 -0
  36. {dbagent_cli-0.9.0 → dbagent_cli-0.9.1}/dbagent/llm/gemini_provider.py +0 -0
  37. {dbagent_cli-0.9.0 → dbagent_cli-0.9.1}/dbagent/llm/groq_provider.py +0 -0
  38. {dbagent_cli-0.9.0 → dbagent_cli-0.9.1}/dbagent/llm/mistral_provider.py +0 -0
  39. {dbagent_cli-0.9.0 → dbagent_cli-0.9.1}/dbagent/llm/mock_provider.py +0 -0
  40. {dbagent_cli-0.9.0 → dbagent_cli-0.9.1}/dbagent/llm/ollama_provider.py +0 -0
  41. {dbagent_cli-0.9.0 → dbagent_cli-0.9.1}/dbagent/llm/openai_provider.py +0 -0
  42. {dbagent_cli-0.9.0 → dbagent_cli-0.9.1}/dbagent/llm/openrouter_provider.py +0 -0
  43. {dbagent_cli-0.9.0 → dbagent_cli-0.9.1}/dbagent/schema/cache.py +0 -0
  44. {dbagent_cli-0.9.0 → dbagent_cli-0.9.1}/dbagent/schema/formatter.py +0 -0
  45. {dbagent_cli-0.9.0 → dbagent_cli-0.9.1}/dbagent/schema/ground_truth.py +0 -0
  46. {dbagent_cli-0.9.0 → dbagent_cli-0.9.1}/dbagent/schema/models.py +0 -0
  47. {dbagent_cli-0.9.0 → dbagent_cli-0.9.1}/dbagent/schema/retriever.py +0 -0
  48. {dbagent_cli-0.9.0 → dbagent_cli-0.9.1}/dbagent/schema/selector.py +0 -0
  49. {dbagent_cli-0.9.0 → dbagent_cli-0.9.1}/dbagent/security/__init__.py +0 -0
  50. {dbagent_cli-0.9.0 → dbagent_cli-0.9.1}/dbagent/security/audit.py +0 -0
  51. {dbagent_cli-0.9.0 → dbagent_cli-0.9.1}/dbagent/security/environment.py +0 -0
  52. {dbagent_cli-0.9.0 → dbagent_cli-0.9.1}/dbagent/security/masking.py +0 -0
  53. {dbagent_cli-0.9.0 → dbagent_cli-0.9.1}/dbagent/security/modes.py +0 -0
  54. {dbagent_cli-0.9.0 → dbagent_cli-0.9.1}/dbagent/security/rbac.py +0 -0
  55. {dbagent_cli-0.9.0 → dbagent_cli-0.9.1}/dbagent/semantic/__init__.py +0 -0
  56. {dbagent_cli-0.9.0 → dbagent_cli-0.9.1}/dbagent/semantic/dictionary.py +0 -0
  57. {dbagent_cli-0.9.0 → dbagent_cli-0.9.1}/dbagent/semantic/templates.py +0 -0
  58. {dbagent_cli-0.9.0 → dbagent_cli-0.9.1}/dbagent/sql/__init__.py +0 -0
  59. {dbagent_cli-0.9.0 → dbagent_cli-0.9.1}/dbagent/sql/optimizer.py +0 -0
  60. {dbagent_cli-0.9.0 → dbagent_cli-0.9.1}/dbagent/sql/validator.py +0 -0
  61. {dbagent_cli-0.9.0 → dbagent_cli-0.9.1}/dbagent/ui/chat_screen.py +0 -0
  62. {dbagent_cli-0.9.0 → dbagent_cli-0.9.1}/dbagent/ui/console.py +0 -0
  63. {dbagent_cli-0.9.0 → dbagent_cli-0.9.1}/dbagent/ui/grid_window.py +0 -0
  64. {dbagent_cli-0.9.0 → dbagent_cli-0.9.1}/dbagent/ui/viewer.py +0 -0
  65. {dbagent_cli-0.9.0 → dbagent_cli-0.9.1}/dbagent/ui/visualizer.py +0 -0
  66. {dbagent_cli-0.9.0 → dbagent_cli-0.9.1}/dbagent_cli.egg-info/dependency_links.txt +0 -0
  67. {dbagent_cli-0.9.0 → dbagent_cli-0.9.1}/dbagent_cli.egg-info/entry_points.txt +0 -0
  68. {dbagent_cli-0.9.0 → dbagent_cli-0.9.1}/dbagent_cli.egg-info/requires.txt +0 -0
  69. {dbagent_cli-0.9.0 → dbagent_cli-0.9.1}/dbagent_cli.egg-info/top_level.txt +0 -0
  70. {dbagent_cli-0.9.0 → dbagent_cli-0.9.1}/setup.cfg +0 -0
  71. {dbagent_cli-0.9.0 → dbagent_cli-0.9.1}/tests/test_cache.py +0 -0
  72. {dbagent_cli-0.9.0 → dbagent_cli-0.9.1}/tests/test_cli.py +0 -0
  73. {dbagent_cli-0.9.0 → dbagent_cli-0.9.1}/tests/test_connectors.py +0 -0
  74. {dbagent_cli-0.9.0 → dbagent_cli-0.9.1}/tests/test_direct_sql.py +0 -0
  75. {dbagent_cli-0.9.0 → dbagent_cli-0.9.1}/tests/test_doctor_and_viz.py +0 -0
  76. {dbagent_cli-0.9.0 → dbagent_cli-0.9.1}/tests/test_generator.py +0 -0
  77. {dbagent_cli-0.9.0 → dbagent_cli-0.9.1}/tests/test_graph.py +0 -0
  78. {dbagent_cli-0.9.0 → dbagent_cli-0.9.1}/tests/test_providers.py +0 -0
  79. {dbagent_cli-0.9.0 → dbagent_cli-0.9.1}/tests/test_recovery.py +0 -0
  80. {dbagent_cli-0.9.0 → dbagent_cli-0.9.1}/tests/test_safety.py +0 -0
  81. {dbagent_cli-0.9.0 → dbagent_cli-0.9.1}/tests/test_sandbox.py +0 -0
  82. {dbagent_cli-0.9.0 → dbagent_cli-0.9.1}/tests/test_schema.py +0 -0
  83. {dbagent_cli-0.9.0 → dbagent_cli-0.9.1}/tests/test_security.py +0 -0
  84. {dbagent_cli-0.9.0 → dbagent_cli-0.9.1}/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.0
3
+ Version: 0.9.1
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.0"
5
+ __version__ = "0.9.1"
6
6
  __author__ = "Santhosh Gupta"
@@ -25,6 +25,7 @@ from dbagent.sql.validator import SQLValidator
25
25
  from dbagent.execution.cost import QueryCostEstimator, QueryComplexity
26
26
  from dbagent.execution.cache import QueryResultCache
27
27
  from dbagent.semantic.dictionary import BusinessDictionary
28
+ from dbagent.semantic.knowledge import KnowledgeBase, LearnedRule
28
29
  from dbagent.execution.recovery import RecoveryManager, RowSnapshotRecoveryStrategy, RecoveryStatus
29
30
  from dbagent.agent.confidence import ConfidenceEvaluator, ConfidenceScore
30
31
  from dbagent.agent.conversation import ConversationContext
@@ -123,6 +124,7 @@ class QueryPipeline:
123
124
  self.choice_callback = choice_callback
124
125
 
125
126
  self.dictionary = BusinessDictionary()
127
+ self.knowledge_base = KnowledgeBase()
126
128
  self.result_cache = QueryResultCache()
127
129
  self.audit_logger = AuditLogger()
128
130
  self.recovery_mgr = recovery_manager or RecoveryManager()
@@ -215,10 +217,25 @@ class QueryPipeline:
215
217
  sub_schema, join_steps = self._get_fast_schema_context(user_prompt, exact, fuzzy)
216
218
  result.schema = sub_schema
217
219
  result.join_steps = join_steps
220
+
221
+ # Auto-Learn from user guidance/corrections
222
+ prompt_l = user_prompt.lower()
223
+ if any(w in prompt_l for w in ["mapped with", "mapped to", "observe the database", "the rule is", "remember that", "notice that"]):
224
+ self.knowledge_base.learn(
225
+ db_name=self.db_name,
226
+ topic=f"Mapping for {', '.join(exact[:3]) if exact else 'Entities'}",
227
+ rule_description=user_prompt,
228
+ relevant_tables=exact + fuzzy[:4],
229
+ source="user_guidance",
230
+ )
231
+
232
+ kb_context = self.knowledge_base.format_prompt_context(self.db_name, user_prompt, exact + fuzzy)
218
233
  conv_history = self.conversation.format_context_for_prompt()
234
+ analysis_prompt = f"{kb_context}\n\n{user_prompt}" if kb_context else user_prompt
235
+
219
236
  result.message = self.generator.generate_analysis_response(
220
237
  schema=sub_schema,
221
- user_prompt=user_prompt,
238
+ user_prompt=analysis_prompt,
222
239
  conversation_history=conv_history,
223
240
  model=self.model,
224
241
  )
@@ -384,13 +401,16 @@ class QueryPipeline:
384
401
  result.error = f"Schema retrieval failed: {str(e)}"
385
402
  return result
386
403
 
387
- # --- 4. Semantic Business Dictionary Context Injection ---
404
+ # --- 4. Semantic Business Dictionary & Knowledge Base Injection ---
388
405
  dict_context = self.dictionary.format_prompt_context(user_prompt)
406
+ kb_context = self.knowledge_base.format_prompt_context(self.db_name, user_prompt, exact + fuzzy)
389
407
  conv_context = self.conversation.format_context_for_prompt()
390
408
 
391
409
  augmented_prompt = user_prompt
392
410
  if dict_context:
393
411
  augmented_prompt = f"{dict_context}\n\n{augmented_prompt}"
412
+ if kb_context:
413
+ augmented_prompt = f"{kb_context}\n\n{augmented_prompt}"
394
414
  if conv_context:
395
415
  augmented_prompt = f"{conv_context}\n\n{augmented_prompt}"
396
416
 
@@ -633,6 +633,9 @@ def chat_command(
633
633
  cmd_table.add_row(":fk <name>", "View foreign key relationships & dependency graph")
634
634
  cmd_table.add_row(":path <t1> <t2>", "Discover shortest SQL join path between two tables")
635
635
  cmd_table.add_row(":doctor", "Run database health, index, and query performance diagnostics")
636
+ cmd_table.add_row(":learn <rule>", "Teach DB-Agent custom domain rules & mappings (:learn topic = rule)")
637
+ cmd_table.add_row(":memory, :learned", "View all learned domain rules & relationship memory")
638
+ cmd_table.add_row(":forget <id|topic>", "Remove a learned rule from persistent memory")
636
639
  cmd_table.add_row(":mode <name>", "Switch safety mode (SAFE, CONFIRM, ADMIN, READ_ONLY)")
637
640
  cmd_table.add_row(":history", "View session query history, execution timing & row counts")
638
641
  cmd_table.add_row(":page <n>, :card", "Inspect wide multi-column query results")
@@ -776,6 +779,52 @@ def chat_command(
776
779
  console.print("")
777
780
  continue
778
781
 
782
+ if cmd_name in [":learn", ":remember"]:
783
+ if not cmd_arg or "=" not in cmd_arg:
784
+ print_warning("Usage: :learn <topic> = <description> (e.g. :learn site_user_map = ivis_site maps to ivis_user via site_group -> group)")
785
+ continue
786
+ parts = cmd_arg.split("=", 1)
787
+ topic = parts[0].strip()
788
+ desc = parts[1].strip()
789
+ try:
790
+ exact, fuzzy = connector.resolve_tables(desc)
791
+ except Exception:
792
+ exact, fuzzy = [], []
793
+ learned_rule = pipeline.knowledge_base.learn(
794
+ db_name=db_name,
795
+ topic=topic,
796
+ rule_description=desc,
797
+ relevant_tables=exact + fuzzy[:4],
798
+ source="user_explicit",
799
+ )
800
+ print_success(f"Learned & Persisted Rule: [bold cyan]{learned_rule.topic}[/bold cyan]")
801
+ console.print(f" [dim]• Relevant tables: {', '.join(learned_rule.relevant_tables) or 'general'}[/dim]")
802
+ console.print(f" [dim]• DB-Agent will automatically apply this rule in all future queries.[/dim]\n")
803
+ continue
804
+
805
+ if cmd_name in [":memory", ":learned", ":knowledge"]:
806
+ rules = pipeline.knowledge_base.list_all(db_name)
807
+ if not rules:
808
+ console.print(f"[dim]No learned rules stored for '{db_name}' yet. Use [bold]:learn <topic> = <desc>[/bold] to teach DB-Agent.[/dim]\n")
809
+ else:
810
+ console.print(f"\n[bold cyan]🧠 DB-Agent Learned Knowledge Base for '{db_name}':[/bold cyan]")
811
+ for r in rules:
812
+ tbl_str = f" [cyan]({', '.join(r.relevant_tables)})[/cyan]" if r.relevant_tables else ""
813
+ console.print(f" * [bold white]{r.topic}[/bold white]{tbl_str}:")
814
+ console.print(f" [dim]{r.rule_description}[/dim] [dim italic]({r.rule_id})[/dim italic]")
815
+ console.print("\n[dim]To remove a rule: [bold]:forget <rule_id|topic>[/bold][/dim]\n")
816
+ continue
817
+
818
+ if cmd_name in [":forget", ":unlearn"]:
819
+ if not cmd_arg:
820
+ print_warning("Usage: :forget <rule_id or topic>")
821
+ continue
822
+ if pipeline.knowledge_base.delete(db_name, cmd_arg):
823
+ print_success(f"Removed rule '{cmd_arg}' from knowledge memory.")
824
+ else:
825
+ print_warning(f"No matching rule found for '{cmd_arg}'.")
826
+ continue
827
+
779
828
  if cmd_name == ":clear":
780
829
  conversation_ctx.clear()
781
830
  print_success("Conversation context cleared.")
@@ -176,6 +176,36 @@ class RelationshipGraph:
176
176
 
177
177
  return None
178
178
 
179
+ def find_all_paths(self, start_table: str, end_table: str, max_hops: int = 4) -> List[JoinPath]:
180
+ """
181
+ Finds all valid multi-hop join paths between start_table and end_table up to max_hops.
182
+ Discovers direct paths, junction tables, and indirect hierarchical relationships.
183
+ """
184
+ start = start_table.lower()
185
+ end = end_table.lower()
186
+
187
+ if start not in self.adj or end not in self.adj:
188
+ return []
189
+
190
+ all_paths: List[JoinPath] = []
191
+ queue: deque = deque([(start, [], {start})]) # (curr, list_of_steps, visited_set)
192
+
193
+ while queue:
194
+ curr, steps, visited = queue.popleft()
195
+
196
+ if curr == end and steps:
197
+ all_paths.append(JoinPath(start_table, end_table, steps))
198
+ continue
199
+
200
+ if len(steps) >= max_hops:
201
+ continue
202
+
203
+ for neighbor, step in self.adj.get(curr, []):
204
+ if neighbor not in visited:
205
+ queue.append((neighbor, steps + [step], visited | {neighbor}))
206
+
207
+ return all_paths
208
+
179
209
  def find_connecting_path_for_tables(self, table_names: List[str]) -> List[JoinStep]:
180
210
  """
181
211
  Given a set of seed tables (e.g. ['users', 'events']), finds the minimal join tree / path
@@ -0,0 +1,146 @@
1
+ """
2
+ Persistent Knowledge Base & Self-Learning Memory Engine.
3
+ Stores domain rules, verified multi-hop relationship paths, and custom business logic per database in ~/.dbagent/knowledge/<db_name>.json.
4
+ Automatically injects learned domain knowledge into LLM generation context.
5
+ """
6
+
7
+ import json
8
+ import time
9
+ from pathlib import Path
10
+ from typing import Dict, Any, List, Optional
11
+ from dataclasses import dataclass, field, asdict
12
+
13
+
14
+ @dataclass
15
+ class LearnedRule:
16
+ """A verified domain rule or relationship path learned from user interaction."""
17
+ rule_id: str
18
+ topic: str
19
+ rule_description: str
20
+ relevant_tables: List[str] = field(default_factory=list)
21
+ created_at: str = ""
22
+ source: str = "user_feedback"
23
+
24
+
25
+ class KnowledgeBase:
26
+ """
27
+ Persistent Knowledge Base & Self-Learning Memory for DB-Agent.
28
+ Stores and retrieves domain-specific relationship insights per database in ~/.dbagent/knowledge/<db_name>.json.
29
+ """
30
+
31
+ def __init__(self, storage_dir: Optional[Path] = None):
32
+ self.storage_dir = storage_dir or Path.home() / ".dbagent" / "knowledge"
33
+ self.storage_dir.mkdir(parents=True, exist_ok=True)
34
+
35
+ def _get_db_file(self, db_name: str) -> Path:
36
+ safe_name = "".join(c if c.isalnum() or c in ("-", "_") else "_" for c in db_name.lower())
37
+ return self.storage_dir / f"{safe_name}.json"
38
+
39
+ def learn(
40
+ self,
41
+ db_name: str,
42
+ topic: str,
43
+ rule_description: str,
44
+ relevant_tables: Optional[List[str]] = None,
45
+ source: str = "user_feedback",
46
+ ) -> LearnedRule:
47
+ """Persist a new learned domain rule or mapping path."""
48
+ db_file = self._get_db_file(db_name)
49
+ rules = self.list_all(db_name)
50
+
51
+ # Check if identical topic exists and update, else create new
52
+ existing_idx = None
53
+ for i, r in enumerate(rules):
54
+ if r.topic.lower().strip() == topic.lower().strip():
55
+ existing_idx = i
56
+ break
57
+
58
+ rule_id = f"rule_{len(rules) + 1}_{int(time.time())}"
59
+ new_rule = LearnedRule(
60
+ rule_id=rule_id,
61
+ topic=topic.strip(),
62
+ rule_description=rule_description.strip(),
63
+ relevant_tables=[t.lower() for t in (relevant_tables or [])],
64
+ created_at=time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
65
+ source=source,
66
+ )
67
+
68
+ if existing_idx is not None:
69
+ rules[existing_idx] = new_rule
70
+ else:
71
+ rules.append(new_rule)
72
+
73
+ with open(db_file, "w", encoding="utf-8") as f:
74
+ json.dump([asdict(r) for r in rules], f, indent=2)
75
+ return new_rule
76
+
77
+ def list_all(self, db_name: str) -> List[LearnedRule]:
78
+ """List all learned rules for a database."""
79
+ db_file = self._get_db_file(db_name)
80
+ if not db_file.exists():
81
+ return []
82
+ try:
83
+ with open(db_file, "r", encoding="utf-8") as f:
84
+ data = json.load(f)
85
+ return [LearnedRule(**item) for item in data]
86
+ except Exception:
87
+ return []
88
+
89
+ def delete(self, db_name: str, target: str) -> bool:
90
+ """Delete a rule by ID or topic."""
91
+ rules = self.list_all(db_name)
92
+ target_lower = target.lower().strip()
93
+ filtered = [r for r in rules if r.rule_id != target and r.topic.lower() != target_lower]
94
+ if len(filtered) < len(rules):
95
+ db_file = self._get_db_file(db_name)
96
+ with open(db_file, "w", encoding="utf-8") as f:
97
+ json.dump([asdict(r) for r in filtered], f, indent=2)
98
+ return True
99
+ return False
100
+
101
+ def clear(self, db_name: str) -> None:
102
+ """Clear all learned rules for a database."""
103
+ db_file = self._get_db_file(db_name)
104
+ if db_file.exists():
105
+ db_file.unlink()
106
+
107
+ def find_relevant(self, db_name: str, user_prompt: str, table_names: Optional[List[str]] = None) -> List[LearnedRule]:
108
+ """Finds learned rules relevant to the user prompt or active tables."""
109
+ all_rules = self.list_all(db_name)
110
+ if not all_rules:
111
+ return []
112
+
113
+ prompt_lower = user_prompt.lower()
114
+ active_tables = {t.lower() for t in (table_names or [])}
115
+ matched = []
116
+
117
+ for r in all_rules:
118
+ # 1. Match by topic in prompt
119
+ if r.topic.lower() in prompt_lower:
120
+ matched.append(r)
121
+ continue
122
+ # 2. Match by relevant tables
123
+ if any(t in active_tables or t in prompt_lower for t in r.relevant_tables):
124
+ matched.append(r)
125
+ continue
126
+ # 3. Keyword overlap
127
+ keywords = [w for w in r.topic.lower().split() if len(w) > 3]
128
+ if any(kw in prompt_lower for kw in keywords):
129
+ matched.append(r)
130
+
131
+ return matched
132
+
133
+ def format_prompt_context(self, db_name: str, user_prompt: str, table_names: Optional[List[str]] = None) -> str:
134
+ """Format learned domain knowledge for LLM prompt context injection."""
135
+ matched = self.find_relevant(db_name, user_prompt, table_names)
136
+ if not matched:
137
+ return ""
138
+
139
+ lines = [
140
+ "### 🧠 Persistent Database Domain Knowledge & Verified Mappings (Learned from User Feedback):"
141
+ ]
142
+ for m in matched:
143
+ tbl_str = f" [Tables: {', '.join(m.relevant_tables)}]" if m.relevant_tables else ""
144
+ lines.append(f"- **{m.topic}**{tbl_str}: {m.rule_description}")
145
+ lines.append("Always apply these verified mapping paths and domain rules when generating SQL or architectural explanations.\n")
146
+ return "\n".join(lines)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dbagent-cli
3
- Version: 0.9.0
3
+ Version: 0.9.1
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
@@ -49,6 +49,7 @@ dbagent/security/modes.py
49
49
  dbagent/security/rbac.py
50
50
  dbagent/semantic/__init__.py
51
51
  dbagent/semantic/dictionary.py
52
+ dbagent/semantic/knowledge.py
52
53
  dbagent/semantic/templates.py
53
54
  dbagent/sql/__init__.py
54
55
  dbagent/sql/optimizer.py
@@ -71,6 +72,7 @@ tests/test_direct_sql.py
71
72
  tests/test_doctor_and_viz.py
72
73
  tests/test_generator.py
73
74
  tests/test_graph.py
75
+ tests/test_knowledge.py
74
76
  tests/test_providers.py
75
77
  tests/test_recovery.py
76
78
  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.0"
7
+ version = "0.9.1"
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
+ Tests for Persistent KnowledgeBase & Multi-Hop find_all_paths.
3
+ """
4
+
5
+ import pytest
6
+ from pathlib import Path
7
+ from dbagent.semantic.knowledge import KnowledgeBase, LearnedRule
8
+ from dbagent.schema.graph import RelationshipGraph
9
+ from dbagent.schema.models import DatabaseSchema, TableModel, ColumnModel, ForeignKeyModel
10
+
11
+
12
+ @pytest.fixture
13
+ def temp_kb(tmp_path):
14
+ return KnowledgeBase(storage_dir=tmp_path)
15
+
16
+
17
+ def test_knowledge_base_learn_and_find(temp_kb):
18
+ rule = temp_kb.learn(
19
+ db_name="test_db",
20
+ topic="Site to User Group Mapping",
21
+ rule_description="ivis_site maps to ivis_user through ivis_site_group -> ivis_group -> ivis_user_group_map",
22
+ relevant_tables=["ivis_site", "ivis_site_group", "ivis_group", "ivis_user_group_map", "ivis_user"],
23
+ source="user_correction",
24
+ )
25
+ assert rule.rule_id.startswith("rule_")
26
+ assert rule.topic == "Site to User Group Mapping"
27
+
28
+ # List all
29
+ all_rules = temp_kb.list_all("test_db")
30
+ assert len(all_rules) == 1
31
+
32
+ # Find relevant by prompt
33
+ matched = temp_kb.find_relevant("test_db", "how to map sites to user")
34
+ assert len(matched) == 1
35
+ assert matched[0].topic == "Site to User Group Mapping"
36
+
37
+ # Context formatting
38
+ ctx = temp_kb.format_prompt_context("test_db", "how to map sites to user")
39
+ assert "Persistent Database Domain Knowledge" in ctx
40
+ assert "Site to User Group Mapping" in ctx
41
+
42
+ # Delete
43
+ deleted = temp_kb.delete("test_db", rule.rule_id)
44
+ assert deleted is True
45
+ assert len(temp_kb.list_all("test_db")) == 0
46
+
47
+
48
+ def test_relationship_graph_find_all_paths():
49
+ schema = DatabaseSchema(
50
+ dialect_name="postgresql",
51
+ database_name="test_db",
52
+ tables=[
53
+ TableModel(
54
+ name="users",
55
+ columns=[ColumnModel(name="id", data_type="INT", is_primary_key=True)],
56
+ primary_key=["id"],
57
+ ),
58
+ TableModel(
59
+ name="user_groups",
60
+ columns=[
61
+ ColumnModel(name="user_id", data_type="INT"),
62
+ ColumnModel(name="group_id", data_type="INT"),
63
+ ],
64
+ foreign_keys=[
65
+ ForeignKeyModel(name="fk_u", constrained_columns=["user_id"], referred_table="users", referred_columns=["id"]),
66
+ ForeignKeyModel(name="fk_g", constrained_columns=["group_id"], referred_table="groups", referred_columns=["id"]),
67
+ ],
68
+ ),
69
+ TableModel(
70
+ name="groups",
71
+ columns=[ColumnModel(name="id", data_type="INT", is_primary_key=True)],
72
+ primary_key=["id"],
73
+ ),
74
+ ],
75
+ )
76
+
77
+ graph = RelationshipGraph(schema)
78
+ paths = graph.find_all_paths("users", "groups", max_hops=3)
79
+ assert len(paths) >= 1
80
+ assert paths[0].tables == ["users", "user_groups", "groups"]
File without changes
File without changes