Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 6 additions & 26 deletions apps/google-docs/contentful-app-manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@
"parameters": {
"installation": [
{
"id": "apiKey",
"id": "openAiApiKey",
"type": "Secret",
"name": "API Key",
"name": "OpenAI API Key",
"description": "API Key for the app to use the OpenAI API."
}
]
Expand All @@ -27,7 +27,9 @@
"description": "Function to create content blocks from App Action.",
"path": "functions/createEntriesFromDocument.js",
"entryFile": "functions/createEntriesFromDocument.ts",
"allowNetworks": [],
"allowNetworks": [
"https://api.openai.com"
],
"accepts": [
"appaction.call"
]
Expand Down Expand Up @@ -85,27 +87,5 @@
]
}
],
"actions": [
{
"id": "createEntriesFromDocumentAction",
"name": "Create entries from an uploaded document",
"type": "function-invocation",
"functionId": "createEntriesFromDocumentFunction",
"category": "Custom",
"parameters": [
{
"id": "contentTypeId",
"name": "Content Type ID",
"type": "Symbol",
"required": true
},
{
"id": "prompt",
"name": "Prompt from the user",
"type": "Symbol",
"required": true
}
]
}
]
"actions": []
}
51 changes: 0 additions & 51 deletions apps/google-docs/functions/agents/contentTypeParser.agent.ts

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import { createOpenAI } from '@ai-sdk/openai';
import { FinalContentTypesResultSummary, FinalContentTypesAnalysisSchema } from './schema';
import { generateObject } from 'ai';
import { ContentTypeProps } from 'contentful-management';

export interface ContentTypeParserConfig {
contentTypes: ContentTypeProps[];
openAiApiKey: string;
}

/**
* AI Agent that parses an array of Contentful content types and generates structured summaries
*
* @param contentTypes - Array of Contentful content type objects
* @param config - Optional configuration for the AI model
* @returns Promise resolving to structured parse result with summaries
*
*/
export async function analyzeContentTypes({
contentTypes,
openAiApiKey,
}: ContentTypeParserConfig): Promise<FinalContentTypesResultSummary> {
// TODO: Double check these values and make sure they are compatible because not every user will have a key
// to access all models
const modelVersion = 'gpt-4o';
const temperature = 0.3;
Comment on lines +23 to +26

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we go with using OpenAI for the GA release, we could allow the user to select their model from a dropdown list in the app configuration. We could get the list of available models from an API, similar to what we do in the other OpenAI apps: https://github.com/contentful/apps/blob/master/apps/ai-content-generator/src/components/config/model/Model.tsx#L23

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll add this to a fast follow ticket


const openaiClient = createOpenAI({
apiKey: openAiApiKey,
});

const prompt = buildAnalysisPrompt(contentTypes);

const result = await generateObject({
model: openaiClient(modelVersion),
schema: FinalContentTypesAnalysisSchema,
temperature,
system: buildSystemPrompt(),
prompt,
});

const finalAnalysis = result.object as FinalContentTypesResultSummary;
return finalAnalysis;
}

/**
* Builds the system prompt for the AI
*/
function buildSystemPrompt(): string {
return `You are an expert Contentful content modeling analyst. Your role is to analyze Contentful content type definitions and provide clear, actionable summaries.

Your analysis should:
1. Identify the purpose and intended use of each content type
2. Explain what each field represents and how it should be used
3. Identify relationships between content types
4. Assess the overall model complexity
5. Provide practical recommendations for content editors and developers

Focus on clarity and actionability. Your summaries will be used by:
- Content editors who need to understand how to use content types
- Content strategists planning content architecture`;
}

