From 4a141171c5e01767df44adaafae4806affec2bab Mon Sep 17 00:00:00 2001 From: Matthias Nannt Date: Wed, 6 Aug 2025 13:12:46 +0200 Subject: [PATCH] chore: introduce ai package --- apps/web/locales/de-DE.json | 4 +- apps/web/locales/en-US.json | 4 +- apps/web/locales/fr-FR.json | 6 +- apps/web/locales/pt-BR.json | 4 +- apps/web/locales/pt-PT.json | 4 +- apps/web/locales/zh-Hant-TW.json | 4 +- packages/ai/.eslintrc.cjs | 10 ++ packages/ai/README.md | 198 +++++++++++++++++++++++++++++++ packages/ai/package.json | 39 ++++++ packages/ai/src/ai.ts | 196 ++++++++++++++++++++++++++++++ packages/ai/src/config.ts | 168 ++++++++++++++++++++++++++ packages/ai/src/index.ts | 20 ++++ packages/ai/src/types.ts | 102 ++++++++++++++++ packages/ai/tsconfig.json | 12 ++ packages/ai/vite.config.ts | 49 ++++++++ pnpm-lock.yaml | 149 +++++++++++++++++++++++ 16 files changed, 962 insertions(+), 7 deletions(-) create mode 100644 packages/ai/.eslintrc.cjs create mode 100644 packages/ai/README.md create mode 100644 packages/ai/package.json create mode 100644 packages/ai/src/ai.ts create mode 100644 packages/ai/src/config.ts create mode 100644 packages/ai/src/index.ts create mode 100644 packages/ai/src/types.ts create mode 100644 packages/ai/tsconfig.json create mode 100644 packages/ai/vite.config.ts diff --git a/apps/web/locales/de-DE.json b/apps/web/locales/de-DE.json index e8d254ca54..0a99a54c01 100644 --- a/apps/web/locales/de-DE.json +++ b/apps/web/locales/de-DE.json @@ -124,6 +124,7 @@ "add_action": "Aktion hinzufügen", "add_filter": "Filter hinzufügen", "add_logo": "Logo hinzufügen", + "add_member": "Mitglied hinzufügen", "add_project": "Projekt hinzufügen", "add_to_team": "Zum Team hinzufügen", "all": "Alle", @@ -280,6 +281,7 @@ "only_one_file_allowed": "Es ist nur eine Datei erlaubt", "only_owners_managers_and_manage_access_members_can_perform_this_action": "Nur Eigentümer, Manager und Mitglieder mit Zugriff auf das Management können diese Aktion ausführen.", "option_id": "Option-ID", + "option_ids": "Option-IDs", "or": "oder", "organization": "Organisation", "organization_id": "Organisations-ID", @@ -1314,7 +1316,7 @@ "columns": "Spalten", "company": "Firma", "company_logo": "Firmenlogo", - "completed_responses": "abgeschlossene Antworten", + "completed_responses": "unvollständige oder vollständige Antworten.", "concat": "Verketten +", "conditional_logic": "Bedingte Logik", "confirm_default_language": "Standardsprache bestätigen", diff --git a/apps/web/locales/en-US.json b/apps/web/locales/en-US.json index 6bba350c13..9455598ccd 100644 --- a/apps/web/locales/en-US.json +++ b/apps/web/locales/en-US.json @@ -124,6 +124,7 @@ "add_action": "Add action", "add_filter": "Add filter", "add_logo": "Add logo", + "add_member": "Add member", "add_project": "Add project", "add_to_team": "Add to team", "all": "All", @@ -280,6 +281,7 @@ "only_one_file_allowed": "Only one file is allowed", "only_owners_managers_and_manage_access_members_can_perform_this_action": "Only owners and managers can perform this action.", "option_id": "Option ID", + "option_ids": "Option IDs", "or": "or", "organization": "Organization", "organization_id": "Organization ID", @@ -1314,7 +1316,7 @@ "columns": "Columns", "company": "Company", "company_logo": "Company logo", - "completed_responses": "completed responses.", + "completed_responses": "partial or completed responses.", "concat": "Concat +", "conditional_logic": "Conditional Logic", "confirm_default_language": "Confirm default language", diff --git a/apps/web/locales/fr-FR.json b/apps/web/locales/fr-FR.json index 7337821a25..e53a357cb9 100644 --- a/apps/web/locales/fr-FR.json +++ b/apps/web/locales/fr-FR.json @@ -124,6 +124,7 @@ "add_action": "Ajouter une action", "add_filter": "Ajouter un filtre", "add_logo": "Ajouter un logo", + "add_member": "Ajouter un membre", "add_project": "Ajouter un projet", "add_to_team": "Ajouter à l'équipe", "all": "Tout", @@ -280,6 +281,7 @@ "only_one_file_allowed": "Un seul fichier est autorisé", "only_owners_managers_and_manage_access_members_can_perform_this_action": "Seules les propriétaires, les gestionnaires et les membres ayant accès à la gestion peuvent effectuer cette action.", "option_id": "Identifiant de l'option", + "option_ids": "Identifiants des options", "or": "ou", "organization": "Organisation", "organization_id": "ID de l'organisation", @@ -1205,7 +1207,7 @@ "delete_survey_and_responses_warning": "Êtes-vous sûr de vouloir supprimer cette enquête et toutes ses réponses?", "edit": { "1_choose_the_default_language_for_this_survey": "1. Choisissez la langue par défaut pour ce sondage :", - "2_activate_translation_for_specific_languages": "2. Activer la traduction pour des langues spécifiques :", + "2_activate_translation_for_specific_languages": "2. Activer la traduction pour des langues spécifiques:", "add": "Ajouter +", "add_a_delay_or_auto_close_the_survey": "Ajouter un délai ou fermer automatiquement l'enquête", "add_a_four_digit_pin": "Ajoutez un code PIN à quatre chiffres.", @@ -1314,7 +1316,7 @@ "columns": "Colonnes", "company": "Société", "company_logo": "Logo de l'entreprise", - "completed_responses": "réponses complètes.", + "completed_responses": "des réponses partielles ou complètes.", "concat": "Concat +", "conditional_logic": "Logique conditionnelle", "confirm_default_language": "Confirmer la langue par défaut", diff --git a/apps/web/locales/pt-BR.json b/apps/web/locales/pt-BR.json index 14746d5d69..143d10c591 100644 --- a/apps/web/locales/pt-BR.json +++ b/apps/web/locales/pt-BR.json @@ -124,6 +124,7 @@ "add_action": "Adicionar ação", "add_filter": "Adicionar filtro", "add_logo": "Adicionar logo", + "add_member": "Adicionar membro", "add_project": "Adicionar projeto", "add_to_team": "Adicionar à equipe", "all": "Todos", @@ -280,6 +281,7 @@ "only_one_file_allowed": "É permitido apenas um arquivo", "only_owners_managers_and_manage_access_members_can_perform_this_action": "Apenas proprietários, gerentes e membros com acesso de gerenciamento podem realizar essa ação.", "option_id": "ID da opção", + "option_ids": "IDs da Opção", "or": "ou", "organization": "organização", "organization_id": "ID da Organização", @@ -1314,7 +1316,7 @@ "columns": "colunas", "company": "empresa", "company_logo": "Logo da empresa", - "completed_responses": "respostas completas", + "completed_responses": "respostas parciais ou completas.", "concat": "Concatenar +", "conditional_logic": "Lógica Condicional", "confirm_default_language": "Confirmar idioma padrão", diff --git a/apps/web/locales/pt-PT.json b/apps/web/locales/pt-PT.json index 5801bdebbf..c5efdb1d70 100644 --- a/apps/web/locales/pt-PT.json +++ b/apps/web/locales/pt-PT.json @@ -124,6 +124,7 @@ "add_action": "Adicionar ação", "add_filter": "Adicionar filtro", "add_logo": "Adicionar logótipo", + "add_member": "Adicionar membro", "add_project": "Adicionar projeto", "add_to_team": "Adicionar à equipa", "all": "Todos", @@ -280,6 +281,7 @@ "only_one_file_allowed": "Apenas um ficheiro é permitido", "only_owners_managers_and_manage_access_members_can_perform_this_action": "Apenas proprietários e gestores podem realizar esta ação.", "option_id": "ID de Opção", + "option_ids": "IDs de Opção", "or": "ou", "organization": "Organização", "organization_id": "ID da Organização", @@ -1314,7 +1316,7 @@ "columns": "Colunas", "company": "Empresa", "company_logo": "Logotipo da empresa", - "completed_responses": "respostas concluídas", + "completed_responses": "respostas parciais ou completas", "concat": "Concatenar +", "conditional_logic": "Lógica Condicional", "confirm_default_language": "Confirmar idioma padrão", diff --git a/apps/web/locales/zh-Hant-TW.json b/apps/web/locales/zh-Hant-TW.json index b083345cf7..862ccd5da6 100644 --- a/apps/web/locales/zh-Hant-TW.json +++ b/apps/web/locales/zh-Hant-TW.json @@ -124,6 +124,7 @@ "add_action": "新增操作", "add_filter": "新增篩選器", "add_logo": "新增標誌", + "add_member": "新增成員", "add_project": "新增專案", "add_to_team": "新增至團隊", "all": "全部", @@ -280,6 +281,7 @@ "only_one_file_allowed": "僅允許一個檔案", "only_owners_managers_and_manage_access_members_can_perform_this_action": "只有擁有者、管理員和管理存取權限的成員才能執行此操作。", "option_id": "選項 ID", + "option_ids": "選項 IDs", "or": "或", "organization": "組織", "organization_id": "組織 ID", @@ -1314,7 +1316,7 @@ "columns": "欄位", "company": "公司", "company_logo": "公司標誌", - "completed_responses": "完成的回應。", + "completed_responses": "部分或完整答复。", "concat": "串連 +", "conditional_logic": "條件邏輯", "confirm_default_language": "確認預設語言", diff --git a/packages/ai/.eslintrc.cjs b/packages/ai/.eslintrc.cjs new file mode 100644 index 0000000000..0cdaf7acce --- /dev/null +++ b/packages/ai/.eslintrc.cjs @@ -0,0 +1,10 @@ +module.exports = { + extends: ["@formbricks/eslint-config/library.js"], + parserOptions: { + project: "tsconfig.json", + tsconfigRootDir: __dirname, + }, + rules: { + "no-console": "off", + }, +}; diff --git a/packages/ai/README.md b/packages/ai/README.md new file mode 100644 index 0000000000..1a0b64ec34 --- /dev/null +++ b/packages/ai/README.md @@ -0,0 +1,198 @@ +# @formbricks/ai + +A model-agnostic AI package for Formbricks, providing a unified interface for LLM operations across different providers. + +## Features + +- **Multi-Provider Support**: OpenAI and Anthropic models with easy switching +- **Type-Safe**: Full TypeScript support with schema validation +- **Environment-Based Configuration**: Automatic provider selection via environment variables +- **Structured Output**: Generate validated JSON objects from prompts using schemas +- **Helper Functions**: Built-in summarization and translation utilities + +## Installation + +This package is part of the Formbricks monorepo and is intended for internal use. + +```bash +pnpm install @formbricks/ai +``` + +## Quick Start + +### Environment Configuration + +Set up your environment variables: + +```bash +# Provider selection (defaults to "openai") +AI_PROVIDER=openai # or "anthropic" + +# Model selection (uses sensible defaults if not specified) +AI_MODEL=gpt-4 # or "claude-3-sonnet-20240229" + +# API Keys +OPENAI_API_KEY=your_openai_key +ANTHROPIC_API_KEY=your_anthropic_key + +# Optional: Custom base URL +AI_BASE_URL=https://your-custom-endpoint.com +``` + +### Basic Usage + +#### Text Generation + +```typescript +import { generateText } from "@formbricks/ai"; + +const result = await generateText({ + prompt: "Explain quantum computing in simple terms", + system: "You are a helpful science teacher", + temperature: 0.7, + maxTokens: 200, +}); + +console.log(result.text); +``` + +#### Structured Object Generation + +```typescript +import { z } from "zod"; +import { generateObject } from "@formbricks/ai"; + +const analysisSchema = z.object({ + sentiment: z.enum(["positive", "negative", "neutral"]), + summary: z.string(), + keyTopics: z.array(z.string()), + confidence: z.number().min(0).max(1), +}); + +const result = await generateObject({ + prompt: "Analyze this customer feedback: 'The product is amazing but delivery was slow'", + schema: analysisSchema, + temperature: 0.3, +}); + +console.log(result.object.sentiment); // Type-safe access +console.log(result.object.keyTopics); +``` + +#### Helper Functions + +```typescript +import { summarizeText, translateText } from "@formbricks/ai"; + +// Summarization +const summary = await summarizeText(longText, 150); + +// Translation +const translated = await translateText("Hello, how are you?", "Spanish", "English"); +``` + +## Configuration + +### Programmatic Configuration + +You can override environment configuration programmatically: + +```typescript +import { createAIModel, generateText } from "@formbricks/ai"; + +const customConfig = { + provider: "anthropic" as const, + model: "claude-3-haiku-20240307", + apiKey: "your-api-key", +}; + +// Use custom config for specific calls +const result = await generateText( + { + prompt: "Hello world", + }, + customConfig +); + +// Or create a reusable model instance +const aiModel = createAIModel(customConfig); +``` + +### Supported Models + +#### OpenAI + +- `gpt-4` (default) +- `gpt-4-turbo` +- `gpt-3.5-turbo` + +#### Anthropic + +- `claude-3-sonnet-20240229` (default) +- `claude-3-haiku-20240307` +- `claude-3-opus-20240229` + +## Error Handling + +The package provides clear error messages for common issues: + +```typescript +import { generateText, isAIConfigured } from "@formbricks/ai"; + +// Check if AI is properly configured +if (!isAIConfigured()) { + throw new Error("AI is not properly configured. Please check your environment variables."); +} + +try { + const result = await generateText({ + prompt: "Your prompt here", + }); +} catch (error) { + console.error("AI generation failed:", error.message); +} +``` + +## Usage in Formbricks + +This package is designed to be used across the Formbricks ecosystem: + +- **NextJS API Routes**: For server-side AI operations +- **Background Jobs**: For processing surveys and responses +- **Future NestJS Backend**: Modular design allows easy integration + +## Development + +### Building + +```bash +pnpm build +``` + +### Development Mode + +```bash +pnpm dev +``` + +### Code Quality + +```bash +pnpm lint +``` + +## Architecture + +The package follows a layered architecture: + +1. **Types Layer** (`types.ts`): TypeScript definitions and interfaces +2. **Configuration Layer** (`config.ts`): Provider setup and validation +3. **Abstraction Layer** (`ai.ts`): Main API functions +4. **Export Layer** (`index.ts`): Public API exports + +This design ensures: + +- Easy testing and mocking +- Provider-agnostic implementation +- Type safety throughout +- Consistent error handling diff --git a/packages/ai/package.json b/packages/ai/package.json new file mode 100644 index 0000000000..9246809d86 --- /dev/null +++ b/packages/ai/package.json @@ -0,0 +1,39 @@ +{ + "name": "@formbricks/ai", + "packageManager": "pnpm@9.15.9", + "private": true, + "version": "0.1.0", + "main": "./dist/index.cjs", + "types": "./dist/index.d.ts", + "type": "module", + "files": [ + "dist" + ], + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js", + "require": "./dist/index.cjs" + } + }, + "scripts": { + "clean": "rimraf .turbo node_modules dist", + "build": "vite build", + "dev": "vite build --watch", + "lint": "eslint ./src --fix", + "type-check": "tsc --noEmit" + }, + "dependencies": { + "@ai-sdk/anthropic": "^1.0.6", + "@ai-sdk/openai": "^1.0.20", + "ai": "^5.0.2", + "zod": "^3.25.76" + }, + "devDependencies": { + "@formbricks/config-typescript": "workspace:*", + "@formbricks/eslint-config": "workspace:*", + "typescript": "5.8.3", + "vite": "6.3.5", + "vite-plugin-dts": "4.5.3" + } +} diff --git a/packages/ai/src/ai.ts b/packages/ai/src/ai.ts new file mode 100644 index 0000000000..fa8cfed1a4 --- /dev/null +++ b/packages/ai/src/ai.ts @@ -0,0 +1,196 @@ +import { generateObject as aiGenerateObject, generateText as aiGenerateText } from "ai"; +import type { z } from "zod"; +import { createAIModel } from "./config"; +import type { + GenerateObjectOptions, + GenerateObjectResult, + GenerateTextOptions, + GenerateTextResult, + ProviderConfig, +} from "./types"; + +/** + * Singleton AI model instance for reuse across calls + */ +let aiModelInstance: ReturnType | null = null; + +/** + * Get or create the AI model instance + */ +function getAIModel(customConfig?: ProviderConfig) { + if (!aiModelInstance || customConfig) { + aiModelInstance = createAIModel(customConfig); + } + return aiModelInstance; +} + +/** + * Generate text using the configured AI model + * + * @param options - Text generation options + * @returns Promise resolving to generated text and usage information + * + * @example + * ```typescript + * const result = await generateText({ + * prompt: "Summarize the following text: Lorem ipsum...", + * system: "You are a helpful assistant that provides concise summaries.", + * temperature: 0.7, + * maxTokens: 150 + * }); + * + * console.log(result.text); + * ``` + */ +export async function generateText( + options: GenerateTextOptions, + customConfig?: ProviderConfig +): Promise { + const { model } = getAIModel(customConfig); + + try { + const result = await aiGenerateText({ + model, + prompt: options.prompt, + system: options.system, + temperature: options.temperature, + ...(options.maxTokens && { maxTokens: options.maxTokens }), + }); + + return { + text: result.text, + usage: result.usage + ? { + inputTokens: result.usage.inputTokens, + outputTokens: result.usage.outputTokens, + totalTokens: result.usage.totalTokens, + } + : undefined, + }; + } catch (error) { + throw new Error(`Failed to generate text: ${error instanceof Error ? error.message : String(error)}`); + } +} + +/** + * Generate a structured object using the configured AI model + * + * @param options - Object generation options including Zod schema + * @returns Promise resolving to generated object and usage information + * + * @example + * ```typescript + * import { z } from "zod"; + * + * const summarySchema = z.object({ + * title: z.string(), + * summary: z.string(), + * keyPoints: z.array(z.string()), + * sentiment: z.enum(['positive', 'negative', 'neutral']) + * }); + * + * const result = await generateObject({ + * prompt: "Analyze the following article: Lorem ipsum...", + * schema: summarySchema, + * system: "You are an expert content analyzer.", + * temperature: 0.3 + * }); + * + * console.log(result.object.title); + * console.log(result.object.keyPoints); + * ``` + */ +export async function generateObject( + options: GenerateObjectOptions, + customConfig?: ProviderConfig +): Promise>> { + const { model } = getAIModel(customConfig); + + try { + const result = await aiGenerateObject({ + model, + prompt: options.prompt, + schema: options.schema, + system: options.system, + temperature: options.temperature, + ...(options.maxTokens && { maxTokens: options.maxTokens }), + }); + + return { + object: result.object, + usage: result.usage + ? { + inputTokens: result.usage.inputTokens, + outputTokens: result.usage.outputTokens, + totalTokens: result.usage.totalTokens, + } + : undefined, + }; + } catch (error) { + throw new Error(`Failed to generate object: ${error instanceof Error ? error.message : String(error)}`); + } +} + +/** + * Helper function for text summarization + * + * @param text - Text to summarize + * @param maxLength - Optional maximum length for the summary + * @returns Promise resolving to the summary + */ +export async function summarizeText( + text: string, + maxLength?: number, + customConfig?: ProviderConfig +): Promise { + const prompt = `Summarize the following text${maxLength ? ` in approximately ${maxLength} characters` : ""}:\n\n${text}`; + + const result = await generateText( + { + prompt, + system: + "You are a helpful assistant that creates clear, concise summaries. Focus on the key points and main ideas.", + temperature: 0.3, + maxTokens: maxLength ? Math.ceil(maxLength / 3) : undefined, // Rough token estimate + }, + customConfig + ); + + return result.text; +} + +/** + * Helper function for text translation + * + * @param text - Text to translate + * @param targetLanguage - Target language for translation + * @param sourceLanguage - Optional source language (auto-detected if not provided) + * @returns Promise resolving to the translated text + */ +export async function translateText( + text: string, + targetLanguage: string, + sourceLanguage?: string, + customConfig?: ProviderConfig +): Promise { + const sourceText = sourceLanguage ? `from ${sourceLanguage} ` : ""; + const prompt = `Translate the following text ${sourceText}to ${targetLanguage}:\n\n${text}`; + + const result = await generateText( + { + prompt, + system: `You are a professional translator. Provide only the translated text without any additional commentary or explanations. Maintain the original tone and style.`, + temperature: 0.1, // Low temperature for consistency + }, + customConfig + ); + + return result.text; +} + +/** + * Reset the AI model instance (useful for testing or when configuration changes) + */ +export function resetAIModel(): void { + aiModelInstance = null; +} diff --git a/packages/ai/src/config.ts b/packages/ai/src/config.ts new file mode 100644 index 0000000000..6a326a2c91 --- /dev/null +++ b/packages/ai/src/config.ts @@ -0,0 +1,168 @@ +import { anthropic } from "@ai-sdk/anthropic"; +import { openai } from "@ai-sdk/openai"; +import type { LanguageModelV1 } from "ai"; +import type { + AIEnvironmentConfig, + AIModelInstance, + AIProvider, + AnthropicConfig, + OpenAIConfig, + ProviderConfig, +} from "./types"; + +/** + * Default models for each provider + */ +const DEFAULT_MODELS: Record = { + openai: "gpt-4", + anthropic: "claude-3-sonnet-20240229", +}; + +/** + * Get environment configuration from process.env + */ +function getEnvironmentConfig(): AIEnvironmentConfig { + return { + AI_PROVIDER: process.env.AI_PROVIDER, + AI_MODEL: process.env.AI_MODEL, + OPENAI_API_KEY: process.env.OPENAI_API_KEY, + ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY, + AI_BASE_URL: process.env.AI_BASE_URL, + }; +} + +/** + * Create provider configuration from environment variables + */ +function createProviderConfigFromEnv(): ProviderConfig { + const env = getEnvironmentConfig(); + + // Determine provider (default to openai if not specified) + const provider = (env.AI_PROVIDER as AIProvider) || "openai"; + + // Get model for the provider + const model = env.AI_MODEL || DEFAULT_MODELS[provider]; + + // Create configuration based on provider + switch (provider) { + case "openai": { + const config: OpenAIConfig = { + provider: "openai", + model, + apiKey: env.OPENAI_API_KEY, + baseURL: env.AI_BASE_URL, + }; + return config; + } + case "anthropic": { + const config: AnthropicConfig = { + provider: "anthropic", + model, + apiKey: env.ANTHROPIC_API_KEY, + baseURL: env.AI_BASE_URL, + }; + return config; + } + default: + throw new Error(`Unsupported AI provider: ${provider}`); + } +} + +/** + * Create a language model instance from provider configuration + */ +function createModelFromConfig(config: ProviderConfig): LanguageModelV1 { + switch (config.provider) { + case "openai": { + const options: any = {}; + + if (config.apiKey) { + options.apiKey = config.apiKey; + } + + if (config.baseURL) { + options.baseURL = config.baseURL; + } + + return openai(config.model, options); + } + case "anthropic": { + const options: any = {}; + + if (config.apiKey) { + options.apiKey = config.apiKey; + } + + if (config.baseURL) { + options.baseURL = config.baseURL; + } + + return anthropic(config.model, options); + } + default: + throw new Error(`Unsupported provider: ${(config as ProviderConfig).provider}`); + } +} + +/** + * Validate that required API keys are present for the configured provider + */ +function validateConfiguration(config: ProviderConfig): void { + switch (config.provider) { + case "openai": + if (!config.apiKey && !process.env.OPENAI_API_KEY) { + throw new Error( + "OpenAI API key is required. Set OPENAI_API_KEY environment variable or provide apiKey in configuration." + ); + } + break; + case "anthropic": + if (!config.apiKey && !process.env.ANTHROPIC_API_KEY) { + throw new Error( + "Anthropic API key is required. Set ANTHROPIC_API_KEY environment variable or provide apiKey in configuration." + ); + } + break; + default: + throw new Error(`Unsupported provider: ${(config as ProviderConfig).provider}`); + } +} + +/** + * Create and configure the AI model instance + */ +export function createAIModel(customConfig?: ProviderConfig): AIModelInstance { + // Use custom config or create from environment + const config = customConfig || createProviderConfigFromEnv(); + + // Validate the configuration + validateConfiguration(config); + + // Create the model instance + const model = createModelFromConfig(config); + + return { + model, + config, + }; +} + +/** + * Get the current provider configuration without creating a model + */ +export function getProviderConfig(): ProviderConfig { + return createProviderConfigFromEnv(); +} + +/** + * Check if AI is properly configured + */ +export function isAIConfigured(): boolean { + try { + const config = createProviderConfigFromEnv(); + validateConfiguration(config); + return true; + } catch { + return false; + } +} diff --git a/packages/ai/src/index.ts b/packages/ai/src/index.ts new file mode 100644 index 0000000000..f69412f93b --- /dev/null +++ b/packages/ai/src/index.ts @@ -0,0 +1,20 @@ +// Main AI functions +export { generateText, generateObject, summarizeText, translateText, resetAIModel } from "./ai"; + +// Configuration functions +export { createAIModel, getProviderConfig, isAIConfigured } from "./config"; + +// Types +export type { + AIProvider, + AIProviderConfig, + OpenAIConfig, + AnthropicConfig, + ProviderConfig, + AIEnvironmentConfig, + GenerateTextOptions, + GenerateObjectOptions, + GenerateTextResult, + GenerateObjectResult, + AIModelInstance, +} from "./types"; diff --git a/packages/ai/src/types.ts b/packages/ai/src/types.ts new file mode 100644 index 0000000000..d02aa8fefe --- /dev/null +++ b/packages/ai/src/types.ts @@ -0,0 +1,102 @@ +import type { LanguageModelV1 } from "ai"; +import type { z } from "zod"; + +/** + * Supported AI providers + */ +export type AIProvider = "openai" | "anthropic"; + +/** + * Configuration for different AI providers + */ +export interface AIProviderConfig { + provider: AIProvider; + model: string; + apiKey?: string; + baseURL?: string; +} + +/** + * OpenAI specific configuration + */ +export interface OpenAIConfig extends AIProviderConfig { + provider: "openai"; + model: string; // e.g., "gpt-4", "gpt-3.5-turbo" +} + +/** + * Anthropic specific configuration + */ +export interface AnthropicConfig extends AIProviderConfig { + provider: "anthropic"; + model: string; // e.g., "claude-3-sonnet-20240229", "claude-3-haiku-20240307" +} + +/** + * Union type for all provider configurations + */ +export type ProviderConfig = OpenAIConfig | AnthropicConfig; + +/** + * Environment variables for AI configuration + */ +export interface AIEnvironmentConfig { + AI_PROVIDER?: string; + AI_MODEL?: string; + OPENAI_API_KEY?: string; + ANTHROPIC_API_KEY?: string; + AI_BASE_URL?: string; +} + +/** + * Options for text generation + */ +export interface GenerateTextOptions { + prompt: string; + system?: string; + temperature?: number; + maxTokens?: number; +} + +/** + * Options for object generation + */ +export interface GenerateObjectOptions { + prompt: string; + schema: T; + system?: string; + temperature?: number; + maxTokens?: number; +} + +/** + * Result from text generation + */ +export interface GenerateTextResult { + text: string; + usage?: { + inputTokens?: number; + outputTokens?: number; + totalTokens?: number; + }; +} + +/** + * Result from object generation + */ +export interface GenerateObjectResult { + object: T; + usage?: { + inputTokens?: number; + outputTokens?: number; + totalTokens?: number; + }; +} + +/** + * Internal type for the language model instance + */ +export interface AIModelInstance { + model: LanguageModelV1; + config: ProviderConfig; +} diff --git a/packages/ai/tsconfig.json b/packages/ai/tsconfig.json new file mode 100644 index 0000000000..b4149356f4 --- /dev/null +++ b/packages/ai/tsconfig.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "baseUrl": ".", + "outDir": "dist", + "paths": { + "@/*": ["./src/*"] + } + }, + "exclude": ["node_modules", "dist", "**/*.test.ts", "**/*.spec.ts"], + "extends": "@formbricks/config-typescript/js-library.json", + "include": ["src/**/*"] +} diff --git a/packages/ai/vite.config.ts b/packages/ai/vite.config.ts new file mode 100644 index 0000000000..09e38daa46 --- /dev/null +++ b/packages/ai/vite.config.ts @@ -0,0 +1,49 @@ +import { resolve } from "path"; +import { UserConfig, defineConfig } from "vite"; +import dts from "vite-plugin-dts"; + +export default defineConfig((): UserConfig => { + return { + resolve: { + alias: { + "@": resolve(__dirname, "src"), + }, + }, + build: { + rollupOptions: { + input: { + index: resolve(__dirname, "src/index.ts"), + }, + output: [ + { + format: "esm", + entryFileNames: "[name].js", + chunkFileNames: "[name].js", + }, + { + format: "cjs", + entryFileNames: "[name].cjs", + chunkFileNames: "[name].cjs", + }, + ], + external: [ + // External dependencies that should not be bundled + "@ai-sdk/anthropic", + "@ai-sdk/openai", + "ai", + "zod", + ], + }, + emptyOutDir: true, + ssr: true, // Server-side rendering mode for Node.js + }, + plugins: [ + dts({ + rollupTypes: false, + include: ["src/**/*"], + exclude: ["src/**/*.test.ts", "src/**/*.spec.ts"], + insertTypesEntry: true, + }), + ], + }; +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d69c6bd4a2..b75e2111b3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -536,6 +536,37 @@ importers: specifier: 3.1.0 version: 3.1.0(typescript@5.8.3)(vitest@3.1.3(@types/node@22.15.18)(jiti@2.4.2)(jsdom@26.1.0)(terser@5.39.1)(tsx@4.19.4)(yaml@2.8.0)) + packages/ai: + dependencies: + '@ai-sdk/anthropic': + specifier: ^1.0.6 + version: 1.2.12(zod@3.24.4) + '@ai-sdk/openai': + specifier: ^1.0.20 + version: 1.3.23(zod@3.24.4) + ai: + specifier: ^5.0.2 + version: 5.0.2(zod@3.24.4) + zod: + specifier: 3.24.4 + version: 3.24.4 + devDependencies: + '@formbricks/config-typescript': + specifier: workspace:* + version: link:../config-typescript + '@formbricks/eslint-config': + specifier: workspace:* + version: link:../config-eslint + typescript: + specifier: 5.8.3 + version: 5.8.3 + vite: + specifier: 6.3.5 + version: 6.3.5(@types/node@22.15.18)(jiti@2.4.2)(terser@5.39.1)(tsx@4.19.4)(yaml@2.8.0) + vite-plugin-dts: + specifier: 4.5.3 + version: 4.5.3(@types/node@22.15.18)(rollup@4.46.1)(typescript@5.8.3)(vite@6.3.5(@types/node@22.15.18)(jiti@2.4.2)(terser@5.39.1)(tsx@4.19.4)(yaml@2.8.0)) + packages/config-eslint: devDependencies: '@next/eslint-plugin-next': @@ -824,6 +855,44 @@ packages: '@adobe/css-tools@4.4.3': resolution: {integrity: sha512-VQKMkwriZbaOgVCby1UDY/LDk5fIjhQicCvVPFqfe+69fWaPWydbWJ3wRt59/YzIwda1I81loas3oCoHxnqvdA==} + '@ai-sdk/anthropic@1.2.12': + resolution: {integrity: sha512-YSzjlko7JvuiyQFmI9RN1tNZdEiZxc+6xld/0tq/VkJaHpEzGAb1yiNxxvmYVcjvfu/PcvCxAAYXmTYQQ63IHQ==} + engines: {node: '>=18'} + peerDependencies: + zod: ^3.0.0 + + '@ai-sdk/gateway@1.0.0': + resolution: {integrity: sha512-VEm87DyRx1yIPywbTy8ntoyh4jEDv1rJ88m+2I7zOm08jJI5BhFtAWh0OF6YzZu1Vu4NxhOWO4ssGdsqydDQ3A==} + engines: {node: '>=18'} + peerDependencies: + zod: ^3.25.76 || ^4 + + '@ai-sdk/openai@1.3.23': + resolution: {integrity: sha512-86U7rFp8yacUAOE/Jz8WbGcwMCqWvjK33wk5DXkfnAOEn3mx2r7tNSJdjukQFZbAK97VMXGPPHxF+aEARDXRXQ==} + engines: {node: '>=18'} + peerDependencies: + zod: ^3.0.0 + + '@ai-sdk/provider-utils@2.2.8': + resolution: {integrity: sha512-fqhG+4sCVv8x7nFzYnFo19ryhAa3w096Kmc3hWxMQfW/TubPOmt3A6tYZhl4mUfQWWQMsuSkLrtjlWuXBVSGQA==} + engines: {node: '>=18'} + peerDependencies: + zod: ^3.23.8 + + '@ai-sdk/provider-utils@3.0.0': + resolution: {integrity: sha512-BoQZtGcBxkeSH1zK+SRYNDtJPIPpacTeiMZqnG4Rv6xXjEwM0FH4MGs9c+PlhyEWmQCzjRM2HAotEydFhD4dYw==} + engines: {node: '>=18'} + peerDependencies: + zod: ^3.25.76 || ^4 + + '@ai-sdk/provider@1.1.3': + resolution: {integrity: sha512-qZMxYJ0qqX/RfnuIaab+zp8UAeJn/ygXXAffR5I4N0n1IrvA6qBsjc8hXLmBiMV2zoXlifkacF7sEFnYnjBcqg==} + engines: {node: '>=18'} + + '@ai-sdk/provider@2.0.0': + resolution: {integrity: sha512-6o7Y2SeO9vFKB8lArHXehNuusnpddKPk7xqL7T2/b+OvXMRIXUO1rR4wcv1hAFUAT9avGZshty3Wlua/XA7TvA==} + engines: {node: '>=18'} + '@alloc/quick-lru@5.2.0': resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} engines: {node: '>=10'} @@ -3734,6 +3803,9 @@ packages: '@sqltools/formatter@1.2.5': resolution: {integrity: sha512-Uy0+khmZqUrUGm5dmMqVlnvufZRSK0FbYzVgp0UMstm+F5+W2/jnEEQyc9vo1ZR/E5ZI/B1WjjoTqBqwJL6Krw==} + '@standard-schema/spec@1.0.0': + resolution: {integrity: sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==} + '@standard-schema/utils@0.3.0': resolution: {integrity: sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==} @@ -4677,6 +4749,12 @@ packages: resolution: {integrity: sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==} engines: {node: '>=8'} + ai@5.0.2: + resolution: {integrity: sha512-Uk4lmwlr2b/4G9DUYCWYKcWz93xQ6p6AEeRZN+/AO9NbOyCm9axrDru26c83Ax8OB8IHUvoseA3CqaZkg9Z0Kg==} + engines: {node: '>=18'} + peerDependencies: + zod: ^3.25.76 || ^4 + ajv-draft-04@1.0.0: resolution: {integrity: sha512-mv00Te6nmYbRp5DCwclxtt7yV/joXJPGS7nM+97GdxvuttCOfgI3K4U25zboyeX0O+myI8ERluxQe5wljMmVIw==} peerDependencies: @@ -5887,6 +5965,10 @@ packages: resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} engines: {node: '>=0.8.x'} + eventsource-parser@3.0.3: + resolution: {integrity: sha512-nVpZkTMM9rF6AQ9gPJpFsNAMt48wIzB5TQgiTLdHiuO8XEDhUgZEhqKlZWXbIzo9VmJ/HvysHqEaVeD5v9TPvA==} + engines: {node: '>=20.0.0'} + expand-template@2.0.3: resolution: {integrity: sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==} engines: {node: '>=6'} @@ -6723,6 +6805,9 @@ packages: json-schema-traverse@1.0.0: resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + json-schema@0.4.0: + resolution: {integrity: sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==} + json-stable-stringify-without-jsonify@1.0.1: resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} @@ -9490,6 +9575,11 @@ packages: peerDependencies: zod: ^3.21.4 + zod-to-json-schema@3.24.6: + resolution: {integrity: sha512-h/z3PKvcTcTetyjl1fkj79MHNEjm+HpD6NXheWjzOekY7kV+lwDYnHw+ivHkijnCSMz1yJaWBD9vu/Fcmk+vEg==} + peerDependencies: + zod: ^3.24.1 + zod@3.24.4: resolution: {integrity: sha512-OdqJE9UDRPwWsrHjLN2F8bPxvwJBK22EHLWtanu0LSYr5YqzsaaW3RMgmjwr8Rypg5k+meEJdSPXJZXE/yqOMg==} @@ -9497,6 +9587,47 @@ snapshots: '@adobe/css-tools@4.4.3': {} + '@ai-sdk/anthropic@1.2.12(zod@3.24.4)': + dependencies: + '@ai-sdk/provider': 1.1.3 + '@ai-sdk/provider-utils': 2.2.8(zod@3.24.4) + zod: 3.24.4 + + '@ai-sdk/gateway@1.0.0(zod@3.24.4)': + dependencies: + '@ai-sdk/provider': 2.0.0 + '@ai-sdk/provider-utils': 3.0.0(zod@3.24.4) + zod: 3.24.4 + + '@ai-sdk/openai@1.3.23(zod@3.24.4)': + dependencies: + '@ai-sdk/provider': 1.1.3 + '@ai-sdk/provider-utils': 2.2.8(zod@3.24.4) + zod: 3.24.4 + + '@ai-sdk/provider-utils@2.2.8(zod@3.24.4)': + dependencies: + '@ai-sdk/provider': 1.1.3 + nanoid: 3.3.11 + secure-json-parse: 2.7.0 + zod: 3.24.4 + + '@ai-sdk/provider-utils@3.0.0(zod@3.24.4)': + dependencies: + '@ai-sdk/provider': 2.0.0 + '@standard-schema/spec': 1.0.0 + eventsource-parser: 3.0.3 + zod: 3.24.4 + zod-to-json-schema: 3.24.6(zod@3.24.4) + + '@ai-sdk/provider@1.1.3': + dependencies: + json-schema: 0.4.0 + + '@ai-sdk/provider@2.0.0': + dependencies: + json-schema: 0.4.0 + '@alloc/quick-lru@5.2.0': {} '@ampproject/remapping@2.3.0': @@ -13506,6 +13637,8 @@ snapshots: '@sqltools/formatter@1.2.5': {} + '@standard-schema/spec@1.0.0': {} + '@standard-schema/utils@0.3.0': {} '@storybook/addon-a11y@9.0.15(storybook@9.0.15(@testing-library/dom@8.20.1)(prettier@3.5.3))': @@ -14624,6 +14757,14 @@ snapshots: indent-string: 4.0.0 optional: true + ai@5.0.2(zod@3.24.4): + dependencies: + '@ai-sdk/gateway': 1.0.0(zod@3.24.4) + '@ai-sdk/provider': 2.0.0 + '@ai-sdk/provider-utils': 3.0.0(zod@3.24.4) + '@opentelemetry/api': 1.9.0 + zod: 3.24.4 + ajv-draft-04@1.0.0(ajv@8.13.0): optionalDependencies: ajv: 8.13.0 @@ -16043,6 +16184,8 @@ snapshots: events@3.3.0: {} + eventsource-parser@3.0.3: {} + expand-template@2.0.3: {} expect-type@1.2.2: {} @@ -16926,6 +17069,8 @@ snapshots: json-schema-traverse@1.0.0: {} + json-schema@0.4.0: {} + json-stable-stringify-without-jsonify@1.0.1: {} json5@1.0.2: @@ -19879,4 +20024,8 @@ snapshots: dependencies: zod: 3.24.4 + zod-to-json-schema@3.24.6(zod@3.24.4): + dependencies: + zod: 3.24.4 + zod@3.24.4: {}