dbagent-cli 0.9.0__tar.gz → 0.9.2__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.2}/PKG-INFO +1 -1
  2. {dbagent_cli-0.9.0 → dbagent_cli-0.9.2}/dbagent/__init__.py +1 -1
  3. {dbagent_cli-0.9.0 → dbagent_cli-0.9.2}/dbagent/agent/conversation.py +5 -1
  4. {dbagent_cli-0.9.0 → dbagent_cli-0.9.2}/dbagent/agent/generator.py +41 -25
  5. {dbagent_cli-0.9.0 → dbagent_cli-0.9.2}/dbagent/agent/pipeline.py +35 -3
  6. {dbagent_cli-0.9.0 → dbagent_cli-0.9.2}/dbagent/cli.py +49 -0
  7. {dbagent_cli-0.9.0 → dbagent_cli-0.9.2}/dbagent/connectors/base.py +32 -4
  8. {dbagent_cli-0.9.0 → dbagent_cli-0.9.2}/dbagent/schema/graph.py +30 -0
  9. dbagent_cli-0.9.2/dbagent/semantic/knowledge.py +146 -0
  10. {dbagent_cli-0.9.0 → dbagent_cli-0.9.2}/dbagent_cli.egg-info/PKG-INFO +1 -1
  11. {dbagent_cli-0.9.0 → dbagent_cli-0.9.2}/dbagent_cli.egg-info/SOURCES.txt +2 -0
  12. {dbagent_cli-0.9.0 → dbagent_cli-0.9.2}/pyproject.toml +1 -1
  13. dbagent_cli-0.9.2/tests/test_knowledge.py +80 -0
  14. {dbagent_cli-0.9.0 → dbagent_cli-0.9.2}/README.md +0 -0
  15. {dbagent_cli-0.9.0 → dbagent_cli-0.9.2}/dbagent/agent/confidence.py +0 -0
  16. {dbagent_cli-0.9.0 → dbagent_cli-0.9.2}/dbagent/agent/doctor.py +0 -0
  17. {dbagent_cli-0.9.0 → dbagent_cli-0.9.2}/dbagent/agent/error_classifier.py +0 -0
  18. {dbagent_cli-0.9.0 → dbagent_cli-0.9.2}/dbagent/agent/recommender.py +0 -0
  19. {dbagent_cli-0.9.0 → dbagent_cli-0.9.2}/dbagent/agent/validator.py +0 -0
  20. {dbagent_cli-0.9.0 → dbagent_cli-0.9.2}/dbagent/config.py +0 -0
  21. {dbagent_cli-0.9.0 → dbagent_cli-0.9.2}/dbagent/connectors/factory.py +0 -0
  22. {dbagent_cli-0.9.0 → dbagent_cli-0.9.2}/dbagent/connectors/mongo.py +0 -0
  23. {dbagent_cli-0.9.0 → dbagent_cli-0.9.2}/dbagent/connectors/relational.py +0 -0
  24. {dbagent_cli-0.9.0 → dbagent_cli-0.9.2}/dbagent/execution/__init__.py +0 -0
  25. {dbagent_cli-0.9.0 → dbagent_cli-0.9.2}/dbagent/execution/cache.py +0 -0
  26. {dbagent_cli-0.9.0 → dbagent_cli-0.9.2}/dbagent/execution/changeset.py +0 -0
  27. {dbagent_cli-0.9.0 → dbagent_cli-0.9.2}/dbagent/execution/cost.py +0 -0
  28. {dbagent_cli-0.9.0 → dbagent_cli-0.9.2}/dbagent/execution/recovery.py +0 -0
  29. {dbagent_cli-0.9.0 → dbagent_cli-0.9.2}/dbagent/execution/sandbox.py +0 -0
  30. {dbagent_cli-0.9.0 → dbagent_cli-0.9.2}/dbagent/execution/streaming.py +0 -0
  31. {dbagent_cli-0.9.0 → dbagent_cli-0.9.2}/dbagent/llm/anthropic_provider.py +0 -0
  32. {dbagent_cli-0.9.0 → dbagent_cli-0.9.2}/dbagent/llm/auto_setup.py +0 -0
  33. {dbagent_cli-0.9.0 → dbagent_cli-0.9.2}/dbagent/llm/base.py +0 -0
  34. {dbagent_cli-0.9.0 → dbagent_cli-0.9.2}/dbagent/llm/deepseek_provider.py +0 -0
  35. {dbagent_cli-0.9.0 → dbagent_cli-0.9.2}/dbagent/llm/factory.py +0 -0
  36. {dbagent_cli-0.9.0 → dbagent_cli-0.9.2}/dbagent/llm/gemini_provider.py +0 -0
  37. {dbagent_cli-0.9.0 → dbagent_cli-0.9.2}/dbagent/llm/groq_provider.py +0 -0
  38. {dbagent_cli-0.9.0 → dbagent_cli-0.9.2}/dbagent/llm/mistral_provider.py +0 -0
  39. {dbagent_cli-0.9.0 → dbagent_cli-0.9.2}/dbagent/llm/mock_provider.py +0 -0
  40. {dbagent_cli-0.9.0 → dbagent_cli-0.9.2}/dbagent/llm/ollama_provider.py +0 -0
  41. {dbagent_cli-0.9.0 → dbagent_cli-0.9.2}/dbagent/llm/openai_provider.py +0 -0
  42. {dbagent_cli-0.9.0 → dbagent_cli-0.9.2}/dbagent/llm/openrouter_provider.py +0 -0
  43. {dbagent_cli-0.9.0 → dbagent_cli-0.9.2}/dbagent/schema/cache.py +0 -0
  44. {dbagent_cli-0.9.0 → dbagent_cli-0.9.2}/dbagent/schema/formatter.py +0 -0
  45. {dbagent_cli-0.9.0 → dbagent_cli-0.9.2}/dbagent/schema/ground_truth.py +0 -0
  46. {dbagent_cli-0.9.0 → dbagent_cli-0.9.2}/dbagent/schema/models.py +0 -0
  47. {dbagent_cli-0.9.0 → dbagent_cli-0.9.2}/dbagent/schema/retriever.py +0 -0
  48. {dbagent_cli-0.9.0 → dbagent_cli-0.9.2}/dbagent/schema/selector.py +0 -0
  49. {dbagent_cli-0.9.0 → dbagent_cli-0.9.2}/dbagent/security/__init__.py +0 -0
  50. {dbagent_cli-0.9.0 → dbagent_cli-0.9.2}/dbagent/security/audit.py +0 -0
  51. {dbagent_cli-0.9.0 → dbagent_cli-0.9.2}/dbagent/security/environment.py +0 -0
  52. {dbagent_cli-0.9.0 → dbagent_cli-0.9.2}/dbagent/security/masking.py +0 -0
  53. {dbagent_cli-0.9.0 → dbagent_cli-0.9.2}/dbagent/security/modes.py +0 -0
  54. {dbagent_cli-0.9.0 → dbagent_cli-0.9.2}/dbagent/security/rbac.py +0 -0
  55. {dbagent_cli-0.9.0 → dbagent_cli-0.9.2}/dbagent/semantic/__init__.py +0 -0
  56. {dbagent_cli-0.9.0 → dbagent_cli-0.9.2}/dbagent/semantic/dictionary.py +0 -0
  57. {dbagent_cli-0.9.0 → dbagent_cli-0.9.2}/dbagent/semantic/templates.py +0 -0
  58. {dbagent_cli-0.9.0 → dbagent_cli-0.9.2}/dbagent/sql/__init__.py +0 -0
  59. {dbagent_cli-0.9.0 → dbagent_cli-0.9.2}/dbagent/sql/optimizer.py +0 -0
  60. {dbagent_cli-0.9.0 → dbagent_cli-0.9.2}/dbagent/sql/validator.py +0 -0
  61. {dbagent_cli-0.9.0 → dbagent_cli-0.9.2}/dbagent/ui/chat_screen.py +0 -0
  62. {dbagent_cli-0.9.0 → dbagent_cli-0.9.2}/dbagent/ui/console.py +0 -0
  63. {dbagent_cli-0.9.0 → dbagent_cli-0.9.2}/dbagent/ui/grid_window.py +0 -0
  64. {dbagent_cli-0.9.0 → dbagent_cli-0.9.2}/dbagent/ui/viewer.py +0 -0
  65. {dbagent_cli-0.9.0 → dbagent_cli-0.9.2}/dbagent/ui/visualizer.py +0 -0
  66. {dbagent_cli-0.9.0 → dbagent_cli-0.9.2}/dbagent_cli.egg-info/dependency_links.txt +0 -0
  67. {dbagent_cli-0.9.0 → dbagent_cli-0.9.2}/dbagent_cli.egg-info/entry_points.txt +0 -0
  68. {dbagent_cli-0.9.0 → dbagent_cli-0.9.2}/dbagent_cli.egg-info/requires.txt +0 -0
  69. {dbagent_cli-0.9.0 → dbagent_cli-0.9.2}/dbagent_cli.egg-info/top_level.txt +0 -0
  70. {dbagent_cli-0.9.0 → dbagent_cli-0.9.2}/setup.cfg +0 -0
  71. {dbagent_cli-0.9.0 → dbagent_cli-0.9.2}/tests/test_cache.py +0 -0
  72. {dbagent_cli-0.9.0 → dbagent_cli-0.9.2}/tests/test_cli.py +0 -0
  73. {dbagent_cli-0.9.0 → dbagent_cli-0.9.2}/tests/test_connectors.py +0 -0
  74. {dbagent_cli-0.9.0 → dbagent_cli-0.9.2}/tests/test_direct_sql.py +0 -0
  75. {dbagent_cli-0.9.0 → dbagent_cli-0.9.2}/tests/test_doctor_and_viz.py +0 -0
  76. {dbagent_cli-0.9.0 → dbagent_cli-0.9.2}/tests/test_generator.py +0 -0
  77. {dbagent_cli-0.9.0 → dbagent_cli-0.9.2}/tests/test_graph.py +0 -0
  78. {dbagent_cli-0.9.0 → dbagent_cli-0.9.2}/tests/test_providers.py +0 -0
  79. {dbagent_cli-0.9.0 → dbagent_cli-0.9.2}/tests/test_recovery.py +0 -0
  80. {dbagent_cli-0.9.0 → dbagent_cli-0.9.2}/tests/test_safety.py +0 -0
  81. {dbagent_cli-0.9.0 → dbagent_cli-0.9.2}/tests/test_sandbox.py +0 -0
  82. {dbagent_cli-0.9.0 → dbagent_cli-0.9.2}/tests/test_schema.py +0 -0
  83. {dbagent_cli-0.9.0 → dbagent_cli-0.9.2}/tests/test_security.py +0 -0
  84. {dbagent_cli-0.9.0 → dbagent_cli-0.9.2}/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.2
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.2"
6
6
  __author__ = "Santhosh Gupta"
