File size: 5,876 Bytes
b1df986 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 | 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
}
|