Update metaSearchAgent.ts

This commit is contained in:
Lucas 2025-01-06 10:22:47 +01:00 committed by GitHub
parent 3e53bab9b4
commit 19148eeba7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -11,7 +11,11 @@ import {
RunnableMap, RunnableMap,
RunnableSequence, RunnableSequence,
} from '@langchain/core/runnables'; } from '@langchain/core/runnables';
import { BaseMessage } from '@langchain/core/messages'; import {
BaseMessage,
AIMessage,
HumanMessage,
} from '@langchain/core/messages';
import { StringOutputParser } from '@langchain/core/output_parsers'; import { StringOutputParser } from '@langchain/core/output_parsers';
import LineListOutputParser from '../lib/outputParsers/listLineOutputParser'; import LineListOutputParser from '../lib/outputParsers/listLineOutputParser';
import LineOutputParser from '../lib/outputParsers/lineOutputParser'; import LineOutputParser from '../lib/outputParsers/lineOutputParser';
@ -33,10 +37,9 @@ import { SearxngSearchOptions } from '../lib/searxng';
import { ChromaClient } from 'chromadb'; import { ChromaClient } from 'chromadb';
import { OpenAIEmbeddings } from '@langchain/openai'; import { OpenAIEmbeddings } from '@langchain/openai';
import { EventEmitter } from 'events'; import { EventEmitter } from 'events';
import { MemoryVectorStore } from "langchain/vectorstores/memory"; import { webSearchRetrieverPrompt, webSearchResponsePrompt } from '../prompts/webSearch';
export interface MetaSearchAgentType { export interface MetaSearchAgentType {
initialize: (embeddings: Embeddings) => Promise<void>;
searchAndAnswer: ( searchAndAnswer: (
message: string, message: string,
history: BaseMessage[], history: BaseMessage[],
@ -49,8 +52,8 @@ export interface MetaSearchAgentType {
interface Config { interface Config {
activeEngines: string[]; activeEngines: string[];
queryGeneratorPrompt: string; queryGeneratorPrompt?: string;
responsePrompt: string; responsePrompt?: string;
rerank: boolean; rerank: boolean;
rerankThreshold: number; rerankThreshold: number;
searchWeb: boolean; searchWeb: boolean;
@ -99,52 +102,26 @@ export class MetaSearchAgent implements MetaSearchAgentType {
private config: Config; private config: Config;
private strParser = new StringOutputParser(); private strParser = new StringOutputParser();
private fileIds: string[]; private fileIds: string[];
private memoryStore: MemoryVectorStore; private conversationHistory: BaseMessage[] = [];
constructor(config: Config) { constructor(config: Config) {
this.config = config; this.config = config;
this.fileIds = []; this.fileIds = [];
} }
async initialize(embeddings: Embeddings) { private updateMemory(message: BaseMessage) {
try { this.conversationHistory.push(message);
if (!this.memoryStore) {
console.log("🔄 Initialisation du memory store...");
this.memoryStore = new MemoryVectorStore(embeddings);
await this.memoryStore.addDocuments([{
pageContent: "Initialisation du memory store",
metadata: { timestamp: Date.now() }
}]);
console.log("✅ Memory store initialisé avec succès");
}
} catch (error) {
console.error("❌ Erreur lors de l'initialisation du memory store:", error);
throw error;
}
} }
private async enrichWithMemory(message: string, embeddings: Embeddings): Promise<string> { public getMemory(): BaseMessage[] {
try { return this.conversationHistory;
if (!this.memoryStore) {
await this.initialize(embeddings);
}
await this.memoryStore.addDocuments([{
pageContent: message,
metadata: { timestamp: Date.now() }
}]);
const results = await this.memoryStore.similaritySearch(message, 2);
return results.map(doc => doc.pageContent).join('\n');
} catch (error) {
console.error("Erreur mémoire:", error);
return '';
}
} }
private async createSearchRetrieverChain(llm: BaseChatModel) { private async createSearchRetrieverChain(llm: BaseChatModel) {
(llm as unknown as ChatOpenAI).temperature = 0; (llm as unknown as ChatOpenAI).temperature = 0;
return RunnableSequence.from([ return RunnableSequence.from([
PromptTemplate.fromTemplate(this.config.queryGeneratorPrompt), PromptTemplate.fromTemplate(webSearchRetrieverPrompt),
llm, llm,
this.strParser, this.strParser,
RunnableLambda.from(async (input: string) => { RunnableLambda.from(async (input: string) => {
@ -191,7 +168,7 @@ export class MetaSearchAgent implements MetaSearchAgentType {
// Recherche d'experts si activée // Recherche d'experts si activée
if (this.config.searchDatabase) { if (this.config.searchDatabase) {
try { try {
console.log("🔍 Recherche d'experts..."); console.log('🔍 Recherche d\'experts...');
const expertResults = await handleExpertSearch( const expertResults = await handleExpertSearch(
{ {
query: question, query: question,
@ -202,7 +179,7 @@ export class MetaSearchAgent implements MetaSearchAgentType {
llm llm
); );
console.log("🔍 Experts trouvés:", expertResults.experts.length); console.log('🔍 Experts trouvés:', expertResults.experts.length);
const expertDocs = expertResults.experts.map(expert => const expertDocs = expertResults.experts.map(expert =>
new Document({ new Document({
pageContent: `Expert: ${expert.prenom} ${expert.nom} pageContent: `Expert: ${expert.prenom} ${expert.nom}
@ -225,7 +202,7 @@ export class MetaSearchAgent implements MetaSearchAgentType {
documents = [...expertDocs, ...documents]; documents = [...expertDocs, ...documents];
} catch (error) { } catch (error) {
console.error("Erreur lors de la recherche d'experts:", error); console.error('Erreur lors de la recherche d\'experts:', error);
} }
} }
@ -242,7 +219,7 @@ export class MetaSearchAgent implements MetaSearchAgentType {
} }
private async loadUploadedDocuments(fileIds: string[]): Promise<Document[]> { private async loadUploadedDocuments(fileIds: string[]): Promise<Document[]> {
console.log("📂 Chargement des documents:", fileIds); console.log('📂 Chargement des documents:', fileIds);
const docs: Document[] = []; const docs: Document[] = [];
for (const fileId of fileIds) { for (const fileId of fileIds) {
@ -310,56 +287,46 @@ export class MetaSearchAgent implements MetaSearchAgentType {
query: (input: BasicChainInput) => input.query, query: (input: BasicChainInput) => input.query,
chat_history: (input: BasicChainInput) => input.chat_history, chat_history: (input: BasicChainInput) => input.chat_history,
docs: RunnableLambda.from(async (input: BasicChainInput) => { docs: RunnableLambda.from(async (input: BasicChainInput) => {
console.log("Début de la recherche avec contexte enrichi..."); console.log('Début de la recherche...');
let docs: Document[] = []; let docs: Document[] = [];
// Récupérer le contexte historique // 1. D'abord chercher dans les documents uploadés
const memoryContext = await this.getRelevantContext(input.query);
console.log("💭 Contexte historique pour la recherche:", memoryContext);
// Enrichir la requête avec le contexte
const enrichedQuery = `
Contexte précédent:
${memoryContext}
Question actuelle:
${input.query}
`;
// 1. D'abord chercher dans les documents uploadés avec le contexte enrichi
if (fileIds.length > 0) { if (fileIds.length > 0) {
try { try {
const uploadedDocs = await this.loadUploadedDocuments(fileIds); const uploadedDocs = await this.loadUploadedDocuments(fileIds);
console.log("📚 Documents uploadés chargés:", uploadedDocs.length); console.log('📚 Documents uploadés chargés:', uploadedDocs.length);
// Utiliser RAGDocumentChain pour la recherche dans les documents
const ragChain = new RAGDocumentChain(); const ragChain = new RAGDocumentChain();
await ragChain.initializeVectorStoreFromDocuments(uploadedDocs, embeddings); await ragChain.initializeVectorStoreFromDocuments(uploadedDocs, embeddings);
// Utiliser le type 'specific' pour une recherche précise
const searchChain = ragChain.createSearchChain(llm); const searchChain = ragChain.createSearchChain(llm);
const relevantDocs = await searchChain.invoke({ const relevantDocs = await searchChain.invoke({
query: enrichedQuery, query: input.query,
chat_history: input.chat_history, chat_history: input.chat_history,
type: 'specific' type: 'specific'
}); });
// Ajouter les documents pertinents avec un score élevé
docs = uploadedDocs.map(doc => ({ docs = uploadedDocs.map(doc => ({
...doc, ...doc,
metadata: { metadata: {
...doc.metadata, ...doc.metadata,
score: 0.8 score: 0.8 // Score élevé pour les documents uploadés
} }
})); }));
console.log("📄 Documents pertinents trouvés:", docs.length); console.log('📄 Documents pertinents trouvés:', docs.length);
} catch (error) { } catch (error) {
console.error("❌ Erreur lors de la recherche dans les documents:", error); console.error('❌ Erreur lors de la recherche dans les documents:', error);
} }
} }
// 2. Ensuite chercher les experts si pertinent // 2. Ensuite chercher les experts si pertinent
if (this.config.searchDatabase) { if (this.config.searchDatabase) {
try { try {
console.log("👥 Recherche d'experts..."); console.log('👥 Recherche d\'experts...');
const expertResults = await handleExpertSearch( const expertResults = await handleExpertSearch(
{ {
query: input.query, query: input.query,
@ -375,7 +342,7 @@ export class MetaSearchAgent implements MetaSearchAgentType {
docs = [...docs, ...expertDocs]; docs = [...docs, ...expertDocs];
} }
} catch (error) { } catch (error) {
console.error("❌ Erreur lors de la recherche d'experts:", error); console.error('❌ Erreur lors de la recherche d\'experts:', error);
} }
} }
@ -385,13 +352,13 @@ export class MetaSearchAgent implements MetaSearchAgentType {
const webResults = await this.performWebSearch(input.query); const webResults = await this.performWebSearch(input.query);
docs = [...docs, ...webResults]; docs = [...docs, ...webResults];
} catch (error) { } catch (error) {
console.error("❌ Erreur lors de la recherche web:", error); console.error('❌ Erreur lors de la recherche web:', error);
} }
} }
console.log("🔍 DEBUG - Avant appel rerankDocs - Mode:", optimizationMode, "Query:", input.query); console.log('🔍 DEBUG - Avant appel rerankDocs - Mode:', optimizationMode, 'Query:', input.query);
return this.rerankDocs( return this.rerankDocs(
enrichedQuery, input.query,
docs, docs,
fileIds, fileIds,
embeddings, embeddings,
@ -406,14 +373,14 @@ export class MetaSearchAgent implements MetaSearchAgentType {
chat_history: (input) => input.chat_history, chat_history: (input) => input.chat_history,
date: () => new Date().toISOString(), date: () => new Date().toISOString(),
context: (input) => { context: (input) => {
console.log("Préparation du contexte..."); console.log('Préparation du contexte...');
return this.processDocs(input.docs); return this.processDocs(input.docs);
}, },
docs: (input) => input.docs, docs: (input) => input.docs,
}), }),
ChatPromptTemplate.fromMessages([ ChatPromptTemplate.fromMessages([
['system', this.config.responsePrompt], ['system', webSearchResponsePrompt],
new MessagesPlaceholder('chat_history'), new MessagesPlaceholder('chat_history'),
['user', '{context}\n\n{query}'], ['user', '{context}\n\n{query}'],
]), ]),
@ -465,8 +432,8 @@ export class MetaSearchAgent implements MetaSearchAgentType {
private processDocs(docs: Document[]) { private processDocs(docs: Document[]) {
// Trier les documents par score si disponible // Trier les documents par score si disponible
const sortedDocs = docs.sort((a, b) => const sortedDocs = docs.sort(
(b.metadata?.score || 0) - (a.metadata?.score || 0) (a, b) => (b.metadata?.score || 0) - (a.metadata?.score || 0)
); );
// Limiter à 5 documents maximum // Limiter à 5 documents maximum
@ -475,8 +442,9 @@ export class MetaSearchAgent implements MetaSearchAgentType {
// Limiter la taille de chaque document à 1000 caractères // Limiter la taille de chaque document à 1000 caractères
return limitedDocs return limitedDocs
.map((doc, index) => { .map((doc, index) => {
const content = doc.pageContent.length > 1000 const content =
? doc.pageContent.substring(0, 1000) + "..." doc.pageContent.length > 1000
? doc.pageContent.substring(0, 1000) + '...'
: doc.pageContent; : doc.pageContent;
return `${content} [${index + 1}]`; return `${content} [${index + 1}]`;
@ -486,7 +454,7 @@ export class MetaSearchAgent implements MetaSearchAgentType {
private async handleStream( private async handleStream(
stream: IterableReadableStream<StreamEvent>, stream: IterableReadableStream<StreamEvent>,
emitter: eventEmitter, emitter: eventEmitter
) { ) {
for await (const event of stream) { for await (const event of stream) {
if ( if (
@ -496,7 +464,8 @@ export class MetaSearchAgent implements MetaSearchAgentType {
const sources = event.data.output; const sources = event.data.output;
// Normaliser les sources pour le frontend // Normaliser les sources pour le frontend
const normalizedSources = sources?.map(source => { const normalizedSources =
sources?.map(source => {
const isUploadedDoc = source.metadata?.type === 'uploaded'; const isUploadedDoc = source.metadata?.type === 'uploaded';
const isExpert = source.metadata?.type === 'expert'; const isExpert = source.metadata?.type === 'expert';
const pageNumber = source.metadata?.pageNumber || 1; const pageNumber = source.metadata?.pageNumber || 1;
@ -531,7 +500,9 @@ export class MetaSearchAgent implements MetaSearchAgentType {
url: url, url: url,
source: sourceId, source: sourceId,
pageNumber: pageNumber, pageNumber: pageNumber,
searchText: source.metadata?.searchText?.substring(0, 200) || limitedContent.substring(0, 200), searchText:
source.metadata?.searchText?.substring(0, 200) ||
limitedContent.substring(0, 200),
expertData: source.metadata?.expertData, expertData: source.metadata?.expertData,
illustrationImage: source.metadata?.illustrationImage, illustrationImage: source.metadata?.illustrationImage,
imageTitle: source.metadata?.imageTitle, imageTitle: source.metadata?.imageTitle,
@ -542,7 +513,7 @@ export class MetaSearchAgent implements MetaSearchAgentType {
}; };
}) || []; }) || [];
console.log("🔍 Sources normalisées:", normalizedSources.length); console.log('🔍 Sources normalisées:', normalizedSources.length);
emitter.emit( emitter.emit(
'data', 'data',
@ -572,13 +543,100 @@ export class MetaSearchAgent implements MetaSearchAgentType {
} }
} }
private async handleStreamWithMemory(
stream: IterableReadableStream<StreamEvent>,
emitter: eventEmitter
) {
let fullAssistantResponse = '';
for await (const event of stream) {
if (event.event === 'on_chain_stream') {
if (event.name === 'FinalResponseGenerator') {
fullAssistantResponse += event.data.chunk;
emitter.emit(
'data',
JSON.stringify({ type: 'response', data: event.data.chunk })
);
}
} else if (event.event === 'on_chain_end') {
if (event.name === 'FinalResponseGenerator') {
this.updateMemory(new AIMessage(fullAssistantResponse.trim()));
emitter.emit('end');
}
if (event.name === 'FinalSourceRetriever') {
const sources = event.data.output;
const normalizedSources =
sources?.map(source => {
const isUploadedDoc = source.metadata?.type === 'uploaded';
const isExpert = source.metadata?.type === 'expert';
const pageNumber = source.metadata?.pageNumber || 1;
const sourceId = source.metadata?.source;
let url;
if (isUploadedDoc && sourceId) {
url = `/api/uploads/${sourceId}/content?page=${pageNumber}`;
} else if (isExpert) {
url = source.metadata?.url;
} else if (source.metadata?.type === 'web') {
url = source.metadata?.url;
}
let title = source.metadata?.title || '';
if (isUploadedDoc && title) {
title = `${title} - Page ${pageNumber}`;
} else if (isExpert) {
title = source.metadata?.displayTitle || title;
}
const limitedContent =
source.pageContent?.substring(0, 1000) || '';
return {
pageContent: limitedContent,
metadata: {
title: title,
type: source.metadata?.type || 'web',
url: url,
source: sourceId,
pageNumber: pageNumber,
searchText:
source.metadata?.searchText?.substring(0, 200) ||
limitedContent.substring(0, 200),
expertData: source.metadata?.expertData,
illustrationImage: source.metadata?.illustrationImage,
imageTitle: source.metadata?.imageTitle,
favicon: source.metadata?.favicon,
linkText: source.metadata?.linkText,
expertName: source.metadata?.expertName
}
};
}) || [];
console.log('🔍 Sources normalisées:', normalizedSources.length);
emitter.emit(
'data',
JSON.stringify({
type: 'sources',
data: normalizedSources,
illustrationImage: normalizedSources[0]?.metadata?.illustrationImage || null,
imageTitle: normalizedSources[0]?.metadata?.imageTitle || null
})
);
}
} else {
emitter.emit(event.event, event.data);
}
}
}
private async searchExperts( private async searchExperts(
query: string, query: string,
embeddings: Embeddings, embeddings: Embeddings,
llm: BaseChatModel llm: BaseChatModel
): Promise<SearchResult[]> { ): Promise<SearchResult[]> {
try { try {
console.log("👥 Recherche d'experts pour:", query); console.log('👥 Recherche d\'experts pour:', query);
const expertResults = await handleExpertSearch( const expertResults = await handleExpertSearch(
{ {
query, query,
@ -608,14 +666,14 @@ export class MetaSearchAgent implements MetaSearchAgentType {
} }
})); }));
} catch (error) { } catch (error) {
console.error("❌ Erreur lors de la recherche d'experts:", error); console.error('❌ Erreur lors de la recherche d\'experts:', error);
return []; return [];
} }
} }
private async searchWeb(query: string): Promise<SearchResult[]> { private async searchWeb(query: string): Promise<SearchResult[]> {
try { try {
console.log("🌐 Recherche web pour:", query); console.log('🌐 Recherche web pour:', query);
const res = await searchSearxng(query, { const res = await searchSearxng(query, {
language: 'fr', language: 'fr',
engines: this.config.activeEngines, engines: this.config.activeEngines,
@ -632,7 +690,7 @@ export class MetaSearchAgent implements MetaSearchAgentType {
} }
})); }));
} catch (error) { } catch (error) {
console.error("❌ Erreur lors de la recherche web:", error); console.error('❌ Erreur lors de la recherche web:', error);
return []; return [];
} }
} }
@ -645,13 +703,13 @@ export class MetaSearchAgent implements MetaSearchAgentType {
optimizationMode: 'speed' | 'balanced' | 'quality', optimizationMode: 'speed' | 'balanced' | 'quality',
llm: BaseChatModel llm: BaseChatModel
) { ) {
console.log("🔍 Mode d'optimisation:", optimizationMode); console.log('🔍 Mode d\'optimisation:', optimizationMode);
console.log("🔍 Query pour la recherche d'image:", query); console.log('🔍 Query pour la recherche d\'image:', query);
if (optimizationMode === 'balanced' || optimizationMode === 'quality') { if (optimizationMode === 'balanced' || optimizationMode === 'quality') {
console.log("🔍 Démarrage de la recherche d'images..."); console.log('🔍 Démarrage de la recherche d\'images...');
try { try {
console.log("🔍 Appel de handleImageSearch avec la query:", query); console.log('🔍 Appel de handleImageSearch avec la query:', query);
const images = await handleImageSearch( const images = await handleImageSearch(
{ {
query, query,
@ -659,11 +717,11 @@ export class MetaSearchAgent implements MetaSearchAgentType {
}, },
llm llm
); );
console.log("🔍 Résultat brut de handleImageSearch:", JSON.stringify(images, null, 2)); console.log('🔍 Résultat brut de handleImageSearch:', JSON.stringify(images, null, 2));
console.log("🔍 Images trouvées:", images?.length); console.log('🔍 Images trouvées:', images?.length);
if (images && images.length > 0) { if (images && images.length > 0) {
console.log("🔍 Première image trouvée:", { console.log('🔍 Première image trouvée:', {
src: images[0].img_src, src: images[0].img_src,
title: images[0].title, title: images[0].title,
url: images[0].url url: images[0].url
@ -673,238 +731,102 @@ export class MetaSearchAgent implements MetaSearchAgentType {
metadata: { metadata: {
...doc.metadata, ...doc.metadata,
illustrationImage: images[0].img_src, illustrationImage: images[0].img_src,
title: images[0].title imageTitle: images[0].title
} }
})); }));
} else { } else {
console.log("⚠️ Aucune image trouvée dans le résultat"); console.log('⚠️ Aucune image trouvée dans le résultat');
} }
} catch (error) { } catch (error) {
console.error("❌ Erreur détaillée lors de la recherche d'image:", { console.error('❌ Erreur détaillée lors de la recherche d\'image:', {
message: error.message, message: error.message,
stack: error.stack stack: error.stack
}); });
} }
} else { } else {
console.log("🔍 Mode speed: pas de recherche d'images"); console.log('🔍 Mode speed: pas de recherche d\'images');
} }
return docs.slice(0, 15); return docs.slice(0, 15);
} }
private async updateMemoryStore(message: string, history: BaseMessage[]) {
try {
if (!this.memoryStore) {
throw new Error("Memory store not initialized");
}
// Créer un document avec le contexte actuel et sa structure
const contextDoc = {
pageContent: `Question: ${message}\nContext: ${history.map(m =>
`${m._getType()}: ${m.content}`).join('\n')}`,
metadata: {
timestamp: Date.now(),
themes: this.extractThemes(message + ' ' + history.map(m => m.content).join(' ')),
type: 'conversation'
}
};
console.log("💾 Ajout à la mémoire:", contextDoc);
await this.memoryStore.addDocuments([contextDoc]);
console.log("✅ Mémoire mise à jour avec succès");
} catch (error) {
console.error("❌ Erreur lors de la mise à jour de la mémoire:", error);
}
}
private async getRelevantContext(message: string): Promise<string> {
try {
if (!this.memoryStore) {
throw new Error("Memory store not initialized");
}
console.log("🔍 Recherche dans la mémoire pour:", message);
const results = await this.memoryStore.similaritySearch(message, 3);
console.log("📚 Contexte trouvé dans la mémoire:", results);
return results.map(doc => doc.pageContent).join('\n');
} catch (error) {
console.error("❌ Erreur lors de la récupération du contexte:", error);
return '';
}
}
private extractThemes(context: string): string[] {
const commonThemes = ['sas', 'entreprise', 'création', 'chomage', 'juridique', 'financement'];
return commonThemes.filter(theme =>
context.toLowerCase().includes(theme.toLowerCase())
);
}
private async analyzeConversationContext(
message: string,
history: BaseMessage[],
llm: BaseChatModel
): Promise<{ context: string; relevance: number }> {
try {
const formattedHistory = formatChatHistoryAsString(history);
const analysis = await llm.invoke(`
Analysez cette conversation en profondeur en donnant une importance particulière aux 3 derniers échanges.
Assurez-vous de maintenir la cohérence du contexte entre les questions.
Historique de la conversation:
${formattedHistory}
Nouvelle question: "${message}"
Instructions spécifiques:
1. Identifiez les thèmes récurrents des 3 derniers échanges
2. Notez les contraintes ou conditions mentionnées précédemment qui restent pertinentes
3. Évaluez comment la nouvelle question s'inscrit dans la continuité des échanges
Répondez au format JSON:
{
"mainTopic": "sujet principal actuel",
"recentContext": {
"lastThemes": ["thèmes des 3 derniers échanges"],
"activeConstraints": ["contraintes toujours actives"],
"continuityScore": <0.0 à 1.0>
},
"contextualFactors": {
"financial": ["facteurs financiers"],
"legal": ["aspects juridiques"],
"administrative": ["aspects administratifs"]
},
"impactAnalysis": {
"primary": "impact principal",
"secondary": ["impacts secondaires"],
"constraints": ["contraintes identifiées"]
},
"relevanceScore": <0.0 à 1.0>
}
`);
const result = JSON.parse(String(analysis.content));
// Construction d'un contexte enrichi qui met l'accent sur la continuité
const enrichedContext = `
Contexte principal: ${result.mainTopic}
Thèmes récents: ${result.recentContext.lastThemes.join(', ')}
Contraintes actives: ${result.recentContext.activeConstraints.join(', ')}
Facteurs impactants: ${result.contextualFactors.financial.join(', ')}
Implications légales: ${result.contextualFactors.legal.join(', ')}
Impact global: ${result.impactAnalysis.primary}
Contraintes à considérer: ${result.impactAnalysis.constraints.join(', ')}
`.trim();
// Ajuster le score de pertinence en fonction de la continuité
const adjustedRelevance = (result.relevanceScore + result.recentContext.continuityScore) / 2;
return {
context: enrichedContext,
relevance: adjustedRelevance
};
} catch (error) {
console.error("Erreur lors de l'analyse du contexte:", error);
return { context: '', relevance: 0 };
}
}
async searchAndAnswer( async searchAndAnswer(
message: string, message: string,
history: BaseMessage[], history: BaseMessage[],
llm: BaseChatModel, llm: BaseChatModel,
embeddings: Embeddings, embeddings: Embeddings,
optimizationMode: 'speed' | 'balanced' | 'quality', optimizationMode: 'speed' | 'balanced' | 'quality',
fileIds: string[], fileIds: string[]
) { ) {
const effectiveMode = 'balanced'; const effectiveMode = 'balanced';
const emitter = new EventEmitter(); const emitter = new eventEmitter();
try { try {
// S'assurer que le memoryStore est initialisé // Ajouter le message utilisateur à la mémoire
if (!this.memoryStore) { this.updateMemory(new HumanMessage(message));
console.log("🔄 Initialisation du memory store dans searchAndAnswer...");
try {
await this.initialize(embeddings);
if (!this.memoryStore) {
throw new Error("Memory store initialization failed");
}
} catch (error) {
console.error("❌ Erreur lors de l'initialisation du memory store:", error);
throw error;
}
}
// Analyser le contexte de la conversation // Fusionner l'historique
const conversationContext = await this.analyzeConversationContext(message, history, llm); const mergedHistory: BaseMessage[] = [
console.log("🧠 Analyse du contexte:", conversationContext); ...this.conversationHistory,
...history,
// Mettre à jour la mémoire avec le contexte actuel ];
await this.updateMemoryStore(message, history);
// Récupérer le contexte pertinent de la mémoire
const memoryContext = await this.getRelevantContext(message);
console.log("💭 Contexte mémoire récupéré:", memoryContext);
// Enrichir le message avec le contexte historique
const enrichedMessage = `
Contexte précédent:
${memoryContext}
Contexte actuel:
${conversationContext.context}
Question actuelle:
${message}
`;
// Analyse sophistiquée de la requête avec LLM // Analyse sophistiquée de la requête avec LLM
const queryAnalysis = await llm.invoke(` const queryAnalysis = await llm.invoke(`En tant qu'expert en analyse de requêtes, examine cette demande et détermine la stratégie de recherche optimale.
En tant qu'expert en analyse de requêtes, examine cette demande dans son contexte complet.
${enrichedMessage} Question/Requête: "${message}"
Documents disponibles: ${fileIds.length > 0 ? "Oui" : "Non"} Documents disponibles: ${fileIds.length > 0 ? 'Oui' : 'Non'}
Analyse et réponds au format JSON: Analyse et réponds au format JSON:
{ {
"primaryIntent": "DOCUMENT_QUERY" | "WEB_SEARCH" | "EXPERT_ADVICE" | "HYBRID", "primaryIntent": "DOCUMENT_QUERY" | "WEB_SEARCH" | "EXPERT_ADVICE" | "HYBRID",
"requiresDocumentSearch": <boolean>, "requiresDocumentSearch": <boolean>,
"requiresWebSearch": <boolean>, "requiresWebSearch": <boolean>,
"requiresExpertSearch": <boolean>, "requiresExpertSearch": <boolean>,
"documentRelevance": <0.0 à 1.0>, "documentRelevance": <0.0 à 1.0>,
"contextRelevance": ${conversationContext.relevance},
"reasoning": "<courte explication>" "reasoning": "<courte explication>"
}`); }
Critères d'analyse:
- DOCUMENT_QUERY: La question porte spécifiquement sur le contenu des documents
- WEB_SEARCH: Recherche d'informations générales ou actuelles
- EXPERT_ADVICE: Demande nécessitant une expertise spécifique
- HYBRID: Combinaison de plusieurs sources
Prends en compte:
- La présence ou non de documents uploadés
- La spécificité de la question
- Le besoin d'expertise externe
- L'actualité du sujet`);
const analysis = JSON.parse(String(queryAnalysis.content)); const analysis = JSON.parse(String(queryAnalysis.content));
console.log("🎯 Analyse de la requête enrichie:", analysis); console.log('🎯 Analyse de la requête:', analysis);
// 1. Analyse des documents uploadés avec RAG // 1. Analyse des documents uploadés avec RAG
const uploadedDocs = await this.loadUploadedDocuments(fileIds); const uploadedDocs = await this.loadUploadedDocuments(fileIds);
console.log("📚 Documents uploadés chargés:", uploadedDocs.length); console.log('📚 Documents uploadés chargés:', uploadedDocs.length);
if (uploadedDocs.length > 0) { if (uploadedDocs.length > 0) {
// Création du vectorStore temporaire pour les documents // Création du vectorStore temporaire pour les documents
const vectorStore = await Chroma.fromDocuments(uploadedDocs, embeddings, { const vectorStore = await Chroma.fromDocuments(uploadedDocs, embeddings, {
collectionName: "temp_docs", collectionName: 'temp_docs',
url: "http://chroma:8000", url: 'http://chroma:8000',
numDimensions: 1536 numDimensions: 1536
}); });
// Recherche sémantique sans filtre pour l'instant // Recherche sémantique sans filtre pour l'instant
const relevantDocs = await vectorStore.similaritySearch(message, 5); const relevantDocs = await vectorStore.similaritySearch(message, 5);
console.log("📄 Documents pertinents trouvés:", relevantDocs.length); console.log('📄 Documents pertinents trouvés:', relevantDocs.length);
// Extraction du contexte pour enrichir la recherche // Extraction du contexte pour enrichir la recherche
const documentContext = relevantDocs const documentContext = relevantDocs
.map(doc => doc.pageContent) .map(doc => doc.pageContent)
.join("\n") .join('\n')
.substring(0, 500); .substring(0, 500);
const documentTitle = uploadedDocs[0]?.metadata?.title || ""; const documentTitle = uploadedDocs[0]?.metadata?.title || '';
const enrichedQuery = `${message} ${documentTitle} ${documentContext}`; const enrichedQuery = `${message} ${documentTitle} ${documentContext}`;
// 2. Recherche d'experts en BDD // 2. Recherche d'experts en BDD
@ -958,7 +880,7 @@ export class MetaSearchAgent implements MetaSearchAgentType {
const stream = answeringChain.streamEvents( const stream = answeringChain.streamEvents(
{ {
chat_history: history, chat_history: mergedHistory,
query: `${message}\n\nContexte pertinent:\n${finalResults.map(doc => doc.pageContent).join('\n\n')}` query: `${message}\n\nContexte pertinent:\n${finalResults.map(doc => doc.pageContent).join('\n\n')}`
}, },
{ {
@ -966,7 +888,7 @@ export class MetaSearchAgent implements MetaSearchAgentType {
} }
); );
this.handleStream(stream, emitter); this.handleStreamWithMemory(stream, emitter);
} else { } else {
// Fallback sans documents uploadés // Fallback sans documents uploadés
const answeringChain = await this.createAnsweringChain( const answeringChain = await this.createAnsweringChain(
@ -978,7 +900,7 @@ export class MetaSearchAgent implements MetaSearchAgentType {
const stream = answeringChain.streamEvents( const stream = answeringChain.streamEvents(
{ {
chat_history: history, chat_history: mergedHistory,
query: message query: message
}, },
{ {
@ -986,10 +908,10 @@ export class MetaSearchAgent implements MetaSearchAgentType {
} }
); );
this.handleStream(stream, emitter); this.handleStreamWithMemory(stream, emitter);
} }
} catch (error) { } catch (error) {
console.error("❌ Erreur:", error); console.error('❌ Erreur:', error);
// Fallback en mode standard // Fallback en mode standard
const answeringChain = await this.createAnsweringChain( const answeringChain = await this.createAnsweringChain(
llm, llm,
@ -1000,7 +922,7 @@ export class MetaSearchAgent implements MetaSearchAgentType {
const stream = answeringChain.streamEvents( const stream = answeringChain.streamEvents(
{ {
chat_history: history, chat_history: this.conversationHistory,
query: message query: message
}, },
{ {
@ -1008,7 +930,7 @@ export class MetaSearchAgent implements MetaSearchAgentType {
} }
); );
this.handleStream(stream, emitter); this.handleStreamWithMemory(stream, emitter);
} }
return emitter; return emitter;
@ -1018,25 +940,25 @@ export class MetaSearchAgent implements MetaSearchAgentType {
export const searchHandlers: Record<string, MetaSearchAgentType> = { export const searchHandlers: Record<string, MetaSearchAgentType> = {
// ... existing handlers ... // ... existing handlers ...
legal: { legal: {
initialize: async (embeddings: Embeddings) => {
// Pas besoin d'initialisation spécifique pour le handler legal
},
searchAndAnswer: async ( searchAndAnswer: async (
message, message,
history, history,
llm, llm,
embeddings, embeddings,
optimizationMode, optimizationMode,
fileIds, fileIds
) => { ) => {
const emitter = new eventEmitter(); const emitter = new eventEmitter();
try { try {
const chain = new RAGDocumentChain(); const chain = new RAGDocumentChain();
await chain.initializeVectorStoreFromDocuments(fileIds.map(fileId => new Document({ await chain.initializeVectorStoreFromDocuments(
fileIds.map(fileId => new Document({
pageContent: '', pageContent: '',
metadata: { source: fileId } metadata: { source: fileId }
})), embeddings); })),
embeddings
);
const searchChain = chain.createSearchChain(llm); const searchChain = chain.createSearchChain(llm);
const results = await searchChain.invoke({ const results = await searchChain.invoke({
@ -1056,7 +978,7 @@ export const searchHandlers: Record<string, MetaSearchAgentType> = {
'data', 'data',
JSON.stringify({ JSON.stringify({
type: 'response', type: 'response',
data: response.text, data: response.text
}) })
); );
@ -1066,25 +988,22 @@ export const searchHandlers: Record<string, MetaSearchAgentType> = {
'error', 'error',
JSON.stringify({ JSON.stringify({
type: 'error', type: 'error',
data: error.message, data: error.message
}) })
); );
} }
return emitter; return emitter;
}, }
}, },
documents: { documents: {
initialize: async (embeddings: Embeddings) => {
// Pas besoin d'initialisation spécifique pour le handler documents
},
searchAndAnswer: async ( searchAndAnswer: async (
message, message,
history, history,
llm, llm,
embeddings, embeddings,
optimizationMode, optimizationMode,
fileIds, fileIds
) => { ) => {
const emitter = new eventEmitter(); const emitter = new eventEmitter();
const ragChain = new RAGDocumentChain(); const ragChain = new RAGDocumentChain();
@ -1098,7 +1017,7 @@ export const searchHandlers: Record<string, MetaSearchAgentType> = {
pageContent: content.contents.join('\n'), pageContent: content.contents.join('\n'),
metadata: { metadata: {
title: content.title, title: content.title,
source: fileId, source: fileId
} }
}); });
}); });
@ -1121,38 +1040,44 @@ export const searchHandlers: Record<string, MetaSearchAgentType> = {
})) }))
}; };
emitter.emit('data', JSON.stringify({ emitter.emit(
'data',
JSON.stringify({
type: 'response', type: 'response',
data: response.text data: response.text
})); })
);
emitter.emit('data', JSON.stringify({ emitter.emit(
'data',
JSON.stringify({
type: 'sources', type: 'sources',
data: response.sources data: response.sources
})); })
);
emitter.emit('end'); emitter.emit('end');
} catch (error) { } catch (error) {
emitter.emit('error', JSON.stringify({ emitter.emit(
'error',
JSON.stringify({
type: 'error', type: 'error',
data: error.message data: error.message
})); })
);
} }
return emitter; return emitter;
} }
}, },
uploads: { uploads: {
initialize: async (embeddings: Embeddings) => {
// Pas besoin d'initialisation spécifique pour le handler uploads
},
searchAndAnswer: async ( searchAndAnswer: async (
message, message,
history, history,
llm, llm,
embeddings, embeddings,
optimizationMode, optimizationMode,
fileIds, fileIds
) => { ) => {
const emitter = new eventEmitter(); const emitter = new eventEmitter();
@ -1171,10 +1096,11 @@ export const searchHandlers: Record<string, MetaSearchAgentType> = {
`); `);
const intent = String(queryIntent.content).trim(); const intent = String(queryIntent.content).trim();
console.log("🎯 Intention détectée:", intent); console.log('🎯 Intention détectée:', intent);
// Chargement optimisé des documents // Chargement optimisé des documents
const docs = await Promise.all(fileIds.map(async fileId => { const docs = await Promise.all(
fileIds.map(async fileId => {
const filePath = path.join(process.cwd(), 'uploads', fileId); const filePath = path.join(process.cwd(), 'uploads', fileId);
const contentPath = `${filePath}-extracted.json`; const contentPath = `${filePath}-extracted.json`;
@ -1188,7 +1114,7 @@ export const searchHandlers: Record<string, MetaSearchAgentType> = {
const chunkSize = 1000; // Taille optimale pour le traitement const chunkSize = 1000; // Taille optimale pour le traitement
const overlap = 100; // Chevauchement pour maintenir le contexte const overlap = 100; // Chevauchement pour maintenir le contexte
const chunks = []; const chunks: string[] = [];
let currentChunk = ''; let currentChunk = '';
let currentSize = 0; let currentSize = 0;
@ -1221,14 +1147,18 @@ export const searchHandlers: Record<string, MetaSearchAgentType> = {
pageNumber: pageNumber, pageNumber: pageNumber,
chunkIndex: index, chunkIndex: index,
totalChunks: chunks.length, totalChunks: chunks.length,
searchText: chunk.substring(0, 100).replace(/[\n\r]+/g, ' ').trim() searchText: chunk
.substring(0, 100)
.replace(/[\n\r]+/g, ' ')
.trim()
} }
}); });
}); });
})); })
);
const flatDocs = docs.flat(); const flatDocs = docs.flat();
console.log("📚 Nombre total de chunks:", flatDocs.length); console.log('📚 Nombre total de chunks:', flatDocs.length);
const ragChain = new RAGDocumentChain(); const ragChain = new RAGDocumentChain();
await ragChain.initializeVectorStoreFromDocuments(flatDocs, embeddings); await ragChain.initializeVectorStoreFromDocuments(flatDocs, embeddings);
@ -1236,9 +1166,10 @@ export const searchHandlers: Record<string, MetaSearchAgentType> = {
// Adaptation de la requête selon l'intention détectée par le LLM // Adaptation de la requête selon l'intention détectée par le LLM
let queryPrompt = message; let queryPrompt = message;
switch(intent) { switch (intent) {
case 'SUMMARY': case 'SUMMARY':
queryPrompt = "Fais un résumé complet et structuré de ce document en te concentrant sur les points clés"; queryPrompt =
'Fais un résumé complet et structuré de ce document en te concentrant sur les points clés';
break; break;
case 'ANALYSIS': case 'ANALYSIS':
queryPrompt = `Analyse en détail les aspects suivants du document concernant : ${message}. Fournis une analyse structurée avec des exemples du texte.`; queryPrompt = `Analyse en détail les aspects suivants du document concernant : ${message}. Fournis une analyse structurée avec des exemples du texte.`;
@ -1300,13 +1231,15 @@ export const searchHandlers: Record<string, MetaSearchAgentType> = {
emitter.emit('end'); emitter.emit('end');
} }
} }
} catch (error) { } catch (error) {
console.error("Erreur lors de la recherche dans les documents:", error); console.error('Erreur lors de la recherche dans les documents:', error);
emitter.emit('error', JSON.stringify({ emitter.emit(
'error',
JSON.stringify({
type: 'error', type: 'error',
data: error.message data: error.message
})); })
);
} }
return emitter; return emitter;