Spaces:
Sleeping
Sleeping
| # 🔒 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?** | |