rag-system-pgvector
Version:
A complete Retrieval-Augmented Generation system using pgvector, LangChain, and LangGraph for Node.js applications with dynamic embedding and model providers, structured data queries, and chat history - supports OpenAI, Anthropic, HuggingFace, Azure, Goog
877 lines (739 loc) ⢠32.1 kB
JavaScript
import { StateGraph, END } from "@langchain/langgraph";
import { BaseMessage, HumanMessage, AIMessage, SystemMessage } from "@langchain/core/messages";
import { ChatOpenAI } from "@langchain/openai";
import DocumentProcessor from '../utils/documentProcessor.js';
import DocumentStoreLangChain from '../services/documentStoreLangChain.js';
import { ragState } from "./state.js";
class RAGWorkflow {
constructor(documentStore, config = {}) {
if (!config.llm) {
throw new Error('Language model (LLM) instance is required in config.llm');
}
this.documentProcessor = new DocumentProcessor(config);
this.documentStore = documentStore; // Use the passed documentStore
this.llm = config.llm;
// Track if database is available
this.hasDatabase = documentStore && documentStore.hasDatabase;
if (!this.hasDatabase) {
console.log('ā ļø RAGWorkflow initialized without database - some features will be limited');
}
// History management configuration
this.historyConfig = {
maxMessages: config.chatHistory?.maxMessages || 20,
maxTokens: config.chatHistory?.maxTokens || 3000,
summarizeThreshold: config.chatHistory?.summarizeThreshold || 30,
keepRecentCount: config.chatHistory?.keepRecentCount || 10,
alwaysKeepFirst: config.chatHistory?.alwaysKeepFirst ?? true
};
this.workflow = this.createWorkflow();
}
createWorkflow() {
const workflow = new StateGraph({
channels: ragState
});
// Add nodes
workflow.addNode("retrieve", this.retrieveNode.bind(this));
workflow.addNode("rerank", this.rerankNode.bind(this));
workflow.addNode("generate", this.generateNode.bind(this));
// Add edges
workflow.addEdge("retrieve", "rerank");
workflow.addEdge("rerank", "generate");
workflow.addEdge("generate", END);
// Set entry point
workflow.setEntryPoint("retrieve");
return workflow.compile();
}
async retrieveNode(state) {
console.log("š Retrieving relevant chunks...");
try {
// Check if documentStore has database capabilities
if (!this.documentStore || !this.documentStore.hasDatabase) {
console.log("ā ļø No database available - using chat history, structured data, and direct context");
// DEBUG: Check what we received
console.log("š DEBUG - State contents:");
console.log(" - directContext:", state.directContext ? `${Array.isArray(state.directContext) ? state.directContext.length + ' items' : 'single item'}` : 'null');
console.log(" - structuredData:", state.structuredData ? 'present' : 'null');
console.log(" - context:", state.context ? (Array.isArray(state.context) ? state.context.length + ' items' : 'present') : 'null');
// Build context from direct context array (highest priority)
let directContextStr = '';
if (state.directContext) {
console.log("š Building context from direct context data");
if (Array.isArray(state.directContext)) {
console.log(`š Processing ${state.directContext.length} direct context items`);
directContextStr = state.directContext.map((item, index) => {
if (typeof item === 'object' && item !== null) {
// Format structured data objects nicely
const formatted = Object.entries(item)
.filter(([key, value]) => key !== '_id' && key !== '__v') // Filter MongoDB fields
.map(([key, value]) => {
if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
// Handle nested objects (like vitals)
const nestedFormatted = Object.entries(value)
.map(([nestedKey, nestedValue]) => ` ${nestedKey}: ${nestedValue}`)
.join('\n');
return `${key}:\n${nestedFormatted}`;
} else if (Array.isArray(value)) {
return `${key}: [${value.join(', ')}]`;
} else {
return `${key}: ${value}`;
}
})
.join('\n');
return `Record ${index + 1}:\n${formatted}`;
}
return `Item ${index + 1}: ${String(item)}`;
}).join('\n\n---\n\n');
} else if (typeof state.directContext === 'object') {
// Handle single object
directContextStr = Object.entries(state.directContext)
.filter(([key, value]) => key !== '_id' && key !== '__v')
.map(([key, value]) => {
if (typeof value === 'object' && value !== null) {
return `${key}: ${JSON.stringify(value, null, 2)}`;
}
return `${key}: ${value}`;
})
.join('\n');
} else {
directContextStr = String(state.directContext);
}
}
// Build context from chat history
let contextFromHistory = '';
// Extract relevant information from chat history
if (state.chatHistory && state.chatHistory.length > 0) {
console.log(`š Building context from ${state.chatHistory.length} chat history messages`);
// Get recent conversation context (last few exchanges)
const recentHistory = state.chatHistory.slice(-6); // Last 3 exchanges (user + assistant)
contextFromHistory = recentHistory
.map((msg, index) => {
const speaker = msg.role === 'user' ? 'Previous User Question' : 'Previous Assistant Response';
return `${speaker}: ${msg.content}`;
})
.join('\n\n');
}
// Build context from structured data
let structuredContext = '';
if (state.structuredData) {
console.log("šļø Building context from structured data");
const structured = state.structuredData;
let contextParts = [];
if (structured.intent) {
contextParts.push(`Query Intent: ${structured.intent}`);
}
if (structured.entities && Object.keys(structured.entities).length > 0) {
const entityInfo = Object.entries(structured.entities)
.filter(([_, value]) => value)
.map(([key, value]) => `${key}: ${value}`)
.join(', ');
contextParts.push(`Key Entities: ${entityInfo}`);
}
if (structured.constraints && structured.constraints.length > 0) {
contextParts.push(`Requirements: ${structured.constraints.join(', ')}`);
}
if (structured.context && Object.keys(structured.context).length > 0) {
const additionalContext = Object.entries(structured.context)
.map(([key, value]) => `${key}: ${value}`)
.join(', ');
contextParts.push(`Additional Context: ${additionalContext}`);
}
structuredContext = contextParts.join('\n');
}
// Combine all context sources in priority order
const combinedContext = [
directContextStr, // Highest priority - direct structured data
contextFromHistory, // Chat history context
structuredContext, // Structured data context
// Also include any explicitly provided context
Array.isArray(state.context) ? state.context.join('\n\n') : String(state.context || '')
].filter(Boolean).join('\n\nāāāāā\n\n');
console.log(`š Combined context length: ${combinedContext.length} characters`);
if (state.directContext) {
console.log(`ā
Direct context included: ${Array.isArray(state.directContext) ? state.directContext.length + ' items' : '1 item'}`);
}
return {
...state,
retrievedChunks: [],
searchResults: [],
context: combinedContext,
metadata: {
...state.metadata,
retrievalTimestamp: new Date().toISOString(),
chunksRetrieved: 0,
retrievalMethod: 'chat_history_and_structured_data',
historyMessages: state.chatHistory?.length || 0,
hasStructuredData: !!state.structuredData,
note: 'No database configured - using chat history and structured data for context'
},
};
}
// Extract filter options from state metadata
const filterOptions = state.metadata?.filter || {};
const userId = state.metadata?.userId;
const knowledgebotId = state.metadata?.knowledgebotId; // Build comprehensive filter
const searchFilter = { ...filterOptions };
if (userId) searchFilter.userId = userId;
if (knowledgebotId) searchFilter.knowledgebotId = knowledgebotId;
// Enhance search query with structured data if available
let enhancedQuery = state.query;
if (state.structuredData) {
enhancedQuery = this.enhanceQueryWithStructuredData(state.query, state.structuredData);
// Add entity-based filters if present
if (state.structuredData.entities) {
Object.entries(state.structuredData.entities).forEach(([entityType, entityValue]) => {
if (entityValue && typeof entityValue === 'string') {
searchFilter[entityType] = entityValue;
}
});
}
}
// Use retriever with dynamic filtering and enhanced query
const retrievedChunks = await this.documentStore.retrieveDocuments(
enhancedQuery,
{
k: state.metadata?.limit || 10, // Number of documents to retrieve
searchType: 'similarity', // Use similarity search
scoreThreshold: state.metadata?.threshold || 0.1, // Minimum relevance score
filter: searchFilter // Dynamic metadata filtering
}
);
console.log(`ā Retrieved ${retrievedChunks.length} relevant chunks using retriever`);
if (Object.keys(searchFilter).length > 0) {
console.log(`ā Applied filters:`, searchFilter);
}
return {
...state,
retrievedChunks,
searchResults: retrievedChunks,
metadata: {
...state.metadata,
retrievalTimestamp: new Date().toISOString(),
chunksRetrieved: retrievedChunks.length,
retrievalMethod: 'retriever', // Track that we used retriever
appliedFilters: searchFilter,
},
};
} catch (error) {
console.error("Error in retrieve node:", error);
// Fallback to using provided context
console.log("ā ļø Retrieval failed - falling back to provided context");
const providedContext = state.context || [];
const contextString = Array.isArray(providedContext)
? providedContext.join('\n\n')
: String(providedContext);
return {
...state,
retrievedChunks: [],
searchResults: [],
context: contextString,
metadata: {
...state.metadata,
retrievalTimestamp: new Date().toISOString(),
chunksRetrieved: 0,
retrievalMethod: 'fallback',
retrievalError: error.message
},
};
}
}
async rerankNode(state) {
console.log("š Reranking chunks...");
try {
// Check if we have retrieved chunks to rerank
if (!state.retrievedChunks || state.retrievedChunks.length === 0) {
console.log(`ā No chunks to rerank - preserving existing context`);
return {
...state,
metadata: {
...state.metadata,
rerankingTimestamp: new Date().toISOString(),
finalChunkCount: 0,
note: 'No chunks to rerank - context preserved from retrieval'
},
};
}
// Simple reranking based on similarity score and chunk relevance
const rerankedChunks = state.retrievedChunks
.sort((a, b) => {
// Primary sort by similarity
if (b.similarity !== a.similarity) {
return b.similarity - a.similarity;
}
// Secondary sort by chunk index (prefer earlier chunks)
return a.chunk_index - b.chunk_index;
})
.slice(0, 5); // Take top 5 chunks
// Create context from reranked chunks
const context = rerankedChunks
.map((chunk, index) => {
return `[Source ${index + 1}: ${chunk.title} - Part ${chunk.chunk_index + 1}]\n${chunk.content}`;
})
.join('\n\n');
console.log(`ā Reranked to top ${rerankedChunks.length} chunks`);
return {
...state,
retrievedChunks: rerankedChunks,
context,
metadata: {
...state.metadata,
rerankingTimestamp: new Date().toISOString(),
finalChunkCount: rerankedChunks.length,
},
};
} catch (error) {
console.error("Error in rerank node:", error);
throw error;
}
}
// Helper function to format chat history
formatChatHistory(history, maxLength = 10) {
if (!history || history.length === 0) return '';
// Take only recent messages
const recentHistory = history.slice(-maxLength);
// Format for LLM
return recentHistory.map(msg =>
`${msg.role === 'user' ? 'Human' : 'Assistant'}: ${msg.content}`
).join('\n');
}
// Helper: Summarize old history
async summarizeHistory(history, options = {}) {
const {
summarizeThreshold = 20,
keepRecentCount = 10,
maxSummaryLength = 500
} = options;
if (history.length < summarizeThreshold) {
return history;
}
const oldHistory = history.slice(0, -keepRecentCount);
const recentHistory = history.slice(-keepRecentCount);
const conversationText = oldHistory.map(msg =>
`${msg.role === 'user' ? 'User' : 'Assistant'}: ${msg.content}`
).join('\n');
try {
const summary = await this.llm.invoke([
new SystemMessage(`Summarize this conversation in ${maxSummaryLength} tokens or less. Focus on key topics, decisions, and important context:`),
new HumanMessage(conversationText)
]);
return [
{
role: 'system',
content: `[Conversation Summary]: ${summary.content}`,
timestamp: new Date().toISOString(),
type: 'summary'
},
...recentHistory
];
} catch (error) {
console.warn('Failed to summarize history, returning original:', error.message);
return history;
}
}
// Helper: Truncate by token count
async truncateByTokens(history, maxTokens = 2000) {
// Simple approximation: ~4 chars per token
const charsPerToken = 4;
const maxChars = maxTokens * charsPerToken;
let totalChars = 0;
const truncated = [];
// Process from newest to oldest
for (let i = history.length - 1; i >= 0; i--) {
const message = history[i];
const messageChars = message.content.length;
if (totalChars + messageChars <= maxChars) {
truncated.unshift(message);
totalChars += messageChars;
} else {
break;
}
}
return truncated;
}
// Main: Hybrid history management
async manageHistory(history, options = {}) {
const {
maxMessages = this.historyConfig.maxMessages,
maxTokens = this.historyConfig.maxTokens,
summarizeThreshold = this.historyConfig.summarizeThreshold,
keepRecentCount = this.historyConfig.keepRecentCount,
alwaysKeepFirst = this.historyConfig.alwaysKeepFirst
} = options;
// Step 1: If under threshold, return as-is
if (history.length <= maxMessages) {
return history;
}
console.log(`š Managing chat history: ${history.length} messages (threshold: ${maxMessages})`);
// Step 2: If way over threshold, summarize
if (history.length > summarizeThreshold) {
console.log(`š Summarizing history (threshold: ${summarizeThreshold})`);
const summarized = await this.summarizeHistory(history, {
summarizeThreshold,
keepRecentCount
});
history = summarized;
console.log(`ā Summarized to ${history.length} messages`);
}
// Step 3: Always keep first message (conversation starter)
const firstMessage = alwaysKeepFirst && history.length > 0 ? [history[0]] : [];
const remainingHistory = alwaysKeepFirst && history.length > 0 ? history.slice(1) : history;
// Step 4: Truncate by tokens
const tokenLimited = await this.truncateByTokens(
remainingHistory,
maxTokens
);
// Step 5: Combine
const managedHistory = [...firstMessage, ...tokenLimited];
console.log(`ā Final managed history: ${managedHistory.length} messages`);
return managedHistory;
}
async generateNode(state) {
console.log("š¤ Generating response...");
try {
// Manage history before using it (applies summarization if needed)
const managedHistory = await this.manageHistory(
state.chatHistory || [],
{
maxMessages: this.historyConfig.maxMessages,
maxTokens: this.historyConfig.maxTokens,
summarizeThreshold: this.historyConfig.summarizeThreshold,
keepRecentCount: this.historyConfig.keepRecentCount,
alwaysKeepFirst: this.historyConfig.alwaysKeepFirst
}
);
// Format managed history for prompt
const formattedHistory = this.formatChatHistory(managedHistory);
// Build conversation context section
const conversationSection = formattedHistory ? `
Previous Conversation:
${formattedHistory}
Continue the conversation naturally, referencing previous context when relevant.
` : '';
// Build structured data section
const structuredDataSection = this.formatStructuredDataForPrompt(state.structuredData);
// Handle different context scenarios
let contextSection = '';
let guidelines = '';
// Check if we have database context vs chat history context
const retrievalMethod = state.metadata?.retrievalMethod;
const hasDbContext = retrievalMethod === 'retriever' && state.context && state.context.trim();
const hasChatHistoryContext = retrievalMethod === 'chat_history_and_structured_data' && state.context && state.context.trim();
if (hasDbContext) {
// We have context from database
contextSection = `Use the following context from the knowledge base to answer the question:
${state.context}
`;
guidelines = `Guidelines:
- If continuing a conversation, acknowledge previous context when relevant
- Be conversational and natural
- If the question references "it", "that", "them" etc., use chat history for context
- If structured data is provided, use it to guide your response format and focus
- Cite sources when using information from the knowledge base by referencing [Source X]
- If the context doesn't contain enough information, say so clearly
- Be accurate and concise`;
} else if (hasChatHistoryContext) {
// We have context from chat history and structured data
const hasDirectContext = state.directContext && Array.isArray(state.directContext) && state.directContext.length > 0;
if (hasDirectContext) {
contextSection = `The user has provided the following data for analysis:
${state.context}
IMPORTANT: This data has been explicitly provided by the user for analysis. You should analyze and work with this data as requested.
`;
guidelines = `Guidelines:
- The user has provided specific data (above) that they want you to analyze
- Provide comprehensive analysis of the provided data as requested
- If structured data is provided, use it to guide your response format and focus
- Answer based on the data provided by the user
- Be thorough and detailed in your analysis
- Reference specific data points from the provided information`;
} else {
contextSection = `Use the following conversation context and structured information to answer the question:
${state.context}
`;
guidelines = `Guidelines:
- Build upon the previous conversation naturally
- Reference earlier parts of the conversation when relevant
- If structured data is provided, use it to guide your response format and focus
- If the question references "it", "that", "them" etc., use the conversation history for context
- Maintain conversation continuity and acknowledge context shifts appropriately
- Provide helpful responses based on the conversation flow and structured guidance
- Be conversational and engaging`;
}
} else {
// No specific context available
contextSection = `Note: No specific knowledge base context is available for this query.
Provide a helpful response based on your general knowledge.
`;
guidelines = `Guidelines:
- If continuing a conversation, acknowledge previous context when relevant
- Be conversational and natural
- If the question references "it", "that", "them" etc., use chat history for context
- If structured data is provided, use it to guide your response format and focus
- Use your general knowledge to provide helpful, accurate information
- Be clear that you're responding based on general knowledge rather than specific documents
- Be accurate and concise`;
}
const systemPrompt = `You are a helpful AI assistant${hasDbContext ? ' with access to a knowledge base' : hasChatHistoryContext ? ' with access to conversation history' : ''}.
${conversationSection}${structuredDataSection}${contextSection}${guidelines}
Question: ${state.query}`;
const messages = [
new HumanMessage(systemPrompt),
];
const response = await this.llm.invoke(messages);
// Add current interaction to history
const updatedHistory = [
...(state.chatHistory || []),
{
role: 'user',
content: state.query,
timestamp: new Date().toISOString()
},
{
role: 'assistant',
content: response.content,
timestamp: new Date().toISOString(),
sources: state.searchResults?.map(chunk => ({
title: chunk.title,
similarity: chunk.similarity
}))
}
];
// Apply history management to updated history
const finalManagedHistory = await this.manageHistory(updatedHistory);
console.log(`ā Generated response with chat history (${state.chatHistory?.length || 0} previous messages)`);
return {
...state,
response: response.content,
chatHistory: finalManagedHistory,
messages: [...(state.messages || []), state.query, response.content],
metadata: {
...state.metadata,
generationTimestamp: new Date().toISOString(),
conversationLength: finalManagedHistory.length,
historyManaged: finalManagedHistory.length !== updatedHistory.length,
},
};
} catch (error) {
console.error("Error in generate node:", error);
throw error;
}
}
async processQuery(query) {
try {
console.log(`\nš Processing query: "${query}"`);
// Ensure all required fields are initialized
const initialState = {
query,
chatHistory: [],
retrievedChunks: [],
context: '',
response: '',
searchResults: [],
metadata: {},
messages: [],
structuredData: null
}; console.log("š Initial state:", JSON.stringify(initialState, null, 2));
const result = await this.workflow.invoke(initialState);
// Save search session only if database is available
if (this.documentStore && this.documentStore.hasDatabase) {
try {
await this.documentStore.saveSearchSession(query, {
retrievedChunks: result.retrievedChunks,
response: result.response,
metadata: result.metadata,
});
} catch (error) {
console.warn('Failed to save search session:', error.message);
}
}
return result;
} catch (error) {
console.error("Error processing query:", error);
throw error;
}
}
// Method to add conversational context
async processConversationalQuery(query, conversationHistory = []) {
try {
console.log(`\nš¬ Processing conversational query: "${query}"`);
// Enhance query with conversation context if needed
let enhancedQuery = query;
if (conversationHistory.length > 0) {
const recentHistory = conversationHistory.slice(-4); // Last 2 exchanges
const contextualPrompt = `Based on our previous conversation: ${recentHistory.map(msg => `${msg.type}: ${msg.content}`).join(', ')}\n\nCurrent question: ${query}`;
enhancedQuery = contextualPrompt;
}
// Ensure all required fields are initialized
const initialState = {
query: enhancedQuery,
chatHistory: conversationHistory,
retrievedChunks: [],
context: '',
response: '',
searchResults: [],
metadata: {},
messages: conversationHistory,
structuredData: null
};
const result = await this.workflow.invoke(initialState);
return result;
} catch (error) {
console.error("Error processing conversational query:", error);
throw error;
}
}
/**
* Process query with structured data
*/
async processStructuredQuery(query, structuredData = null, chatHistory = []) {
try {
console.log(`\nšļø Processing structured query: "${query}"`);
if (structuredData) {
console.log("š Structured data:", JSON.stringify(structuredData, null, 2));
}
// Ensure all required fields are initialized
const initialState = {
query,
chatHistory: chatHistory || [],
retrievedChunks: [],
context: '',
response: '',
searchResults: [],
metadata: {},
messages: chatHistory || [],
structuredData: structuredData
};
const result = await this.workflow.invoke(initialState);
// Save search session only if database is available
if (this.documentStore && this.documentStore.hasDatabase) {
try {
await this.documentStore.saveSearchSession(query, {
retrievedChunks: result.retrievedChunks,
response: result.response,
metadata: result.metadata,
structuredData: structuredData
});
} catch (error) {
console.warn('Failed to save search session:', error.message);
}
}
return result;
} catch (error) {
console.error("Error processing structured query:", error);
throw error;
}
}
// Comparison method to test retriever vs similarity search
async compareRetrievalMethods(query) {
try {
console.log(`\nš Comparing retrieval methods for: "${query}"`);
// Check if database is available
if (!this.documentStore || !this.documentStore.hasDatabase) {
console.log('ā Cannot compare retrieval methods - no database configured');
return {
error: 'Database not configured',
message: 'Retrieval method comparison requires database configuration'
};
}
// Method 1: Retriever
console.log("\n--- Using Retriever ---");
const retrieverStart = Date.now();
const retrieverResults = await this.documentStore.retrieveDocuments(query, {
k: 5,
scoreThreshold: 0.1
});
const retrieverTime = Date.now() - retrieverStart;
// Method 2: Similarity Search
console.log("\n--- Using Similarity Search ---");
const simSearchStart = Date.now();
const simSearchResults = await this.documentStore.searchSimilarChunksByText(query, 5, 0.1);
const simSearchTime = Date.now() - simSearchStart;
// Compare results
console.log("\nš Comparison Results:");
console.log(`Retriever: ${retrieverResults.length} results in ${retrieverTime}ms`);
console.log(`Similarity Search: ${simSearchResults.length} results in ${simSearchTime}ms`);
return {
retriever: {
results: retrieverResults,
time: retrieverTime,
count: retrieverResults.length
},
similaritySearch: {
results: simSearchResults,
time: simSearchTime,
count: simSearchResults.length
}
};
} catch (error) {
console.error("Error comparing retrieval methods:", error);
throw error;
}
}
/**
* Enhance query with structured data for better retrieval
*/
enhanceQueryWithStructuredData(originalQuery, structuredData) {
if (!structuredData) return originalQuery;
let enhancedQuery = originalQuery;
// Add intent context
if (structuredData.intent) {
enhancedQuery += ` [Intent: ${structuredData.intent}]`;
}
// Add entity information
if (structuredData.entities && Object.keys(structuredData.entities).length > 0) {
const entityStrings = Object.entries(structuredData.entities)
.filter(([_, value]) => value)
.map(([key, value]) => `${key}: ${value}`);
if (entityStrings.length > 0) {
enhancedQuery += ` [Entities: ${entityStrings.join(', ')}]`;
}
}
// Add constraints
if (structuredData.constraints && structuredData.constraints.length > 0) {
enhancedQuery += ` [Requirements: ${structuredData.constraints.join(', ')}]`;
}
console.log(`š Enhanced query: "${enhancedQuery}"`);
return enhancedQuery;
}
/**
* Format structured data for inclusion in the prompt
*/
formatStructuredDataForPrompt(structuredData) {
if (!structuredData) return '';
let section = '\nStructured Query Information:\n';
// Add intent
if (structuredData.intent) {
section += `Intent/Category: ${structuredData.intent}\n`;
}
// Add entities
if (structuredData.entities && Object.keys(structuredData.entities).length > 0) {
section += 'Key Entities:\n';
Object.entries(structuredData.entities).forEach(([key, value]) => {
if (value) {
section += ` - ${key}: ${value}\n`;
}
});
}
// Add constraints
if (structuredData.constraints && structuredData.constraints.length > 0) {
section += 'Requirements/Constraints:\n';
structuredData.constraints.forEach(constraint => {
section += ` - ${constraint}\n`;
});
}
// Add context
if (structuredData.context && Object.keys(structuredData.context).length > 0) {
section += 'Additional Context:\n';
Object.entries(structuredData.context).forEach(([key, value]) => {
section += ` - ${key}: ${value}\n`;
});
}
// Add response format
if (structuredData.responseFormat) {
section += `Desired Response Format: ${structuredData.responseFormat}\n`;
}
section += '\n';
return section;
}
}
export { RAGWorkflow };
export default RAGWorkflow;