@@ -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-
173111function 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
221159AVAILABLE CONTENT TYPES: ${ contentTypeList }
222160TOTAL CONTENT TYPES: ${ contentTypes . length }
@@ -226,29 +164,143 @@ LOCALE TO USE: ${locale}
226164CONTENT 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
232276CRITICAL 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
253305Return the extracted entries in the specified JSON schema format.` ;
254306}
0 commit comments