own-rag-cli 0.0.1-snapshot

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.
package/MCP_USAGE.md ADDED
@@ -0,0 +1,315 @@
1
+ # MCP RAG Server — Como Usar
2
+
3
+ Este documento explica como usar o MCP Server RAG para buscar código, atualizar índices e trabalhar com a codebase de forma semântica.
4
+
5
+ ## Setup Rápido
6
+
7
+ O servidor MCP já deve estar configurado em `~/.claude.json`. Se não estiver:
8
+
9
+ ```json
10
+ "mcpServers": {
11
+ "rag-codebase": {
12
+ "command": "~/.local/bin/mcp-rag-server",
13
+ "args": [],
14
+ "env": {
15
+ "CHROMA_HOST": "localhost",
16
+ "CHROMA_PORT": "8000"
17
+ }
18
+ }
19
+ }
20
+ ```
21
+
22
+ **Pré-requisitos:**
23
+ - Docker rodando (`docker ps` deve mostrar o container chromadb)
24
+ - ChromaDB inicializado com dados indexados (`~/.rag_db`)
25
+ - mcp-rag-server instalado em `~/.local/bin/`
26
+
27
+ ---
28
+
29
+ ## Ferramentas Disponíveis
30
+
31
+ ### 1. **semantic_search_code** — Busca Semântica
32
+
33
+ Encontra trechos de código relevantes usando busca vetorial. Funciona com descrições em linguagem natural.
34
+
35
+ **Parâmetros:**
36
+ - `query` (string): O que você está procurando — pode ser uma descrição vaga
37
+ - `top_k` (int, opcional): Quantos resultados retornar (padrão: 7, máximo: 20)
38
+
39
+ **Exemplos de uso:**
40
+
41
+ ```
42
+ semantic_search_code("função que valida email")
43
+ semantic_search_code("como fazer requisição HTTP com retry", top_k=5)
44
+ semantic_search_code("integração com banco de dados", top_k=10)
45
+ ```
46
+
47
+ **Resultado:**
48
+ ```
49
+ # Resultados para: 'função que valida email'
50
+
51
+ ## [1] /home/<usuario>/projeto/src/validators.py
52
+ **Similaridade:** 92.3%
53
+
54
+ ```
55
+ def validate_email(email: str) -> bool:
56
+ return "@" in email and "." in email.split("@")[1]
57
+ ```
58
+ ```
59
+
60
+ **Dicas:**
61
+ - Use descrições em português ou inglês (o modelo entende bem)
62
+ - Seja específico: "função de autenticação" é melhor que "função"
63
+ - Se poucos resultados úteis, tente reformular a query
64
+ - Resultados acima de 85% geralmente são muito relevantes
65
+
66
+ ---
67
+
68
+ ### 2. **update_file_index** — Atualizar Índice de Arquivo
69
+
70
+ Após editar um arquivo, use isto para manter o índice RAG sincronizado.
71
+
72
+ **Parâmetros:**
73
+ - `file_path` (string): Caminho absoluto ou relativo do arquivo
74
+
75
+ **Exemplos:**
76
+
77
+ ```
78
+ update_file_index("/home/<usuario>/projeto/src/auth.py")
79
+ update_file_index("src/validators.py")
80
+ update_file_index("<PROJECT_ROOT>/bin/mcp_server.py")
81
+ ```
82
+
83
+ **Resultado:**
84
+ ```
85
+ Arquivo reindexado com sucesso.
86
+ Arquivo : /home/<usuario>/projeto/src/auth.py
87
+ Chunks antigos removidos: 5
88
+ Novos chunks inseridos : 6
89
+ ```
90
+
91
+ **Quando usar:**
92
+ - Depois de editar um arquivo no projeto
93
+ - Depois de criar um novo arquivo
94
+ - Quando a IA faz mudanças no código que deveriam ser searchable
95
+
96
+ **Nota:** O arquivo é dividido em chunks (~2400 caracteres cada). Alterações pequenas podem afetar múltiplos chunks.
97
+
98
+ ---
99
+
100
+ ### 3. **delete_file_index** — Remover Arquivo do Índice
101
+
102
+ Remove um arquivo completamente do banco de dados vetorial.
103
+
104
+ **Parâmetros:**
105
+ - `file_path` (string): Caminho absoluto ou relativo
106
+
107
+ **Exemplos:**
108
+
109
+ ```
110
+ delete_file_index("/home/<usuario>/projeto/src/old_module.py")
111
+ delete_file_index("temp/debug.py")
112
+ ```
113
+
114
+ **Quando usar:**
115
+ - Quando um arquivo é deletado do projeto
116
+ - Quando você quer excluir um arquivo dos resultados de busca
117
+
118
+ ---
119
+
120
+ ### 4. **index_specific_folder** — Reindexar Pasta
121
+
122
+ Indexa ou reindexar todos os arquivos de um diretório.
123
+
124
+ **Parâmetros:**
125
+ - `folder_path` (string): Caminho da pasta a indexar recursivamente
126
+
127
+ **Exemplos:**
128
+
129
+ ```
130
+ index_specific_folder("/home/<usuario>/projeto/src")
131
+ index_specific_folder("./src/auth")
132
+ ```
133
+
134
+ **Resultado:**
135
+ ```
136
+ Indexação da pasta concluída.
137
+ Pasta : /home/<usuario>/projeto/src
138
+ Arquivos processados: 12/12
139
+ Total de chunks : 145
140
+ ```
141
+
142
+ **Quando usar:**
143
+ - Depois de criar vários arquivos novos em uma pasta
144
+ - Para reindexar uma seção do projeto após alterações em massa
145
+ - Quando o índice está desatualizado para um diretório
146
+
147
+ ---
148
+
149
+ ## Fluxo Típico de Uso
150
+
151
+ ### Buscar código existente
152
+ ```
153
+ 1. semantic_search_code("descrição do que procura")
154
+ 2. Analisa os resultados
155
+ 3. Navega para os arquivos mencionados
156
+ ```
157
+
158
+ ### Editar código
159
+ ```
160
+ 1. Lê o arquivo (Read tool)
161
+ 2. Edita o arquivo (Edit tool)
162
+ 3. update_file_index(file_path) — IMPORTANTE!
163
+ 4. Próximas buscas já veem a versão nova
164
+ ```
165
+
166
+ ### Criar novo arquivo
167
+ ```
168
+ 1. Escreve o arquivo (Write tool)
169
+ 2. index_specific_folder(folder_path) OU update_file_index(file_path)
170
+ 3. Agora está pronto para ser encontrado em buscas
171
+ ```
172
+
173
+ ---
174
+
175
+ ## Entendendo os Resultados
176
+
177
+ Cada resultado de busca inclui:
178
+
179
+ - **Número e rank** (ex: [1], [2], etc.)
180
+ - **Arquivo** — caminho completo para localizar o código
181
+ - **Similaridade %** — confiança da correspondência (70-100%)
182
+ - **Snippet** — até 800 caracteres do trecho mais relevante
183
+
184
+ **Como interpretar similaridade:**
185
+ - 90-100% → Muito relevante, provavelmente exatamente o que procura
186
+ - 80-89% → Bastante relevante, confira o contexto completo
187
+ - 70-79% → Relevante, mas pode precisar revisar mais contexto
188
+ - <70% → Pode ser relevante apenas parcialmente
189
+
190
+ ---
191
+
192
+ ## Limitações e Comportamento
193
+
194
+ **O que o RAG sabe:**
195
+ - Código-fonte de linguagens de programação
196
+ - Documentação e markdown
197
+ - Configurações (JSON, YAML, etc.)
198
+ - Comentários no código
199
+
200
+ **O que ignora automaticamente:**
201
+ - Binários (`*.exe`, `*.so`, `*.dll`)
202
+ - Imagens (`*.png`, `*.jpg`)
203
+ - Mídia (`*.mp4`, `*.mp3`)
204
+ - Pacotes (`node_modules/`, `venv/`, `.git/`)
205
+ - Arquivos muito grandes (>500KB)
206
+
207
+ **Precisão:**
208
+ - O RAG é baseado em similaridade vetorial, não busca literal
209
+ - Reformule a query se os resultados não forem úteis
210
+ - A primeira chamada aquece o modelo (pode levar 1-2s)
211
+
212
+ ---
213
+
214
+ ## Troubleshooting
215
+
216
+ ### "Erro de conexão: Não foi possível conectar ao ChromaDB"
217
+ ```bash
218
+ # Verifique se Docker está rodando
219
+ docker ps | grep chromadb
220
+
221
+ # Se não estiver, inicie
222
+ docker compose -f "$HOME/docker-chromadb/docker-compose.yml" up -d
223
+ ```
224
+
225
+ ### "Nenhum resultado encontrado"
226
+ - O índice pode estar vazio — rode `python3 indexer_full.py /seu/projeto`
227
+ - Tente uma query mais simples
228
+ - Verifique se os arquivos estão indexados com `./chroma_monitor.sh chunks`
229
+
230
+ ### Busca retorna resultados muito antigos
231
+ - Arquivo pode ter sido editado mas não atualizado no índice
232
+ - Use `update_file_index(file_path)` para sincronizar
233
+ - Ou `index_specific_folder(folder_path)` para toda a pasta
234
+
235
+ ### Arquivo editado não aparece nos resultados
236
+ 1. Confirme que o arquivo foi salvo
237
+ 2. Use `update_file_index(file_path)` imediatamente após editar
238
+ 3. Aguarde a próxima busca (o modelo está aquecido)
239
+
240
+ ---
241
+
242
+ ## Configuração Avançada
243
+
244
+ Todas as configurações são definidas em `~/.rag_venv/bin/mcp_server.py`:
245
+
246
+ ```python
247
+ CHUNK_SIZE = 2400 # Caracteres por chunk (~600 tokens)
248
+ CHUNK_OVERLAP = 400 # Sobreposição entre chunks
249
+ EMBEDDING_MODEL = "jinaai/jina-embeddings-v3" # Modelo de embeddings
250
+ TOP_K_RESULTS = 7 # Resultados padrão por busca
251
+ MAX_FILE_SIZE = 500KB # Limite de tamanho de arquivo
252
+ ```
253
+
254
+ Para alterar:
255
+ 1. Edite `~/.rag_venv/bin/mcp_server.py`
256
+ 2. Reinicie o servidor (reinicie o Claude Code)
257
+ 3. Reindexe os dados: `python3 indexer_full.py /seu/projeto`
258
+
259
+ ---
260
+
261
+ ## Performance
262
+
263
+ - **Primeira busca:** 1-2 segundos (aquecimento do modelo)
264
+ - **Buscas subsequentes:** <1 segundo
265
+ - **Indexação de 1 arquivo:** <1 segundo
266
+ - **Indexação de pasta com 100 arquivos:** 10-30 segundos
267
+
268
+ ---
269
+
270
+ ## Exemplos Práticos
271
+
272
+ ### Encontrar tratamento de erros
273
+ ```
274
+ semantic_search_code("como fazer try catch ou exception handling")
275
+ ```
276
+
277
+ ### Localizar função de login
278
+ ```
279
+ semantic_search_code("autenticação de usuário login", top_k=5)
280
+ ```
281
+
282
+ ### Buscar integração com API
283
+ ```
284
+ semantic_search_code("chamada a API externa requests HTTP")
285
+ ```
286
+
287
+ ### Encontrar testes
288
+ ```
289
+ semantic_search_code("testes unitários pytest")
290
+ ```
291
+
292
+ ### Procurar por padrões de cache
293
+ ```
294
+ semantic_search_code("cache memoização performance")
295
+ ```
296
+
297
+ ---
298
+
299
+ ## Integração com Claude Code
300
+
301
+ O MCP Server é automaticamente chamado pelo Claude Code quando você:
302
+
303
+ 1. **Usa a ferramenta de busca** — procura por "semantic_search_code"
304
+ 2. **Edita um arquivo** — sincroniza automaticamente com `update_file_index`
305
+ 3. **Pede recomendações de código** — busca contexto relevante antes de responder
306
+
307
+ A IA pode usar todas as 4 ferramentas para:
308
+ - Entender o projeto antes de fazer mudanças
309
+ - Encontrar padrões existentes e seguir convenções
310
+ - Manter o índice atualizado enquanto trabalha
311
+ - Evitar duplicação de código ao sugerir novos
312
+
313
+ ---
314
+
315
+ **Último update:** 2026-03-05 — Documentação para mcp-rag-server v1.0
package/README.md ADDED
@@ -0,0 +1,133 @@
1
+ # MCP binary checksum (SHA-256, payload without shebang): `3246eeb57f901742d915e0bce37fa96f059e149a57bbce73095ff4e5ea51d8d4`
2
+
3
+ # own-rag
4
+
5
+ Local RAG for codebases with:
6
+ - ChromaDB (Docker)
7
+ - MCP server (`mcp-rag-server`)
8
+ - CPU embeddings (`jina`, `bge`, `hybrid`)
9
+ - Cross-platform setup (`Linux` and `macOS`)
10
+
11
+ ## Why this repo
12
+
13
+ `own-rag` lets you index local projects and query them from MCP-compatible tools (Claude/Cursor).
14
+
15
+ It includes:
16
+ - installers: `rag-setup.run`, `rag-setup-macos.run`
17
+ - MCP server source: `bin/mcp_server.py`
18
+ - indexer source: `bin/indexer_full.py`
19
+ - monitor and wrapper scripts
20
+
21
+ ## Source vs Artifact
22
+
23
+ This repo is fully auditable:
24
+ - source of logic: `bin/*.py` and `bin/*.sh`
25
+ - generated artifacts: `rag-setup.run`, `rag-setup-macos.run`
26
+
27
+ To refresh embedded payloads from source files:
28
+
29
+ ```bash
30
+ ./bin/build_run.sh
31
+ ./bin/build_run_macos.sh
32
+ ```
33
+
34
+ ## Quick Start
35
+
36
+ ### Linux
37
+
38
+ ```bash
39
+ chmod +x rag-setup.run
40
+ ./rag-setup.run /path/to/project
41
+ ```
42
+
43
+ ### macOS
44
+
45
+ ```bash
46
+ chmod +x rag-setup-macos.run
47
+ ./rag-setup-macos.run /path/to/project
48
+ ```
49
+
50
+ ### NPM wrapper
51
+
52
+ ```bash
53
+ rag run /path/to/project
54
+ rag monitor
55
+ rag remove
56
+ ```
57
+
58
+ ## What setup does
59
+
60
+ 1. Creates `~/.rag_venv` and installs dependencies.
61
+ 2. Starts ChromaDB in Docker with persistent volume (`~/.rag_db`).
62
+ 3. Installs `mcp-rag-server` in `~/.local/bin`.
63
+ 4. Optionally updates MCP config files (`.claude.json`, Cursor config).
64
+ 5. Indexes the project.
65
+
66
+ ## ChromaDB Port Behavior
67
+
68
+ - Default host port is `8000`.
69
+ - During ChromaDB install/reinstall, setup asks for the port.
70
+ - The installer checks if the chosen port is already in use using native tools:
71
+ - Linux: `ss` (fallback: `lsof` / `netstat`)
72
+ - macOS: `lsof` (fallback: `netstat`)
73
+ - If the port is busy, it asks for another one.
74
+ - In non-interactive runs, it auto-selects the next free port.
75
+ - Selected port is propagated to:
76
+ - Docker Compose mapping
77
+ - health checks
78
+ - MCP config (`CHROMA_PORT`)
79
+ - indexer runtime (`MCP_CHROMA_PORT`)
80
+
81
+ ## Performance Profiles
82
+
83
+ Available in indexer/setup:
84
+ - `autotune` (recommended): uses local machine metrics and short benchmark.
85
+ - `max-performance`: higher throughput and higher memory usage risk.
86
+
87
+ `max-performance` warning shown by setup:
88
+
89
+ ```text
90
+ Este modo pode elevar consideravelmente o consumo de memória e causar encerramento por OOM (exit 137).
91
+ ```
92
+
93
+ ## Key Environment Variables
94
+
95
+ - `MCP_EMBEDDING_MODEL=jina|bge|hybrid`
96
+ - `MCP_JINA_QUANTIZATION=default|dynamic-int8`
97
+ - `MCP_PERF_PROFILE=autotune|max-performance`
98
+ - `MCP_CHROMA_PORT=8000`
99
+ - `MCP_CHUNK_SIZE`
100
+ - `MCP_CHUNK_OVERLAP`
101
+ - `MCP_EMBEDDING_BATCH_SIZE`
102
+
103
+ ## Useful Commands
104
+
105
+ ```bash
106
+ # only index (infra already installed)
107
+ ./rag-setup.run /path/to/project --only-index
108
+
109
+ # skip indexing
110
+ ./rag-setup.run /path/to/project --skip-index
111
+
112
+ # force reinstall
113
+ ./rag-setup.run /path/to/project --reinstall
114
+
115
+ # force model change flow
116
+ ./rag-setup.run --change-model /path/to/project
117
+ ```
118
+
119
+ ## Development
120
+
121
+ - Edit source files in `bin/`.
122
+ - Rebuild payloads with `./bin/build_run.sh` and `./bin/build_run_macos.sh`.
123
+ - Validate before commit:
124
+
125
+ ```bash
126
+ bash -n rag-setup.run
127
+ bash -n rag-setup-macos.run
128
+ python3 -m py_compile bin/indexer_full.py
129
+ ```
130
+
131
+ ## License
132
+
133
+ MIT (or your preferred OSS license file in this repo).
@@ -0,0 +1,21 @@
1
+ services:
2
+ chromadb:
3
+ image: chromadb/chroma:latest
4
+ container_name: chromadb-rag
5
+ ports:
6
+ - "8000:8000"
7
+ volumes:
8
+ # Persiste o banco diretamente na pasta do usuário no host
9
+ - ${HOME}/.rag_db:/chroma/chroma
10
+ environment:
11
+ # Habilita autenticação anônima (sem token) para uso local
12
+ - ANONYMIZED_TELEMETRY=false
13
+ - CHROMA_SERVER_AUTHN_CREDENTIALS_FILE=""
14
+ - CHROMA_SERVER_AUTHN_PROVIDER=""
15
+ restart: always
16
+ healthcheck:
17
+ test: ["CMD", "curl", "-f", "http://localhost:8000/api/v1/heartbeat"]
18
+ interval: 30s
19
+ timeout: 10s
20
+ retries: 3
21
+ start_period: 10s
@@ -0,0 +1,219 @@
1
+ #!/usr/bin/env python3
2
+ from __future__ import annotations
3
+
4
+ """
5
+ download_model_from_hugginface.py
6
+
7
+ Camada de download de modelos com prioridade de provedores e fallback de modelo.
8
+ Fluxo padrão:
9
+ 1) tenta baixar o modelo preferido via Hugging Face;
10
+ 2) se falhar, tenta provedores alternativos (quando disponíveis);
11
+ 3) se o modelo preferido falhar em todos os provedores, tenta modelo fallback.
12
+ """
13
+
14
+ from dataclasses import dataclass
15
+ import getpass
16
+ import os
17
+ from pathlib import Path
18
+ import shutil
19
+ import sys
20
+ from typing import Protocol
21
+
22
+
23
+ class ModelDownloadStrategy(Protocol):
24
+ name: str
25
+
26
+ def download(self, model_id: str, local_dir: Path) -> None:
27
+ """Baixa model_id para local_dir ou levanta exceção."""
28
+
29
+
30
+ class HuggingFaceDownloadStrategy:
31
+ name = "huggingface"
32
+
33
+ def download(self, model_id: str, local_dir: Path) -> None:
34
+ from huggingface_hub import snapshot_download
35
+
36
+ hf_token = os.environ.get("HF_TOKEN") or os.environ.get("HUGGING_FACE_HUB_TOKEN")
37
+ _download_with_hf_token_recovery(
38
+ repo_id=model_id,
39
+ local_dir=local_dir,
40
+ hf_token=hf_token,
41
+ snapshot_download=snapshot_download,
42
+ )
43
+
44
+
45
+ @dataclass(frozen=True)
46
+ class DownloadSelection:
47
+ model_id: str
48
+ provider: str
49
+ local_dir: Path
50
+
51
+
52
+ def _load_optional_strategies() -> list[ModelDownloadStrategy]:
53
+ strategies: list[ModelDownloadStrategy] = []
54
+
55
+ try:
56
+ from download_model_from_modelscope import ModelScopeDownloadStrategy
57
+
58
+ strategies.append(ModelScopeDownloadStrategy())
59
+ except Exception:
60
+ # Provider opcional: ignora se não estiver disponível no ambiente.
61
+ pass
62
+
63
+ return strategies
64
+
65
+
66
+ def build_default_strategies() -> list[ModelDownloadStrategy]:
67
+ """Factory simples: ordem de prioridade de provedores de download."""
68
+ return [HuggingFaceDownloadStrategy(), *_load_optional_strategies()]
69
+
70
+
71
+ _MODEL_READY_MARKER = ".download_complete"
72
+
73
+
74
+ def _prepare_destination(local_dir: Path, *, clean: bool) -> None:
75
+ if clean and local_dir.exists():
76
+ shutil.rmtree(local_dir)
77
+ local_dir.mkdir(parents=True, exist_ok=True)
78
+
79
+
80
+ def _model_cache_dir(base_dir: Path, model_id: str) -> Path:
81
+ # Evita colisão de nomes e mantém diretório seguro em qualquer SO.
82
+ safe_name = model_id.replace("/", "__").replace(":", "_")
83
+ return base_dir / safe_name
84
+
85
+
86
+ def _cache_ready(local_dir: Path) -> bool:
87
+ marker = local_dir / _MODEL_READY_MARKER
88
+ if not marker.exists() or not local_dir.exists():
89
+ return False
90
+ return any(p.name != _MODEL_READY_MARKER for p in local_dir.iterdir())
91
+
92
+
93
+ def _mark_cache_ready(local_dir: Path) -> None:
94
+ (local_dir / _MODEL_READY_MARKER).write_text("ok\n", encoding="utf-8")
95
+
96
+
97
+ def _status_code_from_error(exc: Exception) -> int | None:
98
+ response = getattr(exc, "response", None)
99
+ if response is None:
100
+ return None
101
+ status_code = getattr(response, "status_code", None)
102
+ if isinstance(status_code, int):
103
+ return status_code
104
+ return None
105
+
106
+
107
+ def _is_invalid_hf_token_error(exc: Exception) -> bool:
108
+ message = str(exc).lower()
109
+ status_code = _status_code_from_error(exc)
110
+ token_keywords = ("invalid token", "token is invalid", "unauthorized", "401")
111
+ if status_code == 401:
112
+ return True
113
+ return any(keyword in message for keyword in token_keywords)
114
+
115
+
116
+ def _prompt_recover_invalid_hf_token() -> tuple[str, str | None]:
117
+ if not sys.stdin.isatty():
118
+ return ("no-token", None)
119
+
120
+ while True:
121
+ print(
122
+ "[!] O token do HuggingFace parece inválido. Escolha: "
123
+ "[1] informar novo token, [2] continuar sem token.",
124
+ file=sys.stderr,
125
+ )
126
+ answer = input("> Escolha [1/2]: ").strip().lower()
127
+ if answer in {"1", "novo", "new"}:
128
+ new_token = getpass.getpass("Cole o novo HF_TOKEN: ").strip()
129
+ if new_token:
130
+ return ("new-token", new_token)
131
+ print("[!] Token vazio. Tente novamente.", file=sys.stderr)
132
+ continue
133
+ if answer in {"2", "", "sem", "no"}:
134
+ return ("no-token", None)
135
+ print("[!] Opção inválida. Digite 1 ou 2.", file=sys.stderr)
136
+
137
+
138
+ def _download_with_hf_token_recovery(
139
+ *,
140
+ repo_id: str,
141
+ local_dir: Path,
142
+ hf_token: str | None,
143
+ snapshot_download,
144
+ ) -> None:
145
+ attempt_token = hf_token
146
+
147
+ while True:
148
+ try:
149
+ snapshot_download(
150
+ repo_id=repo_id,
151
+ local_dir=str(local_dir),
152
+ token=attempt_token,
153
+ )
154
+ if attempt_token:
155
+ os.environ["HF_TOKEN"] = attempt_token
156
+ else:
157
+ os.environ.pop("HF_TOKEN", None)
158
+ return
159
+ except Exception as exc:
160
+ if attempt_token and _is_invalid_hf_token_error(exc):
161
+ print(
162
+ "[!] Falha de autenticação no HuggingFace com o token atual. "
163
+ "Você pode informar outro token ou seguir sem token.",
164
+ file=sys.stderr,
165
+ )
166
+ action, replacement = _prompt_recover_invalid_hf_token()
167
+ if action == "new-token" and replacement:
168
+ attempt_token = replacement
169
+ continue
170
+ attempt_token = None
171
+ continue
172
+ raise
173
+
174
+
175
+ def download_model_with_fallback(
176
+ preferred_model_id: str,
177
+ fallback_model_id: str,
178
+ local_dir: Path,
179
+ strategies: list[ModelDownloadStrategy] | None = None,
180
+ ) -> DownloadSelection:
181
+ """
182
+ Tenta baixar `preferred_model_id`; se falhar em todos os provedores,
183
+ tenta `fallback_model_id`.
184
+ """
185
+ base_dir = local_dir.expanduser()
186
+ base_dir.mkdir(parents=True, exist_ok=True)
187
+ providers = strategies or build_default_strategies()
188
+ errors: list[str] = []
189
+
190
+ for model_id in (preferred_model_id, fallback_model_id):
191
+ model_local_dir = _model_cache_dir(base_dir, model_id)
192
+ if _cache_ready(model_local_dir):
193
+ return DownloadSelection(
194
+ model_id=model_id,
195
+ provider="local-cache",
196
+ local_dir=model_local_dir,
197
+ )
198
+
199
+ for strategy in providers:
200
+ try:
201
+ print(
202
+ f"[+] Iniciando download do modelo '{model_id}' via {strategy.name} em: {model_local_dir}",
203
+ file=sys.stderr,
204
+ )
205
+ _prepare_destination(model_local_dir, clean=True)
206
+ strategy.download(model_id=model_id, local_dir=model_local_dir)
207
+ _mark_cache_ready(model_local_dir)
208
+ return DownloadSelection(
209
+ model_id=model_id,
210
+ provider=strategy.name,
211
+ local_dir=model_local_dir,
212
+ )
213
+ except Exception as exc:
214
+ errors.append(f"{strategy.name}:{model_id}: {exc}")
215
+
216
+ raise RuntimeError(
217
+ "Falha no download dos modelos em todos os provedores configurados. "
218
+ + " | ".join(errors)
219
+ )
@@ -0,0 +1,26 @@
1
+ #!/usr/bin/env python3
2
+ from __future__ import annotations
3
+
4
+ """
5
+ Provider opcional de download via ModelScope.
6
+ Usado apenas se o pacote `modelscope` estiver instalado.
7
+ """
8
+
9
+ from pathlib import Path
10
+
11
+
12
+ class ModelScopeDownloadStrategy:
13
+ name = "modelscope"
14
+
15
+ def download(self, model_id: str, local_dir: Path) -> None:
16
+ try:
17
+ from modelscope.hub.snapshot_download import snapshot_download
18
+ except Exception as exc:
19
+ raise RuntimeError(
20
+ "Pacote `modelscope` indisponível para provider alternativo"
21
+ ) from exc
22
+
23
+ snapshot_download(
24
+ model_id=model_id,
25
+ local_dir=str(local_dir),
26
+ )