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
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
"""MCP transport implementations."""
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
from abc import ABC, abstractmethod
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from sif.utils.logging import get_logger
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
logger = get_logger(__name__)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class Transport(ABC):
|
|
14
|
+
"""Abstract base class for MCP transports."""
|
|
15
|
+
|
|
16
|
+
@abstractmethod
|
|
17
|
+
def start(self) -> None:
|
|
18
|
+
"""Start the transport."""
|
|
19
|
+
...
|
|
20
|
+
|
|
21
|
+
@abstractmethod
|
|
22
|
+
def stop(self) -> None:
|
|
23
|
+
"""Stop the transport."""
|
|
24
|
+
...
|
|
25
|
+
|
|
26
|
+
@abstractmethod
|
|
27
|
+
def send(self, message: dict[str, Any]) -> None:
|
|
28
|
+
"""Send a message."""
|
|
29
|
+
...
|
|
30
|
+
|
|
31
|
+
@abstractmethod
|
|
32
|
+
def receive(self) -> dict[str, Any] | None:
|
|
33
|
+
"""Receive a message."""
|
|
34
|
+
...
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class StdioTransport(Transport):
|
|
38
|
+
"""stdio transport for MCP.
|
|
39
|
+
|
|
40
|
+
Uses standard input/output for communication.
|
|
41
|
+
Suitable for CLI integration and local tools.
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
def __init__(self) -> None:
|
|
45
|
+
"""Initialize stdio transport."""
|
|
46
|
+
self._running = False
|
|
47
|
+
|
|
48
|
+
def start(self) -> None:
|
|
49
|
+
"""Start stdio transport."""
|
|
50
|
+
logger.info("Starting stdio transport")
|
|
51
|
+
self._running = True
|
|
52
|
+
|
|
53
|
+
# Send initialization message
|
|
54
|
+
self.send(
|
|
55
|
+
{
|
|
56
|
+
"jsonrpc": "2.0",
|
|
57
|
+
"method": "initialize",
|
|
58
|
+
"params": {
|
|
59
|
+
"protocolVersion": "1.0",
|
|
60
|
+
"capabilities": {},
|
|
61
|
+
},
|
|
62
|
+
}
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
# Main loop
|
|
66
|
+
while self._running:
|
|
67
|
+
try:
|
|
68
|
+
message = self.receive()
|
|
69
|
+
if message:
|
|
70
|
+
self._handle_message(message)
|
|
71
|
+
except EOFError:
|
|
72
|
+
break
|
|
73
|
+
except Exception as e:
|
|
74
|
+
logger.error(f"Error handling message: {e}")
|
|
75
|
+
|
|
76
|
+
def stop(self) -> None:
|
|
77
|
+
"""Stop stdio transport."""
|
|
78
|
+
logger.info("Stopping stdio transport")
|
|
79
|
+
self._running = False
|
|
80
|
+
|
|
81
|
+
def send(self, message: dict[str, Any]) -> None:
|
|
82
|
+
"""Send a message via stdout."""
|
|
83
|
+
import json # noqa: PLC0415
|
|
84
|
+
|
|
85
|
+
json_str = json.dumps(message)
|
|
86
|
+
print(json_str, flush=True)
|
|
87
|
+
|
|
88
|
+
def receive(self) -> dict[str, Any] | None:
|
|
89
|
+
"""Receive a message from stdin."""
|
|
90
|
+
import json # noqa: PLC0415
|
|
91
|
+
|
|
92
|
+
try:
|
|
93
|
+
line = sys.stdin.readline()
|
|
94
|
+
if not line:
|
|
95
|
+
raise EOFError()
|
|
96
|
+
return json.loads(line)
|
|
97
|
+
except json.JSONDecodeError:
|
|
98
|
+
logger.error("Invalid JSON received")
|
|
99
|
+
return None
|
|
100
|
+
|
|
101
|
+
def _handle_message(self, message: dict[str, Any]) -> None:
|
|
102
|
+
"""Handle an incoming message."""
|
|
103
|
+
method = message.get("method")
|
|
104
|
+
|
|
105
|
+
if method == "tools/list":
|
|
106
|
+
self._handle_tools_list(message)
|
|
107
|
+
elif method == "tools/call":
|
|
108
|
+
self._handle_tool_call(message)
|
|
109
|
+
elif method == "resources/list":
|
|
110
|
+
self._handle_resources_list(message)
|
|
111
|
+
else:
|
|
112
|
+
logger.warning(f"Unknown method: {method}")
|
|
113
|
+
|
|
114
|
+
def _handle_tools_list(self, message: dict[str, Any]) -> None:
|
|
115
|
+
"""Handle tools/list request."""
|
|
116
|
+
self.send(
|
|
117
|
+
{
|
|
118
|
+
"jsonrpc": "2.0",
|
|
119
|
+
"id": message.get("id"),
|
|
120
|
+
"result": {
|
|
121
|
+
"tools": [
|
|
122
|
+
{
|
|
123
|
+
"name": "search",
|
|
124
|
+
"description": "Search indexed documents",
|
|
125
|
+
"inputSchema": {
|
|
126
|
+
"type": "object",
|
|
127
|
+
"properties": {
|
|
128
|
+
"query": {"type": "string"},
|
|
129
|
+
"collection": {"type": "string"},
|
|
130
|
+
},
|
|
131
|
+
"required": ["query"],
|
|
132
|
+
},
|
|
133
|
+
},
|
|
134
|
+
],
|
|
135
|
+
},
|
|
136
|
+
}
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
def _handle_tool_call(self, message: dict[str, Any]) -> None:
|
|
140
|
+
"""Handle tools/call request."""
|
|
141
|
+
params = message.get("params", {})
|
|
142
|
+
tool_name = params.get("name")
|
|
143
|
+
|
|
144
|
+
# Placeholder: actual implementation would call the tool
|
|
145
|
+
self.send(
|
|
146
|
+
{
|
|
147
|
+
"jsonrpc": "2.0",
|
|
148
|
+
"id": message.get("id"),
|
|
149
|
+
"result": {
|
|
150
|
+
"content": [
|
|
151
|
+
{"type": "text", "text": f"Tool '{tool_name}' called"},
|
|
152
|
+
],
|
|
153
|
+
},
|
|
154
|
+
}
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
def _handle_resources_list(self, message: dict[str, Any]) -> None:
|
|
158
|
+
"""Handle resources/list request."""
|
|
159
|
+
self.send(
|
|
160
|
+
{
|
|
161
|
+
"jsonrpc": "2.0",
|
|
162
|
+
"id": message.get("id"),
|
|
163
|
+
"result": {
|
|
164
|
+
"resources": [],
|
|
165
|
+
},
|
|
166
|
+
}
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
class HTTPTransport(Transport):
|
|
171
|
+
"""HTTP transport for MCP.
|
|
172
|
+
|
|
173
|
+
Uses HTTP/SSE for communication.
|
|
174
|
+
Suitable for remote access and web integration.
|
|
175
|
+
"""
|
|
176
|
+
|
|
177
|
+
def __init__(
|
|
178
|
+
self,
|
|
179
|
+
host: str = "127.0.0.1",
|
|
180
|
+
port: int = 8080,
|
|
181
|
+
) -> None:
|
|
182
|
+
"""Initialize HTTP transport.
|
|
183
|
+
|
|
184
|
+
Args:
|
|
185
|
+
host: Server host
|
|
186
|
+
port: Server port
|
|
187
|
+
"""
|
|
188
|
+
self._host = host
|
|
189
|
+
self._port = port
|
|
190
|
+
self._server: Any | None = None
|
|
191
|
+
|
|
192
|
+
def start(self) -> None:
|
|
193
|
+
"""Start HTTP transport."""
|
|
194
|
+
import uvicorn # noqa: PLC0415
|
|
195
|
+
from fastapi import FastAPI # noqa: PLC0415
|
|
196
|
+
|
|
197
|
+
logger.info(f"Starting HTTP transport on {self._host}:{self._port}")
|
|
198
|
+
|
|
199
|
+
app = FastAPI(title="SIF MCP Server")
|
|
200
|
+
|
|
201
|
+
@app.get("/mcp/v1/tools")
|
|
202
|
+
async def list_tools() -> dict:
|
|
203
|
+
return {"tools": []}
|
|
204
|
+
|
|
205
|
+
@app.post("/mcp/v1/tools/{tool_name}")
|
|
206
|
+
async def call_tool(tool_name: str, params: dict) -> dict:
|
|
207
|
+
return {"result": f"Tool '{tool_name}' called with {params}"}
|
|
208
|
+
|
|
209
|
+
uvicorn.run(app, host=self._host, port=self._port)
|
|
210
|
+
|
|
211
|
+
def stop(self) -> None:
|
|
212
|
+
"""Stop HTTP transport."""
|
|
213
|
+
logger.info("Stopping HTTP transport")
|
|
214
|
+
|
|
215
|
+
def send(self, message: dict[str, Any]) -> None:
|
|
216
|
+
"""Send a message (not applicable for HTTP)."""
|
|
217
|
+
pass
|
|
218
|
+
|
|
219
|
+
def receive(self) -> dict[str, Any] | None:
|
|
220
|
+
"""Receive a message (not applicable for HTTP)."""
|
|
221
|
+
return None
|
sif/models/__init__.py
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"""Pydantic models for data validation and serialization.
|
|
2
|
+
|
|
3
|
+
This package contains all Pydantic models used for:
|
|
4
|
+
- Request/response validation
|
|
5
|
+
- Configuration management
|
|
6
|
+
- Data serialization/deserialization
|
|
7
|
+
- API schema definitions
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from sif.models.collection import (
|
|
11
|
+
CollectionCreate,
|
|
12
|
+
CollectionListResponse,
|
|
13
|
+
CollectionResponse,
|
|
14
|
+
CollectionUpdate,
|
|
15
|
+
)
|
|
16
|
+
from sif.models.context import (
|
|
17
|
+
ContextCreate,
|
|
18
|
+
ContextResponse,
|
|
19
|
+
ContextUpdate,
|
|
20
|
+
)
|
|
21
|
+
from sif.models.document import (
|
|
22
|
+
ChunkResponse,
|
|
23
|
+
DocumentCreate,
|
|
24
|
+
DocumentResponse,
|
|
25
|
+
DocumentSearchResult,
|
|
26
|
+
DocumentUpdate,
|
|
27
|
+
)
|
|
28
|
+
from sif.models.embedding import (
|
|
29
|
+
EmbeddingConfig,
|
|
30
|
+
EmbeddingModelInfo,
|
|
31
|
+
EmbeddingRequest,
|
|
32
|
+
EmbeddingResponse,
|
|
33
|
+
)
|
|
34
|
+
from sif.models.search import (
|
|
35
|
+
SearchOptions,
|
|
36
|
+
SearchQuery,
|
|
37
|
+
SearchResponse,
|
|
38
|
+
SearchResult,
|
|
39
|
+
SearchType,
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
__all__ = [
|
|
44
|
+
# Collection models
|
|
45
|
+
"CollectionCreate",
|
|
46
|
+
"CollectionUpdate",
|
|
47
|
+
"CollectionResponse",
|
|
48
|
+
"CollectionListResponse",
|
|
49
|
+
# Document models
|
|
50
|
+
"DocumentCreate",
|
|
51
|
+
"DocumentUpdate",
|
|
52
|
+
"DocumentResponse",
|
|
53
|
+
"DocumentSearchResult",
|
|
54
|
+
"ChunkResponse",
|
|
55
|
+
# Context models
|
|
56
|
+
"ContextCreate",
|
|
57
|
+
"ContextUpdate",
|
|
58
|
+
"ContextResponse",
|
|
59
|
+
# Search models
|
|
60
|
+
"SearchQuery",
|
|
61
|
+
"SearchResult",
|
|
62
|
+
"SearchResponse",
|
|
63
|
+
"SearchOptions",
|
|
64
|
+
"SearchType",
|
|
65
|
+
# Embedding models
|
|
66
|
+
"EmbeddingConfig",
|
|
67
|
+
"EmbeddingModelInfo",
|
|
68
|
+
"EmbeddingRequest",
|
|
69
|
+
"EmbeddingResponse",
|
|
70
|
+
]
|
sif/models/collection.py
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""Pydantic models for Collection operations."""
|
|
2
|
+
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class CollectionBase(BaseModel):
|
|
10
|
+
"""Base model for Collection data."""
|
|
11
|
+
|
|
12
|
+
name: str = Field(..., min_length=1, max_length=255, description="Collection name")
|
|
13
|
+
description: str | None = Field(None, description="Collection description")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class CollectionCreate(CollectionBase):
|
|
17
|
+
"""Model for creating a new collection."""
|
|
18
|
+
|
|
19
|
+
paths: list[str] = Field(default_factory=list, description="Initial paths to include")
|
|
20
|
+
metadata: dict[str, Any] = Field(default_factory=dict, description="Additional metadata")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class CollectionUpdate(BaseModel):
|
|
24
|
+
"""Model for updating a collection."""
|
|
25
|
+
|
|
26
|
+
name: str | None = Field(None, min_length=1, max_length=255)
|
|
27
|
+
description: str | None = None
|
|
28
|
+
paths: list[str] | None = None
|
|
29
|
+
metadata: dict[str, Any] | None = None
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class CollectionResponse(CollectionBase):
|
|
33
|
+
"""Model for collection response data."""
|
|
34
|
+
|
|
35
|
+
model_config = ConfigDict(from_attributes=True)
|
|
36
|
+
|
|
37
|
+
id: str = Field(..., description="Unique collection ID")
|
|
38
|
+
paths: list[str] = Field(default_factory=list, description="Indexed paths")
|
|
39
|
+
document_count: int = Field(0, ge=0, description="Number of documents")
|
|
40
|
+
chunk_count: int = Field(0, ge=0, description="Number of chunks")
|
|
41
|
+
metadata: dict[str, Any] = Field(default_factory=dict)
|
|
42
|
+
created_at: datetime = Field(..., description="Creation timestamp")
|
|
43
|
+
updated_at: datetime = Field(..., description="Last update timestamp")
|
|
44
|
+
last_indexed_at: datetime | None = Field(None, description="Last indexing timestamp")
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class CollectionListResponse(BaseModel):
|
|
48
|
+
"""Model for listing collections."""
|
|
49
|
+
|
|
50
|
+
collections: list[CollectionResponse]
|
|
51
|
+
total: int = Field(..., ge=0)
|
sif/models/context.py
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""Pydantic models for Context operations."""
|
|
2
|
+
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
from enum import Enum
|
|
5
|
+
|
|
6
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class ContextType(str, Enum):
|
|
10
|
+
"""Type of context."""
|
|
11
|
+
|
|
12
|
+
COLLECTION = "collection"
|
|
13
|
+
PATH = "path"
|
|
14
|
+
DOCUMENT = "document"
|
|
15
|
+
GLOBAL = "global"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class ContextBase(BaseModel):
|
|
19
|
+
"""Base model for Context data."""
|
|
20
|
+
|
|
21
|
+
content: str = Field(..., min_length=1, description="Context content")
|
|
22
|
+
context_type: ContextType = Field(..., description="Type of context")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class ContextCreate(ContextBase):
|
|
26
|
+
"""Model for creating a new context."""
|
|
27
|
+
|
|
28
|
+
target_id: str = Field(..., description="ID of the target (collection, path, etc.)")
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class ContextUpdate(BaseModel):
|
|
32
|
+
"""Model for updating a context."""
|
|
33
|
+
|
|
34
|
+
content: str | None = Field(None, min_length=1)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class ContextResponse(ContextBase):
|
|
38
|
+
"""Model for context response data."""
|
|
39
|
+
|
|
40
|
+
model_config = ConfigDict(from_attributes=True)
|
|
41
|
+
|
|
42
|
+
id: str = Field(..., description="Unique context ID")
|
|
43
|
+
target_id: str = Field(..., description="ID of the target")
|
|
44
|
+
created_at: datetime = Field(...)
|
|
45
|
+
updated_at: datetime = Field(...)
|
sif/models/document.py
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"""Pydantic models for Document operations."""
|
|
2
|
+
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class DocumentMetadata(BaseModel):
|
|
10
|
+
"""Document metadata extracted from frontmatter."""
|
|
11
|
+
|
|
12
|
+
title: str | None = None
|
|
13
|
+
author: str | None = None
|
|
14
|
+
date: datetime | None = None
|
|
15
|
+
tags: list[str] = Field(default_factory=list)
|
|
16
|
+
categories: list[str] = Field(default_factory=list)
|
|
17
|
+
custom: dict[str, Any] = Field(default_factory=dict)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class DocumentBase(BaseModel):
|
|
21
|
+
"""Base model for Document data."""
|
|
22
|
+
|
|
23
|
+
path: str = Field(..., description="File system path")
|
|
24
|
+
content: str = Field(..., description="Document content")
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class DocumentCreate(DocumentBase):
|
|
28
|
+
"""Model for creating a new document."""
|
|
29
|
+
|
|
30
|
+
collection_id: str = Field(..., description="Parent collection ID")
|
|
31
|
+
metadata: DocumentMetadata = Field(default_factory=DocumentMetadata)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class DocumentUpdate(BaseModel):
|
|
35
|
+
"""Model for updating a document."""
|
|
36
|
+
|
|
37
|
+
content: str | None = None
|
|
38
|
+
metadata: DocumentMetadata | None = None
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class ChunkResponse(BaseModel):
|
|
42
|
+
"""Model for document chunk response."""
|
|
43
|
+
|
|
44
|
+
model_config = ConfigDict(from_attributes=True)
|
|
45
|
+
|
|
46
|
+
id: str = Field(..., description="Chunk ID")
|
|
47
|
+
content: str = Field(..., description="Chunk content")
|
|
48
|
+
start_line: int = Field(..., ge=0)
|
|
49
|
+
end_line: int = Field(..., ge=0)
|
|
50
|
+
token_count: int = Field(..., ge=0)
|
|
51
|
+
embedding_id: str | None = None
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class DocumentResponse(DocumentBase):
|
|
55
|
+
"""Model for document response data."""
|
|
56
|
+
|
|
57
|
+
model_config = ConfigDict(from_attributes=True)
|
|
58
|
+
|
|
59
|
+
id: str = Field(..., description="Unique document ID")
|
|
60
|
+
collection_id: str = Field(..., description="Parent collection ID")
|
|
61
|
+
metadata: DocumentMetadata = Field(default_factory=DocumentMetadata)
|
|
62
|
+
chunks: list[ChunkResponse] = Field(default_factory=list)
|
|
63
|
+
checksum: str = Field(..., description="Content checksum")
|
|
64
|
+
file_size: int = Field(..., ge=0)
|
|
65
|
+
created_at: datetime = Field(...)
|
|
66
|
+
updated_at: datetime = Field(...)
|
|
67
|
+
indexed_at: datetime | None = None
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class DocumentSearchResult(BaseModel):
|
|
71
|
+
"""Model for document search result."""
|
|
72
|
+
|
|
73
|
+
document: DocumentResponse
|
|
74
|
+
score: float = Field(..., ge=0, le=1)
|
|
75
|
+
matched_chunks: list[ChunkResponse] = Field(default_factory=list)
|
|
76
|
+
highlights: list[str] = Field(default_factory=list)
|
sif/models/download.py
ADDED
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
"""Model download from ModelScope."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import hashlib
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from sif.utils.logging import get_logger, is_quiet, suppress_output
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
logger = get_logger(__name__)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class ModelDownloader:
|
|
15
|
+
"""Download models from ModelScope."""
|
|
16
|
+
|
|
17
|
+
DEFAULT_MODELS = {
|
|
18
|
+
"embedding": {
|
|
19
|
+
"model_id": "iic/gte_Qwen2-7B-instruct",
|
|
20
|
+
"description": "General text embedding model",
|
|
21
|
+
},
|
|
22
|
+
"reranker": {
|
|
23
|
+
"model_id": "iic/gte_Qwen2-7B-instruct",
|
|
24
|
+
"description": "Reranking model",
|
|
25
|
+
},
|
|
26
|
+
"query_expansion": {
|
|
27
|
+
"model_id": "qwen/Qwen2.5-0.5B-Instruct",
|
|
28
|
+
"description": "Query expansion model",
|
|
29
|
+
},
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
def __init__(self, cache_dir: str | Path | None = None) -> None:
|
|
33
|
+
"""Initialize model downloader.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
cache_dir: Directory to cache downloaded models
|
|
37
|
+
"""
|
|
38
|
+
if cache_dir is None:
|
|
39
|
+
cache_dir = Path.home() / ".sif" / "models"
|
|
40
|
+
self.cache_dir = Path(cache_dir).expanduser().resolve()
|
|
41
|
+
self.cache_dir.mkdir(parents=True, exist_ok=True)
|
|
42
|
+
|
|
43
|
+
def download(
|
|
44
|
+
self,
|
|
45
|
+
model_id: str,
|
|
46
|
+
revision: str | None = None,
|
|
47
|
+
force: bool = False,
|
|
48
|
+
) -> Path:
|
|
49
|
+
"""Download a model from ModelScope.
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
model_id: ModelScope model ID (e.g., "iic/gte_Qwen2-7B-instruct")
|
|
53
|
+
revision: Model revision to download
|
|
54
|
+
force: Force re-download even if cached
|
|
55
|
+
|
|
56
|
+
Returns:
|
|
57
|
+
Path to downloaded model directory
|
|
58
|
+
"""
|
|
59
|
+
try:
|
|
60
|
+
from modelscope import snapshot_download # noqa: PLC0415
|
|
61
|
+
except ImportError:
|
|
62
|
+
logger.error("modelscope not installed. Install with: pip install modelscope")
|
|
63
|
+
raise
|
|
64
|
+
|
|
65
|
+
# Create cache subdirectory for this model
|
|
66
|
+
model_hash = hashlib.md5(model_id.encode()).hexdigest()[:8]
|
|
67
|
+
model_cache_dir = self.cache_dir / model_hash
|
|
68
|
+
|
|
69
|
+
# Check if already cached
|
|
70
|
+
if not force and model_cache_dir.exists():
|
|
71
|
+
# Check if model files exist
|
|
72
|
+
if any(model_cache_dir.iterdir()):
|
|
73
|
+
logger.info(f"Using cached model: {model_id}")
|
|
74
|
+
return model_cache_dir
|
|
75
|
+
|
|
76
|
+
# Download model
|
|
77
|
+
logger.info(f"Downloading model from ModelScope: {model_id}")
|
|
78
|
+
|
|
79
|
+
download_kwargs = {
|
|
80
|
+
"model_id": model_id,
|
|
81
|
+
"cache_dir": str(self.cache_dir),
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if revision:
|
|
85
|
+
download_kwargs["revision"] = revision
|
|
86
|
+
|
|
87
|
+
try:
|
|
88
|
+
if is_quiet():
|
|
89
|
+
with suppress_output():
|
|
90
|
+
model_path = snapshot_download(**download_kwargs)
|
|
91
|
+
else:
|
|
92
|
+
model_path = snapshot_download(**download_kwargs)
|
|
93
|
+
logger.info(f"Model downloaded to: {model_path}")
|
|
94
|
+
return Path(model_path)
|
|
95
|
+
except Exception as e:
|
|
96
|
+
logger.error(f"Failed to download model {model_id}: {e}")
|
|
97
|
+
raise
|
|
98
|
+
|
|
99
|
+
def download_embedding_model(
|
|
100
|
+
self,
|
|
101
|
+
model_id: str | None = None,
|
|
102
|
+
force: bool = False,
|
|
103
|
+
) -> Path:
|
|
104
|
+
"""Download embedding model.
|
|
105
|
+
|
|
106
|
+
Args:
|
|
107
|
+
model_id: ModelScope model ID (uses default if None)
|
|
108
|
+
force: Force re-download
|
|
109
|
+
|
|
110
|
+
Returns:
|
|
111
|
+
Path to downloaded model
|
|
112
|
+
"""
|
|
113
|
+
if model_id is None:
|
|
114
|
+
model_id = self.DEFAULT_MODELS["embedding"]["model_id"]
|
|
115
|
+
|
|
116
|
+
return self.download(model_id, force=force)
|
|
117
|
+
|
|
118
|
+
def download_reranker_model(
|
|
119
|
+
self,
|
|
120
|
+
model_id: str | None = None,
|
|
121
|
+
force: bool = False,
|
|
122
|
+
) -> Path:
|
|
123
|
+
"""Download reranker model.
|
|
124
|
+
|
|
125
|
+
Args:
|
|
126
|
+
model_id: ModelScope model ID (uses default if None)
|
|
127
|
+
force: Force re-download
|
|
128
|
+
|
|
129
|
+
Returns:
|
|
130
|
+
Path to downloaded model
|
|
131
|
+
"""
|
|
132
|
+
if model_id is None:
|
|
133
|
+
model_id = self.DEFAULT_MODELS["reranker"]["model_id"]
|
|
134
|
+
|
|
135
|
+
return self.download(model_id, force=force)
|
|
136
|
+
|
|
137
|
+
def download_query_expansion_model(
|
|
138
|
+
self,
|
|
139
|
+
model_id: str | None = None,
|
|
140
|
+
force: bool = False,
|
|
141
|
+
) -> Path:
|
|
142
|
+
"""Download query expansion model.
|
|
143
|
+
|
|
144
|
+
Args:
|
|
145
|
+
model_id: ModelScope model ID (uses default if None)
|
|
146
|
+
force: Force re-download
|
|
147
|
+
|
|
148
|
+
Returns:
|
|
149
|
+
Path to downloaded model
|
|
150
|
+
"""
|
|
151
|
+
if model_id is None:
|
|
152
|
+
model_id = self.DEFAULT_MODELS["query_expansion"]["model_id"]
|
|
153
|
+
|
|
154
|
+
return self.download(model_id, force=force)
|
|
155
|
+
|
|
156
|
+
def get_model_info(self, model_id: str) -> dict:
|
|
157
|
+
"""Get information about a model.
|
|
158
|
+
|
|
159
|
+
Args:
|
|
160
|
+
model_id: ModelScope model ID
|
|
161
|
+
|
|
162
|
+
Returns:
|
|
163
|
+
Model information dictionary
|
|
164
|
+
"""
|
|
165
|
+
try:
|
|
166
|
+
from modelscope.hub.api import HubApi # noqa: PLC0415
|
|
167
|
+
|
|
168
|
+
api = HubApi()
|
|
169
|
+
info = api.get_model_info(model_id)
|
|
170
|
+
return {
|
|
171
|
+
"model_id": model_id,
|
|
172
|
+
"name": info.get("Name", ""),
|
|
173
|
+
"description": info.get("Description", ""),
|
|
174
|
+
"tags": info.get("Tags", []),
|
|
175
|
+
}
|
|
176
|
+
except Exception as e:
|
|
177
|
+
logger.error(f"Failed to get model info for {model_id}: {e}")
|
|
178
|
+
return {"model_id": model_id, "error": str(e)}
|
|
179
|
+
|
|
180
|
+
def list_cached_models(self) -> list:
|
|
181
|
+
"""List all cached models.
|
|
182
|
+
|
|
183
|
+
Returns:
|
|
184
|
+
List of cached model directories
|
|
185
|
+
"""
|
|
186
|
+
if not self.cache_dir.exists():
|
|
187
|
+
return []
|
|
188
|
+
|
|
189
|
+
models = []
|
|
190
|
+
for item in self.cache_dir.iterdir():
|
|
191
|
+
if item.is_dir():
|
|
192
|
+
models.append(
|
|
193
|
+
{
|
|
194
|
+
"path": str(item),
|
|
195
|
+
"name": item.name,
|
|
196
|
+
}
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
return models
|
|
200
|
+
|
|
201
|
+
def clear_cache(self) -> None:
|
|
202
|
+
"""Clear all cached models."""
|
|
203
|
+
import shutil # noqa: PLC0415
|
|
204
|
+
|
|
205
|
+
if self.cache_dir.exists():
|
|
206
|
+
shutil.rmtree(self.cache_dir)
|
|
207
|
+
self.cache_dir.mkdir(parents=True, exist_ok=True)
|
|
208
|
+
logger.info("Model cache cleared")
|