-
Notifications
You must be signed in to change notification settings - Fork 2k
Expand file tree
/
Copy pathprompt-formatters.ts
More file actions
158 lines (143 loc) · 6.42 KB
/
prompt-formatters.ts
File metadata and controls
158 lines (143 loc) · 6.42 KB
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
/**
* Prompt and context building utilities for the generation pipeline.
*/
import type { PdfImage } from '@/lib/types/generation';
import { getGenerationLanguageSpec } from './language';
import type { AgentInfo, SceneGenerationContext } from './pipeline-types';
/** Build a course context string for injection into action prompts */
export function buildCourseContext(ctx?: SceneGenerationContext): string {
if (!ctx) return '';
const lines: string[] = [];
// Course outline with position marker
lines.push('Course Outline:');
ctx.allTitles.forEach((t, i) => {
const marker = i === ctx.pageIndex - 1 ? ' ← current' : '';
lines.push(` ${i + 1}. ${t}${marker}`);
});
// Position information
lines.push('');
lines.push(
'IMPORTANT: All pages belong to the SAME class session. Do NOT greet again after the first page. When referencing content from earlier pages, say "we just covered" or "as mentioned on page N" — NEVER say "last class" or "previous session" because there is no previous session.',
);
lines.push('');
if (ctx.pageIndex === 1) {
lines.push('Position: This is the FIRST page. Open with a greeting and course introduction.');
} else if (ctx.pageIndex === ctx.totalPages) {
lines.push('Position: This is the LAST page. Conclude the course with a summary and closing.');
lines.push(
'Transition: Continue naturally from the previous page. Do NOT greet or re-introduce.',
);
} else {
lines.push(`Position: Page ${ctx.pageIndex} of ${ctx.totalPages} (middle of the course).`);
lines.push(
'Transition: Continue naturally from the previous page. Do NOT greet or re-introduce.',
);
}
// Previous page speech for transition reference
if (ctx.previousSpeeches.length > 0) {
lines.push('');
lines.push('Previous page speech (for transition reference):');
const lastSpeech = ctx.previousSpeeches[ctx.previousSpeeches.length - 1];
lines.push(` "...${lastSpeech.slice(-150)}"`);
}
return lines.join('\n');
}
/** Format agent list for injection into action prompts */
export function formatAgentsForPrompt(agents?: AgentInfo[]): string {
if (!agents || agents.length === 0) return '';
const lines = ['Classroom Agents:'];
for (const a of agents) {
const personaPart = a.persona ? ` — ${a.persona}` : '';
lines.push(`- id: "${a.id}", name: "${a.name}", role: ${a.role}${personaPart}`);
}
return lines.join('\n');
}
/** Extract the teacher agent's persona for injection into outline/content prompts */
export function formatTeacherPersonaForPrompt(agents?: AgentInfo[]): string {
if (!agents || agents.length === 0) return '';
const teacher = agents.find((a) => a.role === 'teacher');
if (!teacher?.persona) return '';
return `Teacher Persona:\nName: ${teacher.name}\n${teacher.persona}\n\nAdapt the content style and tone to match this teacher's personality. IMPORTANT: The teacher's name and identity must NOT appear on the slides — no "Teacher ${teacher.name}'s tips", no "Teacher's message", etc. Slides should read as neutral, professional visual aids.`;
}
/**
* Format a single PdfImage description for prompt inclusion.
* Includes dimension/aspect-ratio info when available.
*/
export function formatImageDescription(img: PdfImage, language: string): string {
const spec = getGenerationLanguageSpec(language);
let dimInfo = '';
if (img.width && img.height) {
const ratio = (img.width / img.height).toFixed(2);
dimInfo =
spec.code === 'zh-CN'
? ` | 尺寸: ${img.width}×${img.height} (宽高比${ratio})`
: ` | dimensions: ${img.width}×${img.height} (aspect ratio ${ratio})`;
}
const desc = img.description ? ` | ${img.description}` : '';
if (spec.code === 'zh-CN') {
return `- **${img.id}**: 来自PDF第${img.pageNumber}页${dimInfo}${desc}`;
}
if (spec.code === 'ru-RU') {
return `- **${img.id}**: из PDF, страница ${img.pageNumber}${dimInfo}${desc}`;
}
return `- **${img.id}**: from PDF page ${img.pageNumber}${dimInfo}${desc}`;
}
/**
* Format a short image placeholder for vision mode.
* Only ID + page + dimensions + aspect ratio (no description), since the model can see the actual image.
*/
export function formatImagePlaceholder(img: PdfImage, language: string): string {
const spec = getGenerationLanguageSpec(language);
let dimInfo = '';
if (img.width && img.height) {
const ratio = (img.width / img.height).toFixed(2);
dimInfo =
spec.code === 'zh-CN'
? ` | 尺寸: ${img.width}×${img.height} (宽高比${ratio})`
: ` | dimensions: ${img.width}×${img.height} (aspect ratio ${ratio})`;
}
if (spec.code === 'zh-CN') {
return `- **${img.id}**: PDF第${img.pageNumber}页的图片${dimInfo} [参见附图]`;
}
if (spec.code === 'ru-RU') {
return `- **${img.id}**: изображение со страницы ${img.pageNumber} PDF${dimInfo} [см. вложение]`;
}
return `- **${img.id}**: image from PDF page ${img.pageNumber}${dimInfo} [see attached]`;
}
/**
* Build a multimodal user content array for the AI SDK.
* Interleaves text and images so the model can associate img_id with actual image.
* Each image label includes dimensions when available so the model knows the size
* before seeing the image (important for layout decisions).
*/
export function buildVisionUserContent(
userPrompt: string,
images: Array<{ id: string; src: string; width?: number; height?: number }>,
): Array<{ type: 'text'; text: string } | { type: 'image'; image: string; mimeType?: string }> {
const parts: Array<
{ type: 'text'; text: string } | { type: 'image'; image: string; mimeType?: string }
> = [{ type: 'text', text: userPrompt }];
if (images.length > 0) {
parts.push({ type: 'text', text: '\n\n--- Attached Images ---' });
for (const img of images) {
let dimInfo = '';
if (img.width && img.height) {
const ratio = (img.width / img.height).toFixed(2);
dimInfo = ` (${img.width}×${img.height}, aspect ratio ${ratio})`;
}
parts.push({ type: 'text', text: `\n**${img.id}**${dimInfo}:` });
// Strip data URI prefix — AI SDK only accepts http(s) URLs or raw base64
const dataUriMatch = img.src.match(/^data:([^;]+);base64,(.+)$/);
if (dataUriMatch) {
parts.push({
type: 'image',
image: dataUriMatch[2],
mimeType: dataUriMatch[1],
});
} else {
parts.push({ type: 'image', image: img.src });
}
}
}
return parts;
}