import { createCustomOpenAIClient } from './openai-client-factory' import { createChatCompletionText } from './openai-stream' import { buildTokenParams } from '../utils/reasoning-model' import { createLogger } from '../utils/logger' import { getRoleSystemPrompt, getRoleUserPrompt } from '../prompts' import { buildVisionUserMessage, shouldRetryWithoutImages } from './concept-designer-utils' import type { CustomApiConfig, PromptLocale, PromptOverrides, ReferenceImage } from '../types' const logger = createLogger('ProblemFraming') const PLANNER_TEMPERATURE = parseFloat(process.env.PROBLEM_FRAMING_TEMPERATURE || '0.7') const PLANNER_MAX_TOKENS = parseInt(process.env.PROBLEM_FRAMING_MAX_TOKENS || '2400', 10) const PLANNER_THINKING_TOKENS = parseInt(process.env.PROBLEM_FRAMING_THINKING_TOKENS || '4000', 10) export interface ProblemFramingStep { title: string content: string } export interface ProblemFramingPlan { mode: 'clarify' | 'invent' headline: string summary: string steps: ProblemFramingStep[] visualMotif: string designerHint: string } interface ProblemFramingParams { concept: string feedback?: string feedbackHistory?: string[] currentPlan?: ProblemFramingPlan referenceImages?: ReferenceImage[] customApiConfig: CustomApiConfig locale?: PromptLocale promptOverrides?: PromptOverrides } function stripCodeFence(text: string): string { return text .replace(/^```json\s*/i, '') .replace(/^```\s*/i, '') .replace(/\s*```$/, '') .trim() } function extractJsonObject(text: string): string { const cleaned = stripCodeFence(text) if (/^\s* { const item = step && typeof step === 'object' ? step as { title?: unknown; content?: unknown } : {} return { title: sanitizeString(item.title, `${fallbackStepTitle} ${index + 1}`), content: sanitizeString(item.content, '') } }) .filter((step) => step.content) while (normalizedSteps.length < 3) { normalizedSteps.push({ title: `${fallbackStepTitle} ${normalizedSteps.length + 1}`, content: fallbackStepContent }) } return { mode: input.mode === 'clarify' ? 'clarify' : 'invent', headline: sanitizeString(input.headline, fallbackHeadline), summary: sanitizeString(input.summary, fallbackSummary), steps: normalizedSteps, visualMotif: sanitizeString(input.visualMotif ?? input.visual_motif, fallbackMotif), designerHint: sanitizeString(input.designerHint ?? input.designer_hint, fallbackHint) } } export async function generateProblemFramingPlan(params: ProblemFramingParams): Promise { const locale = params.locale === 'en-US' ? 'en-US' : 'zh-CN' const client = createCustomOpenAIClient(params.customApiConfig) const model = (params.customApiConfig.model || '').trim() if (!model) { throw new Error('No model available') } logger.info('Problem framing started', { locale, conceptLength: params.concept.length, hasFeedback: !!params.feedback, hasCurrentPlan: !!params.currentPlan, hasImages: !!params.referenceImages?.length }) const promptOverrides: PromptOverrides = { ...params.promptOverrides, locale } const systemPrompt = getRoleSystemPrompt('problemFraming', promptOverrides) const userPrompt = getRoleUserPrompt( 'problemFraming', { concept: params.concept, instructions: params.feedback, feedbackHistory: params.feedbackHistory?.length ? params.feedbackHistory.map((item, index) => `${index + 1}. ${item}`).join('\n') : undefined, sceneDesign: params.currentPlan ? JSON.stringify(params.currentPlan, null, 2) : undefined }, promptOverrides ) let response: Awaited> try { response = await createChatCompletionText( client, { model, messages: [ { role: 'system', content: systemPrompt }, { role: 'user', content: buildVisionUserMessage(userPrompt, params.referenceImages) } ], temperature: PLANNER_TEMPERATURE, ...buildTokenParams(PLANNER_THINKING_TOKENS, PLANNER_MAX_TOKENS) }, { fallbackToNonStream: true, usageLabel: 'problem-framing' } ) } catch (error) { if (params.referenceImages && params.referenceImages.length > 0 && shouldRetryWithoutImages(error)) { logger.warn('Problem framing model does not support reference images, retrying with text only', { concept: params.concept, error: error instanceof Error ? error.message : String(error) }) response = await createChatCompletionText( client, { model, messages: [ { role: 'system', content: systemPrompt }, { role: 'user', content: userPrompt } ], temperature: PLANNER_TEMPERATURE, ...buildTokenParams(PLANNER_THINKING_TOKENS, PLANNER_MAX_TOKENS) }, { fallbackToNonStream: true, usageLabel: 'problem-framing-text-fallback' } ) } else { throw error } } const parsed = JSON.parse(extractJsonObject(response.content)) const plan = normalizePlan(parsed, locale) logger.info('Problem framing completed', { mode: plan.mode, headline: plan.headline, stepCount: plan.steps.length }) return plan }