File size: 10,275 Bytes
f5eb34f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
# 🔒 Setup Multi-Usuário

Guia para implementar isolamento de dados por usuário no RAG Template.

---

## Situação Atual

**Comportamento:**
- Todos os documentos são compartilhados entre todos os usuários
- Qualquer usuário pode buscar em todos os documentos
- Apenas o histórico de chat é isolado por sessão

**Tabelas atuais:**
```sql
documents (
    id,
    title,
    content,
    embedding,
    created_at
)  -- SEM user_id!
```

---

## Opções de Implementação

### Opção 1: Isolamento por Sessão (Simples)

**Quando usar:**
- App pessoal (1 usuário por instância)
- Prototipagem rápida
- Demos e testes

**Implementação:**
- Adicionar `session_id` na tabela `documents`
- Filtrar documentos por `session_id` atual
- Problema: Sessão se perde ao recarregar página

### Opção 2: Autenticação com Hugging Face Spaces (Recomendado)

**Quando usar:**
- Deploy público no Hugging Face Spaces
- Múltiplos usuários
- Dados privados por usuário

**Implementação:**
- Usar autenticação integrada do Spaces
- Capturar `user_id` do Gradio
- Adicionar coluna `user_id` nas tabelas

### Opção 3: Autenticação Custom (Avançado)

**Quando usar:**
- Deploy standalone
- Sistema de autenticação próprio
- Controle total

**Implementação:**
- Sistema de login/registro
- JWT ou sessions
- Tabela de usuários

---

## Implementação Recomendada: Opção 2

### 1. Modificar Schema do Banco

```sql
-- Adicionar coluna user_id em documents
ALTER TABLE documents ADD COLUMN user_id TEXT;

-- Adicionar índice para performance
CREATE INDEX idx_documents_user_id ON documents(user_id);

-- Adicionar coluna user_id em chats
ALTER TABLE chats ADD COLUMN user_id TEXT;

-- Adicionar coluna user_id em query_metrics
ALTER TABLE query_metrics ADD COLUMN user_id TEXT;
```

### 2. Atualizar database.py

```python
# src/database.py

def insert_document(
    self,
    title: str,
    content: str,
    embedding: List[float],
    user_id: str  # NOVO parâmetro
) -> Optional[int]:
    """Insere documento no banco com user_id"""
    conn = self.connect()
    if not conn:
        return None

    try:
        with conn.cursor() as cur:
            cur.execute(
                """
                INSERT INTO documents (title, content, embedding, user_id)
                VALUES (%s, %s, %s::vector, %s)
                RETURNING id
                """,
                (title, content, embedding, user_id)
            )
            row = cur.fetchone()
            return row[0] if row else None
    except Exception as e:
        self.last_error = f"Falha ao inserir documento: {str(e)}"
        return None

def search_similar(
    self,
    query_embedding: List[float],
    k: int = 4,
    user_id: str = None  # NOVO parâmetro
) -> List[Dict[str, Any]]:
    """Busca documentos similares, opcionalmente filtrados por user_id"""
    conn = self.connect()
    if not conn:
        return []

    try:
        with conn.cursor() as cur:
            if user_id:
                # Busca apenas documentos do usuário
                cur.execute(
                    """
                    SELECT id, title, content, 1 - (embedding <=> %s::vector) as score
                    FROM documents
                    WHERE user_id = %s
                    ORDER BY embedding <=> %s::vector
                    LIMIT %s
                    """,
                    (query_embedding, user_id, query_embedding, k)
                )
            else:
                # Busca em todos (modo público)
                cur.execute(
                    """
                    SELECT id, title, content, 1 - (embedding <=> %s::vector) as score
                    FROM documents
                    ORDER BY embedding <=> %s::vector
                    LIMIT %s
                    """,
                    (query_embedding, query_embedding, k)
                )

            rows = cur.fetchall()
            return [
                {
                    "id": r[0],
                    "title": r[1],
                    "content": r[2],
                    "score": float(r[3])
                }
                for r in rows
            ]
    except Exception as e:
        self.last_error = f"Falha na busca: {str(e)}"
        return []
```

### 3. Capturar user_id no Gradio

```python
# app.py

def create_app():
    """Cria aplicação Gradio com todas as abas"""

    # Inicializa gerenciadores
    db_manager = DatabaseManager()
    embedding_manager = EmbeddingManager()
    generation_manager = GenerationManager()

    # Inicializa schema do banco
    db_ok = db_manager.init_schema()

    # Interface Gradio
    with gr.Blocks(title="RAG Template", css=CUSTOM_CSS) as demo:

        # Captura user_id (funciona no Hugging Face Spaces)
        def get_user_id(request: gr.Request):
            # No Spaces com autenticação habilitada
            if hasattr(request, 'username') and request.username:
                return request.username
            # Fallback: usar session
            return str(uuid.uuid4())

        # Estado global do usuário
        user_id_state = gr.State(value=None)

        # Header
        with gr.Row():
            gr.Markdown("""
            # RAG Template

            Template interativo de Retrieval-Augmented Generation com PostgreSQL + pgvector

            Explore cada etapa do processo RAG de forma visual e educativa.
            """)

        # Inicializar user_id ao carregar
        demo.load(
            fn=get_user_id,
            outputs=[user_id_state]
        )

        # Abas principais
        with gr.Tabs():
            # Passar user_id_state para as abas
            create_ingestion_tab(db_manager, embedding_manager, user_id_state)
            create_exploration_tab(db_manager, embedding_manager, user_id_state)
            create_chat_tab(db_manager, embedding_manager, generation_manager, user_id_state)
            # ...

    return demo
```

