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 +315 -0
- package/README.md +133 -0
- package/bin/docker-compose.yml +21 -0
- package/bin/download_model_from_hugginface.py +219 -0
- package/bin/download_model_from_modelscope.py +26 -0
- package/bin/indexer_full.py +1426 -0
- package/bin/mcp_server.py +1433 -0
- package/bin/postinstall.sh +102 -0
- package/bin/rag-remove.sh +198 -0
- package/bin/rag-wrapper.sh +186 -0
- package/bin/requirements.txt +21 -0
- package/chroma_monitor.sh +857 -0
- package/how-its-work.md +285 -0
- package/package.json +49 -0
- package/rag-setup-macos.run +1129 -0
- package/rag-setup.run +1179 -0
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
|
+
)
|