|
1 | 1 | /** |
2 | | - * INTEG-3263: Content Type Parser Agent |
| 2 | + * INTEG-3263: Document Parser Agent |
3 | 3 | * |
4 | | - * Simple agent that takes JSON input and sends it to OpenAI/ChatGPT |
5 | | - * to generate Contentful content type definitions. |
| 4 | + * Agent that takes a Google Doc URL and content type definitions, |
| 5 | + * then uses OpenAI to extract structured entries from the document |
| 6 | + * that can be directly created in Contentful. |
6 | 7 | * See https://contentful.atlassian.net/wiki/spaces/ECO/pages/5850955777/RFC+Google+Docs+V1+AI-Gen |
7 | 8 | * for more details. |
8 | 9 | */ |
9 | 10 |
|
10 | | -import { openai } from '@ai-sdk/openai'; |
11 | | -import { generateText } from 'ai'; |
| 11 | +import { createOpenAI } from '@ai-sdk/openai'; |
| 12 | +import { generateObject } from 'ai'; |
12 | 13 | import { ContentTypeProps } from 'contentful-management'; |
13 | 14 | import { fetchGoogleDoc } from '../../service/googleDriveService'; |
| 15 | +import { FinalEntriesResultSchema, FinalEntriesResult } from './schema'; |
14 | 16 |
|
15 | 17 | /** |
16 | | - * Configuration for the content type parser |
| 18 | + * Configuration for the document parser |
17 | 19 | */ |
18 | | -interface DocumentParserConfig { |
19 | | - // contentTypes: ContentTypeProps[]; |
| 20 | +export interface DocumentParserConfig { |
| 21 | + openAiApiKey: string; |
20 | 22 | googleDocUrl: string; |
| 23 | + contentTypes: ContentTypeProps[]; |
| 24 | + locale?: string; |
21 | 25 | } |
22 | 26 |
|
23 | 27 | /** |
24 | | - * @param jsonData - JSON data to analyze |
25 | | - * @param config - Parser configuration |
26 | | - * @returns Promise resolving to LLM response |
| 28 | + * AI Agent that parses a Google Doc and extracts structured entries |
| 29 | + * based on provided Contentful content type definitions. |
| 30 | + * |
| 31 | + * @param config - Parser configuration including API key, document URL, and content types |
| 32 | + * @returns Promise resolving to entries ready for CMA client |
27 | 33 | */ |
28 | | -export async function createDocument(config: DocumentParserConfig) { |
29 | | - const { googleDocUrl } = config; |
| 34 | +export async function createDocument(config: DocumentParserConfig): Promise<FinalEntriesResult> { |
| 35 | + // TODO: Double check these values and make sure they are compatible because not every user will have a key |
| 36 | + // to access all models |
| 37 | + const modelVersion = 'gpt-4o'; |
| 38 | + const temperature = 0.3; |
| 39 | + |
| 40 | + const { googleDocUrl, openAiApiKey, contentTypes, locale = 'en-US' } = config; |
30 | 41 | const googleDocContent = await fetchGoogleDoc(googleDocUrl); |
31 | | - // const prompt = buildPrompt(jsonData); |
32 | | - // return await callOpenAI(prompt, modelVersion, openaiApiKey); |
33 | | - return googleDocContent as string; |
| 42 | + |
| 43 | + const openaiClient = createOpenAI({ |
| 44 | + apiKey: openAiApiKey, |
| 45 | + }); |
| 46 | + |
| 47 | + const prompt = buildExtractionPrompt({ contentTypes, googleDocContent, locale }); |
| 48 | + const result = await generateObject({ |
| 49 | + model: openaiClient(modelVersion), |
| 50 | + schema: FinalEntriesResultSchema, |
| 51 | + temperature, |
| 52 | + system: buildSystemPrompt(), |
| 53 | + prompt, |
| 54 | + }); |
| 55 | + |
| 56 | + return result.object as FinalEntriesResult; |
34 | 57 | } |
35 | 58 |
|
36 | | -function buildPrompt(jsonData: any): string { |
37 | | - // TODO: Create prompt template for the AI to consume |
38 | | - // 1. Add instructions for content type generation |
39 | | - // 2. Include JSON data |
40 | | - // 3. Specify output format |
41 | | - return `Parse the document and create a json object that represents the document for the Contetful API to consume: ${JSON.stringify( |
42 | | - jsonData, |
43 | | - null, |
44 | | - 2 |
45 | | - )}`; |
| 59 | +function buildSystemPrompt(): string { |
| 60 | + return `You are an expert content extraction AI that analyzes documents and extracts structured content based on Contentful content type definitions. |
| 61 | +
|
| 62 | +Your role is to: |
| 63 | +1. Carefully read and understand the document content |
| 64 | +2. Analyze the provided Contentful content type definitions (their fields, types, and validations) |
| 65 | +3. Extract relevant information from the document that matches the content type structure |
| 66 | +4. Create properly formatted entries that are ready to be created in Contentful via the CMA API |
| 67 | +
|
| 68 | +Important guidelines: |
| 69 | +- Each entry must have a contentTypeId that matches one of the provided content types |
| 70 | +- Fields must be in the correct format: { "fieldId": { "locale": value } } |
| 71 | +- Respect field types (Text, Symbol, RichText, Number, Boolean, Date, Reference, etc.) |
| 72 | +- Only include fields that exist in the content type definition |
| 73 | +- Extract all relevant content from the document - don't skip entries |
| 74 | +- If a field is required in the content type, ensure it's populated |
| 75 | +- For rich text fields, extract formatted content when possible |
| 76 | +- Be thorough and extract as many valid entries as you can find in the document`; |
46 | 77 | } |
47 | 78 |
|
48 | | -async function callOpenAI(prompt: string, modelVersion: string, openaiApiKey: string) { |
49 | | - const model = openai(modelVersion); |
| 79 | +function buildExtractionPrompt({ |
| 80 | + contentTypes, |
| 81 | + googleDocContent, |
| 82 | + locale, |
| 83 | +}: { |
| 84 | + contentTypes: ContentTypeProps[]; |
| 85 | + googleDocContent: string; |
| 86 | + locale: string; |
| 87 | +}): string { |
| 88 | + const contentTypeList = contentTypes.map((ct) => `${ct.name} (ID: ${ct.sys.id})`).join(', '); |
| 89 | + const totalFields = contentTypes.reduce((sum, ct) => sum + (ct.fields?.length || 0), 0); |
50 | 90 |
|
51 | | - // ai-sdk documentation: https://ai-sdk.dev/docs/reference/ai-sdk-core/generate-text |
52 | | - // Select the appropriate core function from the list of options |
53 | | - const result = await generateText({ |
54 | | - model, |
55 | | - prompt, |
56 | | - }); |
57 | | - return result; |
| 91 | + // Create a simplified view of content types for the prompt |
| 92 | + const contentTypeDefinitions = contentTypes.map((ct) => ({ |
| 93 | + id: ct.sys.id, |
| 94 | + name: ct.name, |
| 95 | + description: ct.description, |
| 96 | + fields: |
| 97 | + ct.fields?.map((field) => ({ |
| 98 | + id: field.id, |
| 99 | + name: field.name, |
| 100 | + type: field.type, |
| 101 | + required: field.required, |
| 102 | + localized: field.localized, |
| 103 | + validations: field.validations, |
| 104 | + })) || [], |
| 105 | + })); |
| 106 | + |
| 107 | + return `Extract structured entries from the following document based on the provided Contentful content type definitions. |
| 108 | +
|
| 109 | +AVAILABLE CONTENT TYPES: ${contentTypeList} |
| 110 | +TOTAL CONTENT TYPES: ${contentTypes.length} |
| 111 | +TOTAL FIELDS ACROSS ALL TYPES: ${totalFields} |
| 112 | +LOCALE TO USE: ${locale} |
| 113 | +
|
| 114 | +CONTENT TYPE DEFINITIONS: |
| 115 | +${JSON.stringify(contentTypeDefinitions, null, 2)} |
| 116 | +
|
| 117 | +DOCUMENT CONTENT: |
| 118 | +${googleDocContent} |
| 119 | +
|
| 120 | +INSTRUCTIONS: |
| 121 | +1. Analyze the document and identify content that matches the provided content type structures |
| 122 | +2. Extract all relevant entries from the document |
| 123 | +3. For each entry, use the contentTypeId that best matches the content |
| 124 | +4. Format fields correctly: { "fieldId": { "${locale}": value } } |
| 125 | +5. Ensure all required fields are populated |
| 126 | +6. Be thorough - extract all valid content from the document |
| 127 | +
|
| 128 | +Return the extracted entries in the specified JSON schema format.`; |
58 | 129 | } |
0 commit comments