Selaa lähdekoodia

chore: create first api version to rag

tiago.cipriano 2 päivää sitten
commit
b5ae6f1934
18 muutettua tiedostoa jossa 1086 lisäystä ja 0 poistoa
  1. 20 0
      .dockerignore
  2. 1 0
      .tool-versions
  3. 39 0
      Dockerfile
  4. 227 0
      README.md
  5. 45 0
      api.ts
  6. 0 0
      arquivos/erro/.keep
  7. 0 0
      arquivos/novos/.keep
  8. 0 0
      arquivos/processados/.keep
  9. 26 0
      bun.lock
  10. 117 0
      busca.ts
  11. 153 0
      database.ts
  12. BIN
      embeddings.sqlite
  13. 7 0
      index.ts
  14. 311 0
      insert-embeddings.ts
  15. 12 0
      package.json
  16. 29 0
      tsconfig.json
  17. 44 0
      types.ts
  18. 55 0
      utils.ts

+ 20 - 0
.dockerignore

@@ -0,0 +1,20 @@
+node_modules
+Dockerfile*
+docker-compose*
+.dockerignore
+.git
+.gitignore
+README.md
+LICENSE
+.vscode
+Makefile
+helm-charts
+.env
+.editorconfig
+.idea
+coverage*
+prompts
+.vscode
+.tool-versions
+via-stdio.ts
+embeddings.sqlite

+ 1 - 0
.tool-versions

@@ -0,0 +1 @@
+bun 1.3.6

+ 39 - 0
Dockerfile

