diff options
Diffstat (limited to 'src/client/views/nodes/chatbot/tools/TakeQuizTool.ts')
-rw-r--r-- | src/client/views/nodes/chatbot/tools/TakeQuizTool.ts | 224 |
1 files changed, 184 insertions, 40 deletions
diff --git a/src/client/views/nodes/chatbot/tools/TakeQuizTool.ts b/src/client/views/nodes/chatbot/tools/TakeQuizTool.ts index 78d9859b8..12b2d1e91 100644 --- a/src/client/views/nodes/chatbot/tools/TakeQuizTool.ts +++ b/src/client/views/nodes/chatbot/tools/TakeQuizTool.ts @@ -4,85 +4,229 @@ import { ParametersType, ToolInfo } from '../types/tool_types'; import { AgentDocumentManager } from '../utils/AgentDocumentManager'; import { GPTCallType, gptAPICall } from '../../../../apis/gpt/GPT'; import { v4 as uuidv4 } from 'uuid'; +import { Doc } from '../../../../../fields/Doc'; +import { StrCast } from '../../../../../fields/Types'; +import { DocumentView } from '../../DocumentView'; const parameterRules = [ { - name: 'userAnswer', + name: 'action', type: 'string', - description: 'User-provided answer to the quiz question.', + description: 'Quiz action to perform: "start" to begin quiz with random document, "answer" to submit answer to current quiz question, "next" to get next random question', required: true, }, + { + name: 'userAnswer', + type: 'string', + description: 'User-provided answer to the quiz question (required when action is "answer")', + required: false, + }, ] as const; const toolInfo: ToolInfo<typeof parameterRules> = { name: 'takeQuiz', - description: - 'Tests the user\'s knowledge and evaluates a user\'s answer for a randomly selected document.', + description: 'Interactive quiz system that tests user knowledge. Use "start" when user says "take quiz" or "start quiz". Use "answer" when user provides their answer to the quiz question. Use "next" when user asks for "next question". After starting a quiz, the agent should WAIT for the user to provide their answer naturally in conversation before calling this tool again.', parameterRules, citationRules: 'No citation needed for quiz operations.', }; export class TakeQuizTool extends BaseTool<typeof parameterRules> { private _docManager: AgentDocumentManager; + private _currentQuizDoc: Doc | null = null; + private _currentQuizDescription: string = ''; constructor(docManager: AgentDocumentManager) { super(toolInfo); this._docManager = docManager; - this._docManager.initializeFindDocsFreeform(); + this._docManager.initializeDocuments(); } - private async generateRubric(docId: string, description: string): Promise<string> { - const docMeta = this._docManager.extractDocumentMetadata(docId); - if (docMeta && docMeta.fields.layout.gptRubric) { - return docMeta.fields.layout.gptRubric; - } else { + /** + * Generate or retrieve a rubric for evaluating the user's description of a document + */ + private async generateRubric(doc: Doc, description: string): Promise<string> { + // Check if we already have a cached rubric + const existingRubric = StrCast(doc.gptRubric); + if (existingRubric) { + return existingRubric; + } + + // Generate new rubric using GPT + try { const rubric = await gptAPICall(description, GPTCallType.MAKERUBRIC); if (rubric) { - await this._docManager.editDocumentField(docId, 'layout.gptRubric', rubric); + // Cache the rubric on the document + doc.gptRubric = rubric; } return rubric || ''; + } catch (error) { + console.error('Failed to generate rubric:', error); + return ''; + } + } + + /** + * Randomly select a document for the quiz and highlight it (like GPTPopup does) + */ + private selectRandomDocument(): Doc | null { + // Ensure we have documents initialized + this._docManager.initializeDocuments(); + const allDocIds = this._docManager.docIds; + + if (allDocIds.length === 0) { + return null; } + + // Select a random document ID + const randomIndex = Math.floor(Math.random() * allDocIds.length); + const randomDocId = allDocIds[randomIndex]; + + // Get the actual document + const randomDoc = this._docManager.getDocument(randomDocId) || null; + + if (randomDoc) { + // Highlight the selected document, exactly like GPTPopup does + const docView = DocumentView.getDocumentView(randomDoc); + if (docView) { + docView.select(false); // false means don't extend selection, replaces current selection + console.log(`[TakeQuizTool] Selected and highlighted document: ${randomDoc.title || 'Untitled'}`); + } + } + + return randomDoc; } async execute(args: ParametersType<typeof parameterRules>): Promise<Observation[]> { const chunkId = uuidv4(); try { - const allDocIds = this._docManager.docIds; - const randomDocId = allDocIds[Math.floor(Math.random() * allDocIds.length)]; - const docMeta = this._docManager.extractDocumentMetadata(randomDocId); + switch (args.action) { + case 'start': + case 'next': + return await this.startQuiz(chunkId); + + case 'answer': + if (!args.userAnswer) { + return [{ + type: 'text', + text: `<chunk chunk_id="${chunkId}" chunk_type="error"> +userAnswer is required when action is "answer" +</chunk>` + }]; + } + return await this.evaluateAnswer(chunkId, args.userAnswer); + + default: + return [{ + type: 'text', + text: `<chunk chunk_id="${chunkId}" chunk_type="error"> +Unknown action: ${args.action}. Use "start", "answer", or "next" +</chunk>` + }]; + } + } catch (err) { + return [{ + type: 'text', + text: `<chunk chunk_id="${chunkId}" chunk_type="error"> +Quiz operation failed: ${err instanceof Error ? err.message : err} +</chunk>` + }]; + } + } - if (!docMeta) throw new Error('Randomly selected document metadata is undefined'); + /** + * Start a new quiz with a random document + */ + private async startQuiz(chunkId: string): Promise<Observation[]> { + const doc = this.selectRandomDocument(); + + if (!doc) { + return [{ + type: 'text', + text: `<chunk chunk_id="${chunkId}" chunk_type="error"> +No documents available for quiz. Please ensure you have documents in your collection. +</chunk>` + }]; + } - const description = docMeta.fields.layout.description.replace(/\n/g, ' ').trim(); - const rubric = await this.generateRubric(randomDocId, description); + this._currentQuizDoc = doc; + + // Get document description for the quiz question + try { + this._currentQuizDescription = await Doc.getDescription(doc); + + return [{ + type: 'text', + text: `<chunk chunk_id="${chunkId}" chunk_type="quiz_question"> +🎯 **Quiz Question** + +I've randomly selected a document for you to describe. Please provide your answer when you're ready. + +**Document:** ${doc.title || 'Untitled'} + +**Content to describe:** ${this._currentQuizDescription} + +**Instructions:** Please provide your description or explanation of this document content. I'll evaluate your answer once you submit it. - const prompt = ` - Question: ${description}; - UserAnswer: ${args.userAnswer}; - Rubric: ${rubric} - `; +**Status:** Waiting for your answer... +</chunk>` + }]; + } catch (error) { + return [{ + type: 'text', + text: `<chunk chunk_id="${chunkId}" chunk_type="error"> +Failed to get description for document "${doc.title}": ${error} +</chunk>` + }]; + } + } + + /** + * Evaluate the user's answer against the current quiz document + */ + private async evaluateAnswer(chunkId: string, userAnswer: string): Promise<Observation[]> { + if (!this._currentQuizDoc || !this._currentQuizDescription) { + return [{ + type: 'text', + text: `<chunk chunk_id="${chunkId}" chunk_type="error"> +No active quiz question. Please use action "start" to begin a quiz. +</chunk>` + }]; + } + try { + // Generate or get rubric for evaluation + const rubric = await this.generateRubric(this._currentQuizDoc, this._currentQuizDescription); + + // Prepare prompt for GPT evaluation + const prompt = `Question: ${this._currentQuizDescription}; +UserAnswer: ${userAnswer}; +Rubric: ${rubric}`; + + // Get GPT evaluation const evaluation = await gptAPICall(prompt, GPTCallType.QUIZDOC); - return [ - { - type: 'text', - text: `<chunk chunk_id="${chunkId}" chunk_type="quiz_result"> -Evaluation result: ${evaluation || 'GPT provided no answer'}. -Document evaluated: "${docMeta.title}" -</chunk>`, - }, - ]; - } catch (err) { - return [ - { - type: 'text', - text: `<chunk chunk_id="${chunkId}" chunk_type="error"> -Quiz evaluation failed: ${err instanceof Error ? err.message : err} -</chunk>`, - }, - ]; + return [{ + type: 'text', + text: `<chunk chunk_id="${chunkId}" chunk_type="quiz_evaluation"> +📝 **Quiz Evaluation Complete** + +**Document:** ${this._currentQuizDoc.title || 'Untitled'} + +**Your Answer:** ${userAnswer} + +**Evaluation:** ${evaluation || 'GPT provided no evaluation'} + +**Status:** Quiz answer evaluated. If you'd like another quiz question, please ask me to give you the next question. +</chunk>` + }]; + } catch (error) { + return [{ + type: 'text', + text: `<chunk chunk_id="${chunkId}" chunk_type="error"> +Failed to evaluate answer: ${error} +</chunk>` + }]; } } } |