@@ -54,9 +54,13 @@ class ConversationContext:
54
54
  if len(self.turns) > self.max_history:
55
55
  self.turns.pop(0)
56
56
 
57
- if tables and not is_chat:
57
+ if tables:
58
58
  self.active_tables.update(t.lower() for t in tables)
59
59
 
60
+ def get_active_tables(self) -> List[str]:
61
+ """Returns currently active database table names discussed in this session."""
62
+ return list(self.active_tables)
63
+
60
64
  def get_last_turn(self) -> Optional[ConversationTurn]:
61
65
  if self.turns:
62
66
  return self.turns[-1]
@@ -106,6 +106,11 @@ Provide ready-to-run, dialect-accurate SQL JOIN queries answering the user's spe
106
106
 
107
107
  ### 4. 💡 Architectural Insights & Best Practices
108
108
  Mention indexing recommendations, cascade rules, or performance considerations for these join paths.
109
+
110
+ ### 🛑 Critical Operational Directives:
111
+ 1. NEVER ask passive questions like "Would you like me to inspect tables?" or "Should I run a query?". Always proactively inspect and deliver the full architectural blueprint immediately.
112
+ 2. NEVER give generic textbook definitions. Always ground every explanation in the real database schema provided above.
113
+ 3. Always include ALL 4 sections (Diagram, Theoretical Breakdown, SQL Queries, and Insights).
109
114
  """
110
115
 
111
116
 
@@ -142,37 +147,52 @@ class ScriptGenerator:
142
147
  prompt_lower = user_prompt.lower().strip()
143
148
  clean_prompt = prompt_lower.rstrip(".!?,:;~ ")
144
149
 
145
- # Chat / Conversational indicators (greetings, feedback, praise, acknowledgements)
146
- chat_triggers = [
147
- "hello", "hi", "hey", "howdy", "greetings", "good morning", "good evening", "good afternoon",
148
- "who are you", "what can you do", "what are you", "help me", "how to use",
149
- "what is this", "tell me about this database", "how does this work",
150
- "can you help", "thanks", "thank you", "thx", "bye", "goodbye", "see ya",
151
- "good", "great", "nice", "cool", "awesome", "perfect", "ok", "okay",
152
- "yes", "no", "sure", "got it", "understood", "nice work", "well done",
153
- "sounds good", "looks good", "alright", "super", "neat", "excellent",
154
- "amazing", "wonderful", "cool thanks", "great thanks", "thanks a lot",
155
- ]
156
- for trig in chat_triggers:
157
- if clean_prompt == trig or prompt_lower.startswith(f"{trig} ") or prompt_lower.startswith(f"{trig},") or prompt_lower.startswith(f"{trig}!"):
158
- return "chat"
159
-
160
- # Architecture / Relationship / Linkage / Analysis indicators
150
+ # 1. Architecture / Relationship / Linkage / Analysis indicators (Highest Priority)
161
151
  analysis_keywords = [
162
152
  "linkage architecture", "linkage", "architecture", "relationship between",
163
153
  "how are", "how is", "how were", "how they were related", "how were they related",
164
- "how they were mapped", "how to map", "mapped to", "er diagram", "diagram of",
154
+ "how they were mapped", "how to map", "where they mapped", "where is it mapped",
155
+ "how they were assigned", "how are they assigned", "how will be linked", "how is it linked",
156
+ "mapped to", "mapped with", "er diagram", "diagram of",
165
157
  "data model", "explain relationship", "explain schema", "schema analysis",
166
158
  "analyze the database", "analyze database", "table relationship",
167
159
  "foreign key relationship", "how are tables related", "which sites in",
168
160
  "how to know that which", "how to know which", "mapping architecture",
169
161
  "entity relationship", "theoritical information", "theoretical information",
162
+ "check with my previous context", "check with previous context", "previous context",
163
+ "complete architecture", "scope architecture", "tenant level to user level",
164
+ "tenant level", "user level",
170
165
  ]
171
166
  for kw in analysis_keywords:
172
167
  if kw in prompt_lower:
173
168
  return "analysis"
174
169
 
175
- # DDL indicators
170
+ # Contextual clarification for analysis (e.g. "i mean ivis_scope", "i mean table_x")
171
+ if re.search(r'\bi mean\b', prompt_lower) and not re.search(r'\b(insert|update|delete|drop)\b', prompt_lower):
172
+ return "analysis"
173
+
174
+ # 2. Pure Greeting / Feedback / Help triggers (Only when not asking a technical question)
175
+ pure_chat_greetings = ["hello", "hi", "hey", "howdy", "greetings", "good morning", "good evening", "good afternoon"]
176
+ pure_chat_feedback = [
177
+ "thanks", "thank you", "thx", "bye", "goodbye", "see ya",
178
+ "good", "great", "nice", "cool", "awesome", "perfect", "ok", "okay",
179
+ "yes", "no", "sure", "got it", "understood", "nice work", "well done",
180
+ "sounds good", "looks good", "alright", "super", "neat", "excellent",
181
+ "amazing", "wonderful", "cool thanks", "great thanks", "thanks a lot",
182
+ ]
183
+ pure_chat_help = [
184
+ "who are you", "what can you do", "what are you", "help me", "how to use",
185
+ "what is this", "tell me about this database", "how does this work", "can you help",
186
+ ]
187
+
188
+ if clean_prompt in pure_chat_greetings or clean_prompt in pure_chat_feedback or clean_prompt in pure_chat_help:
189
+ return "chat"
190
+
191
+ for trig in pure_chat_greetings + pure_chat_help:
192
+ if prompt_lower.startswith(f"{trig} ") or prompt_lower.startswith(f"{trig},") or prompt_lower.startswith(f"{trig}!"):
193
+ return "chat"
194
+
195
+ # 3. DDL indicators
176
196
  ddl_keywords = [
177
197
  "create table", "alter table", "drop table", "add column",
178
198
  "remove column", "rename column", "rename table", "modify column",
@@ -182,7 +202,7 @@ class ScriptGenerator:
182
202
  if kw in prompt_lower:
183
203
  return "ddl"
184
204
 
185
- # Write indicators (word-boundary matching to avoid false positives like "show dataset")
205
+ # 4. Write indicators (word-boundary matching to avoid false positives like "show dataset")
186
206
  write_phrases = [
187
207
  "insert", "add record", "add row", "add new",
188
208
  "update", "delete", "drop record",
@@ -191,14 +211,10 @@ class ScriptGenerator:
191
211
  for kw in write_phrases:
192
212
  if kw in prompt_lower:
193
213
  return "write"
194
- # Single-word write verbs need word boundary to avoid false positives
195
- # e.g. "set" should not match "dataset", "change" should not match "show changes"
196
- import re as _re
197
214
  write_verbs = ["set", "change", "modify", "remove"]
198
215
  for verb in write_verbs:
199
- if _re.search(r'\b' + verb + r'\b', prompt_lower):
200
- # Exclude when preceded by 'show', 'list', 'display', 'view', 'get'
201
- read_context = _re.search(r'\b(?:show|list|display|view|get|count)\b.*\b' + verb, prompt_lower)
216
+ if re.search(r'\b' + verb + r'\b', prompt_lower):
217
+ read_context = re.search(r'\b(?:show|list|display|view|get|count)\b.*\b' + verb, prompt_lower)
202
218
  if not read_context:
203
219
  return "write"
204
220
 
@@ -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()
@@ -212,13 +214,34 @@ class QueryPipeline:
212
214
  except Exception:
213
215
  exact, fuzzy = [], []
214
216
 
217
+ # Inherit active tables from conversation context if follow-up prompt doesn't name tables explicitly
218
+ if not exact and not fuzzy:
219
+ inherited = self.conversation.get_active_tables()
220
+ if inherited:
221
+ exact = inherited[:4]
222
+
215
223
  sub_schema, join_steps = self._get_fast_schema_context(user_prompt, exact, fuzzy)
216
224
  result.schema = sub_schema
217
225
  result.join_steps = join_steps
226
+
227
+ # Auto-Learn from user guidance/corrections
228
+ prompt_l = user_prompt.lower()
229
+ if any(w in prompt_l for w in ["mapped with", "mapped to", "observe the database", "the rule is", "remember that", "notice that"]):
230
+ self.knowledge_base.learn(
231
+ db_name=self.db_name,
232
+ topic=f"Mapping for {', '.join(exact[:3]) if exact else 'Entities'}",
233
+ rule_description=user_prompt,
234
+ relevant_tables=exact + fuzzy[:4],
235
+ source="user_guidance",
236
+ )
237
+
238
+ kb_context = self.knowledge_base.format_prompt_context(self.db_name, user_prompt, exact + fuzzy)
218
239
  conv_history = self.conversation.format_context_for_prompt()
240
+ analysis_prompt = f"{kb_context}\n\n{user_prompt}" if kb_context else user_prompt
241
+
219
242
  result.message = self.generator.generate_analysis_response(
220
243
  schema=sub_schema,
221
- user_prompt=user_prompt,
244
+ user_prompt=analysis_prompt,
222
245
  conversation_history=conv_history,
223
246
  model=self.model,
224
247
  )
@@ -227,7 +250,13 @@ class QueryPipeline:
227
250
  result.was_executed = True
228
251
  result.query_type = "analysis"
229
252
  result.execution_time_ms = (time.time() - t_start) * 1000
230
- self.conversation.add_turn(user_prompt=user_prompt, sql="", assistant_response=result.message, is_chat=True)
253
+ self.conversation.add_turn(
254
+ user_prompt=user_prompt,
255
+ sql="",
256
+ assistant_response=result.message,
257
+ tables=exact + fuzzy,
258
+ is_chat=True,
259
+ )
231
260
  return result
232
261
  except Exception as e:
233
262
  result.error = f"Architecture analysis failed: {str(e)}"
@@ -384,13 +413,16 @@ class QueryPipeline:
384
413
  result.error = f"Schema retrieval failed: {str(e)}"
385
414
  return result
386
415
 
387
- # --- 4. Semantic Business Dictionary Context Injection ---
416
+ # --- 4. Semantic Business Dictionary & Knowledge Base Injection ---
388
417
  dict_context = self.dictionary.format_prompt_context(user_prompt)
418
+ kb_context = self.knowledge_base.format_prompt_context(self.db_name, user_prompt, exact + fuzzy)
389
419
  conv_context = self.conversation.format_context_for_prompt()
390
420
 
391
421
  augmented_prompt = user_prompt
392
422
  if dict_context:
393
423
  augmented_prompt = f"{dict_context}\n\n{augmented_prompt}"
424
+ if kb_context:
425
+ augmented_prompt = f"{kb_context}\n\n{augmented_prompt}"
394
426
  if conv_context:
395
427
  augmented_prompt = f"{conv_context}\n\n{augmented_prompt}"
396
428
 
@@ -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.")
@@ -44,14 +44,42 @@ class BaseConnector(ABC):
44
44
 
45
45
  def resolve_tables(self, user_prompt: str) -> Tuple[List[str], List[str]]:
46
46
  """
