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
}