### 4. Atualizar Abas para Usar user_id

```python
# ui/ingestion_tab.py

def create_ingestion_tab(
    db_manager: DatabaseManager,
    embedding_manager: EmbeddingManager,
    user_id_state: gr.State  # NOVO
):
    """Cria aba de ingestão de documentos"""

    with gr.Tab("Ingestão de Documentos"):
        # ... UI components ...

        def ingest_documents(files, strategy, chunk_size_val, chunk_overlap_val, user_id):
            # ... processamento ...

            # Inserir com user_id
            for chunk_text, embedding_vec in zip(chunks, embeddings):
                emb_list = embedding_vec.tolist()
                doc_id = db_manager.insert_document(
                    filename,
                    chunk_text,
                    emb_list,
                    user_id  # NOVO
                )

        # Conecta evento com user_id
        ingest_btn.click(
            fn=ingest_documents,
            inputs=[file_upload, chunk_strategy, chunk_size, chunk_overlap, user_id_state],  # NOVO
            outputs=[...]
        )
```

---

## Habilitar Autenticação no Hugging Face Spaces

### 1. No Space Settings

```yaml
# No arquivo README.md do Space (YAML header)
---
title: RAG Template
emoji: 📚
colorFrom: blue
colorTo: purple
sdk: gradio
sdk_version: 4.36.0
app_file: app.py
pinned: true
hf_oauth: true          # ADICIONAR ESTA LINHA
hf_oauth_scopes:        # ADICIONAR ESTAS LINHAS
  - read-repos
  - write-repos
---
```

### 2. Capturar Username

```python
import gradio as gr

def create_app():
    with gr.Blocks() as demo:

        @demo.load(inputs=None, outputs=None)
        def on_load(request: gr.Request):
            if request:
                username = request.username or "anonymous"
                print(f"User logged in: {username}")
                return username
            return "anonymous"
```

---

## Alternativa Simples: Modo "Workspace"

Se não quiser autenticação completa, pode usar um sistema de "workspaces":

```python
# app.py

with gr.Blocks() as demo:

    # Seletor de workspace
    workspace_input = gr.Textbox(
        label="Nome do Workspace",
        placeholder="Digite um nome único para seu workspace",
        value=""
    )

    workspace_state = gr.State(value="default")

    def set_workspace(workspace_name):
        if not workspace_name:
            return "default"
        # Hash simples para consistência
        import hashlib
        return hashlib.md5(workspace_name.encode()).hexdigest()[:16]

    workspace_input.change(
        fn=set_workspace,
        inputs=[workspace_input],
        outputs=[workspace_state]
    )
```

**Vantagens:**
- Sem autenticação
- Simples de implementar
- Usuário escolhe nome do workspace

**Desvantagens:**
- Qualquer um com o nome do workspace pode acessar
- Sem segurança real

---

## Recomendação Final

**Para uso pessoal/demo:**
- Use isolamento por sessão (Opção 1)
- Rápido e simples

**Para deploy público no Spaces:**
- Use autenticação HF OAuth (Opção 2)
- Seguro e integrado

**Para produção empresarial:**
- Use autenticação custom (Opção 3)
- Controle total

---

## Migration Script

Script para migrar banco existente:

```python
# scripts/add_user_id.py

import os
from dotenv import load_dotenv
import psycopg

load_dotenv()

DATABASE_URL = os.getenv("DATABASE_URL")

def migrate():
    conn = psycopg.connect(DATABASE_URL, autocommit=True)

    with conn.cursor() as cur:
        # Adicionar colunas
        cur.execute("ALTER TABLE documents ADD COLUMN IF NOT EXISTS user_id TEXT")
        cur.execute("ALTER TABLE chats ADD COLUMN IF NOT EXISTS user_id TEXT")
        cur.execute("ALTER TABLE query_metrics ADD COLUMN IF NOT EXISTS user_id TEXT")

        # Setar user_id padrão para documentos existentes
        cur.execute("UPDATE documents SET user_id = 'legacy' WHERE user_id IS NULL")
        cur.execute("UPDATE chats SET user_id = 'legacy' WHERE user_id IS NULL")

        # Criar índices
        cur.execute("CREATE INDEX IF NOT EXISTS idx_documents_user_id ON documents(user_id)")
        cur.execute("CREATE INDEX IF NOT EXISTS idx_chats_user_id ON chats(user_id)")

        print("✅ Migração concluída!")

    conn.close()

if __name__ == "__main__":
    migrate()
```

---

**Quer que eu implemente alguma dessas opções agora?**