Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 31 additions & 2 deletions frontend/src/app/(app)/workspace/faq/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ import { Slider } from '@/components/ui/slider'
import { useContextAvailability } from '@/lib/hooks/use-context-availability'
import { getSelectContextDescription } from '@/lib/utils/context-messages'
import { ContextRequirementMessage } from '@/components/context-requirement-message'
import { useCourses } from '@/lib/hooks/use-courses'
import { usePersonaStore } from '@/lib/store/persona-store'
import { Progress } from '@/components/ui/progress'
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'
import { Switch } from '@/components/ui/switch'
Expand All @@ -55,6 +57,8 @@ export default function FAQComponent() {
const [useReranker, setUseReranker] = useState(true)
const { getActiveContextModelName, getContextTypeLabel } = useContextAvailability()
const selectedSources = useSourcesStore((state) => state.selectedSources)
const { data: coursesData } = useCourses()
const { selectedCourseId } = usePersonaStore()

// Function to trigger the API and fetch initial FAQs
const fetchAPI = async () => {
Expand All @@ -66,8 +70,11 @@ export default function FAQComponent() {
return false
}
const selectedSourcesCount = selectedSources.filter((source) => source.selected).length
if (selectedSourcesCount !== 1) {
toast.error('Please select exactly one source.')
// Allow generation with no sources or exactly one source, but not multiple sources
if (selectedSourcesCount > 1) {
toast.error(
'Multiple sources selected. Please select only one source or none to use course context.',
)
return
}

Expand All @@ -82,6 +89,16 @@ export default function FAQComponent() {
try {
const modelName = getActiveContextModelName()

// Get course information from context - using proven method from assessment page
const selectedCourse = coursesData?.docs.find((course) => course.id === selectedCourseId)
const courseDescription = selectedCourse?.description || ''
const courseInfo = selectedCourse
? {
courseName: selectedCourse.name,
courseDescription: courseDescription,
}
: undefined

const response = await fetch('/api/faq', {
method: 'POST',
headers: {
Expand All @@ -95,6 +112,7 @@ export default function FAQComponent() {
multiPassState: null, // No state for initial request
continueFaqs: false,
useReranker: useReranker, // Add this line
courseInfo, // Add course info
}),
})

Expand Down Expand Up @@ -129,6 +147,16 @@ export default function FAQComponent() {
try {
const modelName = getActiveContextModelName()

// Get course information from context (same as initial call) - using proven method
const selectedCourse = coursesData?.docs.find((course) => course.id === selectedCourseId)
const courseDescription = selectedCourse?.description || ''
const courseInfo = selectedCourse
? {
courseName: selectedCourse.name,
courseDescription: courseDescription,
}
: undefined

const response = await fetch('/api/faq', {
method: 'POST',
headers: {
Expand All @@ -142,6 +170,7 @@ export default function FAQComponent() {
multiPassState: multiPassState, // Pass the state for continuation
continueFaqs: true, // Flag that this is a continuation request
useReranker: useReranker, // Add this line
courseInfo, // Add course info
}),
})

Expand Down
126 changes: 108 additions & 18 deletions frontend/src/app/api/faq/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
SystemPromptGenerator,
ContentProcessor,
GenerationFunction,
ProcessResult,
} from '@/lib/rag/multi-pass'
import { ContextChunk } from '@/lib/types/context-chunk'

Expand Down Expand Up @@ -44,9 +45,19 @@ const CONTENT_TYPE_FAQ = 'faq'
*/
const faqSystemPromptGenerator: SystemPromptGenerator = (isFirstPass, query, options) => {
const faqCount = options.faqCount || 5
const hasValidSources = options.hasValidSources || false
const courseInfo = options.courseInfo as
| { courseName?: string; courseDescription?: string }
| undefined

const contextInstruction = hasValidSources
? 'based on the context provided'
: courseInfo?.courseName
? `for the course "${courseInfo.courseName}"${courseInfo.courseDescription ? ` (${courseInfo.courseDescription})` : ''}. Use general academic knowledge relevant to this course`
: 'using general academic knowledge'

return `
Your job is to generate diverse and interesting FAQs with question answer pairs based on the context provided.
Your job is to generate diverse and interesting FAQs with question answer pairs ${contextInstruction}.

Format ALL FAQs as a JSON object with this structure:
{
Expand All @@ -65,7 +76,8 @@ const faqSystemPromptGenerator: SystemPromptGenerator = (isFirstPass, query, opt
4. Focus on different aspects of the content - find unique angles and insights.
5. Ensure all quotes and special characters in the JSON are properly escaped.
6. The JSON must be valid and parsable without errors.
7. Answers should be detailed, descriptive, and provide clear explanations based on the context.
7. Answers should be detailed, descriptive, and provide clear explanations${hasValidSources ? ' based on the context' : ''}.
${!hasValidSources && courseInfo?.courseName ? `8. Focus on questions and answers that would be relevant for students in ${courseInfo.courseName}.` : ''}
`
}

Expand Down Expand Up @@ -233,8 +245,13 @@ export async function POST(req: Request) {
continueFaqs = false,
useReranker, // Add this line with default value true
_recursionDepth = 0,
courseInfo, // Add courseInfo parameter
} = await req.json()

// Debug logging
console.log('DEBUG FAQ API: courseInfo received:', courseInfo)
console.log('DEBUG FAQ API: selectedSources length:', selectedSources?.length || 0)

// Safety check to prevent infinite loops
if (_recursionDepth > 10) {
return NextResponse.json({
Expand All @@ -253,15 +270,18 @@ export async function POST(req: Request) {
}
const ollama = createOllama({ baseURL: ollamaUrl + '/api' })

// Check if we have valid sources
const hasValidSources = Array.isArray(selectedSources) && selectedSources.length > 0

// Process user query
const safeSearchQuery = typeof searchQuery === 'string' ? searchQuery : ''
const hasUserQuery = safeSearchQuery.trim() !== ''
const userQuery = hasUserQuery ? safeSearchQuery.trim() : ''
let retrievedChunks: ContextChunk[] = []
let usedHybridSearch = false

// Only retrieve chunks on initial request
if (!continueFaqs) {
// Only retrieve chunks on initial request and if we have valid sources
if (!continueFaqs && hasValidSources) {
if (hasUserQuery) {
retrievedChunks = await hybridSearch(
userQuery,
Expand All @@ -278,6 +298,10 @@ export async function POST(req: Request) {
}
}

// Additional debug logging
console.log('DEBUG FAQ API: hasValidSources:', hasValidSources)
console.log('DEBUG FAQ API: retrievedChunks length:', retrievedChunks.length)

// Set up processing options
const actualFaqCount = faqCount || 5
const faqContentProcessor = createFaqContentProcessor(actualFaqCount)
Expand All @@ -289,6 +313,8 @@ export async function POST(req: Request) {
temperature: TEMPERATURE + 0.1,
faqCount: actualFaqCount,
preserveOrder: !hasUserQuery,
hasValidSources, // Add this for system prompt
courseInfo, // Add this for system prompt
}

// Start processing timer
Expand All @@ -297,20 +323,84 @@ export async function POST(req: Request) {
// Create the FAQ generation function
const faqGenerationFunction = createFaqGenerationFunction()

// Process chunks using multi-pass approach
const processResult = await processChunksMultiPass<FaqResult>(
userQuery,
continueFaqs ? [] : retrievedChunks,
faqGenerationFunction,
ollama(selectedModel, { numCtx: TOKEN_RESPONSE_BUDGET }),
options,
CONTENT_TYPE_FAQ,
faqSystemPromptGenerator,
(query) =>
`Generate FAQs for the following query: "${query}". Use the provided context to answer.`,
faqContentProcessor,
multiPassState,
)
let processResult: ProcessResult<FaqResult>

// Handle no-sources case differently
if (!hasValidSources && !continueFaqs) {
// Direct generation without chunks for course context
console.log('DEBUG FAQ API: Generating FAQs using course context only')
const systemPrompt = faqSystemPromptGenerator(true, userQuery, options)
const userPrompt = courseInfo?.courseName
? `Generate FAQs for the course "${courseInfo.courseName}"${userQuery ? ` related to: "${userQuery}"` : ''}. Use general academic knowledge relevant to this course.`
: `Generate FAQs${userQuery ? ` for the topic: "${userQuery}"` : ''}. Use general academic knowledge to provide comprehensive answers.`

const messages = [
{ role: 'system' as const, content: systemPrompt },
{ role: 'user' as const, content: userPrompt },
]

try {
const { object: rawResult, usage } = await faqGenerationFunction({
model: ollama(selectedModel, { numCtx: TOKEN_RESPONSE_BUDGET }),
output: 'no-schema',
messages: messages,
temperature: TEMPERATURE + 0.1,
maxTokens: TOKEN_RESPONSE_BUDGET,
})

const processedResult = faqContentProcessor(rawResult, [])

processResult = {
result: processedResult,
state: {
chunks: [],
processedChunkIds: [],
currentIndex: 0,
isComplete: true,
generatedContent: [processedResult],
progress: 100,
lastGenerated: processedResult,
contentType: CONTENT_TYPE_FAQ,
},
debug: {
chunksProcessed: 0,
totalChunks: 0,
remainingChunks: 0,
tokenUsage: {
prompt: usage?.promptTokens || 0,
completion: usage?.completionTokens || 0,
total: usage?.totalTokens || 0,
},
timeTaken: Date.now() - startTime,
},
}
} catch (error) {
console.error('DEBUG FAQ API: Error in direct generation:', error)
throw error
}
} else {
// Use multi-pass approach for sources
processResult = await processChunksMultiPass<FaqResult>(
userQuery,
continueFaqs ? [] : retrievedChunks,
faqGenerationFunction,
ollama(selectedModel, { numCtx: TOKEN_RESPONSE_BUDGET }),
options,
CONTENT_TYPE_FAQ,
faqSystemPromptGenerator,
(query) => {
if (hasValidSources) {
return `Generate FAQs for the following query: "${query}". Use the provided context to answer.`
} else if (courseInfo?.courseName) {
return `Generate FAQs for the course "${courseInfo.courseName}"${query ? ` related to: "${query}"` : ''}. Use general academic knowledge relevant to this course.`
} else {
return `Generate FAQs${query ? ` for the topic: "${query}"` : ''}. Use general academic knowledge to provide comprehensive answers.`
}
},
faqContentProcessor,
multiPassState,
)
}

// Calculate processing time
const timeTakenSeconds = (Date.now() - startTime) / 1000
Expand Down