docsif 0.1.0__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.
- docsif-0.1.0.dist-info/METADATA +393 -0
- docsif-0.1.0.dist-info/RECORD +86 -0
- docsif-0.1.0.dist-info/WHEEL +4 -0
- docsif-0.1.0.dist-info/entry_points.txt +3 -0
- docsif-0.1.0.dist-info/licenses/LICENSE +21 -0
- sif/__init__.py +24 -0
- sif/_version.py +3 -0
- sif/cli/__init__.py +11 -0
- sif/cli/commands/__init__.py +18 -0
- sif/cli/commands/bench.py +148 -0
- sif/cli/commands/collection.py +360 -0
- sif/cli/commands/context.py +154 -0
- sif/cli/commands/get.py +200 -0
- sif/cli/commands/index.py +294 -0
- sif/cli/commands/ls.py +67 -0
- sif/cli/commands/mcp.py +93 -0
- sif/cli/commands/pull.py +111 -0
- sif/cli/commands/search.py +574 -0
- sif/cli/config.py +206 -0
- sif/cli/formatters.py +269 -0
- sif/cli/main.py +205 -0
- sif/config/__init__.py +30 -0
- sif/config/constants.py +52 -0
- sif/config/settings.py +217 -0
- sif/core/__init__.py +21 -0
- sif/core/collection.py +161 -0
- sif/core/context.py +167 -0
- sif/core/document.py +125 -0
- sif/core/models.py +298 -0
- sif/database/__init__.py +23 -0
- sif/database/connection.py +139 -0
- sif/database/database.py +136 -0
- sif/database/migrations.py +187 -0
- sif/database/repositories.py +500 -0
- sif/database/repository.py +171 -0
- sif/database/schema.py +362 -0
- sif/embedding/__init__.py +22 -0
- sif/embedding/cache.py +196 -0
- sif/embedding/embedder.py +328 -0
- sif/embedding/factory.py +92 -0
- sif/embedding/manager.py +200 -0
- sif/embedding/model.py +126 -0
- sif/indexing/__init__.py +21 -0
- sif/indexing/chunker.py +226 -0
- sif/indexing/indexer.py +226 -0
- sif/indexing/parser.py +192 -0
- sif/indexing/scanner.py +148 -0
- sif/indexing/watcher.py +156 -0
- sif/mcp/README.md +258 -0
- sif/mcp/__init__.py +9 -0
- sif/mcp/claude_desktop_config.json +16 -0
- sif/mcp/cli.py +92 -0
- sif/mcp/protocol.py +336 -0
- sif/mcp/server.py +433 -0
- sif/mcp/server_http.py +54 -0
- sif/mcp/test_mcp.py +108 -0
- sif/mcp/tools.py +497 -0
- sif/mcp/transport_http.py +424 -0
- sif/mcp/transport_stdio.py +292 -0
- sif/mcp_server/__init__.py +26 -0
- sif/mcp_server/handlers.py +68 -0
- sif/mcp_server/server.py +58 -0
- sif/mcp_server/tools.py +216 -0
- sif/mcp_server/transport.py +221 -0
- sif/models/__init__.py +70 -0
- sif/models/collection.py +51 -0
- sif/models/context.py +45 -0
- sif/models/document.py +76 -0
- sif/models/download.py +208 -0
- sif/models/embedding.py +71 -0
- sif/models/search.py +92 -0
- sif/search/__init__.py +22 -0
- sif/search/benchmark.py +110 -0
- sif/search/bm25.py +216 -0
- sif/search/expansion.py +157 -0
- sif/search/hybrid.py +323 -0
- sif/search/rerank.py +390 -0
- sif/search/rrf.py +186 -0
- sif/search/snippets.py +130 -0
- sif/search/strategy.py +97 -0
- sif/search/vector.py +177 -0
- sif/utils/__init__.py +26 -0
- sif/utils/logging.py +143 -0
- sif/utils/paths.py +82 -0
- sif/utils/progress.py +83 -0
- sif/utils/text.py +128 -0
sif/database/database.py
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
"""Database connection and management for SIF."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import sqlite3
|
|
6
|
+
from collections.abc import Generator
|
|
7
|
+
from contextlib import contextmanager
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
import sqlite_vec
|
|
11
|
+
|
|
12
|
+
from sif.database.schema import SchemaManager
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class Database:
|
|
16
|
+
"""Main database class for SIF."""
|
|
17
|
+
|
|
18
|
+
def __init__(self, db_path: str | Path) -> None:
|
|
19
|
+
self.db_path = Path(db_path).expanduser().resolve()
|
|
20
|
+
self.db_path.parent.mkdir(parents=True, exist_ok=True)
|
|
21
|
+
self._connection: sqlite3.Connection | None = None
|
|
22
|
+
self._schema: SchemaManager | None = None
|
|
23
|
+
|
|
24
|
+
@property
|
|
25
|
+
def connection(self) -> sqlite3.Connection:
|
|
26
|
+
"""Get or create database connection."""
|
|
27
|
+
if self._connection is None:
|
|
28
|
+
self._connection = sqlite3.connect(
|
|
29
|
+
str(self.db_path),
|
|
30
|
+
detect_types=sqlite3.PARSE_DECLTYPES,
|
|
31
|
+
check_same_thread=False,
|
|
32
|
+
)
|
|
33
|
+
self._connection.row_factory = sqlite3.Row
|
|
34
|
+
self._enable_extensions()
|
|
35
|
+
return self._connection
|
|
36
|
+
|
|
37
|
+
def _enable_extensions(self) -> None:
|
|
38
|
+
"""Enable SQLite extensions."""
|
|
39
|
+
# Enable FTS5
|
|
40
|
+
self._connection.execute("PRAGMA foreign_keys = ON")
|
|
41
|
+
|
|
42
|
+
# Load sqlite-vec extension
|
|
43
|
+
try:
|
|
44
|
+
self._connection.enable_load_extension(True)
|
|
45
|
+
sqlite_vec.load(self._connection)
|
|
46
|
+
self._connection.enable_load_extension(False)
|
|
47
|
+
except Exception:
|
|
48
|
+
pass # sqlite-vec is optional
|
|
49
|
+
|
|
50
|
+
def init_schema(self) -> None:
|
|
51
|
+
"""Initialize database schema."""
|
|
52
|
+
from sif.config.settings import get_settings # noqa: PLC0415
|
|
53
|
+
|
|
54
|
+
settings = get_settings()
|
|
55
|
+
self._schema = SchemaManager(self.connection, embedding_dim=settings.embedding_dim)
|
|
56
|
+
self._schema.create_all()
|
|
57
|
+
|
|
58
|
+
@contextmanager
|
|
59
|
+
def transaction(self) -> Generator[sqlite3.Connection, None, None]:
|
|
60
|
+
"""Execute operations in a transaction."""
|
|
61
|
+
conn = self.connection
|
|
62
|
+
try:
|
|
63
|
+
yield conn
|
|
64
|
+
conn.commit()
|
|
65
|
+
except Exception:
|
|
66
|
+
conn.rollback()
|
|
67
|
+
raise
|
|
68
|
+
|
|
69
|
+
def close(self) -> None:
|
|
70
|
+
"""Close database connection."""
|
|
71
|
+
if self._connection:
|
|
72
|
+
self._connection.close()
|
|
73
|
+
self._connection = None
|
|
74
|
+
|
|
75
|
+
def __enter__(self) -> Database:
|
|
76
|
+
"""Context manager entry."""
|
|
77
|
+
return self
|
|
78
|
+
|
|
79
|
+
def __exit__(self, exc_type, exc_val, exc_tb) -> None:
|
|
80
|
+
"""Context manager exit."""
|
|
81
|
+
self.close()
|
|
82
|
+
|
|
83
|
+
def get_stats(self) -> dict:
|
|
84
|
+
"""Get database statistics."""
|
|
85
|
+
if self._schema is None:
|
|
86
|
+
from sif.config.settings import get_settings # noqa: PLC0415
|
|
87
|
+
|
|
88
|
+
settings = get_settings()
|
|
89
|
+
self._schema = SchemaManager(self.connection, embedding_dim=settings.embedding_dim)
|
|
90
|
+
return self._schema.get_stats()
|
|
91
|
+
|
|
92
|
+
def reset(self) -> None:
|
|
93
|
+
"""Reset database (drop all tables)."""
|
|
94
|
+
if self._schema is None:
|
|
95
|
+
from sif.config.settings import get_settings # noqa: PLC0415
|
|
96
|
+
|
|
97
|
+
settings = get_settings()
|
|
98
|
+
self._schema = SchemaManager(self.connection, embedding_dim=settings.embedding_dim)
|
|
99
|
+
self._schema.drop_all()
|
|
100
|
+
self._schema.create_all()
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
class DatabaseConnection:
|
|
104
|
+
"""Lightweight database connection wrapper."""
|
|
105
|
+
|
|
106
|
+
def __init__(self, db_path: str | Path) -> None:
|
|
107
|
+
self.db_path = Path(db_path).expanduser().resolve()
|
|
108
|
+
self.db_path.parent.mkdir(parents=True, exist_ok=True)
|
|
109
|
+
|
|
110
|
+
@contextmanager
|
|
111
|
+
def connect(self) -> Generator[sqlite3.Connection, None, None]:
|
|
112
|
+
"""Create a connection context."""
|
|
113
|
+
conn = sqlite3.connect(
|
|
114
|
+
str(self.db_path),
|
|
115
|
+
detect_types=sqlite3.PARSE_DECLTYPES,
|
|
116
|
+
check_same_thread=False,
|
|
117
|
+
)
|
|
118
|
+
conn.row_factory = sqlite3.Row
|
|
119
|
+
conn.execute("PRAGMA foreign_keys = ON")
|
|
120
|
+
|
|
121
|
+
# Load sqlite-vec extension
|
|
122
|
+
try:
|
|
123
|
+
conn.enable_load_extension(True)
|
|
124
|
+
sqlite_vec.load(conn)
|
|
125
|
+
conn.enable_load_extension(False)
|
|
126
|
+
except Exception:
|
|
127
|
+
pass # sqlite-vec is optional
|
|
128
|
+
|
|
129
|
+
try:
|
|
130
|
+
yield conn
|
|
131
|
+
conn.commit()
|
|
132
|
+
except Exception:
|
|
133
|
+
conn.rollback()
|
|
134
|
+
raise
|
|
135
|
+
finally:
|
|
136
|
+
conn.close()
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
"""Database migration management."""
|
|
2
|
+
|
|
3
|
+
from sif.config.constants import CURRENT_SCHEMA_VERSION
|
|
4
|
+
from sif.utils.logging import get_logger
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
logger = get_logger(__name__)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
# Schema definition
|
|
11
|
+
SCHEMA_SQL = """
|
|
12
|
+
-- Collections table
|
|
13
|
+
CREATE TABLE IF NOT EXISTS collections (
|
|
14
|
+
id TEXT PRIMARY KEY,
|
|
15
|
+
name TEXT UNIQUE NOT NULL,
|
|
16
|
+
description TEXT,
|
|
17
|
+
paths TEXT NOT NULL DEFAULT '[]',
|
|
18
|
+
document_count INTEGER NOT NULL DEFAULT 0,
|
|
19
|
+
chunk_count INTEGER NOT NULL DEFAULT 0,
|
|
20
|
+
metadata TEXT NOT NULL DEFAULT '{}',
|
|
21
|
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
22
|
+
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
23
|
+
last_indexed_at DATETIME
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
-- Documents table
|
|
27
|
+
CREATE TABLE IF NOT EXISTS documents (
|
|
28
|
+
id TEXT PRIMARY KEY,
|
|
29
|
+
collection_id TEXT NOT NULL,
|
|
30
|
+
path TEXT NOT NULL,
|
|
31
|
+
content TEXT NOT NULL,
|
|
32
|
+
checksum TEXT NOT NULL,
|
|
33
|
+
file_size INTEGER NOT NULL,
|
|
34
|
+
metadata TEXT NOT NULL DEFAULT '{}',
|
|
35
|
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
36
|
+
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
37
|
+
indexed_at DATETIME,
|
|
38
|
+
FOREIGN KEY (collection_id) REFERENCES collections(id) ON DELETE CASCADE,
|
|
39
|
+
UNIQUE(collection_id, path)
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
-- Chunks table
|
|
43
|
+
CREATE TABLE IF NOT EXISTS chunks (
|
|
44
|
+
id TEXT PRIMARY KEY,
|
|
45
|
+
document_id TEXT NOT NULL,
|
|
46
|
+
content TEXT NOT NULL,
|
|
47
|
+
start_line INTEGER NOT NULL,
|
|
48
|
+
end_line INTEGER NOT NULL,
|
|
49
|
+
token_count INTEGER NOT NULL,
|
|
50
|
+
embedding_id TEXT,
|
|
51
|
+
FOREIGN KEY (document_id) REFERENCES documents(id) ON DELETE CASCADE
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
-- Contexts table
|
|
55
|
+
CREATE TABLE IF NOT EXISTS contexts (
|
|
56
|
+
id TEXT PRIMARY KEY,
|
|
57
|
+
content TEXT NOT NULL,
|
|
58
|
+
context_type TEXT NOT NULL,
|
|
59
|
+
target_id TEXT NOT NULL,
|
|
60
|
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
61
|
+
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
62
|
+
UNIQUE(context_type, target_id)
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
-- Schema version tracking
|
|
66
|
+
CREATE TABLE IF NOT EXISTS schema_version (
|
|
67
|
+
version INTEGER PRIMARY KEY,
|
|
68
|
+
applied_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
-- Indexes
|
|
72
|
+
CREATE INDEX IF NOT EXISTS idx_documents_collection ON documents(collection_id);
|
|
73
|
+
CREATE INDEX IF NOT EXISTS idx_documents_path ON documents(path);
|
|
74
|
+
CREATE INDEX IF NOT EXISTS idx_chunks_document ON chunks(document_id);
|
|
75
|
+
CREATE INDEX IF NOT EXISTS idx_contexts_type ON contexts(context_type);
|
|
76
|
+
CREATE INDEX IF NOT EXISTS idx_contexts_target ON contexts(target_id);
|
|
77
|
+
"""
|
|
78
|
+
|
|
79
|
+
# FTS5 virtual table
|
|
80
|
+
FTS_SQL = """
|
|
81
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS documents_fts USING fts5(
|
|
82
|
+
content,
|
|
83
|
+
content_rowid=rowid,
|
|
84
|
+
tokenize='porter'
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
-- Trigger to keep FTS index in sync
|
|
88
|
+
CREATE TRIGGER IF NOT EXISTS documents_fts_insert
|
|
89
|
+
AFTER INSERT ON documents
|
|
90
|
+
BEGIN
|
|
91
|
+
INSERT INTO documents_fts(rowid, content) VALUES (new.rowid, new.content);
|
|
92
|
+
END;
|
|
93
|
+
|
|
94
|
+
CREATE TRIGGER IF NOT EXISTS documents_fts_update
|
|
95
|
+
AFTER UPDATE ON documents
|
|
96
|
+
BEGIN
|
|
97
|
+
UPDATE documents_fts SET content = new.content WHERE rowid = old.rowid;
|
|
98
|
+
END;
|
|
99
|
+
|
|
100
|
+
CREATE TRIGGER IF NOT EXISTS documents_fts_delete
|
|
101
|
+
AFTER DELETE ON documents
|
|
102
|
+
BEGIN
|
|
103
|
+
DELETE FROM documents_fts WHERE rowid = old.rowid;
|
|
104
|
+
END;
|
|
105
|
+
"""
|
|
106
|
+
|
|
107
|
+
# Vector table (sqlite-vec)
|
|
108
|
+
VECTOR_SQL_TEMPLATE = """
|
|
109
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS chunk_embeddings USING vec0(
|
|
110
|
+
embedding_id TEXT PRIMARY KEY,
|
|
111
|
+
embedding FLOAT[{dim}]
|
|
112
|
+
);
|
|
113
|
+
"""
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
class MigrationManager:
|
|
117
|
+
"""Manages database schema migrations."""
|
|
118
|
+
|
|
119
|
+
def __init__(self, connection: "DatabaseConnection") -> None: # noqa: F821
|
|
120
|
+
self._connection = connection
|
|
121
|
+
|
|
122
|
+
def get_current_version(self) -> int:
|
|
123
|
+
"""Get the current schema version."""
|
|
124
|
+
try:
|
|
125
|
+
row = self._connection.fetchone(
|
|
126
|
+
"SELECT version FROM schema_version ORDER BY version DESC LIMIT 1"
|
|
127
|
+
)
|
|
128
|
+
return row["version"] if row else 0
|
|
129
|
+
except Exception:
|
|
130
|
+
return 0
|
|
131
|
+
|
|
132
|
+
def migrate(self, embedding_dim: int = 384) -> None:
|
|
133
|
+
"""Run all pending migrations."""
|
|
134
|
+
current_version = self.get_current_version()
|
|
135
|
+
|
|
136
|
+
if current_version >= CURRENT_SCHEMA_VERSION:
|
|
137
|
+
logger.info("Database schema is up to date")
|
|
138
|
+
return
|
|
139
|
+
|
|
140
|
+
logger.info(
|
|
141
|
+
f"Migrating database from version {current_version} to {CURRENT_SCHEMA_VERSION}"
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
# Apply schema
|
|
145
|
+
self._apply_schema(embedding_dim)
|
|
146
|
+
|
|
147
|
+
# Update version
|
|
148
|
+
self._connection.execute(
|
|
149
|
+
"INSERT INTO schema_version (version) VALUES (?)",
|
|
150
|
+
(CURRENT_SCHEMA_VERSION,),
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
logger.info("Database migration completed successfully")
|
|
154
|
+
|
|
155
|
+
def _apply_schema(self, embedding_dim: int) -> None:
|
|
156
|
+
"""Apply the database schema."""
|
|
157
|
+
# Apply main schema
|
|
158
|
+
self._connection.execute(SCHEMA_SQL)
|
|
159
|
+
|
|
160
|
+
# Apply FTS schema
|
|
161
|
+
self._connection.execute(FTS_SQL)
|
|
162
|
+
|
|
163
|
+
# Apply vector schema
|
|
164
|
+
vector_sql = VECTOR_SQL_TEMPLATE.format(dim=embedding_dim)
|
|
165
|
+
self._connection.execute(vector_sql)
|
|
166
|
+
|
|
167
|
+
def reset(self) -> None:
|
|
168
|
+
"""Reset the database (drop all tables)."""
|
|
169
|
+
logger.warning("Resetting database - all data will be lost")
|
|
170
|
+
|
|
171
|
+
tables = [
|
|
172
|
+
"documents_fts",
|
|
173
|
+
"chunk_embeddings",
|
|
174
|
+
"chunks",
|
|
175
|
+
"documents",
|
|
176
|
+
"contexts",
|
|
177
|
+
"collections",
|
|
178
|
+
"schema_version",
|
|
179
|
+
]
|
|
180
|
+
|
|
181
|
+
for table in tables:
|
|
182
|
+
try:
|
|
183
|
+
self._connection.execute(f"DROP TABLE IF EXISTS {table}")
|
|
184
|
+
except Exception as e:
|
|
185
|
+
logger.warning(f"Error dropping table {table}: {e}")
|
|
186
|
+
|
|
187
|
+
logger.info("Database reset completed")
|