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.
Files changed (86) hide show
  1. docsif-0.1.0.dist-info/METADATA +393 -0
  2. docsif-0.1.0.dist-info/RECORD +86 -0
  3. docsif-0.1.0.dist-info/WHEEL +4 -0
  4. docsif-0.1.0.dist-info/entry_points.txt +3 -0
  5. docsif-0.1.0.dist-info/licenses/LICENSE +21 -0
  6. sif/__init__.py +24 -0
  7. sif/_version.py +3 -0
  8. sif/cli/__init__.py +11 -0
  9. sif/cli/commands/__init__.py +18 -0
  10. sif/cli/commands/bench.py +148 -0
  11. sif/cli/commands/collection.py +360 -0
  12. sif/cli/commands/context.py +154 -0
  13. sif/cli/commands/get.py +200 -0
  14. sif/cli/commands/index.py +294 -0
  15. sif/cli/commands/ls.py +67 -0
  16. sif/cli/commands/mcp.py +93 -0
  17. sif/cli/commands/pull.py +111 -0
  18. sif/cli/commands/search.py +574 -0
  19. sif/cli/config.py +206 -0
  20. sif/cli/formatters.py +269 -0
  21. sif/cli/main.py +205 -0
  22. sif/config/__init__.py +30 -0
  23. sif/config/constants.py +52 -0
  24. sif/config/settings.py +217 -0
  25. sif/core/__init__.py +21 -0
  26. sif/core/collection.py +161 -0
  27. sif/core/context.py +167 -0
  28. sif/core/document.py +125 -0
  29. sif/core/models.py +298 -0
  30. sif/database/__init__.py +23 -0
  31. sif/database/connection.py +139 -0
  32. sif/database/database.py +136 -0
  33. sif/database/migrations.py +187 -0
  34. sif/database/repositories.py +500 -0
  35. sif/database/repository.py +171 -0
  36. sif/database/schema.py +362 -0
  37. sif/embedding/__init__.py +22 -0
  38. sif/embedding/cache.py +196 -0
  39. sif/embedding/embedder.py +328 -0
  40. sif/embedding/factory.py +92 -0
  41. sif/embedding/manager.py +200 -0
  42. sif/embedding/model.py +126 -0
  43. sif/indexing/__init__.py +21 -0
  44. sif/indexing/chunker.py +226 -0
  45. sif/indexing/indexer.py +226 -0
  46. sif/indexing/parser.py +192 -0
  47. sif/indexing/scanner.py +148 -0
  48. sif/indexing/watcher.py +156 -0
  49. sif/mcp/README.md +258 -0
  50. sif/mcp/__init__.py +9 -0
  51. sif/mcp/claude_desktop_config.json +16 -0
  52. sif/mcp/cli.py +92 -0
  53. sif/mcp/protocol.py +336 -0
  54. sif/mcp/server.py +433 -0
  55. sif/mcp/server_http.py +54 -0
  56. sif/mcp/test_mcp.py +108 -0
  57. sif/mcp/tools.py +497 -0
  58. sif/mcp/transport_http.py +424 -0
  59. sif/mcp/transport_stdio.py +292 -0
  60. sif/mcp_server/__init__.py +26 -0
  61. sif/mcp_server/handlers.py +68 -0
  62. sif/mcp_server/server.py +58 -0
  63. sif/mcp_server/tools.py +216 -0
  64. sif/mcp_server/transport.py +221 -0
  65. sif/models/__init__.py +70 -0
  66. sif/models/collection.py +51 -0
  67. sif/models/context.py +45 -0
  68. sif/models/document.py +76 -0
  69. sif/models/download.py +208 -0
  70. sif/models/embedding.py +71 -0
  71. sif/models/search.py +92 -0
  72. sif/search/__init__.py +22 -0
  73. sif/search/benchmark.py +110 -0
  74. sif/search/bm25.py +216 -0
  75. sif/search/expansion.py +157 -0
  76. sif/search/hybrid.py +323 -0
  77. sif/search/rerank.py +390 -0
  78. sif/search/rrf.py +186 -0
  79. sif/search/snippets.py +130 -0
  80. sif/search/strategy.py +97 -0
  81. sif/search/vector.py +177 -0
  82. sif/utils/__init__.py +26 -0
  83. sif/utils/logging.py +143 -0
  84. sif/utils/paths.py +82 -0
  85. sif/utils/progress.py +83 -0
  86. 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
+ ]
@@ -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")