import { createCustomOpenAIClient } from '../../infrastructure/ai-provider/provider-client-factory' import { createChatCompletionText } from '../openai-stream' import { buildCompletionParams, readMaxCompletionTokens } from '../../infrastructure/ai-provider/completion-policy' import { createLogger } from '../../shared/logging/logger' import { getRoleSystemPrompt, getRoleUserPrompt } from '../../prompts' import { buildVisionUserMessage } from '../generation/multimodal-input' import type { ConceptInput, CustomApiConfig, PromptLocale, PromptOverrides } from '../../types' import { ServiceUnavailableError } from '../../shared/errors/errors' import { ProblemFramingFormatError, type ProblemFramingPlan } from './parser' import { parseProblemFramingWithRepair } from './format-repair' const logger = createLogger('ProblemFraming') const PLANNER_TEMPERATURE = parseFloat(process.env.PROBLEM_FRAMING_TEMPERATURE || '0.7') const PLANNER_MAX_TOKENS = readMaxCompletionTokens('PROBLEM_FRAMING') const PLANNER_FORMAT_RETRIES = parseInt(process.env.PROBLEM_FRAMING_FORMAT_RETRIES || '2', 10) const FORMAT_REPAIR_INPUT_LIMIT = 16_000 export type { ProblemFramingPlan } from './parser' interface ProblemFramingParams extends ConceptInput { feedback?: string feedbackHistory?: string[] currentPlan?: ProblemFramingPlan customApiConfig: CustomApiConfig locale?: PromptLocale promptOverrides?: PromptOverrides } 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 ) const response = await createChatCompletionText( client, { model, messages: [ { role: 'system', content: systemPrompt }, { role: 'user', content: buildVisionUserMessage(userPrompt, params.referenceImages) } ], temperature: PLANNER_TEMPERATURE, ...buildCompletionParams(PLANNER_MAX_TOKENS, 'PROBLEM_FRAMING', client) }, { fallbackToNonStream: true, usageLabel: 'problem-framing' } ) let plan: ProblemFramingPlan try { plan = await parseProblemFramingWithRepair({ content: response.content, locale, maxRepairAttempts: Math.max(0, PLANNER_FORMAT_RETRIES), requireDesignFields: !promptOverrides.roles?.problemFraming?.system, repair: async ({ content, error, attempt }) => { logger.warn('Problem framing format invalid, requesting format repair', { attempt, error, contentLength: content.length, }) const repairResponse = await createChatCompletionText( client, { model, messages: [ { role: 'system', content: locale === 'en-US' ? 'Repair formatting only; preserve every decision. Output one complete plan with mode/headline/summary/question/answer/audience/background_rules/selected_route/alternative_routes/out_of_scope/special_cases/visual_motif/designer_hint, and 3–12 repeated step blocks with phase/title/content. phase is background, phenomenon, derivation, or closing. Never truncate later steps or erase fields. No other text.' : '只修复格式,保留全部决定。输出完整plan,包含mode/headline/summary/question/answer/audience/background_rules/selected_route/alternative_routes/out_of_scope/special_cases/visual_motif/designer_hint,以及3–12个含phase/title/content的step;phase为background、phenomenon、derivation或closing。不得截掉后续步骤或删字段,标签外零文字。', }, { role: 'user', content: buildVisionUserMessage(userPrompt, params.referenceImages) }, { role: 'user', content: `Parser error: ${error}\n\nModel output:\n${content.slice(0, FORMAT_REPAIR_INPUT_LIMIT)}`, }, ], temperature: 0, ...buildCompletionParams(PLANNER_MAX_TOKENS, 'PROBLEM_FRAMING', client), }, { fallbackToNonStream: true, usageLabel: `problem-framing-format-repair-${attempt}`, }, ) return repairResponse.content }, }) } catch (error) { if (!(error instanceof ProblemFramingFormatError)) { throw error } logger.error('Problem framing format repair exhausted', { error: error instanceof Error ? error.message : String(error), }) throw new ServiceUnavailableError( locale === 'en-US' ? 'The planning card format could not be recovered. Please retry.' : '规划卡格式修复失败,请重试。', { code: 'PLAN_FORMAT_INVALID', retryable: true }, ) } logger.info('Problem framing completed', { mode: plan.mode, headline: plan.headline, stepCount: plan.steps.length }) return plan }