rag_template / docs /MULTI_USER_SETUP.md
Guilherme Favaron
Initial commit of local project
f5eb34f

A newer version of the Gradio SDK is available: 6.6.0

Upgrade

🔒 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:

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

-- 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

# 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

# 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

# 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

# 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

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":

# 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:

# 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?