@@ -0,0 +1,39 @@
+# docker pull oven/bun:1.3.8-debian
+FROM oven/bun:1.3.8-debian AS base
+WORKDIR /usr/src/app
+
+# install dependencies into temp directory
+# this will cache them and speed up future builds
+FROM base AS install
+RUN mkdir -p /temp/dev
+COPY package.json bun.lock /temp/dev/
+RUN cd /temp/dev && bun install --frozen-lockfile
+
+
+# install with --production (exclude devDependencies)
+RUN mkdir -p /temp/prod
+COPY package.json bun.lock /temp/prod/
+RUN cd /temp/prod && bun install --frozen-lockfile --production
+
+# copy node_modules from temp directory
+# then copy all (non-ignored) project files into the image
+FROM base AS prerelease
+COPY --from=install /temp/dev/node_modules node_modules
+COPY . .
+
+
+# [optional] tests & build
+ENV NODE_ENV=production
+# RUN bun test
+# RUN bun run build  # Removido: não há script build, e Bun executa TS diretamente
+
+# copy production dependencies and source code into final image
+FROM base AS release
+COPY --from=install /temp/prod/node_modules node_modules
+COPY --from=prerelease /usr/src/app/* .
+
+
+# run the app
+USER bun
+EXPOSE 3000/tcp
+ENTRYPOINT [ "bun", "run", "api.ts" ]

+ 227 - 0
README.md

@@ -0,0 +1,227 @@
+# Academic Database RAG System
+
+A Retrieval-Augmented Generation (RAG) system designed for academic document search and retrieval. This system uses vector embeddings to enable semantic search across academic content, providing relevant context for AI-powered question answering.
+
+## Features
+
+- **Vector Search**: Semantic search using cosine similarity on document embeddings
+- **Document Ingestion**: Automatic processing and chunking of Markdown documents
+- **REST API**: HTTP API for querying similar documents
+- **SQLite Vector Database**: Efficient storage of documents and their embeddings
+- **Ollama Integration**: Uses Ollama's embedding models for generating vector representations
+- **Docker Support**: Containerized deployment with persistent storage
+- **TypeScript**: Fully typed codebase with modern JavaScript runtime (Bun)
+
+## Architecture
+
+The system consists of several key components:
+
+1. **Document Processing** (`insert-embeddings.ts`): Reads Markdown files, chunks content, generates embeddings using Ollama, and stores in SQLite
+2. **Vector Database** (`database.ts`): SQLite-based storage for documents and their vector embeddings
+3. **Search Engine** (`busca.ts`): Performs semantic search using cosine similarity
+4. **API Server** (`api.ts`): RESTful API for querying the system
+5. **Utilities** (`utils.ts`): Helper functions for similarity calculations
+
+## Prerequisites
+
+- [Bun](https://bun.sh/) runtime (v1.3.6 or later)
+- [Ollama](https://ollama.ai/) with embedding model (`nomic-embed-text:latest`)
+- SQLite (automatically handled by Bun)
+
+## Installation
+
+1. **Clone the repository**
+
+   ```bash
+   git clone <repository-url>
+   cd base-de-dados-academia
+   ```
+
+2. **Install dependencies**
+
+   ```bash
+   bun install
+   ```
+
+3. **Start Ollama and pull the embedding model**
+
+   ```bash
+   ollama serve
+   ollama pull nomic-embed-text:latest
+   ```
+
+## Usage
+
+### Running the API Server
+
+Start the REST API server:
+
+```bash
+bun run api.ts
+```
+
+The server will start on port 3000 (configurable via `PORT` environment variable).
+
+### Adding Documents
+
+1. Place new Markdown (`.md`) files in the `arquivos/novos/` directory
+2. Run the document ingestion script:
+
+```bash
+bun run insert-embeddings.ts
+```
+
+This will:
+
+- Process all `.md` files in `arquivos/novos/`
+- Generate embeddings for document chunks
+- Store them in the vector database
+- Move processed files to `arquivos/processados/`
+- Move failed files to `arquivos/erro/`
+
+### Testing the Search
+
+Run the example search script:
+
+```bash
+bun run index.ts
+```
+
+This demonstrates searching for similar documents to a sample query.
+
+## API Reference
+
+### POST /api/embeddings
+
+Search for documents similar to a given prompt.
+
+**Request Body:**
+
+```json
+{
+  "prompt": "What is the suffix of a markdown file?",
+  "topK": 3,
+  "limiarSimilaridade": 0.5
+}
+```
+
+**Parameters:**
+
+- `prompt` (required): The search query text
+- `topK` (optional): Number of top results to return (default: 3)
+- `limiarSimilaridade` (optional): Minimum similarity threshold (default: 0.5)
+
+**Response:**
+
+```json
+{
+  "contexto": "Documentos relevantes:\n\n--- Documento 1: file.md (similaridade: 0.85) ---\nContent...",
+  "resultados": [
+    {
+      "documento": {
+        "nome": "file.md",
+        "caminho": "/path/to/file.md",
+        "conteudo": "Content...",
+        "tamanho": 1234,
+        "embedding": [0.1, 0.2, ...]
+      },
+      "similaridade": 0.85
+    }
+  ]
+}
+```
+
+**Example using curl:**
+
+```http
+curl -X POST http://localhost:3000/api/embeddings \
+  -H "Content-Type: application/json" \
+  -d '{"prompt": "on docker?", "topK": 3, "limiarSimilaridade": 0.5}'
+```
+
+## Configuration
+
+The system can be configured using environment variables:
+
+- `PORT`: API server port (default: 3000)
+- `DB_PATH`: Path to SQLite database file (default: `./embeddings.sqlite`)
+- `OLLAMA_BASE_URL`: Ollama API endpoint (default: `http://localhost:11434`)
+
+## Docker Deployment
+
+### Build the Image
+
+```bash
+docker build --pull -t rag-academia-server .
+```
+
+### Run the Container
+
+```bash
+docker run \
+  --restart=always \
+  -v $(pwd)/embeddings.sqlite:/tmp/embeddings.sqlite \
+  --name rag-academia-server \
+  -e OLLAMA_BASE_URL=http://host.docker.internal:11434 \
+  -e DB_PATH=/tmp/embeddings.sqlite \
+  -e PORT=3000 \
+  --network=host \
+  -d \
+  rag-academia-server
+```
+
+**Notes:**
+
+- Mount the database file as a volume to persist data
+- Use `host.docker.internal` to access Ollama running on the host
+- Adjust `OLLAMA_BASE_URL` if Ollama is running in a separate container
+
+## How It Works
+
+1. **Document Ingestion**:
+   - Markdown files are read from `arquivos/novos/`
+   - Content is split into chunks (~2000 characters)
+   - Each chunk is converted to a vector embedding using Ollama
+   - Embeddings are stored in SQLite with metadata
+
+2. **Query Processing**:
+   - User query is converted to an embedding
+   - Cosine similarity is calculated against all stored embeddings
+   - Top-K most similar documents are returned
+   - Results are formatted into a context string for LLM consumption
+
+3. **Similarity Calculation**:
+   - Uses cosine similarity: `cos(θ) = (A · B) / (||A|| × ||B||)`
+   - Values range from -1 to 1, where higher values indicate greater similarity
+
+## Project Structure
+
+```txt
+├── api.ts                 # REST API server
+├── busca.ts               # Search and similarity functions
+├── database.ts            # SQLite vector database operations
+├── index.ts               # Example usage script
+├── insert-embeddings.ts   # Document ingestion pipeline
+├── types.ts               # TypeScript type definitions
+├── utils.ts               # Utility functions
+├── arquivos/              # Document storage
+│   ├── novos/            # New documents to process
+│   ├── processados/      # Successfully processed documents
+│   └── erro/             # Failed processing documents
+├── prompts/              # Prompt templates
+├── Dockerfile            # Container configuration
+├── package.json          # Dependencies and scripts
+└── tsconfig.json         # TypeScript configuration
+```
+
+## Contributing
+
+1. Fork the repository
+2. Create a feature branch
+3. Make your changes
+4. Add tests if applicable
+5. Submit a pull request
+
+## License
+
+This project is licensed under the MIT License - see the LICENSE file for details.

+ 45 - 0
api.ts

@@ -0,0 +1,45 @@
+import { montarContexto, buscarDocumentosSimilares } from "./busca";
+
+async function main() {
+    const port = process.env.PORT ? parseInt(process.env.PORT) : 3000;
+
+    const server = Bun.serve({
+        port: port,
+        async fetch(req) {
+            if (
+                req.url.endsWith("/api/embeddings") &&
+                req.method === "POST" && 
+                req.headers.get("Content-Type")?.includes("application/json")
+            ) {
+                try {
+                    // Here you would handle the embeddings API logic
+                    const body = await req.json() as { prompt?: string; topK?: number; limiarSimilaridade?: number };
+                    let { prompt, topK ,limiarSimilaridade} = body;
+                    if (!prompt) {
+                        return new Response("Faltando o campo 'prompt' no corpo da requisição", { status: 400 });
+                    }
+                    if (!topK) {
+                        topK = 3;
+                    }
+
+                    if (!limiarSimilaridade) {
+                        limiarSimilaridade=0.5;
+                    }
+
+                    const resultados = await buscarDocumentosSimilares(prompt, topK, limiarSimilaridade);
+                    const contexto = montarContexto(resultados);
+                    console.log('Contexto montado:', contexto);
+                    return new Response(JSON.stringify({ contexto, resultados }), { headers: { "Content-Type": "application/json" } });
+                } catch (error) {
+                    console.error('Erro ao processar requisição:', error);
+                    return new Response("Erro ao processar JSON: " + (error instanceof Error ? error.message : String(error)), { status: 400 });
+                }
+            }
+
+            return new Response("Embedding API is running");
+        },
+    });
+    console.log(`Server running on http://:${port}`);
+}
+
+main().catch(error => {console.error(error);process.exit(1);});

+ 0 - 0
arquivos/erro/.keep


+ 0 - 0
arquivos/novos/.keep


+ 0 - 0
arquivos/processados/.keep


+ 26 - 0
bun.lock

@@ -0,0 +1,26 @@
+{
+  "lockfileVersion": 1,
+  "configVersion": 1,
+  "workspaces": {
+    "": {
+      "name": "base-de-dados-academia",
+      "devDependencies": {
+        "@types/bun": "latest",
+      },
+      "peerDependencies": {
+        "typescript": "^5",
+      },
+    },
+  },
+  "packages": {
+    "@types/bun": ["@types/bun@1.3.8", "", { "dependencies": { "bun-types": "1.3.8" } }, "sha512-3LvWJ2q5GerAXYxO2mffLTqOzEu5qnhEAlh48Vnu8WQfnmSwbgagjGZV6BoHKJztENYEDn6QmVd949W4uESRJA=="],
+
+    "@types/node": ["@types/node@25.1.0", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-t7frlewr6+cbx+9Ohpl0NOTKXZNV9xHRmNOvql47BFJKcEG1CxtxlPEEe+gR9uhVWM4DwhnvTF110mIL4yP9RA=="],
+
+    "bun-types": ["bun-types@1.3.8", "", { "dependencies": { "@types/node": "*" } }, "sha512-fL99nxdOWvV4LqjmC+8Q9kW3M4QTtTR1eePs94v5ctGqU8OeceWrSUaRw3JYb7tU3FkMIAjkueehrHPPPGKi5Q=="],
+
+    "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
+
+    "undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
+  }
+}

+ 117 - 0
busca.ts

@@ -0,0 +1,117 @@
+import { BancoVetorial, type DocumentoComEmbedding } from './database';
+import { gerarEmbedding } from './insert-embeddings';
+import { calcularSimilaridadeCosseno } from './utils';
+import type { ResultadoBusca } from './types';
+
+/**
+ * Busca os documentos mais similares a uma pergunta
+ * 
+ * Este é o coração do sistema RAG!
+ * 
+ * Processo:
+ * 1. Converte a pergunta em embedding
+ * 2. Busca todos os documentos do banco
+ * 3. Calcula similaridade entre pergunta e cada documento
+ * 4. Ordena por similaridade (maior primeiro)
+ * 5. Retorna os N mais relevantes
+ * 
+ * @param pergunta - A pergunta do usuário
+ * @param topK - Quantos documentos retornar (padrão: 3)
+ * @param limiarSimilaridade - Similaridade mínima para considerar relevante (padrão: 0.5)
+ * @returns Array com os documentos mais similares
+ */
+async function buscarDocumentosSimilares(
+  pergunta: string,
+  topK: number = 3,
+  limiarSimilaridade: number = 0.5
+): Promise<ResultadoBusca[]> {
+  console.log(`\nBuscando documentos similares a: "${pergunta}"`);
+  console.log(`Parametros: topK=${topK}, limiar=${limiarSimilaridade}\n`);
+
+  try {
+    // Passo 1: Gerar embedding da pergunta
+    console.log('Gerando embedding da pergunta...');
+    const embeddingPergunta = await gerarEmbedding(pergunta);
+    console.log(`  - Embedding gerado: ${embeddingPergunta.length} dimensoes`);
+
+    // Passo 2: Buscar todos os documentos do banco
+    console.log('\nBuscando documentos no banco...');
+    const banco = new BancoVetorial();
+    const todosDocumentos = banco.buscarTodosDocumentos();
+    console.log(`  - Encontrados ${todosDocumentos.length} documentos`);
+
+    // Passo 3: Calcular similaridade para cada documento
+    console.log('\nCalculando similaridades...');
+    const resultados: ResultadoBusca[] = [];
+
+    for (const documento of todosDocumentos) {
+      const similaridade = calcularSimilaridadeCosseno(
+        embeddingPergunta,
+        documento.embedding
+      );
+
+      console.log(`  - ${documento.nome}: ${similaridade.toFixed(4)}`);
+
+      // Só adiciona se passar no limiar de similaridade
+      if (similaridade >= limiarSimilaridade) {
+        resultados.push({
+          documento,
+          similaridade
+        });
+      }
+    }
+
+    // Passo 4: Ordenar por similaridade (maior primeiro)
+    // Sort é in-place, modifica o array original
+    resultados.sort((a, b) => b.similaridade - a.similaridade);
+
+    // Passo 5: Retornar apenas os topK resultados
+    const resultadosFinais = resultados.slice(0, topK);
+
+    console.log(`\nDocumentos relevantes encontrados: ${resultadosFinais.length}`);
+
+    // Fecha a conexão com o banco
+    banco.fechar();
+
+    return resultadosFinais;
+
+  } catch (erro) {
+    console.error('Erro na busca:', erro);
+    throw new Error('Falha ao buscar documentos similares');
+  }
+}
+
+/**
+ * Monta um contexto concatenando os documentos mais relevantes
+ * 
+ * Por que montar um contexto?
+ * - A LLM precisa receber os documentos relevantes junto com a pergunta
+ * - Isso permite que ela responda baseada em informações reais
+ * - Reduz alucinações e aumenta a precisão
+ * 
+ * @param resultados - Array com os resultados da busca
+ * @returns String com o contexto formatado
+ */
+function montarContexto(resultados: ResultadoBusca[]): string {
+  if (resultados.length === 0) {
+    return 'Nenhum documento relevante encontrado.';
+  }
+
+  // Construímos o contexto formatado
+  let contexto = 'Documentos relevantes:\n\n';
+
+  resultados.forEach((resultado, index) => {
+    contexto += `--- Documento ${index + 1}: ${resultado.documento.nome} (similaridade: ${resultado.similaridade.toFixed(2)}) ---\n`;
+    contexto += `${resultado.documento.conteudo}\n\n`;
+  });
+
+  return contexto;
+}
+
+// Exporta para ser usado em outros arquivos
+export { 
+  buscarDocumentosSimilares, 
+  montarContexto,
+  calcularSimilaridadeCosseno,
+  type ResultadoBusca 
+};

