Skip to content

Commit cabf66e

Browse files
fix: ai generates markdown for google docs rich text (#10312)
1 parent c045e73 commit cabf66e

1 file changed

Lines changed: 134 additions & 82 deletions

File tree

apps/google-docs/functions/agents/documentParserAgent/documentParser.agent.ts

Lines changed: 134 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -39,15 +39,12 @@ export async function createDocument(config: DocumentParserConfig): Promise<Fina
3939

4040
const { document, openAiApiKey, contentTypes, locale = 'en-US' } = config;
4141

42-
// Extract text content from Google Docs JSON structure
43-
const documentContent = extractTextFromGoogleDocsJson(document);
44-
4542
const openaiClient = createOpenAI({
4643
apiKey: openAiApiKey,
4744
});
4845

49-
console.log('Document Parser Agent document content Input:', documentContent);
50-
const prompt = buildExtractionPrompt({ contentTypes, documentContent, locale });
46+
console.log('Document Parser Agent document content Input:', document);
47+
const prompt = buildExtractionPrompt({ contentTypes, document, locale });
5148
const result = await generateObject({
5249
model: openaiClient(modelVersion),
5350
schema: FinalEntriesResultSchema,
@@ -57,7 +54,7 @@ export async function createDocument(config: DocumentParserConfig): Promise<Fina
5754
});
5855

5956
const finalResult = result.object as FinalEntriesResult;
60-
console.log('Document Parser Agent Result:', result);
57+
console.log('Document Parser Agent Result:', JSON.stringify(result, null, 2));
6158

6259
return finalResult;
6360
}
@@ -111,72 +108,13 @@ EXTRACTION GUIDELINES:
111108
- Focus on simple fields: Symbol, Text, Number, Boolean, Date`;
112109
}
113110

114-
/**
115-
* Extracts plain text content from Google Docs JSON structure
116-
*/
117-
// TODO: Update this to be more robust and bulletproof
118-
function extractTextFromGoogleDocsJson(document: unknown): string {
119-
if (!document || typeof document !== 'object') {
120-
return '';
121-
}
122-
123-
const doc = document as Record<string, unknown>;
124-
const textParts: string[] = [];
125-
126-
// Extract title if available
127-
if (typeof doc.title === 'string') {
128-
textParts.push(doc.title);
129-
}
130-
131-
// Navigate through tabs -> documentTab -> body -> content
132-
if (Array.isArray(doc.tabs)) {
133-
for (const tab of doc.tabs) {
134-
if (typeof tab === 'object' && tab !== null) {
135-
const tabObj = tab as Record<string, unknown>;
136-
if (tabObj.documentTab && typeof tabObj.documentTab === 'object') {
137-
const docTab = tabObj.documentTab as Record<string, unknown>;
138-
if (docTab.body && typeof docTab.body === 'object') {
139-
const body = docTab.body as Record<string, unknown>;
140-
if (Array.isArray(body.content)) {
141-
for (const item of body.content) {
142-
if (typeof item === 'object' && item !== null) {
143-
const itemObj = item as Record<string, unknown>;
144-
// Extract text from paragraphs
145-
if (itemObj.paragraph && typeof itemObj.paragraph === 'object') {
146-
const para = itemObj.paragraph as Record<string, unknown>;
147-
if (Array.isArray(para.elements)) {
148-
for (const elem of para.elements) {
149-
if (typeof elem === 'object' && elem !== null) {
150-
const elemObj = elem as Record<string, unknown>;
151-
if (elemObj.textRun && typeof elemObj.textRun === 'object') {
152-
const textRun = elemObj.textRun as Record<string, unknown>;
153-
if (typeof textRun.content === 'string') {
154-
textParts.push(textRun.content);
155-
}
156-
}
157-
}
158-
}
159-
}
160-
}
161-
}
162-
}
163-
}
164-
}
165-
}
166-
}
167-
}
168-
}
169-
170-
return textParts.join(' ').trim();
171-
}
172-
173111
function buildExtractionPrompt({
174112
contentTypes,
175-
documentContent,
113+
document,
176114
locale,
177115
}: {
178116
contentTypes: ContentTypeProps[];
179-
documentContent: string;
117+
document: unknown;
180118
locale: string;
181119
}): string {
182120
const contentTypeList = contentTypes.map((ct) => `${ct.name} (ID: ${ct.sys.id})`).join(', ');
@@ -216,7 +154,7 @@ function buildExtractionPrompt({
216154
};
217155
});
218156

219-
return `Extract structured entries from the following document based on the provided Contentful content type definitions.
157+
return `Extract structured entries from the following Google Docs JSON document based on the provided Contentful content type definitions.
220158
221159
AVAILABLE CONTENT TYPES: ${contentTypeList}
222160
TOTAL CONTENT TYPES: ${contentTypes.length}
@@ -226,29 +164,143 @@ LOCALE TO USE: ${locale}
226164
CONTENT TYPE DEFINITIONS:
227165
${JSON.stringify(contentTypeDefinitions, null, 2)}
228166
229-
DOCUMENT CONTENT:
230-
${documentContent}
167+
=== GOOGLE DOCS JSON PARSING GUIDE ===
168+
169+
The document is in Google Docs API JSON format. Here's how to interpret the structure:
170+
171+
**DOCUMENT STRUCTURE:**
172+
- \`documentId\`: Unique identifier for the document
173+
- \`tabs[].documentTab.body.content[]\`: Array of content elements (paragraphs, tables, sections)
174+
- \`tabs[].documentTab.inlineObjects\`: Object mapping inlineObjectId → image/embedded object data
175+
- \`tabs[].documentTab.lists\`: Object mapping listId → list configuration (bullet/numbered)
176+
177+
**CONTENT ELEMENT TYPES:**
178+
179+
1. **Paragraphs** - Main text content:
180+
\`\`\`
181+
{
182+
"paragraph": {
183+
"elements": [{ "textRun": { "content": "text\\n", "textStyle": {...} } }],
184+
"paragraphStyle": { "namedStyleType": "HEADING_1" | "HEADING_2" | "NORMAL_TEXT" | ... },
185+
"bullet": { "listId": "kix.xxx", "nestingLevel": 0 } // if it's a list item
186+
}
187+
}
188+
\`\`\`
189+
- \`namedStyleType\`: HEADING_1, HEADING_2, HEADING_3, HEADING_4, HEADING_5, HEADING_6, NORMAL_TEXT, TITLE, SUBTITLE
190+
- \`bullet.listId\`: References list definition in \`lists\` object (indicates bullet/numbered list)
191+
- \`bullet.nestingLevel\`: Indentation level (0 = top level)
192+
193+
2. **Text Runs** - Inline text with formatting:
194+
\`\`\`
195+
{
196+
"textRun": {
197+
"content": "the actual text content",
198+
"textStyle": {
199+
"bold": true/false,
200+
"italic": true/false,
201+
"underline": true/false,
202+
"strikethrough": true/false,
203+
"link": { "url": "https://..." },
204+
"foregroundColor": { "color": { "rgbColor": { "red": 0-1, "green": 0-1, "blue": 0-1 } } },
205+
"fontSize": { "magnitude": 11, "unit": "PT" }
206+
}
207+
}
208+
}
209+
\`\`\`
210+
211+
3. **Inline Object Elements** - References to images:
212+
\`\`\`
213+
{ "inlineObjectElement": { "inlineObjectId": "kix.xxx" } }
214+
\`\`\`
215+
- Look up the actual image in \`inlineObjects["kix.xxx"]\`
216+
- Image URL is at: \`inlineObjects[id].inlineObjectProperties.embeddedObject.imageProperties.contentUri\`
217+
218+
4. **Rich Links** - Embedded links with previews (YouTube, etc.):
219+
\`\`\`
220+
{ "richLink": { "richLinkId": "kix.xxx", "richLinkProperties": { "title": "...", "uri": "..." } } }
221+
\`\`\`
222+
223+
5. **Tables** - Structured tabular data:
224+
\`\`\`
225+
{
226+
"table": {
227+
"rows": number,
228+
"columns": number,
229+
"tableRows": [{
230+
"tableCells": [{
231+
"content": [/* paragraphs */]
232+
}]
233+
}]
234+
}
235+
}
236+
\`\`\`
237+
- Each cell contains an array of paragraph elements
238+
- First row is typically headers
239+
240+
6. **Lists** - Bullet and numbered lists:
241+
- Paragraphs with \`bullet\` property are list items
242+
- \`bullet.listId\` references the list definition in \`lists\`
243+
- Check \`lists[listId].listProperties.nestingLevels[0].glyphSymbol\` for bullet character
244+
- Check \`lists[listId].listProperties.nestingLevels[0].glyphType\` for numbered list type (DECIMAL, ALPHA, ROMAN)
245+
246+
**EXTRACTING TEXT CONTENT:**
247+
1. Navigate to \`tabs[0].documentTab.body.content\`
248+
2. For each element, check if it has \`paragraph\`, \`table\`, or \`sectionBreak\`
249+
3. For paragraphs:
250+
- Get heading level from \`paragraphStyle.namedStyleType\`
251+
- Concatenate all \`elements[].textRun.content\` values
252+
- Apply formatting based on \`textStyle\` (bold → **, italic → *, underline → _)
253+
- Check for \`bullet\` to identify list items
254+
4. For tables:
255+
- Iterate through \`tableRows[].tableCells[].content\` to get cell text
256+
- Use first row as headers if appropriate
257+
258+
**FORMATTING CONVERSION:**
259+
When extracting RichText fields, convert Google Docs formatting to Markdown:
260+
- textStyle.bold: true → **text**
261+
- textStyle.italic: true → *text*
262+
- textStyle.underline: true → _text_ or <u>text</u>
263+
- textStyle.strikethrough: true → ~~text~~
264+
- textStyle.link.url → [text](url)
265+
- HEADING_1 → # heading
266+
- HEADING_2 → ## heading
267+
- HEADING_3 → ### heading
268+
- Bullet lists → - item
269+
- Numbered lists → 1. item
270+
271+
=== END PARSING GUIDE ===
272+
273+
GOOGLE DOCS JSON DOCUMENT:
274+
${JSON.stringify(document, null, 2)}
231275
232276
CRITICAL INSTRUCTIONS:
233-
1. **SKIP ALL FIELDS WHERE "SKIP": true** - Do NOT include these fields in your output
234-
2. Look at each field definition - if it has "SKIP": true, completely ignore that field
235-
3. Only include fields where "SKIP" is false or not present
236-
4. Analyze the document and identify content that matches the provided content type structures
237-
5. Extract all relevant entries from the document
238-
6. For each entry, use the contentTypeId that best matches the content
239-
7. Format fields correctly: { "fieldId": { "${locale}": value } }
240-
8. Match field types exactly:
277+
1. **PARSE THE GOOGLE DOCS JSON** - Use the parsing guide above to extract text and structure
278+
2. **SKIP ALL FIELDS WHERE "SKIP": true** - Do NOT include these fields in your output
279+
3. Look at each field definition - if it has "SKIP": true, completely ignore that field
280+
4. Only include fields where "SKIP" is false or not present
281+
5. Analyze the document and identify content that matches the provided content type structures
282+
6. Extract all relevant entries from the document
283+
7. For each entry, use the contentTypeId that best matches the content
284+
8. Format fields correctly: { "fieldId": { "${locale}": value } }
285+
9. Match field types exactly:
241286
- Symbol: string (max 256 chars)
242287
- Text: string (any length)
243-
- RichText: string in Markdown (preserve bold **, italics *, underline _)
288+
- RichText: string in Markdown (convert Google Docs formatting using the guide above)
244289
- Number: number
245290
- Boolean: boolean
246291
- Date: ISO 8601 string
247292
- Array: array of primitives (strings or numbers ONLY)
248293
- Object: JSON object
249-
9. For required fields (required: true) that are NOT marked SKIP: true, ensure they are populated
250-
10. If you cannot populate a required field from the document, use a sensible default or placeholder
251-
11. Be thorough - extract all valid content from the document
294+
10. For required fields (required: true) that are NOT marked SKIP: true, ensure they are populated
295+
11. If you cannot populate a required field from the document, use a sensible default or placeholder
296+
12. Be thorough - extract all valid content from the document
297+
298+
**CONTENT EXTRACTION TIPS:**
299+
- Look for HEADING_1 or HEADING_2 paragraphs as entry titles
300+
- Normal paragraphs following headings are typically body content
301+
- Tables may contain structured data that maps to entry fields
302+
- Lists can be extracted as array fields (if type is Array of Symbol/Text)
303+
- Image URLs from inlineObjects can populate URL/Symbol fields
252304
253305
Return the extracted entries in the specified JSON schema format.`;
254306
}

0 commit comments

Comments
 (0)