import { BaseTool } from './BaseTool'; import { Observation } from '../types/types'; 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: 'action', type: 'string', 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 = { name: 'takeQuiz', 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 { private _docManager: AgentDocumentManager; private _currentQuizDoc: Doc | null = null; private _currentQuizDescription: string = ''; constructor(docManager: AgentDocumentManager) { super(toolInfo); this._docManager = docManager; this._docManager.initializeDocuments(); } /** * Generate or retrieve a rubric for evaluating the user's description of a document */ private async generateRubric(doc: Doc, description: string): Promise { // 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) { // 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): Promise { const chunkId = uuidv4(); try { switch (args.action) { case 'start': case 'next': return await this.startQuiz(chunkId); case 'answer': if (!args.userAnswer) { return [{ type: 'text', text: ` userAnswer is required when action is "answer" ` }]; } return await this.evaluateAnswer(chunkId, args.userAnswer); default: return [{ type: 'text', text: ` Unknown action: ${args.action}. Use "start", "answer", or "next" ` }]; } } catch (err) { return [{ type: 'text', text: ` Quiz operation failed: ${err instanceof Error ? err.message : err} ` }]; } } /** * Start a new quiz with a random document */ private async startQuiz(chunkId: string): Promise { const doc = this.selectRandomDocument(); if (!doc) { return [{ type: 'text', text: ` No documents available for quiz. Please ensure you have documents in your collection. ` }]; } this._currentQuizDoc = doc; // Get document description for the quiz question try { this._currentQuizDescription = await Doc.getDescription(doc); return [{ type: 'text', text: ` 🎯 **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. **Status:** Waiting for your answer... ` }]; } catch (error) { return [{ type: 'text', text: ` Failed to get description for document "${doc.title}": ${error} ` }]; } } /** * Evaluate the user's answer against the current quiz document */ private async evaluateAnswer(chunkId: string, userAnswer: string): Promise { if (!this._currentQuizDoc || !this._currentQuizDescription) { return [{ type: 'text', text: ` No active quiz question. Please use action "start" to begin a quiz. ` }]; } 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: ` 📝 **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. ` }]; } catch (error) { return [{ type: 'text', text: ` Failed to evaluate answer: ${error} ` }]; } } }