+ 153 - 0
database.ts

@@ -0,0 +1,153 @@
+import { Database } from 'bun:sqlite';
+import { readdir, readFile } from 'fs/promises';
+import { join } from 'path';
+import { gerarEmbedding } from './insert-embeddings';
+import type { Documento, DocumentoComEmbedding, DocumentoBD } from './types';
+
+/**
+ * Classe para gerenciar o banco de dados de embeddings
+ * 
+ * Por que uma classe?
+ * - Encapsula toda a lógica de banco de dados (Single Responsibility)
+ * - Facilita testes e manutenção
+ * - Permite reutilizar em outros lugares do projeto
+ */
+class BancoVetorial {
+  private db: Database;
+
+  constructor(caminhoDb: string = process.env.DB_PATH || './embeddings.sqlite') {
+    // Cria ou abre o banco de dados
+    this.db = new Database(caminhoDb);
+    
+    // Inicializa as tabelas
+    this.inicializarTabelas();
+  }
+
+  /**
+   * Cria a tabela de documentos se ela não existir
+   * 
+   * Por que IF NOT EXISTS?
+   * - Permite executar múltiplas vezes sem erro
+   * - Facilita deploy e atualizações
+   */
+  private inicializarTabelas(): void {
+    this.db.run(`
+      CREATE TABLE IF NOT EXISTS documentos (
+        id INTEGER PRIMARY KEY AUTOINCREMENT,
+        nome TEXT NOT NULL,
+        caminho TEXT NOT NULL UNIQUE,
+        conteudo TEXT NOT NULL,
+        embedding TEXT NOT NULL,
+        data_indexacao TEXT NOT NULL
+      )
+    `);
+
+    console.log('Tabela de documentos inicializada');
+  }
+
+  /**
+   * Insere ou atualiza um documento no banco
+   * 
+   * Por que INSERT OR REPLACE?
+   * - Se o documento já existe (mesmo caminho), atualiza
+   * - Se não existe, insere novo
+   * - Evita duplicatas
+   */
+  inserirDocumento(documento: DocumentoComEmbedding): void {
+    // Convertemos o array de números para JSON string
+    const embeddingJson = JSON.stringify(documento.embedding);
+    
+    // Data atual no formato ISO
+    const dataIndexacao = new Date().toISOString();
+
+    const stmt = this.db.prepare(`
+      INSERT OR REPLACE INTO documentos (nome, caminho, conteudo, embedding, data_indexacao)
+      VALUES (?, ?, ?, ?, ?)
+    `);
+
+    stmt.run(
+      documento.nome,
+      documento.caminho,
+      documento.conteudo,
+      embeddingJson,
+      dataIndexacao
+    );
+
+    console.log(`Documento inserido: ${documento.nome}`);
+  }
+
+  /**
+   * Insere múltiplos documentos em uma transação
+   * 
+   * Por que transação?
+   * - Muito mais rápido (até 100x)
+   * - Garante atomicidade: ou todos são inseridos ou nenhum
+   * - Se der erro no meio, faz rollback automático
+   */
+  inserirDocumentosEmLote(documentos: DocumentoComEmbedding[]): void {
+    const transaction = this.db.transaction((docs: DocumentoComEmbedding[]) => {
+      for (const doc of docs) {
+        this.inserirDocumento(doc);
+      }
+    });
+
+    transaction(documentos);
+    console.log(`${documentos.length} documentos inseridos em lote`);
+  }
+
+  /**
+   * Busca todos os documentos do banco
+   * 
+   * Por que retornar DocumentoComEmbedding[]?
+   * - Mantém a interface consistente com o resto do código
+   * - Facilita reutilizar em outras partes do sistema
+   */
+  buscarTodosDocumentos(): DocumentoComEmbedding[] {
+    const stmt = this.db.prepare('SELECT * FROM documentos');
+    const rows = stmt.all() as DocumentoBD[];
+
+    // Convertemos os dados do banco para a interface que usamos no código
+    return rows.map(row => ({
+      nome: row.nome,
+      caminho: row.caminho,
+      conteudo: row.conteudo,
+      tamanho: row.conteudo.length,
+      embedding: JSON.parse(row.embedding)  // Converte JSON string de volta para array
+    }));
+  }
+
+  /**
+   * Conta quantos documentos estão indexados
+   */
+  contarDocumentos(): number {
+    const stmt = this.db.prepare('SELECT COUNT(*) as total FROM documentos');
+    const result = stmt.get() as { total: number };
+    return result.total;
+  }
+
+  /**
+   * Limpa todos os documentos do banco
+   * 
+   * Útil para reindexar tudo do zero
+   */
+  limparDocumentos(): void {
+    this.db.run('DELETE FROM documentos');
+    console.log('Todos os documentos foram removidos');
+  }
+
+  /**
+   * Fecha a conexão com o banco
+   * 
+   * Por que fechar?
+   * - Libera recursos
+   * - Garante que todos os dados foram salvos
+   * - Boa prática de gerenciamento de recursos
+   */
+  fechar(): void {
+    this.db.close();
+    console.log('Conexao com banco fechada');
+  }
+}
+
+// Exporta para ser usado em outros arquivos
+export { BancoVetorial, type DocumentoComEmbedding };

