mcp-sqlite-memory-bank 1.5.1__py3-none-any.whl → 1.6.2__py3-none-any.whl
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.
- mcp_sqlite_memory_bank/__init__.py +3 -3
- mcp_sqlite_memory_bank/__main__.py +8 -7
- mcp_sqlite_memory_bank/database.py +166 -48
- mcp_sqlite_memory_bank/prompts.py +64 -48
- mcp_sqlite_memory_bank/resources.py +218 -144
- mcp_sqlite_memory_bank/semantic.py +25 -13
- mcp_sqlite_memory_bank/server.py +174 -32
- mcp_sqlite_memory_bank/tools/__init__.py +26 -29
- mcp_sqlite_memory_bank/tools/analytics.py +179 -130
- mcp_sqlite_memory_bank/tools/basic.py +417 -4
- mcp_sqlite_memory_bank/tools/discovery.py +549 -360
- mcp_sqlite_memory_bank/tools/search.py +147 -71
- mcp_sqlite_memory_bank/types.py +6 -1
- mcp_sqlite_memory_bank/utils.py +154 -105
- {mcp_sqlite_memory_bank-1.5.1.dist-info → mcp_sqlite_memory_bank-1.6.2.dist-info}/METADATA +54 -6
- mcp_sqlite_memory_bank-1.6.2.dist-info/RECORD +21 -0
- mcp_sqlite_memory_bank-1.5.1.dist-info/RECORD +0 -21
- {mcp_sqlite_memory_bank-1.5.1.dist-info → mcp_sqlite_memory_bank-1.6.2.dist-info}/WHEEL +0 -0
- {mcp_sqlite_memory_bank-1.5.1.dist-info → mcp_sqlite_memory_bank-1.6.2.dist-info}/entry_points.txt +0 -0
- {mcp_sqlite_memory_bank-1.5.1.dist-info → mcp_sqlite_memory_bank-1.6.2.dist-info}/licenses/LICENSE +0 -0
- {mcp_sqlite_memory_bank-1.5.1.dist-info → mcp_sqlite_memory_bank-1.6.2.dist-info}/top_level.txt +0 -0
@@ -8,7 +8,7 @@ Cursor, and other LLM-powered tools to interact with structured data in a
|
|
8
8
|
safe, explicit, and extensible way.
|
9
9
|
|
10
10
|
Author: Robert Meisner
|
11
|
-
Version:
|
11
|
+
Version: 1.6.2
|
12
12
|
License: MIT
|
13
13
|
"""
|
14
14
|
|
@@ -73,7 +73,7 @@ from .types import (
|
|
73
73
|
)
|
74
74
|
|
75
75
|
# Package metadata
|
76
|
-
__version__ = "
|
76
|
+
__version__ = "1.6.2"
|
77
77
|
__author__ = "Robert Meisner"
|
78
78
|
__all__ = [
|
79
79
|
# Core tools
|
@@ -93,7 +93,7 @@ __all__ = [
|
|
93
93
|
"explore_tables",
|
94
94
|
"add_embeddings",
|
95
95
|
"semantic_search",
|
96
|
-
"find_related",
|
96
|
+
"find_related",
|
97
97
|
"smart_search",
|
98
98
|
"embedding_stats",
|
99
99
|
"auto_semantic_search",
|
@@ -17,16 +17,16 @@ if project_root not in sys.path:
|
|
17
17
|
|
18
18
|
# Configure logging before any other imports
|
19
19
|
logging.basicConfig(
|
20
|
-
level=logging.INFO,
|
21
|
-
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s"
|
20
|
+
level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s"
|
22
21
|
)
|
23
22
|
|
24
|
-
|
23
|
+
|
24
|
+
def main() -> None:
|
25
25
|
"""Main entry point for the MCP server."""
|
26
26
|
try:
|
27
27
|
# Import here to avoid circular import issues
|
28
28
|
from .server import app, DB_PATH
|
29
|
-
|
29
|
+
|
30
30
|
# Handle help argument
|
31
31
|
if "--help" in sys.argv or "-h" in sys.argv:
|
32
32
|
print("SQLite Memory Bank MCP Server")
|
@@ -41,13 +41,13 @@ def main():
|
|
41
41
|
print("Environment variables:")
|
42
42
|
print(" DB_PATH: Override the default database path")
|
43
43
|
return
|
44
|
-
|
44
|
+
|
45
45
|
# Log startup information
|
46
46
|
logging.info(f"Starting SQLite Memory Bank MCP server with database at {DB_PATH}")
|
47
|
-
|
47
|
+
|
48
48
|
# Run the FastMCP app in stdio mode for MCP clients
|
49
49
|
app.run(transport="stdio")
|
50
|
-
|
50
|
+
|
51
51
|
except KeyboardInterrupt:
|
52
52
|
logging.info("Server stopped by user")
|
53
53
|
sys.exit(0)
|
@@ -55,5 +55,6 @@ def main():
|
|
55
55
|
logging.error(f"Failed to start MCP server: {e}")
|
56
56
|
sys.exit(1)
|
57
57
|
|
58
|
+
|
58
59
|
if __name__ == "__main__":
|
59
60
|
main()
|
@@ -12,7 +12,19 @@ import json
|
|
12
12
|
import logging
|
13
13
|
from functools import wraps
|
14
14
|
from typing import Dict, List, Any, Optional, Callable, cast
|
15
|
-
from sqlalchemy import
|
15
|
+
from sqlalchemy import (
|
16
|
+
create_engine,
|
17
|
+
MetaData,
|
18
|
+
Table,
|
19
|
+
select,
|
20
|
+
insert,
|
21
|
+
update,
|
22
|
+
delete,
|
23
|
+
text,
|
24
|
+
inspect,
|
25
|
+
and_,
|
26
|
+
or_,
|
27
|
+
)
|
16
28
|
from sqlalchemy.engine import Engine
|
17
29
|
from sqlalchemy.exc import SQLAlchemyError
|
18
30
|
from contextlib import contextmanager
|
@@ -63,7 +75,7 @@ class SQLiteMemoryDatabase:
|
|
63
75
|
except Exception as e:
|
64
76
|
logging.warning(f"Error closing database: {e}")
|
65
77
|
|
66
|
-
def __del__(self):
|
78
|
+
def __del__(self) -> None:
|
67
79
|
"""Ensure cleanup when object is garbage collected."""
|
68
80
|
self.close()
|
69
81
|
|
@@ -76,7 +88,7 @@ class SQLiteMemoryDatabase:
|
|
76
88
|
logging.warning(f"Failed to refresh metadata: {e}")
|
77
89
|
|
78
90
|
@contextmanager
|
79
|
-
def get_connection(self):
|
91
|
+
def get_connection(self) -> Any:
|
80
92
|
"""Get a database connection with automatic cleanup."""
|
81
93
|
conn = self.engine.connect()
|
82
94
|
try:
|
@@ -96,12 +108,16 @@ class SQLiteMemoryDatabase:
|
|
96
108
|
|
97
109
|
return self.metadata.tables[table_name]
|
98
110
|
|
99
|
-
def _validate_columns(
|
111
|
+
def _validate_columns(
|
112
|
+
self, table: Table, column_names: List[str], context: str = "operation"
|
113
|
+
) -> None:
|
100
114
|
"""Validate that all column names exist in the table."""
|
101
115
|
valid_columns = set(col.name for col in table.columns)
|
102
116
|
for col_name in column_names:
|
103
117
|
if col_name not in valid_columns:
|
104
|
-
raise ValidationError(
|
118
|
+
raise ValidationError(
|
119
|
+
f"Invalid column '{col_name}' for table " f"'{table.name}' in {context}"
|
120
|
+
)
|
105
121
|
|
106
122
|
def _build_where_conditions(self, table: Table, where: Dict[str, Any]) -> List:
|
107
123
|
"""Build SQLAlchemy WHERE conditions from a dictionary."""
|
@@ -170,7 +186,9 @@ class SQLiteMemoryDatabase:
|
|
170
186
|
try:
|
171
187
|
with self.get_connection() as conn:
|
172
188
|
inspector = inspect(conn)
|
173
|
-
tables = [
|
189
|
+
tables = [
|
190
|
+
name for name in inspector.get_table_names() if not name.startswith("sqlite_")
|
191
|
+
]
|
174
192
|
return {"success": True, "tables": tables}
|
175
193
|
except SQLAlchemyError as e:
|
176
194
|
raise DatabaseError(f"Failed to list tables: {str(e)}")
|
@@ -248,7 +266,10 @@ class SQLiteMemoryDatabase:
|
|
248
266
|
raise DatabaseError(f"Failed to insert into table {table_name}: {str(e)}")
|
249
267
|
|
250
268
|
def read_rows(
|
251
|
-
self,
|
269
|
+
self,
|
270
|
+
table_name: str,
|
271
|
+
where: Optional[Dict[str, Any]] = None,
|
272
|
+
limit: Optional[int] = None,
|
252
273
|
) -> ToolResponse:
|
253
274
|
"""Read rows from a table with optional filtering."""
|
254
275
|
try:
|
@@ -275,7 +296,10 @@ class SQLiteMemoryDatabase:
|
|
275
296
|
raise DatabaseError(f"Failed to read from table {table_name}: {str(e)}")
|
276
297
|
|
277
298
|
def update_rows(
|
278
|
-
self,
|
299
|
+
self,
|
300
|
+
table_name: str,
|
301
|
+
data: Dict[str, Any],
|
302
|
+
where: Optional[Dict[str, Any]] = None,
|
279
303
|
) -> ToolResponse:
|
280
304
|
"""Update rows in a table."""
|
281
305
|
if not data:
|
@@ -363,13 +387,16 @@ class SQLiteMemoryDatabase:
|
|
363
387
|
try:
|
364
388
|
self._refresh_metadata()
|
365
389
|
schemas = {
|
366
|
-
table_name: [col.name for col in table.columns]
|
390
|
+
table_name: [col.name for col in table.columns]
|
391
|
+
for table_name, table in self.metadata.tables.items()
|
367
392
|
}
|
368
393
|
return {"success": True, "schemas": schemas}
|
369
394
|
except SQLAlchemyError as e:
|
370
395
|
raise DatabaseError(f"Failed to list all columns: {str(e)}")
|
371
396
|
|
372
|
-
def search_content(
|
397
|
+
def search_content(
|
398
|
+
self, query: str, tables: Optional[List[str]] = None, limit: int = 50
|
399
|
+
) -> ToolResponse:
|
373
400
|
"""Perform full-text search across table content."""
|
374
401
|
if not query or not query.strip():
|
375
402
|
raise ValidationError("Search query cannot be empty")
|
@@ -413,42 +440,67 @@ class SQLiteMemoryDatabase:
|
|
413
440
|
if col.name in row_dict and row_dict[col.name]:
|
414
441
|
content = str(row_dict[col.name]).lower()
|
415
442
|
content_length = len(content)
|
416
|
-
|
443
|
+
|
417
444
|
if query_lower in content:
|
418
445
|
# Factor 1: Exact phrase frequency (weighted higher)
|
419
446
|
exact_frequency = content.count(query_lower)
|
420
|
-
exact_score = (
|
421
|
-
|
447
|
+
exact_score = (
|
448
|
+
(exact_frequency * 2.0) / content_length
|
449
|
+
if content_length > 0
|
450
|
+
else 0
|
451
|
+
)
|
452
|
+
|
422
453
|
# Factor 2: Individual term frequency
|
423
454
|
term_score = 0.0
|
424
455
|
for term in query_terms:
|
425
456
|
if term in content:
|
426
|
-
term_score +=
|
427
|
-
|
457
|
+
term_score += (
|
458
|
+
content.count(term) / content_length
|
459
|
+
if content_length > 0
|
460
|
+
else 0
|
461
|
+
)
|
462
|
+
|
428
463
|
# Factor 3: Position bonus (early matches score higher)
|
429
464
|
position_bonus = 0.0
|
430
465
|
first_occurrence = content.find(query_lower)
|
431
466
|
if first_occurrence != -1:
|
432
|
-
position_bonus = (
|
433
|
-
|
467
|
+
position_bonus = (
|
468
|
+
(content_length - first_occurrence)
|
469
|
+
/ content_length
|
470
|
+
* 0.1
|
471
|
+
)
|
472
|
+
|
434
473
|
# Factor 4: Column importance (title/name columns get bonus)
|
435
474
|
column_bonus = 0.0
|
436
|
-
if any(
|
475
|
+
if any(
|
476
|
+
keyword in col.name.lower()
|
477
|
+
for keyword in [
|
478
|
+
"title",
|
479
|
+
"name",
|
480
|
+
"summary",
|
481
|
+
"description",
|
482
|
+
]
|
483
|
+
):
|
437
484
|
column_bonus = 0.2
|
438
|
-
|
485
|
+
|
439
486
|
# Combined relevance score
|
440
|
-
col_relevance =
|
487
|
+
col_relevance = (
|
488
|
+
exact_score + term_score + position_bonus + column_bonus
|
489
|
+
)
|
441
490
|
relevance_scores.append(col_relevance)
|
442
|
-
|
491
|
+
|
443
492
|
# Enhanced matched content with context
|
444
493
|
snippet_start = max(0, first_occurrence - 50)
|
445
|
-
snippet_end = min(
|
494
|
+
snippet_end = min(
|
495
|
+
len(row_dict[col.name]),
|
496
|
+
first_occurrence + len(query) + 50,
|
497
|
+
)
|
446
498
|
snippet = str(row_dict[col.name])[snippet_start:snippet_end]
|
447
499
|
if snippet_start > 0:
|
448
500
|
snippet = "..." + snippet
|
449
501
|
if snippet_end < len(str(row_dict[col.name])):
|
450
502
|
snippet = snippet + "..."
|
451
|
-
|
503
|
+
|
452
504
|
matched_content.append(f"{col.name}: {snippet}")
|
453
505
|
|
454
506
|
total_relevance = sum(relevance_scores)
|
@@ -460,8 +512,12 @@ class SQLiteMemoryDatabase:
|
|
460
512
|
"row_data": row_dict,
|
461
513
|
"matched_content": matched_content,
|
462
514
|
"relevance": round(total_relevance, 4),
|
463
|
-
"match_quality":
|
464
|
-
|
515
|
+
"match_quality": (
|
516
|
+
"high"
|
517
|
+
if total_relevance > 0.5
|
518
|
+
else ("medium" if total_relevance > 0.1 else "low")
|
519
|
+
),
|
520
|
+
"match_count": len(relevance_scores),
|
465
521
|
}
|
466
522
|
)
|
467
523
|
|
@@ -471,6 +527,7 @@ class SQLiteMemoryDatabase:
|
|
471
527
|
if isinstance(rel, (int, float)):
|
472
528
|
return float(rel)
|
473
529
|
return 0.0
|
530
|
+
|
474
531
|
results.sort(key=get_relevance, reverse=True)
|
475
532
|
results = results[:limit]
|
476
533
|
|
@@ -486,7 +543,9 @@ class SQLiteMemoryDatabase:
|
|
486
543
|
raise e
|
487
544
|
raise DatabaseError(f"Failed to search content: {str(e)}")
|
488
545
|
|
489
|
-
def explore_tables(
|
546
|
+
def explore_tables(
|
547
|
+
self, pattern: Optional[str] = None, include_row_counts: bool = True
|
548
|
+
) -> ToolResponse:
|
490
549
|
"""Explore table structures and content."""
|
491
550
|
try:
|
492
551
|
self._refresh_metadata()
|
@@ -495,7 +554,11 @@ class SQLiteMemoryDatabase:
|
|
495
554
|
if pattern:
|
496
555
|
table_names = [name for name in table_names if pattern.replace("%", "") in name]
|
497
556
|
|
498
|
-
exploration: Dict[str, Any] = {
|
557
|
+
exploration: Dict[str, Any] = {
|
558
|
+
"tables": [],
|
559
|
+
"total_tables": len(table_names),
|
560
|
+
"total_rows": 0,
|
561
|
+
}
|
499
562
|
|
500
563
|
with self.get_connection() as conn:
|
501
564
|
for table_name in table_names:
|
@@ -518,7 +581,11 @@ class SQLiteMemoryDatabase:
|
|
518
581
|
if "TEXT" in str(col.type).upper() or "VARCHAR" in str(col.type).upper():
|
519
582
|
text_columns.append(col.name)
|
520
583
|
|
521
|
-
table_info: Dict[str, Any] = {
|
584
|
+
table_info: Dict[str, Any] = {
|
585
|
+
"name": table_name,
|
586
|
+
"columns": columns,
|
587
|
+
"text_columns": text_columns,
|
588
|
+
}
|
522
589
|
|
523
590
|
# Add row count if requested
|
524
591
|
if include_row_counts:
|
@@ -538,8 +605,12 @@ class SQLiteMemoryDatabase:
|
|
538
605
|
content_preview: Dict[str, List[Any]] = {}
|
539
606
|
for col_name in text_columns[:3]: # Limit to first 3 text columns
|
540
607
|
col = table.c[col_name]
|
541
|
-
preview_result = conn.execute(
|
542
|
-
|
608
|
+
preview_result = conn.execute(
|
609
|
+
select(col).distinct().where(col.isnot(None)).limit(5)
|
610
|
+
)
|
611
|
+
unique_values: List[Any] = [
|
612
|
+
row[0] for row in preview_result.fetchall() if row[0]
|
613
|
+
]
|
543
614
|
if unique_values:
|
544
615
|
content_preview[col_name] = unique_values
|
545
616
|
|
@@ -554,14 +625,19 @@ class SQLiteMemoryDatabase:
|
|
554
625
|
|
555
626
|
# --- Semantic Search Methods ---
|
556
627
|
|
557
|
-
def add_embedding_column(
|
628
|
+
def add_embedding_column(
|
629
|
+
self, table_name: str, embedding_column: str = "embedding"
|
630
|
+
) -> EmbeddingColumnResponse:
|
558
631
|
"""Add an embedding column to a table for semantic search."""
|
559
632
|
try:
|
560
633
|
table = self._ensure_table_exists(table_name)
|
561
634
|
|
562
635
|
# Check if embedding column already exists
|
563
636
|
if embedding_column in [col.name for col in table.columns]:
|
564
|
-
return {
|
637
|
+
return {
|
638
|
+
"success": True,
|
639
|
+
"message": f"Embedding column '{embedding_column}' already exists",
|
640
|
+
}
|
565
641
|
|
566
642
|
# Add embedding column as TEXT (JSON storage)
|
567
643
|
with self.get_connection() as conn:
|
@@ -569,7 +645,10 @@ class SQLiteMemoryDatabase:
|
|
569
645
|
conn.commit()
|
570
646
|
|
571
647
|
self._refresh_metadata()
|
572
|
-
return {
|
648
|
+
return {
|
649
|
+
"success": True,
|
650
|
+
"message": f"Added embedding column '{embedding_column}' to table '{table_name}'",
|
651
|
+
}
|
573
652
|
|
574
653
|
except (ValidationError, SQLAlchemyError) as e:
|
575
654
|
if isinstance(e, ValidationError):
|
@@ -586,7 +665,9 @@ class SQLiteMemoryDatabase:
|
|
586
665
|
) -> GenerateEmbeddingsResponse:
|
587
666
|
"""Generate embeddings for text content in a table."""
|
588
667
|
if not is_semantic_search_available():
|
589
|
-
raise ValidationError(
|
668
|
+
raise ValidationError(
|
669
|
+
"Semantic search is not available. Please install sentence-transformers."
|
670
|
+
)
|
590
671
|
|
591
672
|
try:
|
592
673
|
table = self._ensure_table_exists(table_name)
|
@@ -656,7 +737,9 @@ class SQLiteMemoryDatabase:
|
|
656
737
|
processed += 1
|
657
738
|
|
658
739
|
conn.commit()
|
659
|
-
logging.info(
|
740
|
+
logging.info(
|
741
|
+
f"Generated embeddings for batch {i//batch_size + 1}, processed {processed} rows"
|
742
|
+
)
|
660
743
|
|
661
744
|
return {
|
662
745
|
"success": True,
|
@@ -683,7 +766,9 @@ class SQLiteMemoryDatabase:
|
|
683
766
|
) -> SemanticSearchResponse:
|
684
767
|
"""Perform semantic search across tables using vector embeddings."""
|
685
768
|
if not is_semantic_search_available():
|
686
|
-
raise ValidationError(
|
769
|
+
raise ValidationError(
|
770
|
+
"Semantic search is not available. Please install sentence-transformers."
|
771
|
+
)
|
687
772
|
|
688
773
|
if not query or not query.strip():
|
689
774
|
raise ValidationError("Search query cannot be empty")
|
@@ -704,7 +789,9 @@ class SQLiteMemoryDatabase:
|
|
704
789
|
|
705
790
|
# Check if table has embedding column
|
706
791
|
if embedding_column not in [col.name for col in table.columns]:
|
707
|
-
logging.warning(
|
792
|
+
logging.warning(
|
793
|
+
f"Table '{table_name}' does not have embedding column '{embedding_column}'"
|
794
|
+
)
|
708
795
|
continue
|
709
796
|
|
710
797
|
# Get all rows with embeddings
|
@@ -753,6 +840,11 @@ class SQLiteMemoryDatabase:
|
|
753
840
|
all_results.sort(key=lambda x: x.get("similarity_score", 0), reverse=True)
|
754
841
|
final_results = all_results[:limit]
|
755
842
|
|
843
|
+
# Remove embedding data from results to keep LLM responses clean
|
844
|
+
for result in final_results:
|
845
|
+
if embedding_column in result:
|
846
|
+
del result[embedding_column]
|
847
|
+
|
756
848
|
return {
|
757
849
|
"success": True,
|
758
850
|
"results": final_results,
|
@@ -779,7 +871,9 @@ class SQLiteMemoryDatabase:
|
|
779
871
|
) -> RelatedContentResponse:
|
780
872
|
"""Find content related to a specific row by semantic similarity."""
|
781
873
|
if not is_semantic_search_available():
|
782
|
-
raise ValidationError(
|
874
|
+
raise ValidationError(
|
875
|
+
"Semantic search is not available. Please install sentence-transformers."
|
876
|
+
)
|
783
877
|
|
784
878
|
try:
|
785
879
|
table = self._ensure_table_exists(table_name)
|
@@ -862,13 +956,23 @@ class SQLiteMemoryDatabase:
|
|
862
956
|
for candidate_idx, similarity_score in similar_indices:
|
863
957
|
original_idx = valid_indices[candidate_idx]
|
864
958
|
row_dict = content_data[original_idx].copy()
|
959
|
+
|
960
|
+
# Remove embedding data to avoid polluting LLM responses
|
961
|
+
if embedding_column in row_dict:
|
962
|
+
del row_dict[embedding_column]
|
963
|
+
|
865
964
|
row_dict["similarity_score"] = round(similarity_score, 3)
|
866
965
|
results.append(row_dict)
|
867
966
|
|
967
|
+
# Remove embedding from target_row as well
|
968
|
+
target_dict_clean = target_dict.copy()
|
969
|
+
if embedding_column in target_dict_clean:
|
970
|
+
del target_dict_clean[embedding_column]
|
971
|
+
|
868
972
|
return {
|
869
973
|
"success": True,
|
870
974
|
"results": results,
|
871
|
-
"target_row":
|
975
|
+
"target_row": target_dict_clean,
|
872
976
|
"total_results": len(results),
|
873
977
|
"similarity_threshold": similarity_threshold,
|
874
978
|
"model": model_name,
|
@@ -950,13 +1054,21 @@ class SQLiteMemoryDatabase:
|
|
950
1054
|
# Enhance with text matching scores
|
951
1055
|
try:
|
952
1056
|
semantic_engine = get_semantic_engine(model_name)
|
953
|
-
|
1057
|
+
|
954
1058
|
# Verify the engine has the required method
|
955
|
-
if not hasattr(semantic_engine,
|
1059
|
+
if not hasattr(semantic_engine, "hybrid_search") or not callable(
|
1060
|
+
getattr(semantic_engine, "hybrid_search")
|
1061
|
+
):
|
956
1062
|
raise DatabaseError("Semantic engine hybrid_search method is not callable")
|
957
|
-
|
1063
|
+
|
958
1064
|
enhanced_results = semantic_engine.hybrid_search(
|
959
|
-
query,
|
1065
|
+
query,
|
1066
|
+
semantic_results,
|
1067
|
+
text_columns or [],
|
1068
|
+
embedding_column,
|
1069
|
+
semantic_weight,
|
1070
|
+
text_weight,
|
1071
|
+
limit,
|
960
1072
|
)
|
961
1073
|
except Exception as e:
|
962
1074
|
# If semantic enhancement fails, return semantic results without text enhancement
|
@@ -979,7 +1091,9 @@ class SQLiteMemoryDatabase:
|
|
979
1091
|
raise e
|
980
1092
|
raise DatabaseError(f"Hybrid search failed: {str(e)}")
|
981
1093
|
|
982
|
-
def get_embedding_stats(
|
1094
|
+
def get_embedding_stats(
|
1095
|
+
self, table_name: str, embedding_column: str = "embedding"
|
1096
|
+
) -> ToolResponse:
|
983
1097
|
"""Get statistics about embeddings in a table."""
|
984
1098
|
try:
|
985
1099
|
table = self._ensure_table_exists(table_name)
|
@@ -989,8 +1103,10 @@ class SQLiteMemoryDatabase:
|
|
989
1103
|
# Return 0% coverage when column doesn't exist (for compatibility with tests)
|
990
1104
|
total_count = 0
|
991
1105
|
with self.get_connection() as conn:
|
992
|
-
total_count =
|
993
|
-
|
1106
|
+
total_count = (
|
1107
|
+
conn.execute(select(text("COUNT(*)")).select_from(table)).scalar() or 0
|
1108
|
+
)
|
1109
|
+
|
994
1110
|
return {
|
995
1111
|
"success": True,
|
996
1112
|
"table_name": table_name,
|
@@ -1003,7 +1119,9 @@ class SQLiteMemoryDatabase:
|
|
1003
1119
|
|
1004
1120
|
with self.get_connection() as conn:
|
1005
1121
|
# Count total rows
|
1006
|
-
total_count =
|
1122
|
+
total_count = (
|
1123
|
+
conn.execute(select(text("COUNT(*)")).select_from(table)).scalar() or 0
|
1124
|
+
)
|
1007
1125
|
|
1008
1126
|
# Count rows with embeddings
|
1009
1127
|
embedded_count = (
|
@@ -1072,7 +1190,7 @@ def get_database(db_path: Optional[str] = None) -> SQLiteMemoryDatabase:
|
|
1072
1190
|
actual_path = db_path or os.environ.get("DB_PATH", "./test.db")
|
1073
1191
|
if actual_path is None:
|
1074
1192
|
actual_path = "./test.db"
|
1075
|
-
|
1193
|
+
|
1076
1194
|
if _db_instance is None or (db_path and db_path != _db_instance.db_path):
|
1077
1195
|
# Close previous instance if it exists
|
1078
1196
|
if _db_instance is not None:
|