ManimCat / src /workflow /problem-framing /problem-framing.ts
Bin29's picture
Sync from main: 5548f84 fix(workflow): preserve reference images across generation and recovery [skip ci]
b1df986
Raw History Blame Contribute Delete
5.88 kB
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<ProblemFramingPlan> {
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
}