BIN
embeddings.sqlite


+ 7 - 0
index.ts

@@ -0,0 +1,7 @@
+import { montarContexto, buscarDocumentosSimilares } from "./busca";
+
+// Exemplo de uso
+const pergunta = 'Bun applications with Docker?';
+const resultados = await buscarDocumentosSimilares(pergunta, 2, 0.3);
+const contexto = montarContexto(resultados);
+console.log('Contexto montado:', contexto);

+ 311 - 0
insert-embeddings.ts

@@ -0,0 +1,311 @@
+import { readdir, readFile, rename } from 'fs/promises';
+import { join, dirname } from 'path';
+import { BancoVetorial } from './database';
+import type { Documento, DocumentoComEmbedding, OllamaEmbeddingRequest, OllamaEmbeddingResponse } from './types';
+
+// Importamos a URL do Ollama do arquivo de configuração
+// Seguindo o princípio DRY (Don't Repeat Yourself)
+const OLLAMA_BASE_URL = process.env.OLLAMA_BASE_URL || 'http://localhost:11434';
+
+
+/**
+ * Função que gera o embedding de um texto usando o Ollama
+ * 
+ * Por que uma função separada?
+ * - Facilita testes unitários
+ * - Permite reutilizar em outros lugares
+ * - Segue o princípio de Responsabilidade Única (SOLID)
+ * 
+ * @param texto - O texto que será convertido em embedding
+ * @returns Promise com o vetor de números (embedding)
+ */
+async function gerarEmbedding(texto: string, model: string = 'nomic-embed-text:latest'): Promise<number[]> {
+  try {
+    // Preparamos o corpo da requisição
+    // O modelo nomic-embed-text é específico para gerar embeddings
+    const requestBody: OllamaEmbeddingRequest = {
+      model: model,
+      prompt: texto
+    };
+
+    // Fazemos a requisição para o endpoint de embeddings
+    // Note que é /api/embeddings, diferente do /api/chat usado antes
+    console.log('Enviando requisição de embedding para o Ollama... OLLAMA_BASE_URL:', OLLAMA_BASE_URL);
+    const response = await fetch(`${OLLAMA_BASE_URL}/api/embeddings`, {
+      method: 'POST',
+      headers: {
+        'Content-Type': 'application/json',
+      },
+      body: JSON.stringify(requestBody)
+    });
+
+    // Verificamos se deu certo
+    if (!response.ok) {
+      throw new Error(`Ollama retornou status ${response.status}`);
+    }
+
+    // Extraímos o embedding da resposta
+    const data = await response.json() as OllamaEmbeddingResponse;
+
+    return data.embedding;
+
+  } catch (erro) {
+    console.error('Erro ao gerar embedding:', erro);
+    throw new Error('Falha na geração do embedding');
+  }
+}
+
+
+
+/**
+ * Classe responsável por inserir novos embeddings no banco de dados
+ * 
+ * Responsabilidades:
+ * - Ler arquivos da pasta "novos"
+ * - Gerar embeddings para os arquivos
+ * - Inserir no banco de dados
+ * - Mover arquivos processados para "processados" ou "erro"
+ */
+export class InsertEmbeddings {
+  private pastaNovos: string;
+  private pastaProcessados: string;
+  private pastaErro: string;
+  private banco: BancoVetorial;
+
+  constructor(
+    pastaBase: string = join(__dirname, 'arquivos'),
+    caminhoDb: string = process.env.DB_PATH || join(__dirname, 'embeddings.sqlite')
+  ) {
+    this.pastaNovos = join(pastaBase, 'novos');
+    this.pastaProcessados = join(pastaBase, 'processados');
+    this.pastaErro = join(pastaBase, 'erro');
+    this.banco = new BancoVetorial(caminhoDb);
+  }
+
+  /**
+   * Lê todos os arquivos Markdown da pasta "novos"
+   */
+  private async lerArquivosNovos(): Promise<Documento[]> {
+    try {
+      console.log(`Lendo arquivos da pasta: ${this.pastaNovos}`);
+
+      const arquivos = await readdir(this.pastaNovos);
+      const arquivosMarkdown = arquivos.filter(arquivo =>
+        arquivo.endsWith('.md')
+      );
+
+      console.log(`Encontrados ${arquivosMarkdown.length} arquivos Markdown`);
+
+      const documentos: Documento[] = [];
+
+      for (const nomeArquivo of arquivosMarkdown) {
+        const caminhoCompleto = join(this.pastaNovos, nomeArquivo);
+        console.log(`Processando: ${nomeArquivo}`);
+
+        const conteudo = await readFile(caminhoCompleto, 'utf-8');
+
+        const documento: Documento = {
+          nome: nomeArquivo,
+          caminho: caminhoCompleto,
+          conteudo: conteudo,
+          tamanho: conteudo.length
+        };
+
+        documentos.push(documento);
+        console.log(`  - Tamanho: ${documento.tamanho} caracteres`);
+      }
+
+      return documentos;
+
+    } catch (erro) {
+      console.error('Erro ao ler arquivos novos:', erro);
+      throw new Error('Falha na leitura dos arquivos novos');
+    }
+  }
+
+  /**
+   * Divide o conteúdo de um documento em chunks menores
+   * @param conteudo - Texto completo do documento
+   * @param tamanhoChunk - Número máximo de caracteres por chunk (padrão: 1000)
+   * @returns Array de strings (chunks)
+   */
+  private dividirEmChunks(conteudo: string, tamanhoChunk: number = 2000): string[] {
+    const chunks: string[] = [];
+    let inicio = 0;
+    
+    while (inicio < conteudo.length) {
+      let fim = inicio + tamanhoChunk;
+      
+      // Tenta cortar em uma quebra de linha para não dividir sentenças
+      if (fim < conteudo.length) {
+        const quebraLinha = conteudo.lastIndexOf('\n', fim);
+        if (quebraLinha > inicio) {
+          fim = quebraLinha;
+        }
+      }
+      
+      chunks.push(conteudo.slice(inicio, fim).trim());
+      inicio = fim;
+    }
+    
+    return chunks;
+  }
+
+  /**
+   * Processa um documento individual, gerando embeddings para seus chunks
+   */
+  private async processarDocumento(documento: Documento): Promise<DocumentoComEmbedding[]> {
+    console.log(`Processando chunks para: ${documento.nome}`);
+    
+    const chunks = this.dividirEmChunks(documento.conteudo);
+    const documentosComEmbeddings: DocumentoComEmbedding[] = [];
+    
+    for (let i = 0; i < chunks.length; i++) {
+      const chunk = chunks[i];
+      console.log(`  - Chunk ${i + 1}/${chunks.length}: ${chunk.length} caracteres`);
+      
+      try {
+        const embedding = await gerarEmbedding(chunk);
+        
+        const documentoChunk: DocumentoComEmbedding = {
+          nome: `${documento.nome}_chunk_${i + 1}`,
+          caminho: documento.caminho,
+          conteudo: chunk,
+          tamanho: chunk.length,
+          embedding: embedding
+        };
+        
+        documentosComEmbeddings.push(documentoChunk);
+      } catch (erro) {
+        console.error(`Erro no chunk ${i + 1}:`, erro);
+      }
+    }
+    
+    return documentosComEmbeddings;
+  }
+
+  /**
+   * Move um arquivo para a pasta de processados
+   */
+  private async moverParaProcessados(caminhoArquivo: string): Promise<void> {
+    const nomeArquivo = caminhoArquivo.split('/').pop()!;
+    const novoCaminho = join(this.pastaProcessados, nomeArquivo);
+
+    try {
+      await rename(caminhoArquivo, novoCaminho);
+      console.log(`Arquivo movido para processados: ${nomeArquivo}`);
+    } catch (erro) {
+      console.error(`Erro ao mover arquivo para processados: ${nomeArquivo}`, erro);
+    }
+  }
+
+  /**
+   * Move um arquivo para a pasta de erro
+   */
+  private async moverParaErro(caminhoArquivo: string): Promise<void> {
+    const nomeArquivo = caminhoArquivo.split('/').pop()!;
+    const novoCaminho = join(this.pastaErro, nomeArquivo);
+
+    try {
+      await rename(caminhoArquivo, novoCaminho);
+      console.log(`Arquivo movido para erro: ${nomeArquivo}`);
+    } catch (erro) {
+      console.error(`Erro ao mover arquivo para erro: ${nomeArquivo}`, erro);
+    }
+  }
+
+  /**
+   * Processa todos os novos documentos e insere no banco
+   */
+  async processarNovosEmbeddings(): Promise<void> {
+    console.log('=== INICIANDO PROCESSAMENTO DE NOVOS EMBEDDINGS ===\n');
+
+    try {
+      // 1. Ler arquivos novos
+      const documentos = await this.lerArquivosNovos();
+
+      if (documentos.length === 0) {
+        console.log('Nenhum arquivo novo encontrado.');
+        return;
+      }
+
+      console.log('\nGerando embeddings...\n');
+
+      const documentosComEmbeddings: DocumentoComEmbedding[] = [];
+      const arquivosComErro: string[] = [];
+
+      // 2. Processar cada documento
+      for (const documento of documentos) {
+        try {
+          const documentosComEmbedding = await this.processarDocumento(documento);
+          documentosComEmbeddings.push(...documentosComEmbedding);
+
+          // Mover para processados
+          await this.moverParaProcessados(documento.caminho);
+
+        } catch (erro) {
+          console.error(`Falha ao processar ${documento.nome}, movendo para erro`);
+          arquivosComErro.push(documento.caminho);
+          await this.moverParaErro(documento.caminho);
+        }
+      }
+
+      // 3. Inserir no banco se houver documentos válidos
+      if (documentosComEmbeddings.length > 0) {
+        console.log('\nInserindo no banco de dados...');
+        this.banco.inserirDocumentosEmLote(documentosComEmbeddings);
+        console.log(`${documentosComEmbeddings.length} documentos inseridos com sucesso`);
+      }
+
+      // 4. Resumo
+      console.log('\n=== RESUMO DO PROCESSAMENTO ===');
+      console.log(`Total de arquivos encontrados: ${documentos.length}`);
+      console.log(`Processados com sucesso: ${documentosComEmbeddings.length}`);
+      console.log(`Com erro: ${arquivosComErro.length}`);
+
+      if (arquivosComErro.length > 0) {
+        console.log('\nArquivos com erro:');
+        arquivosComErro.forEach(caminho => {
+          console.log(`  - ${caminho.split('/').pop()}`);
+        });
+      }
+
+    } catch (erro) {
+      console.error('Erro no processamento de novos embeddings:', erro);
+      throw erro;
+    } finally {
+      // Fechar conexão com o banco
+      this.banco.fechar();
+    }
+  }
+
+  /**
+   * Fecha a conexão com o banco de dados
+   */
+  fechar(): void {
+    this.banco.fechar();
+  }
+}
+
+/**
+ * Função principal para executar o processamento de novos embeddings
+ */
+async function main() {
+  const insertEmbeddings = new InsertEmbeddings();
+
+  try {
+    await insertEmbeddings.processarNovosEmbeddings();
+    console.log('\nProcessamento concluído com sucesso!');
+  } catch (erro) {
+    console.error('Erro no processamento:', erro);
+    process.exit(1);
+  }
+}
+
+// Executa apenas se for chamado diretamente
+if (import.meta.main) {
+  main().catch(console.error);
+}
+
+export { gerarEmbedding };
+export default InsertEmbeddings;

