1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
|
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<typeof parameterRules> = {
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<typeof parameterRules> {
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<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) {
// 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 {
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>`
}];
}
}
/**
* 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>`
}];
}
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.
**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_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>`
}];
}
}
}
|