/**
* Builds the analysis prompt from content type data
*/
function buildAnalysisPrompt(contentTypes: ContentTypeProps[]): string {
const contentTypeList = contentTypes.map((ct) => ct.name).join(', ');
const totalFields = contentTypes.reduce((sum, ct) => sum + (ct.fields?.length || 0), 0);

return `Analyze the following Contentful content type definitions and provide concise summaries.

CONTENT TYPES TO ANALYZE: ${contentTypeList}
TOTAL CONTENT TYPES: ${contentTypes.length}
TOTAL FIELDS ACROSS ALL TYPES: ${totalFields}

CONTENT TYPE DEFINITIONS:
${JSON.stringify(contentTypes, null, 2)}

For each content type, provide:
1. Clear description of what this content type represents
2. The intended purpose and use cases
3. Total field count
4. Names of 3-5 most important/key fields
5. 2-3 practical usage recommendations

Also provide:
- Overall summary of the content model (2-3 sentences)
- Complexity assessment (simple/moderate/complex as a string)

Keep responses concise and actionable.`;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { z } from 'zod';

// Schema Definitions for the Content Type Parser Agent

// Each invidual CT analysis by the AI Agent output schema
export const ContentTypeAnalysisSchema = z.object({
id: z.string(),
name: z.string(),
description: z.string(),
purpose: z.string(),
fieldCount: z.number(),
keyFields: z.array(z.string()),
recommendations: z.array(z.string()),
});

// The entire set of CT analyses by the AI Agent output schema
export const FinalContentTypesAnalysisSchema = z.object({
contentTypes: z.array(ContentTypeAnalysisSchema),
summary: z.string(),
complexity: z.string(),
});

export type ContentTypeSummary = z.infer<typeof ContentTypeAnalysisSchema>;
export type FinalContentTypesResultSummary = z.infer<typeof FinalContentTypesAnalysisSchema>;
38 changes: 18 additions & 20 deletions apps/google-docs/functions/createEntriesFromDocument.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,13 @@ import type {
FunctionTypeEnum,
AppActionRequest,
} from '@contentful/node-apps-toolkit';
// INTEG-3262 and INTEG-3263: Likely imports to be used are commented out for now
// import { ContentTypeProps, EntryProps, ContentFields } from 'contentful-management';
// import { KeyValueMap } from 'contentful-management';
import { parseContentType } from './agents/contentTypeParser.agent';
import { analyzeContentTypes } from './agents/contentTypeParserAgent/contentTypeParser.agent';
import { createDocument } from './agents/documentParser.agent';
// import { createEntries, createAssets } from './service/entryService';
import { fetchContentTypes } from './service/contentTypeService';
import { initContentfulManagementClient } from './service/initCMAClient';

export type AppActionParameters = {
contentTypeId: string;
contentTypeIds: string[];
prompt: string;
};
interface AppInstallationParameters {
Expand All @@ -30,28 +28,28 @@ export const handler: FunctionEventHandler<
event: AppActionRequest<'Custom', AppActionParameters>,
context: FunctionEventContext
) => {
const { contentTypeId, prompt } = event.body;
const { contentTypeIds } = event.body;
const { openAiApiKey } = context.appInstallationParameters as AppInstallationParameters;

// INTEG-3262 and INTEG-3263: Take in Content Type, Prompt, and Upload File from user

// INTEG-3262: Implement the content type parser agent
const aiContentTypeResponse = parseContentType({
openaiApiKey: openAiApiKey,
modelVersion: 'gpt-4o',
jsonData: contentType,
});
const cma = initContentfulManagementClient(context);

const contentTypes = await fetchContentTypes(cma, new Set<string>(contentTypeIds));
const contentTypeParserAgentResult = await analyzeContentTypes({ contentTypes, openAiApiKey });

console.log('contentTypeParserAgentResult', contentTypeParserAgentResult);

// INTEG-3261: Pass the ai content type response to the observer for analysis
// createContentTypeObservationsFromLLMResponse()

// INTEG-3263: Implement the document parser agent
const aiDocumentResponse = createDocument({
openaiApiKey: openAiApiKey,
modelVersion: 'gpt-4o',
jsonData: aiContentTypeResponse,
document: document,
});
// const aiDocumentResponse = createDocument({
// openaiApiKey: openAiApiKey,
// modelVersion: 'gpt-4o',
// jsonData: aiContentTypeResponse,
// document: document,
// });

// INTEG-3261: Pass the ai document response to the observer for analysis
// createDocumentObservationsFromLLMResponse()
Expand All @@ -62,5 +60,5 @@ export const handler: FunctionEventHandler<
// INTEG-3265: Create the assets in Contentful using the asset service
// await createAssets()

return { success: true, response: {} };
return { success: true, response: contentTypeParserAgentResult };
};
20 changes: 20 additions & 0 deletions apps/google-docs/functions/service/contentTypeService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { PlainClientAPI } from 'contentful-management';

/*
* Fetches the user selected content types that the user wants the AI to create entries for
* @param cma Content Management API client
* @param contentTypeIds Array of content type IDs
* @returns array of Content type json objects
*/
export const fetchContentTypes = async (
cma: PlainClientAPI,
contentTypeIds: Set<string>
): Promise<any> => {
try {
const response = await cma.contentType.getMany({});
const selectedContentTypes = response.items.filter((item) => contentTypeIds.has(item.sys.id));
return selectedContentTypes;
} catch (error) {
throw new Error(`Failed to fetch content types ${contentTypeIds}: ${error}`);
}
};
17 changes: 17 additions & 0 deletions apps/google-docs/functions/service/initCMAClient.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { FunctionEventContext } from '@contentful/node-apps-toolkit';
import { PlainClientAPI, createClient } from 'contentful-management';

export function initContentfulManagementClient(context: FunctionEventContext): PlainClientAPI {
if (!context.cmaClientOptions) {
throw new Error(
'Contentful Management API client options are only provided for certain function types. To learn more about using the CMA within functions, see https://www.contentful.com/developers/docs/extensibility/app-framework/functions/#using-the-cma.'
);
}
return createClient(context.cmaClientOptions, {
type: 'plain',
defaults: {
spaceId: context.spaceId,
environmentId: context.environmentId,
},
});
}
11 changes: 11 additions & 0 deletions apps/google-docs/functions/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"compilerOptions": {
"target": "ES2020",
"module": "CommonJS",
"lib": ["ES2020"],
"strict": true,

},
"include": ["./**/*.ts"],
"exclude": ["node_modules", "build"]
}
17 changes: 17 additions & 0 deletions apps/google-docs/functions/vitest.config.mts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { defineConfig } from 'vitest/config';
import { loadEnv } from 'vite';
import path from 'path';

export default defineConfig(({ mode }) => ({
test: {
globals: true,
environment: 'node',
// Load .env from parent directory (google-docs root)
env: loadEnv(mode, path.resolve(__dirname, '..'), ''),
// Don't use the React setup file for functions tests
setupFiles: undefined,
// Only include tests in the functions directory
include: ['**/*.test.ts', '**/*.spec.ts'],
},
}));

Loading
Loading