+ 12 - 0
package.json

@@ -0,0 +1,12 @@
+{
+  "name": "base-de-dados-academia",
+  "module": "index.ts",
+  "type": "module",
+  "private": true,
+  "devDependencies": {
+    "@types/bun": "latest"
+  },
+  "peerDependencies": {
+    "typescript": "^5"
+  }
+}

+ 29 - 0
tsconfig.json

@@ -0,0 +1,29 @@
+{
+  "compilerOptions": {
+    // Environment setup & latest features
+    "lib": ["ESNext"],
+    "target": "ESNext",
+    "module": "Preserve",
+    "moduleDetection": "force",
+    "jsx": "react-jsx",
+    "allowJs": true,
+
+    // Bundler mode
+    "moduleResolution": "bundler",
+    "allowImportingTsExtensions": true,
+    "verbatimModuleSyntax": true,
+    "noEmit": true,
+
+    // Best practices
+    "strict": true,
+    "skipLibCheck": true,
+    "noFallthroughCasesInSwitch": true,
+    "noUncheckedIndexedAccess": true,
+    "noImplicitOverride": true,
+
+    // Some stricter flags (disabled by default)
+    "noUnusedLocals": false,
+    "noUnusedParameters": false,
+    "noPropertyAccessFromIndexSignature": false
+  }
+}