47
- Resolve which tables the user is referring to.
47
+ Resolve which tables the user is referring to with prefix stripping, pluralization handling, and fuzzy matching.
48
48
  Returns (exact_matches, fuzzy_matches).
49
- Default implementation uses get_table_names with simple substring matching.
50
49
  """
50
+ import re
51
51
  all_tables = self.get_table_names()
52
52
  prompt_lower = user_prompt.lower()
53
- exact = [t for t in all_tables if t.lower() in prompt_lower]
54
- return exact, []
53
+ exact = []
54
+ fuzzy = []
55
+
56
+ tokens = set(re.findall(r'\b[a-zA-Z0-9_]+\b', prompt_lower))
57
+
58
+ for t in all_tables:
59
+ t_lower = t.lower()
60
+ # 1. Exact full table name in prompt
61
+ if re.search(r'\b' + re.escape(t_lower) + r'\b', prompt_lower):
62
+ exact.append(t)
63
+ continue
64
+
65
+ # 2. Match without common prefixes (e.g. "ivis_scope" -> "scope", "scopes")
66
+ unprefixed = re.sub(r'^(?:ivis_|tbl_|table_|app_)', '', t_lower)
67
+ if unprefixed and len(unprefixed) >= 3:
68
+ if (unprefixed in tokens or
69
+ f"{unprefixed}s" in tokens or
70
+ f"{unprefixed}es" in tokens or
71
+ re.search(r'\b' + re.escape(unprefixed) + r'(?:s|es)?\b', prompt_lower)):
72
+ if t not in exact:
73
+ exact.append(t)
74
+ continue
75
+
76
+ # 3. Fuzzy partial match
77
+ for tok in tokens:
78
+ if len(tok) >= 4 and (tok in t_lower or t_lower in tok):
79
+ if t not in exact and t not in fuzzy:
80
+ fuzzy.append(t)
81
+
82
+ return exact, fuzzy
55
83
 
56
84
  def refresh_cache(self) -> None:
57
85
  """Invalidate any cached metadata. Override in subclasses."""
@@ -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.2
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.2"
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