insert-embeddings.ts 9.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311
  1. import { readdir, readFile, rename } from 'fs/promises';
  2. import { join, dirname } from 'path';
  3. import { BancoVetorial } from './database';
  4. import type { Documento, DocumentoComEmbedding, OllamaEmbeddingRequest, OllamaEmbeddingResponse } from './types';
  5. // Importamos a URL do Ollama do arquivo de configuração
  6. // Seguindo o princípio DRY (Don't Repeat Yourself)
  7. const OLLAMA_BASE_URL = process.env.OLLAMA_BASE_URL || 'http://localhost:11434';
  8. /**
  9. * Função que gera o embedding de um texto usando o Ollama
  10. *
  11. * Por que uma função separada?
  12. * - Facilita testes unitários
  13. * - Permite reutilizar em outros lugares
  14. * - Segue o princípio de Responsabilidade Única (SOLID)
  15. *
  16. * @param texto - O texto que será convertido em embedding
  17. * @returns Promise com o vetor de números (embedding)
  18. */
  19. async function gerarEmbedding(texto: string, model: string = 'nomic-embed-text:latest'): Promise<number[]> {
  20. try {
  21. // Preparamos o corpo da requisição
  22. // O modelo nomic-embed-text é específico para gerar embeddings
  23. const requestBody: OllamaEmbeddingRequest = {
  24. model: model,
  25. prompt: texto
  26. };
  27. // Fazemos a requisição para o endpoint de embeddings
  28. // Note que é /api/embeddings, diferente do /api/chat usado antes
  29. console.log('Enviando requisição de embedding para o Ollama... OLLAMA_BASE_URL:', OLLAMA_BASE_URL);
  30. const response = await fetch(`${OLLAMA_BASE_URL}/api/embeddings`, {
  31. method: 'POST',
  32. headers: {
  33. 'Content-Type': 'application/json',
  34. },
  35. body: JSON.stringify(requestBody)
  36. });
  37. // Verificamos se deu certo
  38. if (!response.ok) {
  39. throw new Error(`Ollama retornou status ${response.status}`);
  40. }
  41. // Extraímos o embedding da resposta
  42. const data = await response.json() as OllamaEmbeddingResponse;
  43. return data.embedding;
  44. } catch (erro) {
  45. console.error('Erro ao gerar embedding:', erro);
  46. throw new Error('Falha na geração do embedding');
  47. }
  48. }
  49. /**
  50. * Classe responsável por inserir novos embeddings no banco de dados
  51. *
  52. * Responsabilidades:
  53. * - Ler arquivos da pasta "novos"
  54. * - Gerar embeddings para os arquivos
  55. * - Inserir no banco de dados
  56. * - Mover arquivos processados para "processados" ou "erro"
  57. */
  58. export class InsertEmbeddings {
  59. private pastaNovos: string;
  60. private pastaProcessados: string;
  61. private pastaErro: string;
  62. private banco: BancoVetorial;
  63. constructor(
  64. pastaBase: string = join(__dirname, 'arquivos'),
  65. caminhoDb: string = process.env.DB_PATH || join(__dirname, 'embeddings.sqlite')
  66. ) {
  67. this.pastaNovos = join(pastaBase, 'novos');
  68. this.pastaProcessados = join(pastaBase, 'processados');
  69. this.pastaErro = join(pastaBase, 'erro');
  70. this.banco = new BancoVetorial(caminhoDb);
  71. }
  72. /**
  73. * Lê todos os arquivos Markdown da pasta "novos"
  74. */
  75. private async lerArquivosNovos(): Promise<Documento[]> {
  76. try {
  77. console.log(`Lendo arquivos da pasta: ${this.pastaNovos}`);
  78. const arquivos = await readdir(this.pastaNovos);
  79. const arquivosMarkdown = arquivos.filter(arquivo =>
  80. arquivo.endsWith('.md')
  81. );
  82. console.log(`Encontrados ${arquivosMarkdown.length} arquivos Markdown`);
  83. const documentos: Documento[] = [];
  84. for (const nomeArquivo of arquivosMarkdown) {
  85. const caminhoCompleto = join(this.pastaNovos, nomeArquivo);
  86. console.log(`Processando: ${nomeArquivo}`);
  87. const conteudo = await readFile(caminhoCompleto, 'utf-8');
  88. const documento: Documento = {
  89. nome: nomeArquivo,
  90. caminho: caminhoCompleto,
  91. conteudo: conteudo,
  92. tamanho: conteudo.length
  93. };
  94. documentos.push(documento);
  95. console.log(` - Tamanho: ${documento.tamanho} caracteres`);
  96. }
  97. return documentos;
  98. } catch (erro) {
  99. console.error('Erro ao ler arquivos novos:', erro);
  100. throw new Error('Falha na leitura dos arquivos novos');
  101. }
  102. }
  103. /**
  104. * Divide o conteúdo de um documento em chunks menores
  105. * @param conteudo - Texto completo do documento
  106. * @param tamanhoChunk - Número máximo de caracteres por chunk (padrão: 1000)
  107. * @returns Array de strings (chunks)
  108. */
  109. private dividirEmChunks(conteudo: string, tamanhoChunk: number = 2000): string[] {
  110. const chunks: string[] = [];
  111. let inicio = 0;
  112. while (inicio < conteudo.length) {
  113. let fim = inicio + tamanhoChunk;
  114. // Tenta cortar em uma quebra de linha para não dividir sentenças
  115. if (fim < conteudo.length) {
  116. const quebraLinha = conteudo.lastIndexOf('\n', fim);
  117. if (quebraLinha > inicio) {
  118. fim = quebraLinha;
  119. }
  120. }
  121. chunks.push(conteudo.slice(inicio, fim).trim());
  122. inicio = fim;
  123. }
  124. return chunks;
  125. }
  126. /**
  127. * Processa um documento individual, gerando embeddings para seus chunks
  128. */
  129. private async processarDocumento(documento: Documento): Promise<DocumentoComEmbedding[]> {
  130. console.log(`Processando chunks para: ${documento.nome}`);
  131. const chunks = this.dividirEmChunks(documento.conteudo);
  132. const documentosComEmbeddings: DocumentoComEmbedding[] = [];
  133. for (let i = 0; i < chunks.length; i++) {
  134. const chunk = chunks[i];
  135. console.log(` - Chunk ${i + 1}/${chunks.length}: ${chunk.length} caracteres`);
  136. try {
  137. const embedding = await gerarEmbedding(chunk);
  138. const documentoChunk: DocumentoComEmbedding = {
  139. nome: `${documento.nome}_chunk_${i + 1}`,
  140. caminho: documento.caminho,
  141. conteudo: chunk,
  142. tamanho: chunk.length,
  143. embedding: embedding
  144. };
  145. documentosComEmbeddings.push(documentoChunk);
  146. } catch (erro) {
  147. console.error(`Erro no chunk ${i + 1}:`, erro);
  148. }
  149. }
  150. return documentosComEmbeddings;
  151. }
  152. /**
  153. * Move um arquivo para a pasta de processados
  154. */
  155. private async moverParaProcessados(caminhoArquivo: string): Promise<void> {
  156. const nomeArquivo = caminhoArquivo.split('/').pop()!;
  157. const novoCaminho = join(this.pastaProcessados, nomeArquivo);
  158. try {
  159. await rename(caminhoArquivo, novoCaminho);
  160. console.log(`Arquivo movido para processados: ${nomeArquivo}`);
  161. } catch (erro) {
  162. console.error(`Erro ao mover arquivo para processados: ${nomeArquivo}`, erro);
  163. }
  164. }
  165. /**
  166. * Move um arquivo para a pasta de erro
  167. */
  168. private async moverParaErro(caminhoArquivo: string): Promise<void> {
  169. const nomeArquivo = caminhoArquivo.split('/').pop()!;
  170. const novoCaminho = join(this.pastaErro, nomeArquivo);
  171. try {
  172. await rename(caminhoArquivo, novoCaminho);
  173. console.log(`Arquivo movido para erro: ${nomeArquivo}`);
  174. } catch (erro) {
  175. console.error(`Erro ao mover arquivo para erro: ${nomeArquivo}`, erro);
  176. }
  177. }
  178. /**
  179. * Processa todos os novos documentos e insere no banco
  180. */
  181. async processarNovosEmbeddings(): Promise<void> {
  182. console.log('=== INICIANDO PROCESSAMENTO DE NOVOS EMBEDDINGS ===\n');
  183. try {
  184. // 1. Ler arquivos novos
  185. const documentos = await this.lerArquivosNovos();
  186. if (documentos.length === 0) {
  187. console.log('Nenhum arquivo novo encontrado.');
  188. return;
  189. }
  190. console.log('\nGerando embeddings...\n');
  191. const documentosComEmbeddings: DocumentoComEmbedding[] = [];
  192. const arquivosComErro: string[] = [];
  193. // 2. Processar cada documento
  194. for (const documento of documentos) {
  195. try {
  196. const documentosComEmbedding = await this.processarDocumento(documento);
  197. documentosComEmbeddings.push(...documentosComEmbedding);
  198. // Mover para processados
  199. await this.moverParaProcessados(documento.caminho);
  200. } catch (erro) {
  201. console.error(`Falha ao processar ${documento.nome}, movendo para erro`);
  202. arquivosComErro.push(documento.caminho);
  203. await this.moverParaErro(documento.caminho);
  204. }
  205. }
  206. // 3. Inserir no banco se houver documentos válidos
  207. if (documentosComEmbeddings.length > 0) {
  208. console.log('\nInserindo no banco de dados...');
  209. this.banco.inserirDocumentosEmLote(documentosComEmbeddings);
  210. console.log(`${documentosComEmbeddings.length} documentos inseridos com sucesso`);
  211. }
  212. // 4. Resumo
  213. console.log('\n=== RESUMO DO PROCESSAMENTO ===');
  214. console.log(`Total de arquivos encontrados: ${documentos.length}`);
  215. console.log(`Processados com sucesso: ${documentosComEmbeddings.length}`);
  216. console.log(`Com erro: ${arquivosComErro.length}`);
  217. if (arquivosComErro.length > 0) {
  218. console.log('\nArquivos com erro:');
  219. arquivosComErro.forEach(caminho => {
  220. console.log(` - ${caminho.split('/').pop()}`);
  221. });
  222. }
  223. } catch (erro) {
  224. console.error('Erro no processamento de novos embeddings:', erro);
  225. throw erro;
  226. } finally {
  227. // Fechar conexão com o banco
  228. this.banco.fechar();
  229. }
  230. }
  231. /**
  232. * Fecha a conexão com o banco de dados
  233. */
  234. fechar(): void {
  235. this.banco.fechar();
  236. }
  237. }
  238. /**
  239. * Função principal para executar o processamento de novos embeddings
  240. */
  241. async function main() {
  242. const insertEmbeddings = new InsertEmbeddings();
  243. try {
  244. await insertEmbeddings.processarNovosEmbeddings();
  245. console.log('\nProcessamento concluído com sucesso!');
  246. } catch (erro) {
  247. console.error('Erro no processamento:', erro);
  248. process.exit(1);
  249. }
  250. }
  251. // Executa apenas se for chamado diretamente
  252. if (import.meta.main) {
  253. main().catch(console.error);
  254. }
  255. export { gerarEmbedding };
  256. export default InsertEmbeddings;