├── .gitignore ├── README.md ├── backend ├── .env.example ├── .eslintrc ├── package-lock.json ├── package.json ├── src │ ├── brains │ │ ├── ContentGeneratorBrain.ts │ │ ├── ContentSummarizerBrain.ts │ │ ├── DocumentStructureBrain.ts │ │ ├── GapAnalyzerBrain.ts │ │ ├── SearchPlannerBrain.ts │ │ └── prompts │ │ │ ├── content-generator.prompt.ts │ │ │ ├── content-summarizer.prompt.ts │ │ │ ├── document-structure.prompt.ts │ │ │ ├── gap-analyzer.prompt.ts │ │ │ └── search-planner.prompt.ts │ ├── config │ │ └── config.ts │ ├── controllers │ │ ├── deepresearch.controller.ts │ │ └── health.controller.ts │ ├── graph │ │ └── research.graph.ts │ ├── interfaces │ │ ├── deepresearch.interface.ts │ │ ├── health.interface.ts │ │ ├── http.interface.ts │ │ ├── state.interface.ts │ │ └── tavily.interface.ts │ ├── middlewares │ │ ├── error.middleware.ts │ │ └── validation.middleware.ts │ ├── routes │ │ └── health.ts │ ├── server.ts │ ├── services │ │ ├── deepresearch.service.ts │ │ └── health.service.ts │ ├── tests │ │ ├── content-generator.test.ts │ │ ├── content-summarizer.test.ts │ │ ├── document-structure.test.ts │ │ ├── gap-analyzer.test.ts │ │ ├── research-graph.test.ts │ │ ├── search-planner.test.ts │ │ ├── tavily-search.test.ts │ │ └── websocket.test.ts │ ├── tools │ │ └── TavilySearchTool.ts │ ├── utils │ │ ├── logger.ts │ │ └── text.utils.ts │ └── websockets │ │ └── index.ts └── tsconfig.json ├── frontend ├── .gitignore ├── README.md ├── app │ ├── components │ │ ├── ProcessingStatus.tsx │ │ └── SearchInput.tsx │ ├── favicon.ico │ ├── globals.css │ ├── layout.tsx │ ├── page.tsx │ └── services │ │ └── websocket.service.ts ├── eslint.config.mjs ├── next.config.ts ├── package-lock.json ├── package.json ├── postcss.config.mjs ├── public │ ├── file.svg │ ├── globe.svg │ ├── next.svg │ ├── vercel.svg │ └── window.svg ├── tailwind.config.ts └── tsconfig.json └── graph.png /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | build 4 | .env 5 | .env.local 6 | .env.*.local 7 | .DS_Store 8 | *.log 9 | npm-debug.log* 10 | yarn-debug.log* 11 | yarn-error.log* 12 | .idea 13 | .vscode 14 | *.suo 15 | *.ntvs* 16 | *.njsproj 17 | *.sln 18 | *.sw? 19 | coverage 20 | .nyc_output 21 | .npm 22 | .yarn 23 | *.tsbuildinfo -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Deep JS Research 🧠 2 | 3 | Una alternativa open source a DeepResearch de OpenAI (200$/mes) que permite investigar cualquier tema y generar documentos de investigación con datos factuales. El proyecto utiliza LangChain y LangGraph junto con modelos LLM de código abierto: 4 | - **DeepSeek R1 (8b)**: Para el análisis y planificación 5 | - **Gemma 3 (12b)**: Para la generación de contenido 6 | - **Tavily API**: Para búsquedas web eficientes y extracción de contenido 7 | 8 | Un sistema avanzado de investigación impulsado por IA que utiliza modelos de lenguaje y búsqueda web para generar documentos de investigación detallados y bien estructurados. 9 | 10 | ## 🌟 Características 11 | 12 | - **Investigación Automatizada**: Genera documentos de investigación completos a partir de una simple consulta 13 | - **Búsqueda Inteligente**: Utiliza Tavily API para búsquedas web precisas y relevantes 14 | - **Análisis de Brechas**: Identifica y llena automáticamente vacíos de información 15 | - **Procesamiento Iterativo**: Mejora continuamente el contenido mediante múltiples ciclos de análisis 16 | - **Interfaz en Tiempo Real**: Muestra el progreso de la investigación en tiempo real 17 | - **Documentos Estructurados**: Genera documentos académicos bien organizados en formato Markdown 18 | 19 | ## 🏗️ Arquitectura 20 | 21 | ### Frontend (Next.js + TypeScript) 22 | 23 | - **Framework**: Next.js 15.1.7 con TypeScript 24 | - **UI**: Diseño moderno con Tailwind CSS 25 | - **Componentes Principales**: 26 | - `ConceptInput`: Entrada de consulta de investigación 27 | - `ThinkingProcess`: Visualización del proceso en tiempo real 28 | - `ResultDisplay`: Presentación del documento final 29 | - `SourcesList`: Lista de fuentes consultadas 30 | - **WebSocket**: Comunicación en tiempo real con el backend 31 | 32 | ### Backend (Node.js + TypeScript) 33 | 34 | - **Framework**: Express.js con TypeScript 35 | - **Características Principales**: 36 | - WebSocket Server para actualizaciones en tiempo real 37 | - Sistema de procesamiento basado en grafos 38 | - Integración con múltiples APIs de IA 39 | 40 | #### Cerebros de IA 41 | 42 | ![Flujo de trabajo del sistema](graph.png) 43 | 44 | 1. **PlanResearchBrain**: 45 | - Optimiza la consulta de investigación 46 | 47 | 2. **TavilySearchTool**: 48 | - Realiza búsquedas web precisas 49 | - Recopila información relevante 50 | 51 | 3. **SummarizeContentBrain**: 52 | - Genera resúmenes coherentes 53 | - Integra nueva información con contenido existente 54 | 55 | 4. **AnalyzeGapsBrain**: 56 | - Identifica brechas de conocimiento 57 | - Genera consultas de seguimiento 58 | 59 | 5. **GenerateStructureBrain**: 60 | - Crea estructuras de documentos jerárquicas 61 | - Organiza el contenido lógicamente 62 | 63 | 6. **GenerateDocumentBrain**: 64 | - Produce documentos académicos completos 65 | - Aplica estándares de escritura académica 66 | 67 | ## 🔄 Flujo de Trabajo 68 | 69 | 1. **Planificación**: 70 | - El usuario ingresa una consulta de investigación 71 | - El sistema genera un plan de búsqueda optimizado 72 | 73 | 2. **Búsqueda y Análisis**: 74 | - Búsqueda web mediante Tavily API 75 | - Generación de resúmenes del contenido encontrado 76 | - Análisis de brechas de conocimiento 77 | - Búsquedas adicionales según sea necesario 78 | 79 | 3. **Generación de Documentos**: 80 | - Creación de estructura jerárquica 81 | - Generación de contenido detallado 82 | - Formateo en Markdown académico 83 | 84 | ## 🛠️ Tecnologías 85 | 86 | ### Frontend 87 | - Next.js 15.1.7 88 | - React 19.0.0 89 | - TypeScript 90 | - Tailwind CSS 91 | - React Markdown 92 | - WebSocket Client 93 | 94 | ### Backend 95 | - Node.js 96 | - Express 97 | - TypeScript 98 | - LangChain 99 | - Ollama 100 | - Tavily API 101 | - WebSocket Server 102 | 103 | ## 🚀 Configuración 104 | 105 | ### Requisitos Previos 106 | - Node.js (versión LTS) 107 | - Ollama instalado y ejecutándose localmente 108 | - Clave API de Tavily 109 | 110 | ### Variables de Entorno 111 | 112 | #### Frontend 113 | ```env 114 | NEXT_PUBLIC_WS_URL=ws://localhost:5000 115 | ``` 116 | 117 | #### Backend 118 | ```env 119 | PORT=5000 120 | NODE_ENV=development 121 | API_PREFIX=/api/v1 122 | OLLAMA_BASE_URL=http://localhost:11434 123 | TAVILY_API_KEY=your_tavily_api_key_here 124 | GENERATION_MODEL=gemma3:12b 125 | THINKING_MODEL=deepseek-r1:8b 126 | MAX_ANALYSIS_COUNT=2 127 | MAX_RESULTS=1 128 | ``` 129 | 130 | ### Instalación 131 | 132 | 1. **Backend** 133 | ```bash 134 | cd backend 135 | npm install 136 | npm run dev 137 | ``` 138 | 139 | 2. **Frontend** 140 | ```bash 141 | cd frontend 142 | npm install 143 | npm run dev 144 | ``` 145 | 146 | ## 📚 Uso 147 | 148 | 1. Accede a la aplicación web (por defecto en http://localhost:3000) 149 | 2. Ingresa tu consulta de investigación en el campo de entrada 150 | 3. El sistema comenzará automáticamente el proceso de investigación 151 | 4. Observa el progreso en tiempo real 152 | 5. Recibe el documento final en formato Markdown 153 | 154 | ## 🤝 Contribución 155 | 156 | Las contribuciones son bienvenidas. Por favor, sigue estos pasos: 157 | 158 | 1. Fork el repositorio 159 | 2. Crea una rama para tu feature (`git checkout -b feature/AmazingFeature`) 160 | 3. Commit tus cambios (`git commit -m 'Add some AmazingFeature'`) 161 | 4. Push a la rama (`git push origin feature/AmazingFeature`) 162 | 5. Abre un Pull Request 163 | 164 | ## 🎪 JSConf España 2025 165 | 166 | Este proyecto formó parte del workshop "Creación de agentes de IA con Langchain.js" presentado en la JSConf España 2025, organizada por [midudev](https://github.com/midudev) y powered by [KeepCoding](https://keepcoding.io/). 167 | 168 | El workshop se llevó a cabo el 1 de marzo de 2025 en La Nave, Madrid, donde los asistentes aprendieron a: 169 | - Implementar las bibliotecas de código abierto de Langchain.js para Node.js 170 | - Integrar modelos de IA generativa 171 | - Ejecutar agentes de IA de manera independiente 172 | 173 | Para más información sobre la conferencia, visita [JSConf España 2025](https://www.jsconf.es/). 174 | 175 | ## 📄 Licencia 176 | 177 | Este proyecto está bajo la licencia ISC. Ver el archivo `LICENSE` para más detalles. 178 | 179 | ## 🙏 Agradecimientos 180 | 181 | - [Tavily API](https://tavily.com) por el motor de búsqueda 182 | - [Ollama](https://ollama.ai) por los modelos de IA locales 183 | - [LangChain](https://js.langchain.com) por el framework de IA -------------------------------------------------------------------------------- /backend/.env.example: -------------------------------------------------------------------------------- 1 | NODE_ENV=development 2 | PORT=3000 3 | CORS_ORIGINS=http://localhost:3000 4 | LOG_LEVEL=debug 5 | 6 | # LLM Configuration 7 | THINKING_MODEL=deepseek-r1:8b 8 | GENERATING_MODEL=gemma3:12b 9 | OLLAMA_BASE_URL=http://localhost:11434 10 | CONTENT_GENERATOR_MAX_TOKENS=5000 11 | 12 | # Tavily Configuration 13 | TAVILY_API_KEY=xxxx 14 | TAVILY_INITIAL_RESULTS=3 15 | TAVILY_MAX_RETRIES=3 16 | 17 | # Research Configuration 18 | MAX_GAP_LOOPS=2 -------------------------------------------------------------------------------- /backend/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "@typescript-eslint/parser", 3 | "extends": [ 4 | "plugin:@typescript-eslint/recommended" 5 | ], 6 | "parserOptions": { 7 | "ecmaVersion": 2020, 8 | "sourceType": "module" 9 | }, 10 | "rules": { 11 | "@typescript-eslint/explicit-function-return-type": "off", 12 | "@typescript-eslint/no-explicit-any": "off", 13 | "@typescript-eslint/no-unused-vars": ["warn", { "argsIgnorePattern": "^_" }], 14 | "@typescript-eslint/no-empty-function": "warn" 15 | } 16 | } -------------------------------------------------------------------------------- /backend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "backend", 3 | "version": "1.0.0", 4 | "main": "dist/server.js", 5 | "scripts": { 6 | "start": "node dist/server.ts", 7 | "dev": "ts-node-dev --respawn --transpile-only src/server.ts", 8 | "build": "tsc", 9 | "watch": "tsc -w", 10 | "test:ws": "ts-node src/tests/websocket.test.ts", 11 | "test:brain": "ts-node src/tests/search-planner.test.ts", 12 | "test:search": "ts-node src/tests/tavily-search.test.ts", 13 | "test:summarize": "ts-node src/tests/content-summarizer.test.ts", 14 | "test:gap": "ts-node src/tests/gap-analyzer.test.ts", 15 | "test:structure": "ts-node src/tests/document-structure.test.ts", 16 | "test:content": "ts-node src/tests/content-generator.test.ts", 17 | "test:graph": "ts-node src/tests/research-graph.test.ts", 18 | "lint": "eslint . --ext .ts" 19 | }, 20 | "keywords": [], 21 | "author": "", 22 | "license": "ISC", 23 | "type": "commonjs", 24 | "description": "", 25 | "dependencies": { 26 | "@langchain/community": "^0.0.32", 27 | "@langchain/core": "^0.2.21", 28 | "@langchain/langgraph": "^0.0.8", 29 | "@langchain/ollama": "^0.2.0", 30 | "@langchain/openai": "^0.0.14", 31 | "@tavily/core": "^0.3.1", 32 | "@types/ws": "^8.5.14", 33 | "compression": "^1.8.0", 34 | "cors": "^2.8.5", 35 | "dotenv": "^16.4.5", 36 | "express": "^4.18.3", 37 | "express-rate-limit": "^7.5.0", 38 | "express-validator": "^7.0.1", 39 | "helmet": "^7.1.0", 40 | "morgan": "^1.10.0", 41 | "winston": "^3.17.0", 42 | "ws": "^8.18.1" 43 | }, 44 | "devDependencies": { 45 | "@types/compression": "^1.7.5", 46 | "@types/cors": "^2.8.17", 47 | "@types/express": "^4.17.21", 48 | "@types/helmet": "^0.0.48", 49 | "@types/morgan": "^1.9.9", 50 | "@types/node": "^20.11.24", 51 | "@typescript-eslint/eslint-plugin": "^7.1.1", 52 | "@typescript-eslint/parser": "^7.1.1", 53 | "eslint": "^8.57.0", 54 | "ts-node": "^10.9.2", 55 | "ts-node-dev": "^2.0.0", 56 | "typescript": "^5.3.3" 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /backend/src/brains/ContentGeneratorBrain.ts: -------------------------------------------------------------------------------- 1 | import { ChatOllama } from '@langchain/community/chat_models/ollama'; 2 | import { config } from '../config/config'; 3 | import { CONTENT_GENERATOR_PROMPT } from './prompts/content-generator.prompt'; 4 | import { ResearchState } from '../interfaces/state.interface'; 5 | import { PromptTemplate } from '@langchain/core/prompts'; 6 | import logger from '../utils/logger'; 7 | 8 | export class ContentGeneratorBrain { 9 | private model: ChatOllama; 10 | private prompt: PromptTemplate; 11 | 12 | constructor() { 13 | this.model = new ChatOllama({ 14 | baseUrl: config.llm.ollamaBaseUrl, 15 | model: config.llm.generatingModel, 16 | temperature: 0.3, 17 | numPredict: config.llm.contentGeneratorMaxTokens 18 | }); 19 | 20 | this.prompt = PromptTemplate.fromTemplate(CONTENT_GENERATOR_PROMPT); 21 | } 22 | 23 | private concatenateSummaries(state: ResearchState): string { 24 | if (!state.searchResults) return ''; 25 | 26 | return state.searchResults 27 | .map((result, index) => { 28 | if (!result.summary) return ''; 29 | return `Source ${index + 1}:\n${result.summary}\n`; 30 | }) 31 | .join('\n'); 32 | } 33 | 34 | async invoke(state: ResearchState): Promise { 35 | if (!state.searchResults || state.searchResults.length === 0) { 36 | throw new Error('No search results available to generate content'); 37 | } 38 | 39 | if (!state.documentStructure) { 40 | throw new Error('No document structure available to generate content'); 41 | } 42 | 43 | const summaries = this.concatenateSummaries(state); 44 | 45 | if (!summaries) { 46 | throw new Error('No summaries available to generate content'); 47 | } 48 | 49 | logger.info('Generating final document content...'); 50 | 51 | try { 52 | const formattedPrompt = await this.prompt.format({ 53 | topic: state.researchQuery, 54 | summaries, 55 | structure: state.documentStructure 56 | }); 57 | 58 | const response = await this.model.invoke(formattedPrompt); 59 | 60 | if (!response || typeof response.content !== 'string') { 61 | throw new Error('Invalid response from model'); 62 | } 63 | 64 | const finalDocument = response.content; 65 | 66 | if (!finalDocument) { 67 | throw new Error('Failed to generate document content'); 68 | } 69 | 70 | logger.info('Document content generated successfully'); 71 | 72 | return { 73 | ...state, 74 | finalDocument 75 | }; 76 | } catch (error: any) { 77 | logger.error('Error generating document content:', error); 78 | throw new Error(`Failed to generate document content: ${error.message}`); 79 | } 80 | } 81 | } -------------------------------------------------------------------------------- /backend/src/brains/ContentSummarizerBrain.ts: -------------------------------------------------------------------------------- 1 | import { ChatOllama } from '@langchain/community/chat_models/ollama'; 2 | import { config } from '../config/config'; 3 | import { CONTENT_SUMMARIZER_PROMPT } from './prompts/content-summarizer.prompt'; 4 | import { ResearchState, SearchResult } from '../interfaces/state.interface'; 5 | import { StringOutputParser } from '@langchain/core/output_parsers'; 6 | import { PromptTemplate } from '@langchain/core/prompts'; 7 | import logger from '../utils/logger'; 8 | 9 | export class ContentSummarizerBrain { 10 | private model: ChatOllama; 11 | private prompt: PromptTemplate; 12 | 13 | constructor() { 14 | this.model = new ChatOllama({ 15 | baseUrl: config.llm.ollamaBaseUrl, 16 | model: config.llm.generatingModel, 17 | temperature: 0 18 | }); 19 | 20 | this.prompt = PromptTemplate.fromTemplate(CONTENT_SUMMARIZER_PROMPT); 21 | } 22 | 23 | private async summarizeContent(result: SearchResult, topic: string): Promise { 24 | try { 25 | const formattedPrompt = await this.prompt.format({ 26 | topic, 27 | content: result.rawContent 28 | }); 29 | 30 | const response = await this.model.invoke(formattedPrompt); 31 | 32 | const summary = response.content.toString(); 33 | 34 | return { 35 | ...result, 36 | summary 37 | }; 38 | } catch (error) { 39 | logger.error(`Error summarizing content from ${result.url}:`, error); 40 | return { 41 | ...result, 42 | summary: 'Failed to generate summary.' 43 | }; 44 | } 45 | } 46 | 47 | async invoke(state: ResearchState): Promise { 48 | if (!state.searchResults || state.searchResults.length === 0) { 49 | throw new Error('No search results available to summarize'); 50 | } 51 | 52 | logger.info(`Starting parallel summarization of ${state.searchResults.length} results`); 53 | 54 | const summarizedResults = await Promise.all( 55 | state.searchResults.map(result => 56 | this.summarizeContent(result, state.researchQuery) 57 | ) 58 | ); 59 | 60 | logger.info('Completed summarization of all results'); 61 | 62 | return { 63 | ...state, 64 | searchResults: summarizedResults 65 | }; 66 | } 67 | } -------------------------------------------------------------------------------- /backend/src/brains/DocumentStructureBrain.ts: -------------------------------------------------------------------------------- 1 | import { ChatOllama } from '@langchain/community/chat_models/ollama'; 2 | import { config } from '../config/config'; 3 | import { DOCUMENT_STRUCTURE_PROMPT } from './prompts/document-structure.prompt'; 4 | import { ResearchState } from '../interfaces/state.interface'; 5 | import { StringOutputParser } from '@langchain/core/output_parsers'; 6 | import { PromptTemplate } from '@langchain/core/prompts'; 7 | import { extractFromTags } from '../utils/text.utils'; 8 | import logger from '../utils/logger'; 9 | 10 | export class DocumentStructureBrain { 11 | private model: ChatOllama; 12 | private prompt: PromptTemplate; 13 | 14 | constructor() { 15 | this.model = new ChatOllama({ 16 | baseUrl: config.llm.ollamaBaseUrl, 17 | model: config.llm.thinkingModel, 18 | temperature: 0 19 | }); 20 | 21 | this.prompt = PromptTemplate.fromTemplate(DOCUMENT_STRUCTURE_PROMPT); 22 | } 23 | 24 | private concatenateSummaries(state: ResearchState): string { 25 | if (!state.searchResults) return ''; 26 | 27 | return state.searchResults 28 | .map((result, index) => { 29 | if (!result.summary) return ''; 30 | return `Source ${index + 1}:\n${result.summary}\n`; 31 | }) 32 | .join('\n'); 33 | } 34 | 35 | async invoke(state: ResearchState): Promise { 36 | if (!state.searchResults || state.searchResults.length === 0) { 37 | throw new Error('No search results available to create document structure'); 38 | } 39 | 40 | const summaries = this.concatenateSummaries(state); 41 | 42 | if (!summaries) { 43 | throw new Error('No summaries available to create document structure'); 44 | } 45 | 46 | logger.info('Generating document structure...'); 47 | 48 | const formattedPrompt = await this.prompt.format({ 49 | topic: state.researchQuery, 50 | summaries 51 | }); 52 | 53 | const response = await this.model.invoke(formattedPrompt) 54 | 55 | const documentStructure = extractFromTags(response.content.toString(), 'structure'); 56 | 57 | if (!documentStructure) { 58 | throw new Error('Failed to generate document structure'); 59 | } 60 | 61 | logger.info('Document structure generated successfully'); 62 | 63 | return { 64 | ...state, 65 | documentStructure 66 | }; 67 | } 68 | } -------------------------------------------------------------------------------- /backend/src/brains/GapAnalyzerBrain.ts: -------------------------------------------------------------------------------- 1 | import { ChatOllama } from '@langchain/community/chat_models/ollama'; 2 | import { config } from '../config/config'; 3 | import { GAP_ANALYZER_PROMPT } from './prompts/gap-analyzer.prompt'; 4 | import { ResearchState } from '../interfaces/state.interface'; 5 | import { StringOutputParser } from '@langchain/core/output_parsers'; 6 | import { PromptTemplate } from '@langchain/core/prompts'; 7 | import { extractFromTags } from '../utils/text.utils'; 8 | import logger from '../utils/logger'; 9 | 10 | export class GapAnalyzerBrain { 11 | private model: ChatOllama; 12 | private prompt: PromptTemplate; 13 | 14 | constructor() { 15 | this.model = new ChatOllama({ 16 | baseUrl: config.llm.ollamaBaseUrl, 17 | model: config.llm.thinkingModel, 18 | temperature: 0 19 | }); 20 | 21 | this.prompt = PromptTemplate.fromTemplate(GAP_ANALYZER_PROMPT); 22 | } 23 | 24 | private concatenateSummaries(state: ResearchState): string { 25 | if (!state.searchResults) return ''; 26 | 27 | return state.searchResults 28 | .map((result, index) => { 29 | if (!result.summary) return ''; 30 | return `Source ${index + 1}:\n${result.summary}\n`; 31 | }) 32 | .join('\n'); 33 | } 34 | 35 | async invoke(state: ResearchState): Promise { 36 | if (!state.searchResults || state.searchResults.length === 0) { 37 | throw new Error('No search results available to analyze'); 38 | } 39 | 40 | const summaries = this.concatenateSummaries(state); 41 | 42 | if (!summaries) { 43 | throw new Error('No summaries available to analyze'); 44 | } 45 | 46 | logger.info('Analyzing knowledge gaps...'); 47 | 48 | const formattedPrompt = await this.prompt.format({ 49 | topic: state.researchQuery, 50 | summaries 51 | }); 52 | 53 | const response = await this.model.invoke(formattedPrompt); 54 | 55 | const gapQuery = extractFromTags(response.content.toString(), 'query'); 56 | 57 | logger.info(gapQuery ? `Found knowledge gap: ${gapQuery}` : 'No knowledge gaps found'); 58 | 59 | return { 60 | ...state, 61 | gapQuery 62 | }; 63 | } 64 | } -------------------------------------------------------------------------------- /backend/src/brains/SearchPlannerBrain.ts: -------------------------------------------------------------------------------- 1 | import { ChatOllama } from '@langchain/community/chat_models/ollama'; 2 | import { config } from '../config/config'; 3 | import { SEARCH_PLANNER_PROMPT } from './prompts/search-planner.prompt'; 4 | import { ResearchState } from '../interfaces/state.interface'; 5 | import { removeThinkingTags } from '../utils/text.utils'; 6 | import { PromptTemplate } from '@langchain/core/prompts'; 7 | 8 | export class SearchPlannerBrain { 9 | private model: ChatOllama; 10 | private prompt: PromptTemplate; 11 | 12 | constructor() { 13 | this.model = new ChatOllama({ 14 | baseUrl: config.llm.ollamaBaseUrl, 15 | model: config.llm.thinkingModel, 16 | temperature: 0 17 | }); 18 | 19 | this.prompt = PromptTemplate.fromTemplate(SEARCH_PLANNER_PROMPT); 20 | } 21 | 22 | async invoke(state: ResearchState): Promise { 23 | const formattedPrompt = await this.prompt.format({ 24 | input: state.researchQuery 25 | }); 26 | 27 | const response = await this.model 28 | .invoke(formattedPrompt); 29 | 30 | const searchPlan = removeThinkingTags(response.content.toString()); 31 | 32 | if (!searchPlan) { 33 | throw new Error('Failed to generate search plan'); 34 | } 35 | 36 | return { 37 | ...state, 38 | searchPlan 39 | }; 40 | } 41 | } -------------------------------------------------------------------------------- /backend/src/brains/prompts/content-generator.prompt.ts: -------------------------------------------------------------------------------- 1 | export const CONTENT_GENERATOR_PROMPT = `You are an expert technical writer and educator with deep knowledge in software development and computer science. Your task is to create a comprehensive, professional, and extremely detailed technical document following a provided structure and using available research information. 2 | 3 | RESEARCH TOPIC: 4 | 5 | {topic} 6 | 7 | 8 | AVAILABLE INFORMATION: 9 | 10 | {summaries} 11 | 12 | 13 | DOCUMENT STRUCTURE: 14 | 15 | {structure} 16 | 17 | 18 | TASK: 19 | Generate a complete, professional, and highly detailed technical document following the provided structure and incorporating the available information. 20 | 21 | REQUIREMENTS: 22 | 1. Follow the provided structure EXACTLY 23 | 2. Write in a clear, professional, and technical style 24 | 3. Include detailed explanations for every concept 25 | 4. Use proper technical terminology 26 | 5. Provide extensive code examples where appropriate 27 | 6. Include practical applications and real-world scenarios 28 | 7. Explain complex concepts with analogies when helpful 29 | 8. Reference industry best practices 30 | 9. Address common pitfalls and misconceptions 31 | 10. Maintain consistent technical depth throughout 32 | 33 | STYLE GUIDELINES: 34 | - Use proper Markdown formatting 35 | - Write in a professional and authoritative tone 36 | - Maintain technical accuracy and precision 37 | - Include code blocks with proper syntax highlighting 38 | - Use tables and lists for better organization 39 | - Provide clear transitions between sections 40 | - Use technical terminology consistently 41 | - Include relevant diagrams descriptions when needed 42 | 43 | Now, generate the complete technical document following these requirements and guidelines. 44 | Only generate the content using the structure and available information, don't include any other text.`; 45 | 46 | export type ContentGeneratorInput = { 47 | topic: string; 48 | summaries: string; 49 | structure: string; 50 | }; -------------------------------------------------------------------------------- /backend/src/brains/prompts/content-summarizer.prompt.ts: -------------------------------------------------------------------------------- 1 | export const CONTENT_SUMMARIZER_PROMPT = ` 2 | You are an expert content summarizer. Your task is to create a clear and concise summary of the provided content, focusing specifically on information relevant to the main topic. 3 | 4 | CONTEXT: 5 | 6 | {topic} 7 | 8 | 9 | {content} 10 | 11 | 12 | GUIDELINES: 13 | 1. Focus on information directly related to the main topic 14 | 2. Be concise but comprehensive 15 | 3. Maintain technical accuracy 16 | 4. Include key insights and findings 17 | 5. Ignore irrelevant information 18 | 6. Keep the summary under 150 words 19 | 20 | FORMAT: 21 | - Start with the most important information 22 | - Use clear, direct language 23 | - Highlight key technical concepts 24 | - Include specific details when relevant 25 | 26 | Please provide a focused summary of the content that would be most useful for understanding {topic}. 27 | 28 | Return only the summary, nothing else. 29 | `; 30 | 31 | export type ContentSummarizerInput = { 32 | topic: string; 33 | content: string; 34 | }; -------------------------------------------------------------------------------- /backend/src/brains/prompts/document-structure.prompt.ts: -------------------------------------------------------------------------------- 1 | export const DOCUMENT_STRUCTURE_PROMPT = `You are an expert technical documentation architect. Your task is to create a professional Markdown structure for a comprehensive technical document based on the provided topic and available information. 2 | 3 | CONTEXT: 4 | 5 | {topic} 6 | 7 | 8 | Available Information: 9 | 10 | {summaries} 11 | 12 | 13 | TASK: 14 | Create a detailed Markdown structure that would effectively organize a comprehensive technical document about this topic. 15 | 16 | GUIDELINES: 17 | - Create a clear hierarchical structure 18 | - Include all necessary sections (introduction, core concepts, examples, etc.) 19 | - Use proper Markdown heading levels (# for main title, ## for sections, ### for subsections) 20 | - Consider the logical flow of information 21 | - Include placeholders for code examples where relevant 22 | - Add sections for practical applications and best practices 23 | - Ensure progressive complexity (basic to advanced) 24 | 25 | REQUIREMENTS: 26 | - Return ONLY the Markdown structure 27 | - Use proper Markdown syntax 28 | - Include brief section descriptions in HTML comments 29 | - Wrap the entire structure in tags 30 | - Don't include actual content, only the structure 31 | 32 | Example Output: 33 | 34 | # Understanding Promises in JavaScript 35 | 36 | 37 | ## Introduction 38 | 39 | 40 | ## How Promises Work 41 | ### Promise States 42 | ### Promise Syntax 43 | 44 | 45 | ## Working with Promises 46 | ### Creating Promises 47 | ### Error Handling 48 | ### Promise Chaining 49 | 50 | 51 | ## Advanced Promise Patterns 52 | ### Promise.all() 53 | ### Promise.race() 54 | ### Error Handling Patterns 55 | 56 | 57 | ## Best Practices and Use Cases 58 | ### Common Patterns 59 | ### Anti-patterns 60 | ### Performance Considerations 61 | 62 | 63 | ## Further Reading 64 | 65 | 66 | Now create a similar structure for the provided topic, focusing on creating a comprehensive and well-organized technical document. 67 | 68 | Only create the structure, don't include any other text.`; 69 | 70 | export type DocumentStructureInput = { 71 | topic: string; 72 | summaries: string; 73 | }; -------------------------------------------------------------------------------- /backend/src/brains/prompts/gap-analyzer.prompt.ts: -------------------------------------------------------------------------------- 1 | export const GAP_ANALYZER_PROMPT = ` 2 | You are an expert research gap analyzer. Your task is to identify if there are any critical information gaps in the provided summaries that would prevent creating a comprehensive research document. 3 | 4 | CONTEXT: 5 | 6 | {topic} 7 | 8 | 9 | 10 | {summaries} 11 | 12 | 13 | TASK: 14 | 1. Analyze if the summaries provide complete coverage of the topic 15 | 2. Identify any missing critical concepts or aspects 16 | 3. If a gap is found, create a focused 3-word search query to fill that gap 17 | 4. If no significant gaps are found, return "NONE" 18 | 19 | GUIDELINES: 20 | - Focus on technical completeness 21 | - Consider core concepts that might be missing 22 | - Look for missing practical examples or implementations 23 | - Check for missing context or prerequisites 24 | - Ensure all key aspects are covered 25 | 26 | If you find a knowledge gap, return ONLY a 3-word query within tags that would help fill that gap. 27 | If no significant gaps are found, return ONLY "NONE". 28 | 29 | Example outputs: 30 | event loop visualization 31 | or 32 | NONE`; 33 | 34 | export type GapAnalyzerInput = { 35 | topic: string; 36 | summaries: string; 37 | }; -------------------------------------------------------------------------------- /backend/src/brains/prompts/search-planner.prompt.ts: -------------------------------------------------------------------------------- 1 | export const SEARCH_PLANNER_PROMPT = ` 2 | You are an expert search query optimizer. Your task is to analyze user queries and transform them into optimal search queries that will yield the most relevant and comprehensive results. 3 | 4 | GUIDELINES: 5 | 1. Focus on extracting key concepts and technical terms 6 | 2. Remove conversational language while preserving intent 7 | 3. Use industry-standard terminology 8 | 4. Include relevant synonyms or alternative phrasings 9 | 5. Maintain technical accuracy 10 | 6. Return only 3 search terms 11 | 12 | 13 | {input} 14 | 15 | 16 | Return only the 3 search terms, nothing else.`; 17 | 18 | export type SearchPlannerInput = { 19 | input: string; 20 | }; -------------------------------------------------------------------------------- /backend/src/config/config.ts: -------------------------------------------------------------------------------- 1 | import dotenv from 'dotenv'; 2 | 3 | dotenv.config(); 4 | 5 | export const config = { 6 | env: process.env.NODE_ENV || 'development', 7 | port: parseInt(process.env.PORT || '3000', 10), 8 | cors: { 9 | origins: process.env.CORS_ORIGINS?.split(',') || ['http://localhost:3000'], 10 | methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'], 11 | }, 12 | logs: { 13 | level: process.env.LOG_LEVEL || 'info', 14 | }, 15 | api: { 16 | prefix: '/api/v1', 17 | }, 18 | ws: { 19 | path: '/api/v1/ws/research' 20 | }, 21 | llm: { 22 | thinkingModel: process.env.THINKING_MODEL || 'deepseek-r1:8b', 23 | generatingModel: process.env.GENERATING_MODEL || 'qwen2.5:7b', 24 | ollamaBaseUrl: process.env.OLLAMA_BASE_URL || 'http://localhost:11434', 25 | contentGeneratorMaxTokens: parseInt(process.env.CONTENT_GENERATOR_MAX_TOKENS || '5000', 10) 26 | }, 27 | tavily: { 28 | apiKey: process.env.TAVILY_API_KEY || '', 29 | initialResults: parseInt(process.env.TAVILY_INITIAL_RESULTS || '3', 10), 30 | maxRetries: parseInt(process.env.TAVILY_MAX_RETRIES || '3', 10) 31 | }, 32 | research: { 33 | maxGapLoops: parseInt(process.env.MAX_GAP_LOOPS || '2', 10) 34 | }, 35 | rateLimiter: { 36 | windowMs: 15 * 60 * 1000, 37 | max: 100, 38 | } 39 | }; -------------------------------------------------------------------------------- /backend/src/controllers/deepresearch.controller.ts: -------------------------------------------------------------------------------- 1 | import { WebSocket } from 'ws'; 2 | import { DeepResearchService } from '../services/deepresearch.service'; 3 | import { StartResearchMessage } from '../interfaces/deepresearch.interface'; 4 | import logger from '../utils/logger'; 5 | 6 | export class DeepResearchController { 7 | private deepResearchService: DeepResearchService; 8 | 9 | constructor() { 10 | this.deepResearchService = new DeepResearchService(); 11 | } 12 | 13 | public handleConnection = (ws: WebSocket): void => { 14 | logger.info('New WebSocket connection established'); 15 | 16 | ws.on('message', async (message: string) => { 17 | try { 18 | const data = JSON.parse(message) as StartResearchMessage; 19 | 20 | if (data.action === 'start') { 21 | if (!data.query?.trim()) { 22 | throw new Error('Query is required'); 23 | } 24 | 25 | logger.info(`Starting research for query: ${data.query}`); 26 | await this.deepResearchService.startResearch(ws, data.query.trim()); 27 | } 28 | } catch (error) { 29 | logger.error('Error processing WebSocket message:', error); 30 | ws.send(JSON.stringify({ 31 | error: error instanceof Error ? error.message : 'Invalid message format', 32 | timestamp: new Date().toISOString() 33 | })); 34 | } 35 | }); 36 | 37 | ws.on('close', () => { 38 | logger.info('WebSocket connection closed'); 39 | }); 40 | 41 | ws.on('error', (error) => { 42 | logger.error('WebSocket error:', error); 43 | }); 44 | }; 45 | } -------------------------------------------------------------------------------- /backend/src/controllers/health.controller.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response, NextFunction } from 'express'; 2 | import { HealthService } from '../services/health.service'; 3 | import { BaseResponse } from '../interfaces/http.interface'; 4 | import { SystemHealth } from '../interfaces/health.interface'; 5 | import logger from '../utils/logger'; 6 | 7 | export class HealthController { 8 | private healthService: HealthService; 9 | 10 | constructor() { 11 | this.healthService = new HealthService(); 12 | } 13 | 14 | public getHealth = async ( 15 | req: Request, 16 | res: Response>, 17 | next: NextFunction 18 | ): Promise => { 19 | try { 20 | const health = await this.healthService.getHealth(); 21 | 22 | if (health.status !== 'UP') { 23 | logger.warn('System health check indicates degraded performance', health); 24 | } 25 | 26 | res.status(health.status === 'UP' ? 200 : 503).json({ 27 | success: true, 28 | data: health, 29 | timestamp: new Date().toISOString() 30 | }); 31 | } catch (error) { 32 | next(error); 33 | } 34 | }; 35 | } -------------------------------------------------------------------------------- /backend/src/graph/research.graph.ts: -------------------------------------------------------------------------------- 1 | import { StateGraph, END } from '@langchain/langgraph'; 2 | import { config } from '../config/config'; 3 | import { ResearchState } from '../interfaces/state.interface'; 4 | import { SearchPlannerBrain } from '../brains/SearchPlannerBrain'; 5 | import { TavilySearchTool } from '../tools/TavilySearchTool'; 6 | import { ContentSummarizerBrain } from '../brains/ContentSummarizerBrain'; 7 | import { GapAnalyzerBrain } from '../brains/GapAnalyzerBrain'; 8 | import { DocumentStructureBrain } from '../brains/DocumentStructureBrain'; 9 | import { ContentGeneratorBrain } from '../brains/ContentGeneratorBrain'; 10 | import logger from '../utils/logger'; 11 | import { ResearchStep } from '../interfaces/deepresearch.interface'; 12 | 13 | export type ProgressCallback = (step: ResearchStep, progress: number, details: string) => void; 14 | 15 | export const createResearchGraph = (onProgress?: ProgressCallback) => { 16 | const graph = new StateGraph({ 17 | channels: { 18 | researchQuery: { value: null, default: () => "" }, 19 | searchPlan: { value: null }, 20 | searchResults: { value: null }, 21 | gapQuery: { value: null }, 22 | documentStructure: { value: null }, 23 | finalDocument: { value: null }, 24 | findGapLoops: { value: null, default: () => 0 } 25 | } 26 | }); 27 | 28 | const searchPlanner = new SearchPlannerBrain(); 29 | const searchTool = new TavilySearchTool(); 30 | const summarizer = new ContentSummarizerBrain(); 31 | const gapAnalyzer = new GapAnalyzerBrain(); 32 | const structureGenerator = new DocumentStructureBrain(); 33 | const contentGenerator = new ContentGeneratorBrain(); 34 | 35 | // Add nodes 36 | graph.addNode("search_planner", async (state) => { 37 | logger.info('Creating search plan...'); 38 | onProgress?.("search_planner", 10, "Planning search strategy..."); 39 | return await searchPlanner.invoke(state); 40 | }); 41 | 42 | graph.addNode("search", async (state) => { 43 | logger.info('Searching for information...'); 44 | onProgress?.("search", 25, "Searching for relevant information..."); 45 | return await searchTool.invoke(state); 46 | }); 47 | 48 | graph.addNode("summarize", async (state) => { 49 | logger.info('Summarizing search results...'); 50 | onProgress?.("summarize", 40, "Summarizing found information..."); 51 | return await summarizer.invoke(state); 52 | }); 53 | 54 | graph.addNode("analyze_gaps", async (state) => { 55 | logger.info(`Analyzing knowledge gaps (Loop ${state.findGapLoops + 1})...`); 56 | onProgress?.("analyze_gaps", 60, `Analyzing knowledge gaps (Loop ${state.findGapLoops + 1})...`); 57 | const newState = await gapAnalyzer.invoke(state); 58 | return { 59 | ...newState, 60 | findGapLoops: state.findGapLoops + 1 61 | }; 62 | }); 63 | 64 | graph.addNode("generate_structure", async (state) => { 65 | logger.info('Generating document structure...'); 66 | onProgress?.("generate_structure", 75, "Generating document structure..."); 67 | return await structureGenerator.invoke(state); 68 | }); 69 | 70 | graph.addNode("generate_content", async (state) => { 71 | logger.info('Generating final document...'); 72 | onProgress?.("generate_content", 90, "Generating final document..."); 73 | return await contentGenerator.invoke(state); 74 | }); 75 | 76 | // Define conditional edges 77 | const shouldContinueSearching = (state: ResearchState) => { 78 | if (state.gapQuery === 'NONE') { 79 | logger.info('No knowledge gaps found, proceeding to document generation...'); 80 | return false; 81 | } 82 | 83 | if (state.findGapLoops >= config.research.maxGapLoops) { 84 | logger.info(`Reached maximum gap search loops (${config.research.maxGapLoops}), proceeding to document generation...`); 85 | return false; 86 | } 87 | 88 | logger.info(`Found knowledge gap "${state.gapQuery}", continuing search...`); 89 | return true; 90 | }; 91 | 92 | // Connect nodes 93 | graph.addEdge("search_planner", "search"); 94 | graph.addEdge("search", "summarize"); 95 | graph.addEdge("summarize", "analyze_gaps"); 96 | 97 | // Conditional branching after gap analysis 98 | graph.addConditionalEdges( 99 | "analyze_gaps", 100 | (state: ResearchState) => { 101 | if (shouldContinueSearching(state)) { 102 | return "search"; 103 | } 104 | return "generate_structure"; 105 | } 106 | ); 107 | 108 | graph.addEdge("generate_structure", "generate_content"); 109 | graph.addEdge("generate_content", END); 110 | 111 | // Set entry point 112 | graph.setEntryPoint("search_planner"); 113 | 114 | return graph.compile(); 115 | }; 116 | 117 | export type ResearchGraph = ReturnType; -------------------------------------------------------------------------------- /backend/src/interfaces/deepresearch.interface.ts: -------------------------------------------------------------------------------- 1 | export type ResearchStep = 2 | | 'search_planner' 3 | | 'search' 4 | | 'summarize' 5 | | 'analyze_gaps' 6 | | 'generate_structure' 7 | | 'generate_content' 8 | | 'complete' 9 | | 'error'; 10 | 11 | export interface ResearchMessage { 12 | step: ResearchStep; 13 | timestamp: string; 14 | progress: number; 15 | details?: string; 16 | query?: string; 17 | completion?: string; 18 | } 19 | 20 | export interface StartResearchMessage { 21 | action: 'start'; 22 | query: string; 23 | } -------------------------------------------------------------------------------- /backend/src/interfaces/health.interface.ts: -------------------------------------------------------------------------------- 1 | export interface SystemHealth { 2 | status: 'UP' | 'DOWN' | 'DEGRADED'; 3 | version: string; 4 | timestamp: string; 5 | uptime: number; 6 | process: { 7 | pid: number; 8 | memory: { 9 | used: number; 10 | total: number; 11 | free: number; 12 | heapTotal: number; 13 | heapUsed: number; 14 | external: number; 15 | percentage: number; 16 | }; 17 | cpu: { 18 | system: number; 19 | user: number; 20 | total: number; 21 | percentage: number; 22 | }; 23 | }; 24 | system: { 25 | platform: string; 26 | arch: string; 27 | cpus: { 28 | model: string; 29 | speed: number; 30 | cores: number; 31 | loadAvg: number[]; 32 | }; 33 | memory: { 34 | total: number; 35 | free: number; 36 | used: number; 37 | percentage: number; 38 | }; 39 | }; 40 | } -------------------------------------------------------------------------------- /backend/src/interfaces/http.interface.ts: -------------------------------------------------------------------------------- 1 | export interface BaseResponse { 2 | success: boolean; 3 | data?: T; 4 | error?: string; 5 | timestamp: string; 6 | } 7 | 8 | export interface PaginatedResponse extends BaseResponse { 9 | data: { 10 | items: T[]; 11 | total: number; 12 | page: number; 13 | limit: number; 14 | totalPages: number; 15 | }; 16 | } 17 | 18 | export interface ErrorResponse extends BaseResponse { 19 | error: string; 20 | stack?: string; 21 | code?: string; 22 | } -------------------------------------------------------------------------------- /backend/src/interfaces/state.interface.ts: -------------------------------------------------------------------------------- 1 | export interface SearchResult { 2 | title: string; 3 | url: string; 4 | content: string; 5 | rawContent: string; 6 | score: number; 7 | summary?: string; 8 | } 9 | 10 | export interface ResearchState { 11 | researchQuery: string; 12 | searchPlan?: string; 13 | searchResults?: SearchResult[]; 14 | gapQuery?: string; 15 | documentStructure?: string; 16 | finalDocument?: string; 17 | findGapLoops: number; 18 | // We'll add more state properties as we develop more nodes 19 | } -------------------------------------------------------------------------------- /backend/src/interfaces/tavily.interface.ts: -------------------------------------------------------------------------------- 1 | export interface TavilyResult { 2 | title: string; 3 | url: string; 4 | content: string; 5 | rawContent?: string; 6 | score: number; 7 | } 8 | 9 | export interface TavilyResponse { 10 | results: TavilyResult[]; 11 | query: string; 12 | responseTime: number; 13 | } -------------------------------------------------------------------------------- /backend/src/middlewares/error.middleware.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response, NextFunction } from 'express'; 2 | import { ErrorResponse } from '../interfaces/http.interface'; 3 | 4 | export class ApiError extends Error { 5 | constructor( 6 | public statusCode: number, 7 | message: string, 8 | public code?: string 9 | ) { 10 | super(message); 11 | this.name = 'ApiError'; 12 | } 13 | } 14 | 15 | export const errorHandler = ( 16 | err: Error | ApiError, 17 | req: Request, 18 | res: Response, 19 | next: NextFunction 20 | ) => { 21 | const statusCode = err instanceof ApiError ? err.statusCode : 500; 22 | const errorResponse: ErrorResponse = { 23 | success: false, 24 | error: err.message || 'Internal Server Error', 25 | timestamp: new Date().toISOString(), 26 | code: err instanceof ApiError ? err.code : 'INTERNAL_ERROR' 27 | }; 28 | 29 | if (process.env.NODE_ENV !== 'production') { 30 | errorResponse.stack = err.stack; 31 | } 32 | 33 | res.status(statusCode).json(errorResponse); 34 | }; -------------------------------------------------------------------------------- /backend/src/middlewares/validation.middleware.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response, NextFunction } from 'express'; 2 | import { validationResult, ValidationChain } from 'express-validator'; 3 | import { ApiError } from './error.middleware'; 4 | 5 | export const validate = (validations: ValidationChain[]) => { 6 | return async (req: Request, res: Response, next: NextFunction) => { 7 | await Promise.all(validations.map(validation => validation.run(req))); 8 | 9 | const errors = validationResult(req); 10 | if (errors.isEmpty()) { 11 | return next(); 12 | } 13 | 14 | const errorMessages = errors.array().map(err => err.msg); 15 | throw new ApiError(400, errorMessages.join(', '), 'VALIDATION_ERROR'); 16 | }; 17 | }; -------------------------------------------------------------------------------- /backend/src/routes/health.ts: -------------------------------------------------------------------------------- 1 | import { Router } from 'express'; 2 | import { HealthController } from '../controllers/health.controller'; 3 | 4 | const router = Router(); 5 | const healthController = new HealthController(); 6 | 7 | router.get('/', healthController.getHealth); 8 | 9 | export default router; -------------------------------------------------------------------------------- /backend/src/server.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import cors from 'cors'; 3 | import helmet from 'helmet'; 4 | import compression from 'compression'; 5 | import rateLimit from 'express-rate-limit'; 6 | import { Server as HttpServer } from 'http'; 7 | import { config } from './config/config'; 8 | import { errorHandler } from './middlewares/error.middleware'; 9 | import healthRoutes from './routes/health'; 10 | import { setupWebSocket } from './websockets'; 11 | import logger from './utils/logger'; 12 | 13 | const app = express(); 14 | const server = new HttpServer(app); 15 | 16 | app.use(helmet()); 17 | app.use(cors({ 18 | origin: config.cors.origins, 19 | methods: config.cors.methods, 20 | })); 21 | app.use(compression()); 22 | app.use(express.json()); 23 | app.use(express.urlencoded({ extended: true })); 24 | 25 | app.use(rateLimit(config.rateLimiter)); 26 | 27 | app.use(`${config.api.prefix}/health`, healthRoutes); 28 | 29 | app.get('/', (req, res) => { 30 | res.json({ message: 'Backend is running!' }); 31 | }); 32 | 33 | setupWebSocket(server); 34 | 35 | app.use(errorHandler); 36 | 37 | server.listen(config.port, () => { 38 | logger.info(`Server is running on port ${config.port} in ${config.env} mode`); 39 | }); -------------------------------------------------------------------------------- /backend/src/services/deepresearch.service.ts: -------------------------------------------------------------------------------- 1 | import { WebSocket } from 'ws'; 2 | import { ResearchMessage, ResearchStep } from '../interfaces/deepresearch.interface'; 3 | import { createResearchGraph } from '../graph/research.graph'; 4 | import { ResearchState } from '../interfaces/state.interface'; 5 | import logger from '../utils/logger'; 6 | 7 | export class DeepResearchService { 8 | private sendMessage(ws: WebSocket, step: ResearchStep, progress: number, details: string, query: string, completion: string = "") { 9 | const message: ResearchMessage = { 10 | step, 11 | timestamp: new Date().toISOString(), 12 | progress, 13 | details, 14 | query, 15 | completion 16 | }; 17 | ws.send(JSON.stringify(message)); 18 | logger.info(`Research step: ${step}`, { progress, details, query }); 19 | } 20 | 21 | public async startResearch(ws: WebSocket, query: string): Promise { 22 | try { 23 | const progressCallback = (step: ResearchStep, progress: number, details: string) => { 24 | this.sendMessage(ws, step, progress, details, query); 25 | }; 26 | 27 | const graph = createResearchGraph(progressCallback); 28 | const initialState: ResearchState = { 29 | researchQuery: query, 30 | findGapLoops: 0 31 | }; 32 | 33 | // Execute graph 34 | const finalState = await graph.invoke(initialState); 35 | 36 | // Send completion message 37 | this.sendMessage( 38 | ws, 39 | "complete", 40 | 100, 41 | `Research completed with ${finalState.searchResults?.length || 0} sources and ${finalState.findGapLoops} search iterations`, 42 | query, 43 | finalState.finalDocument 44 | ); 45 | 46 | } catch (error) { 47 | logger.error('Error in research process:', error); 48 | this.sendMessage( 49 | ws, 50 | "error", 51 | 0, 52 | error instanceof Error ? error.message : 'Unknown error occurred', 53 | query 54 | ); 55 | } 56 | } 57 | } -------------------------------------------------------------------------------- /backend/src/services/health.service.ts: -------------------------------------------------------------------------------- 1 | import os from 'os'; 2 | import { SystemHealth } from '../interfaces/health.interface'; 3 | import { readFileSync } from 'fs'; 4 | import { join } from 'path'; 5 | 6 | export class HealthService { 7 | private startTime: number; 8 | private readonly version: string; 9 | 10 | constructor() { 11 | this.startTime = Date.now(); 12 | this.version = this.getPackageVersion(); 13 | } 14 | 15 | private getPackageVersion(): string { 16 | try { 17 | const packageJson = JSON.parse( 18 | readFileSync(join(__dirname, '../../package.json'), 'utf8') 19 | ); 20 | return packageJson.version; 21 | } catch { 22 | return '1.0.0'; 23 | } 24 | } 25 | 26 | private calculateCpuUsage(): { system: number; user: number; total: number; percentage: number } { 27 | const cpus = os.cpus(); 28 | const cpu = cpus[0]; 29 | const total = Object.values(cpu.times).reduce((acc, tv) => acc + tv, 0); 30 | const system = cpu.times.sys / total * 100; 31 | const user = cpu.times.user / total * 100; 32 | 33 | return { 34 | system, 35 | user, 36 | total: system + user, 37 | percentage: process.cpuUsage().system / 1000000 38 | }; 39 | } 40 | 41 | public async getHealth(): Promise { 42 | const processMemory = process.memoryUsage(); 43 | const systemMemory = { 44 | total: os.totalmem(), 45 | free: os.freemem(), 46 | used: os.totalmem() - os.freemem(), 47 | percentage: ((os.totalmem() - os.freemem()) / os.totalmem()) * 100 48 | }; 49 | 50 | const cpuInfo = os.cpus()[0]; 51 | const processMemoryPercentage = (processMemory.heapUsed / processMemory.heapTotal) * 100; 52 | 53 | const health: SystemHealth = { 54 | status: processMemoryPercentage > 90 || systemMemory.percentage > 90 ? 'DEGRADED' : 'UP', 55 | version: this.version, 56 | timestamp: new Date().toISOString(), 57 | uptime: (Date.now() - this.startTime) / 1000, 58 | process: { 59 | pid: process.pid, 60 | memory: { 61 | used: processMemory.heapUsed, 62 | total: processMemory.heapTotal, 63 | free: processMemory.heapTotal - processMemory.heapUsed, 64 | heapTotal: processMemory.heapTotal, 65 | heapUsed: processMemory.heapUsed, 66 | external: processMemory.external, 67 | percentage: processMemoryPercentage 68 | }, 69 | cpu: this.calculateCpuUsage() 70 | }, 71 | system: { 72 | platform: process.platform, 73 | arch: process.arch, 74 | cpus: { 75 | model: cpuInfo.model, 76 | speed: cpuInfo.speed, 77 | cores: os.cpus().length, 78 | loadAvg: os.loadavg() 79 | }, 80 | memory: systemMemory 81 | } 82 | }; 83 | 84 | return health; 85 | } 86 | } -------------------------------------------------------------------------------- /backend/src/tests/content-generator.test.ts: -------------------------------------------------------------------------------- 1 | import { ContentGeneratorBrain } from '../brains/ContentGeneratorBrain'; 2 | import { ResearchState } from '../interfaces/state.interface'; 3 | 4 | const TEST_STATE: ResearchState = { 5 | researchQuery: "How does the JavaScript event loop work?", 6 | findGapLoops: 2, 7 | searchResults: [ 8 | { 9 | title: "Understanding the JavaScript Event Loop", 10 | url: "https://example.com/js-event-loop", 11 | content: "Brief preview...", 12 | rawContent: "Full content...", 13 | score: 0.95, 14 | summary: `The JavaScript event loop is a core mechanism that enables asynchronous programming. 15 | It continuously monitors the call stack and callback queue, moving callbacks to the stack when it's empty. 16 | This process is fundamental to handling async operations in JavaScript.` 17 | }, 18 | { 19 | title: "JavaScript Runtime Components", 20 | url: "https://example.com/js-runtime", 21 | content: "Brief preview...", 22 | rawContent: "Full content...", 23 | score: 0.88, 24 | summary: `The JavaScript runtime includes several key components: the heap for memory allocation, 25 | the call stack for tracking function calls, and the callback queue for managing async operations. 26 | The event loop orchestrates these components to handle asynchronous code execution.` 27 | } 28 | ], 29 | documentStructure: `# Understanding the JavaScript Event Loop 30 | 31 | ## Introduction 32 | 33 | 34 | ## Core Components 35 | ### Call Stack 36 | ### Callback Queue 37 | ### Event Loop Mechanism 38 | 39 | ## Asynchronous Programming 40 | ### How Async Operations Work 41 | ### Promises and the Event Loop 42 | ### async/await Integration 43 | 44 | ## Best Practices 45 | ### Performance Optimization 46 | ### Common Pitfalls 47 | ### Debugging Techniques 48 | 49 | ## Advanced Concepts 50 | ### Microtasks vs Macrotasks 51 | ### Node.js Event Loop Differences 52 | ### Browser Implementation Details 53 | 54 | ## Practical Examples 55 | ### Real-world Use Cases 56 | ### Performance Patterns 57 | ### Anti-patterns to Avoid` 58 | }; 59 | 60 | async function testContentGenerator() { 61 | const brain = new ContentGeneratorBrain(); 62 | 63 | console.log('\n📝 Testing ContentGeneratorBrain'); 64 | console.log('==============================\n'); 65 | 66 | console.log(`📚 Topic: "${TEST_STATE.researchQuery}"\n`); 67 | 68 | console.log('Available Information:'); 69 | TEST_STATE.searchResults?.forEach((result, index) => { 70 | console.log(`\nSource ${index + 1}:`); 71 | console.log(result.summary); 72 | }); 73 | 74 | console.log('\nDocument Structure:'); 75 | console.log(TEST_STATE.documentStructure); 76 | console.log('\n'); 77 | 78 | try { 79 | console.log('⏳ Generating final document...\n'); 80 | 81 | const result = await brain.invoke(TEST_STATE); 82 | 83 | console.log('✅ Document Generated!'); 84 | console.log('=====================\n'); 85 | 86 | if (result.finalDocument) { 87 | console.log('📄 Final Document:'); 88 | console.log('---------------\n'); 89 | console.log(result.finalDocument); 90 | } else { 91 | console.log('❌ No document generated'); 92 | } 93 | 94 | } catch (error) { 95 | console.error('❌ Error:', error); 96 | process.exit(1); 97 | } 98 | } 99 | 100 | testContentGenerator(); -------------------------------------------------------------------------------- /backend/src/tests/content-summarizer.test.ts: -------------------------------------------------------------------------------- 1 | import { ContentSummarizerBrain } from '../brains/ContentSummarizerBrain'; 2 | import { ResearchState } from '../interfaces/state.interface'; 3 | 4 | const TEST_STATE: ResearchState = { 5 | findGapLoops: 0, 6 | researchQuery: "How does the JavaScript event loop work?", 7 | searchResults: [ 8 | { 9 | title: "Understanding the JavaScript Event Loop", 10 | url: "https://example.com/js-event-loop", 11 | content: "Brief preview of the content...", 12 | rawContent: `The JavaScript event loop is a fundamental concept in JavaScript's concurrency model. 13 | It's responsible for executing code, collecting and processing events, and executing queued sub-tasks. 14 | The event loop continuously checks the call stack and the callback queue. 15 | When the call stack is empty, it takes the first event from the queue and pushes it to the call stack, which effectively runs it. 16 | This process is what makes JavaScript's asynchronous programming possible. 17 | The event loop works in conjunction with the call stack and the callback queue to handle asynchronous operations. 18 | When an async operation completes, its callback is placed in the callback queue. 19 | The event loop constantly checks if the call stack is empty, and if it is, it pushes the next callback from the queue to the stack.`, 20 | score: 0.95 21 | }, 22 | { 23 | title: "Deep Dive into JavaScript Runtime", 24 | url: "https://example.com/js-runtime", 25 | content: "Brief preview of the content...", 26 | rawContent: `JavaScript runtime consists of several key components working together. 27 | The heap is where memory allocation happens for objects. 28 | The call stack is where function calls are tracked. 29 | Web APIs provide additional functionality like setTimeout, DOM events, and HTTP requests. 30 | The callback queue (also known as the task queue) holds callbacks waiting to be executed. 31 | The microtask queue has higher priority than the callback queue and is used for Promises. 32 | The event loop orchestrates all these components to make asynchronous JavaScript work smoothly. 33 | Understanding these concepts is crucial for writing efficient JavaScript code.`, 34 | score: 0.88 35 | } 36 | ] 37 | }; 38 | 39 | async function testContentSummarizer() { 40 | const brain = new ContentSummarizerBrain(); 41 | 42 | console.log('\n🧠 Testing ContentSummarizerBrain'); 43 | console.log('==============================\n'); 44 | 45 | console.log(`📝 Research Query: "${TEST_STATE.researchQuery}"\n`); 46 | console.log(`📚 Processing ${TEST_STATE.searchResults?.length} articles...\n`); 47 | 48 | try { 49 | console.log('⏳ Generating summaries in parallel...\n'); 50 | 51 | const result = await brain.invoke(TEST_STATE); 52 | 53 | console.log('✅ Success!'); 54 | console.log('------------\n'); 55 | 56 | result.searchResults?.forEach((res, index) => { 57 | console.log(`Article ${index + 1}:`); 58 | console.log(`🔗 ${res.url}`); 59 | console.log(`📖 ${res.title}`); 60 | console.log(`📝 Summary:`); 61 | console.log(res.summary); 62 | console.log(); 63 | }); 64 | 65 | } catch (error) { 66 | console.error('❌ Error:', error); 67 | process.exit(1); 68 | } 69 | } 70 | 71 | testContentSummarizer(); -------------------------------------------------------------------------------- /backend/src/tests/document-structure.test.ts: -------------------------------------------------------------------------------- 1 | import { DocumentStructureBrain } from '../brains/DocumentStructureBrain'; 2 | import { ResearchState } from '../interfaces/state.interface'; 3 | 4 | const TEST_STATE: ResearchState = { 5 | findGapLoops: 0, 6 | researchQuery: "How does the JavaScript event loop work?", 7 | searchResults: [ 8 | { 9 | title: "Understanding the JavaScript Event Loop", 10 | url: "https://example.com/js-event-loop", 11 | content: "Brief preview...", 12 | rawContent: "Full content...", 13 | score: 0.95, 14 | summary: `The JavaScript event loop is a core mechanism that enables asynchronous programming. 15 | It continuously monitors the call stack and callback queue, moving callbacks to the stack when it's empty. 16 | This process is fundamental to handling async operations in JavaScript.` 17 | }, 18 | { 19 | title: "JavaScript Runtime Components", 20 | url: "https://example.com/js-runtime", 21 | content: "Brief preview...", 22 | rawContent: "Full content...", 23 | score: 0.88, 24 | summary: `The JavaScript runtime includes several key components: the heap for memory allocation, 25 | the call stack for tracking function calls, and the callback queue for managing async operations. 26 | The event loop orchestrates these components to handle asynchronous code execution.` 27 | } 28 | ] 29 | }; 30 | 31 | async function testDocumentStructure() { 32 | const brain = new DocumentStructureBrain(); 33 | 34 | console.log('\n📝 Testing DocumentStructureBrain'); 35 | console.log('==============================\n'); 36 | 37 | console.log(`📚 Topic: "${TEST_STATE.researchQuery}"\n`); 38 | console.log('Available Information:'); 39 | TEST_STATE.searchResults?.forEach((result, index) => { 40 | console.log(`\nSource ${index + 1}:`); 41 | console.log(result.summary); 42 | }); 43 | console.log('\n'); 44 | 45 | try { 46 | console.log('⏳ Generating document structure...\n'); 47 | 48 | const result = await brain.invoke(TEST_STATE); 49 | 50 | console.log('✅ Structure Generated!'); 51 | console.log('=====================\n'); 52 | 53 | if (result.documentStructure) { 54 | console.log('📋 Document Structure:'); 55 | console.log('-------------------\n'); 56 | console.log(result.documentStructure); 57 | } else { 58 | console.log('❌ No structure generated'); 59 | } 60 | 61 | } catch (error) { 62 | console.error('❌ Error:', error); 63 | process.exit(1); 64 | } 65 | } 66 | 67 | testDocumentStructure(); -------------------------------------------------------------------------------- /backend/src/tests/gap-analyzer.test.ts: -------------------------------------------------------------------------------- 1 | import { GapAnalyzerBrain } from '../brains/GapAnalyzerBrain'; 2 | import { ResearchState } from '../interfaces/state.interface'; 3 | 4 | const TEST_STATE: ResearchState = { 5 | findGapLoops: 0, 6 | researchQuery: "How does the JavaScript event loop work?", 7 | searchResults: [ 8 | { 9 | title: "Understanding the JavaScript Event Loop", 10 | url: "https://example.com/js-event-loop", 11 | content: "Brief preview...", 12 | rawContent: "Full content...", 13 | score: 0.95, 14 | summary: `The JavaScript event loop is a core mechanism that enables asynchronous programming. 15 | It continuously monitors the call stack and callback queue, moving callbacks to the stack when it's empty. 16 | This process is fundamental to handling async operations in JavaScript.` 17 | }, 18 | { 19 | title: "JavaScript Runtime Components", 20 | url: "https://example.com/js-runtime", 21 | content: "Brief preview...", 22 | rawContent: "Full content...", 23 | score: 0.88, 24 | summary: `The JavaScript runtime includes several key components: the heap for memory allocation, 25 | the call stack for tracking function calls, and the callback queue for managing async operations. 26 | The event loop orchestrates these components to handle asynchronous code execution.` 27 | } 28 | ] 29 | }; 30 | 31 | async function testGapAnalyzer() { 32 | const brain = new GapAnalyzerBrain(); 33 | 34 | console.log('\n🔍 Testing GapAnalyzerBrain'); 35 | console.log('=========================\n'); 36 | 37 | console.log(`📝 Research Topic: "${TEST_STATE.researchQuery}"\n`); 38 | console.log('Available Summaries:'); 39 | TEST_STATE.searchResults?.forEach((result, index) => { 40 | console.log(`\nSource ${index + 1}:`); 41 | console.log(result.summary); 42 | }); 43 | console.log('\n'); 44 | 45 | try { 46 | console.log('⏳ Analyzing knowledge gaps...\n'); 47 | 48 | const result = await brain.invoke(TEST_STATE); 49 | 50 | console.log('✅ Analysis Complete!'); 51 | console.log('-------------------\n'); 52 | 53 | if (result.gapQuery) { 54 | console.log('🔍 Knowledge Gap Found!'); 55 | console.log(`Missing Information Query: "${result.gapQuery}"\n`); 56 | } else { 57 | console.log('✨ No Knowledge Gaps Found!'); 58 | console.log('Information coverage appears to be complete.\n'); 59 | } 60 | 61 | } catch (error) { 62 | console.error('❌ Error:', error); 63 | process.exit(1); 64 | } 65 | } 66 | 67 | testGapAnalyzer(); -------------------------------------------------------------------------------- /backend/src/tests/research-graph.test.ts: -------------------------------------------------------------------------------- 1 | import { createResearchGraph } from '../graph/research.graph'; 2 | import { ResearchState } from '../interfaces/state.interface'; 3 | import logger from '../utils/logger'; 4 | 5 | const TEST_QUERIES = [ 6 | "How does the JavaScript event loop work?", 7 | "What are JavaScript Promises and how do they work?", 8 | "Explain React's Virtual DOM and reconciliation process", 9 | "What is TypeScript and how does it enhance JavaScript?" 10 | ]; 11 | 12 | async function testResearchGraph() { 13 | const graph = createResearchGraph(); 14 | const query = TEST_QUERIES[Math.floor(Math.random() * TEST_QUERIES.length)]; 15 | 16 | console.log('\n🔬 Testing Research Graph'); 17 | console.log('=======================\n'); 18 | 19 | console.log(`📝 Research Query: "${query}"\n`); 20 | 21 | try { 22 | const initialState: ResearchState = { 23 | researchQuery: query, 24 | findGapLoops: 0 25 | }; 26 | 27 | console.log('⚡ Starting research process...\n'); 28 | 29 | const result = await graph.invoke(initialState); 30 | 31 | console.log('\n✅ Research Complete!'); 32 | console.log('===================\n'); 33 | 34 | console.log('📊 Research Statistics:'); 35 | console.log('--------------------'); 36 | console.log(`🔄 Gap Search Loops: ${result.findGapLoops}`); 37 | console.log(`🔍 Search Results: ${result.searchResults?.length || 0}`); 38 | console.log(`📑 Document Structure Length: ${result.documentStructure?.split('\n').length || 0} lines`); 39 | console.log(`📚 Final Document Length: ${result.finalDocument?.split('\n').length || 0} lines\n`); 40 | 41 | console.log('📄 Final Document Preview:'); 42 | console.log('----------------------\n'); 43 | if (result.finalDocument) { 44 | // Show first 20 lines of the document 45 | console.log(result.finalDocument.split('\n').slice(0, 20).join('\n')); 46 | console.log('\n... (truncated for brevity)'); 47 | } else { 48 | console.log('❌ No document generated'); 49 | } 50 | 51 | } catch (error) { 52 | console.error('❌ Error:', error); 53 | process.exit(1); 54 | } 55 | } 56 | 57 | testResearchGraph(); -------------------------------------------------------------------------------- /backend/src/tests/search-planner.test.ts: -------------------------------------------------------------------------------- 1 | import { SearchPlannerBrain } from '../brains/SearchPlannerBrain'; 2 | import { ResearchState } from '../interfaces/state.interface'; 3 | 4 | const TEST_QUERIES = [ 5 | "Can you explain how the JavaScript event loop works in detail?", 6 | "What's the difference between Promise.all and Promise.race in JavaScript?", 7 | "How does React's virtual DOM work and why is it important?", 8 | "Explain TypeScript decorators and their use cases" 9 | ]; 10 | 11 | async function testSearchPlanner() { 12 | const brain = new SearchPlannerBrain(); 13 | const query = TEST_QUERIES[Math.floor(Math.random() * TEST_QUERIES.length)]; 14 | 15 | console.log('\n🧠 Testing SearchPlannerBrain'); 16 | console.log('============================\n'); 17 | 18 | console.log(`📝 Input Query: "${query}"\n`); 19 | 20 | try { 21 | const initialState: ResearchState = { 22 | findGapLoops: 0, 23 | researchQuery: query 24 | }; 25 | 26 | console.log('⏳ Processing...\n'); 27 | 28 | const result = await brain.invoke(initialState); 29 | 30 | console.log('✅ Success!'); 31 | console.log('------------\n'); 32 | console.log('🔍 Search Plan:', result.searchPlan); 33 | 34 | } catch (error) { 35 | console.error('❌ Error:', error); 36 | process.exit(1); 37 | } 38 | } 39 | 40 | testSearchPlanner(); -------------------------------------------------------------------------------- /backend/src/tests/tavily-search.test.ts: -------------------------------------------------------------------------------- 1 | import { TavilySearchTool } from '../tools/TavilySearchTool'; 2 | import { ResearchState } from '../interfaces/state.interface'; 3 | 4 | const TEST_SEARCH_PLANS = [ 5 | "JavaScript event loop implementation event-driven programming asynchronous execution call stack", 6 | "JavaScript Promise.all Promise.race concurrent promise handling parallel execution", 7 | "React Virtual DOM rendering optimization diffing algorithm performance benefits", 8 | "TypeScript decorators metadata reflection design patterns implementation examples" 9 | ]; 10 | 11 | async function testTavilySearch() { 12 | const tool = new TavilySearchTool(); 13 | const searchPlan = TEST_SEARCH_PLANS[Math.floor(Math.random() * TEST_SEARCH_PLANS.length)]; 14 | 15 | console.log('\n🔍 Testing TavilySearchTool'); 16 | console.log('=========================\n'); 17 | 18 | console.log(`📝 Search Plan: "${searchPlan}"\n`); 19 | 20 | try { 21 | const initialState: ResearchState = { 22 | findGapLoops: 0, 23 | researchQuery: "original query", 24 | searchPlan: searchPlan 25 | }; 26 | 27 | console.log('⏳ Searching...\n'); 28 | 29 | const result = await tool.invoke(initialState); 30 | 31 | console.log('✅ Success!'); 32 | console.log('------------\n'); 33 | 34 | if (result.searchResults) { 35 | console.log(`Found ${result.searchResults.length} results:\n`); 36 | 37 | result.searchResults.forEach((res, index) => { 38 | console.log(`Result ${index + 1}:`); 39 | console.log(`🔗 ${res.url}`); 40 | console.log(`📖 ${res.title}`); 41 | console.log(`🎯 Score: ${res.score}`); 42 | console.log(`📄 Content Preview: ${res.content.substring(0, 150)}...\n`); 43 | }); 44 | } 45 | 46 | } catch (error) { 47 | console.error('❌ Error:', error); 48 | process.exit(1); 49 | } 50 | } 51 | 52 | testTavilySearch(); -------------------------------------------------------------------------------- /backend/src/tests/websocket.test.ts: -------------------------------------------------------------------------------- 1 | import WebSocket from 'ws'; 2 | import { config } from '../config/config'; 3 | import { StartResearchMessage } from '../interfaces/deepresearch.interface'; 4 | 5 | const TEST_QUERIES = [ 6 | 'How does the JavaScript event loop work?', 7 | 'What are JavaScript Promises and how do they work?', 8 | 'Explain React\'s Virtual DOM and reconciliation process', 9 | 'What is TypeScript and how does it enhance JavaScript?' 10 | ]; 11 | 12 | const wsUrl = `ws://localhost:${config.port}${config.ws.path}`; 13 | console.log(`\n🔌 Conectando a ${wsUrl}\n`); 14 | 15 | const ws = new WebSocket(wsUrl); 16 | 17 | ws.on('open', () => { 18 | console.log('✅ Conexión establecida\n'); 19 | const randomQuery = TEST_QUERIES[Math.floor(Math.random() * TEST_QUERIES.length)]; 20 | 21 | // Formato correcto del mensaje de inicio 22 | const startMessage: StartResearchMessage = { 23 | action: 'start', 24 | query: randomQuery 25 | }; 26 | 27 | console.log(`📤 Enviando mensaje de inicio:`); 28 | console.log(JSON.stringify(startMessage, null, 2)); 29 | console.log(); 30 | 31 | ws.send(JSON.stringify(startMessage)); 32 | }); 33 | 34 | ws.on('message', (data) => { 35 | const message = JSON.parse(data.toString()); 36 | 37 | if (message.error) { 38 | console.log(`❌ Error: ${message.error}`); 39 | return; 40 | } 41 | 42 | const timestamp = new Date(message.timestamp).toLocaleTimeString(); 43 | const progressBar = createProgressBar(message.progress); 44 | 45 | console.log(`[${timestamp}] ${progressBar} ${message.progress}%`); 46 | console.log(`📝 Paso: ${message.step}`); 47 | console.log(`📄 Detalles: ${message.details}\n`); 48 | 49 | if (message.step === 'complete') { 50 | console.log('✅ Investigación completada'); 51 | console.log('Documento final (primeras 10 líneas):'); 52 | if (message.completion) { 53 | console.log(message.completion.split('\n').slice(0, 10).join('\n')); 54 | console.log('...\n'); 55 | } 56 | ws.close(); 57 | process.exit(0); 58 | } 59 | }); 60 | 61 | ws.on('error', (error) => { 62 | console.error('❌ Error de WebSocket:', error); 63 | process.exit(1); 64 | }); 65 | 66 | function createProgressBar(progress: number): string { 67 | const width = 30; 68 | const filled = Math.round(width * (progress / 100)); 69 | const empty = width - filled; 70 | return '█'.repeat(filled) + '░'.repeat(empty); 71 | } -------------------------------------------------------------------------------- /backend/src/tools/TavilySearchTool.ts: -------------------------------------------------------------------------------- 1 | import { tavily } from '@tavily/core'; 2 | import { config } from '../config/config'; 3 | import { ResearchState, SearchResult } from '../interfaces/state.interface'; 4 | import { TavilyResponse, TavilyResult } from '../interfaces/tavily.interface'; 5 | import logger from '../utils/logger'; 6 | 7 | export class TavilySearchTool { 8 | private client: ReturnType; 9 | private retryCount: number = 0; 10 | 11 | constructor() { 12 | this.client = tavily({ apiKey: config.tavily.apiKey }); 13 | } 14 | 15 | private async searchWithRetry( 16 | query: string, 17 | maxResults: number 18 | ): Promise { 19 | try { 20 | const response = await this.client.search(query, { 21 | searchDepth: 'advanced', 22 | maxResults, 23 | includeRawContent: true 24 | }) as TavilyResponse; 25 | 26 | if (!response.results || response.results.length === 0) { 27 | this.retryCount++; 28 | 29 | if (this.retryCount >= config.tavily.maxRetries) { 30 | throw new Error(`No results found after ${config.tavily.maxRetries} attempts`); 31 | } 32 | 33 | logger.info(`No results found, retrying with ${maxResults * 2} results...`); 34 | return this.searchWithRetry(query, maxResults * 2); 35 | } 36 | 37 | return response.results.map((result: TavilyResult) => ({ 38 | title: result.title, 39 | url: result.url, 40 | content: result.content, 41 | rawContent: result.rawContent || '', 42 | score: result.score 43 | })); 44 | 45 | } catch (error) { 46 | if (error instanceof Error && error.message.includes('No results found')) { 47 | throw error; 48 | } 49 | 50 | logger.error('Error searching with Tavily:', error); 51 | throw new Error('Failed to perform search'); 52 | } 53 | } 54 | 55 | async invoke(state: ResearchState): Promise { 56 | if (!state.searchPlan) { 57 | throw new Error('No search plan available'); 58 | } 59 | 60 | this.retryCount = 0; 61 | const results = await this.searchWithRetry( 62 | state.searchPlan, 63 | config.tavily.initialResults 64 | ); 65 | 66 | return { 67 | ...state, 68 | searchResults: results 69 | }; 70 | } 71 | } -------------------------------------------------------------------------------- /backend/src/utils/logger.ts: -------------------------------------------------------------------------------- 1 | import winston from 'winston'; 2 | import { config } from '../config/config'; 3 | 4 | const logger = winston.createLogger({ 5 | level: config.logs.level, 6 | format: winston.format.combine( 7 | winston.format.timestamp(), 8 | winston.format.json() 9 | ), 10 | transports: [ 11 | new winston.transports.Console({ 12 | format: winston.format.combine( 13 | winston.format.colorize(), 14 | winston.format.simple() 15 | ), 16 | }), 17 | ], 18 | }); 19 | 20 | if (config.env === 'production') { 21 | logger.add( 22 | new winston.transports.File({ filename: 'logs/error.log', level: 'error' }) 23 | ); 24 | logger.add(new winston.transports.File({ filename: 'logs/combined.log' })); 25 | } 26 | 27 | export default logger; -------------------------------------------------------------------------------- /backend/src/utils/text.utils.ts: -------------------------------------------------------------------------------- 1 | export const removeThinkingTags = (text: string): string => { 2 | return text.replace(/[\s\S]*?<\/think>/g, '').trim(); 3 | }; 4 | 5 | export const extractFromTags = (text: string, tag: string): string => { 6 | const regex = new RegExp(`<${tag}>(.*?)<\/${tag}>`, 's'); 7 | const match = text.match(regex); 8 | return match ? match[1].trim() : ''; 9 | }; -------------------------------------------------------------------------------- /backend/src/websockets/index.ts: -------------------------------------------------------------------------------- 1 | import { Server as HttpServer } from 'http'; 2 | import { WebSocketServer } from 'ws'; 3 | import { DeepResearchController } from '../controllers/deepresearch.controller'; 4 | import { config } from '../config/config'; 5 | import logger from '../utils/logger'; 6 | 7 | export const setupWebSocket = (server: HttpServer): void => { 8 | const wss = new WebSocketServer({ 9 | server, 10 | path: config.ws.path 11 | }); 12 | const deepResearchController = new DeepResearchController(); 13 | 14 | wss.on('connection', (ws, request) => { 15 | const clientIp = request.socket.remoteAddress; 16 | logger.info(`New WebSocket connection from ${clientIp} on path ${request.url}`); 17 | 18 | deepResearchController.handleConnection(ws); 19 | }); 20 | 21 | wss.on('error', (error) => { 22 | logger.error('WebSocket server error:', error); 23 | }); 24 | 25 | logger.info(`WebSocket server initialized on path: ${config.ws.path}`); 26 | }; -------------------------------------------------------------------------------- /backend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig to read more about this file */ 4 | 5 | /* Projects */ 6 | // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ 7 | // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ 8 | // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ 9 | // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ 10 | // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ 11 | // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ 12 | 13 | /* Language and Environment */ 14 | "target": "es2016", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ 15 | // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ 16 | // "jsx": "preserve", /* Specify what JSX code is generated. */ 17 | // "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */ 18 | // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ 19 | // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ 20 | // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ 21 | // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ 22 | // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ 23 | // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ 24 | // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ 25 | // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ 26 | 27 | /* Modules */ 28 | "module": "commonjs", /* Specify what module code is generated. */ 29 | "rootDir": "./src", 30 | // "moduleResolution": "node10", /* Specify how TypeScript looks up a file from a given module specifier. */ 31 | // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ 32 | // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ 33 | // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ 34 | // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ 35 | // "types": [], /* Specify type package names to be included without being referenced in a source file. */ 36 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 37 | // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ 38 | // "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */ 39 | // "rewriteRelativeImportExtensions": true, /* Rewrite '.ts', '.tsx', '.mts', and '.cts' file extensions in relative import paths to their JavaScript equivalent in output files. */ 40 | // "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */ 41 | // "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */ 42 | // "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */ 43 | // "noUncheckedSideEffectImports": true, /* Check side effect imports. */ 44 | // "resolveJsonModule": true, /* Enable importing .json files. */ 45 | // "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */ 46 | // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ 47 | 48 | /* JavaScript Support */ 49 | // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ 50 | // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ 51 | // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ 52 | 53 | /* Emit */ 54 | // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ 55 | // "declarationMap": true, /* Create sourcemaps for d.ts files. */ 56 | // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ 57 | // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ 58 | // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ 59 | // "noEmit": true, /* Disable emitting files from a compilation. */ 60 | // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ 61 | "outDir": "./dist", 62 | // "removeComments": true, /* Disable emitting comments. */ 63 | // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ 64 | // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ 65 | // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ 66 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 67 | // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ 68 | // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ 69 | // "newLine": "crlf", /* Set the newline character for emitting files. */ 70 | // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ 71 | // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ 72 | // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ 73 | // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ 74 | // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ 75 | 76 | /* Interop Constraints */ 77 | // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ 78 | // "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */ 79 | // "isolatedDeclarations": true, /* Require sufficient annotation on exports so other tools can trivially generate declaration files. */ 80 | // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ 81 | "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */ 82 | // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ 83 | "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ 84 | 85 | /* Type Checking */ 86 | "strict": true, /* Enable all strict type-checking options. */ 87 | // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ 88 | // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ 89 | // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ 90 | // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ 91 | // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ 92 | // "strictBuiltinIteratorReturn": true, /* Built-in iterators are instantiated with a 'TReturn' type of 'undefined' instead of 'any'. */ 93 | // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ 94 | // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ 95 | // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ 96 | // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ 97 | // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ 98 | // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ 99 | // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ 100 | // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ 101 | // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ 102 | // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ 103 | // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ 104 | // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ 105 | // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ 106 | 107 | /* Completeness */ 108 | // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ 109 | "skipLibCheck": true /* Skip type checking all .d.ts files. */ 110 | }, 111 | "include": ["src/**/*"], 112 | "exclude": ["node_modules"] 113 | } 114 | -------------------------------------------------------------------------------- /frontend/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.* 7 | .yarn/* 8 | !.yarn/patches 9 | !.yarn/plugins 10 | !.yarn/releases 11 | !.yarn/versions 12 | 13 | # testing 14 | /coverage 15 | 16 | # next.js 17 | /.next/ 18 | /out/ 19 | 20 | # production 21 | /build 22 | 23 | # misc 24 | .DS_Store 25 | *.pem 26 | 27 | # debug 28 | npm-debug.log* 29 | yarn-debug.log* 30 | yarn-error.log* 31 | .pnpm-debug.log* 32 | 33 | # env files (can opt-in for committing if needed) 34 | .env* 35 | 36 | # vercel 37 | .vercel 38 | 39 | # typescript 40 | *.tsbuildinfo 41 | next-env.d.ts 42 | -------------------------------------------------------------------------------- /frontend/README.md: -------------------------------------------------------------------------------- 1 | This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app). 2 | 3 | ## Getting Started 4 | 5 | First, run the development server: 6 | 7 | ```bash 8 | npm run dev 9 | # or 10 | yarn dev 11 | # or 12 | pnpm dev 13 | # or 14 | bun dev 15 | ``` 16 | 17 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 18 | 19 | You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. 20 | 21 | This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel. 22 | 23 | ## Learn More 24 | 25 | To learn more about Next.js, take a look at the following resources: 26 | 27 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 28 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. 29 | 30 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome! 31 | 32 | ## Deploy on Vercel 33 | 34 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. 35 | 36 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details. 37 | -------------------------------------------------------------------------------- /frontend/app/components/ProcessingStatus.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, ReactNode } from 'react'; 2 | import ReactMarkdown from 'react-markdown'; 3 | import { Components } from 'react-markdown'; 4 | import remarkGfm from 'remark-gfm'; 5 | import rehypeRaw from 'rehype-raw'; 6 | import rehypeHighlight from 'rehype-highlight'; 7 | import confetti from 'canvas-confetti'; 8 | 9 | interface ProcessingStatusProps { 10 | isProcessing: boolean; 11 | status: string; 12 | result: string; 13 | progress: number; 14 | onStop: () => void; 15 | } 16 | 17 | interface MarkdownProps { 18 | children?: ReactNode; 19 | [key: string]: any; 20 | } 21 | 22 | const MarkdownComponents: Partial = { 23 | h1: ({node, ...props}) =>

, 24 | h2: ({node, ...props}) =>

, 25 | h3: ({node, ...props}) =>

, 26 | p: ({node, ...props}) =>

, 27 | ul: ({node, ...props}) =>