+ 44 - 0
types.ts

@@ -0,0 +1,44 @@
+/**
+ * Shared types for the RAG system.
+ * Centralizes interfaces to avoid duplication across modules.
+ */
+
+// Interface for structuring document data
+export interface Documento {
+  nome: string;
+  caminho: string;
+  conteudo: string;
+  tamanho: number;
+}
+
+// Interface for a document with its embedding
+export interface DocumentoComEmbedding extends Documento {
+  embedding: number[];
+}
+
+// Interface for a document in the database
+export interface DocumentoBD {
+  id: number;
+  nome: string;
+  caminho: string;
+  conteudo: string;
+  embedding: string;  // JSON stringified
+  data_indexacao: string;
+}
+
+// Interface for Ollama embedding request
+export interface OllamaEmbeddingRequest {
+  model: string;
+  prompt: string;
+}
+
+// Interface for Ollama embedding response
+export interface OllamaEmbeddingResponse {
+  embedding: number[];
+}
+
+// Interface for search results
+export interface ResultadoBusca {
+  documento: DocumentoComEmbedding;
+  similaridade: number;  // Value between -1 and 1 (higher is more similar)
+}

+ 55 - 0
utils.ts

@@ -0,0 +1,55 @@
+/**
+ * Utility functions for the RAG system
+ */
+
+/**
+ * Calcula a similaridade de cosseno entre dois vetores
+ *
+ * Por que usar cosseno?
+ * - É a métrica padrão para comparar embeddings
+ * - Normaliza automaticamente (não depende do tamanho do vetor)
+ * - Valores entre -1 e 1 são fáceis de interpretar
+ *
+ * Fórmula: cos(θ) = (A · B) / (||A|| × ||B||)
+ *
+ * @param vetorA - Primeiro vetor (ex: embedding da pergunta)
+ * @param vetorB - Segundo vetor (ex: embedding do documento)
+ * @returns Similaridade entre -1 e 1
+ */
+export function calcularSimilaridadeCosseno(vetorA: number[], vetorB: number[]): number {
+  // Validação: vetores devem ter o mesmo tamanho
+  if (vetorA.length !== vetorB.length) {
+    throw new Error('Vetores devem ter o mesmo tamanho');
+  }
+
+  // Passo 1: Calcular o produto escalar (A · B)
+  // Multiplicamos cada elemento correspondente e somamos
+  let produtoEscalar = 0;
+  for (let i = 0; i < vetorA.length; i++) {
+    produtoEscalar += (vetorA[i] ?? 0) * (vetorB[i] ?? 0);
+  }
+
+  // Passo 2: Calcular a magnitude de A (||A||)
+  // Raiz quadrada da soma dos quadrados
+  let magnitudeA = 0;
+  for (let i = 0; i < vetorA.length; i++) {
+    magnitudeA += (vetorA[i] ?? 0) * (vetorA[i] ?? 0);
+  }
+  magnitudeA = Math.sqrt(magnitudeA);
+
+  // Passo 3: Calcular a magnitude de B (||B||)
+  let magnitudeB = 0;
+  for (let i = 0; i < vetorB.length; i++) {
+    magnitudeB += (vetorB[i] ?? 0) * (vetorB[i] ?? 0);
+  }
+  magnitudeB = Math.sqrt(magnitudeB);
+
+  // Passo 4: Calcular a similaridade
+  // Dividimos o produto escalar pelo produto das magnitudes
+  // Evitamos divisão por zero
+  if (magnitudeA === 0 || magnitudeB === 0) {
+    return 0;
+  }
+
+  return produtoEscalar / (magnitudeA * magnitudeB);
+}