Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/main/backend/__tests__/deepseekClient.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ function mockSettings(provider = 'deepseek', model?: string, apiKey = 'test-key'
provider,
model: model ?? (provider === 'kimi' ? 'kimi-k2.6' : 'deepseek-v4-flash'),
language: 'zh-CN',
selectedCodeLanguage: 'Python',
})
}

Expand Down
19 changes: 19 additions & 0 deletions src/main/backend/__tests__/paperAnalyzerFlow.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ describe('analyzePaper flow', () => {
provider: 'deepseek',
model: 'deepseek-v4-flash',
language: 'zh-CN',
selectedCodeLanguage: 'Python',
})
mockedParsePDF.mockResolvedValue({ text: 'paper text', pageCount: 3 })
})
Expand All @@ -95,6 +96,24 @@ describe('analyzePaper flow', () => {
expect(progress).toEqual(expect.arrayContaining(['parsing', 'summarizing', 'generating_code', 'done']))
})

it('passes the selected output code language into the combined analysis prompt', async () => {
mockedGetActiveSettings.mockReturnValueOnce({
apiKey: 'key',
provider: 'deepseek',
model: 'deepseek-v4-flash',
language: 'en-US',
selectedCodeLanguage: 'MATLAB',
})
mockLlmOutput(outputWithoutCode('MATLAB summary'))

await analyzePaper('paper.pdf', () => {})

const messages = mockedCallDeepSeek.mock.calls[0][0]
expect(messages[0].content).toContain('Demo Code and core code files MUST be generated in MATLAB')
expect(messages[1].content).toContain('The selected output code language is MATLAB')
expect(messages[1].content).toContain('core_code/descriptive_file_name.m')
})

it('caches generated core code and returns hasCoreCode for valid code output', async () => {
mockLlmOutput(outputWithCode())

Expand Down
26 changes: 23 additions & 3 deletions src/main/backend/__tests__/promptBuilder.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,22 @@ import { describe, expect, it } from 'vitest'
import { buildCodePrompt, buildCombinedAnalysisPrompt, buildSummaryPrompt } from '../promptBuilder'

describe('promptBuilder', () => {
it.each([
['Python', 'py'],
['C', 'c'],
['C++', 'cpp'],
['Java', 'java'],
['Go', 'go'],
['Rust', 'rs'],
['MATLAB', 'm'],
['R', 'R'],
])('uses the expected source extension for %s code output', (codeLanguage, extension) => {
const prompt = buildCombinedAnalysisPrompt('paper body', 'en-US', codeLanguage)

expect(prompt.system).toContain(`generated in ${codeLanguage}`)
expect(prompt.user).toContain(`core_code/descriptive_file_name.${extension}`)
})

it('builds Chinese summary prompts by default with README and math rules', () => {
const prompt = buildSummaryPrompt('paper body')

Expand All @@ -24,14 +40,16 @@ describe('promptBuilder', () => {
})

it('builds code prompts with strict JSON schema and summary context', () => {
const prompt = buildCodePrompt('paper text', 'summary text', 'en-US')
const prompt = buildCodePrompt('paper text', 'summary text', 'en-US', 'Rust')

expect(prompt.system).toContain('Use English')
expect(prompt.user).toContain('paper text')
expect(prompt.user).toContain('summary text')
expect(prompt.user).toContain('Return ONLY strict JSON')
expect(prompt.user).toContain('"files"')
expect(prompt.user).toContain('"notApplicableReason"')
expect(prompt.user).toContain('Generate all demo/core code in Rust')
expect(prompt.user).toContain('core_code/descriptive_file_name.rs')
expect(prompt.user).toContain('Every file path MUST be relative')
})

Expand All @@ -47,13 +65,15 @@ describe('promptBuilder', () => {
})

it('builds combined prompts with blueprint and exact code bundle constraints', () => {
const prompt = buildCombinedAnalysisPrompt('combined paper', 'en-US')
const prompt = buildCombinedAnalysisPrompt('combined paper', 'en-US', 'C++')

expect(prompt.system).toContain('Use English')
expect(prompt.system).toContain('Demo Code and core code files MUST be generated in C++')
expect(prompt.user).toContain('# Paper Title')
expect(prompt.user).toContain('<P2CC_CODE_BLUEPRINT>')
expect(prompt.user).toContain('<P2CC_CODE_BUNDLE>')
expect(prompt.user).toContain('<P2CC_FILE path="core_code/descriptive_file_name.py">')
expect(prompt.user).toContain('The selected output code language is C++')
expect(prompt.user).toContain('<P2CC_FILE path="core_code/descriptive_file_name.cpp">')
expect(prompt.user).toContain('no more and no fewer')
expect(prompt.user).toContain('Avoid generic files')
})
Expand Down
56 changes: 55 additions & 1 deletion src/main/backend/__tests__/settingsStore.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ describe('settingsStore', () => {
provider: 'deepseek',
model: PROVIDER_SETTINGS.deepseek.defaultModel,
language: 'zh-CN',
selectedCodeLanguage: 'Python',
})
})

Expand All @@ -54,6 +55,7 @@ describe('settingsStore', () => {
provider: 'deepseek',
model: PROVIDER_SETTINGS.deepseek.defaultModel,
language: 'zh-CN',
selectedCodeLanguage: 'Python',
})
})

Expand All @@ -76,6 +78,25 @@ describe('settingsStore', () => {
provider: 'kimi',
model: 'kimi-k2.5',
language: 'en-US',
selectedCodeLanguage: 'Python',
})
})

it('normalizes and preserves the selected output code language', () => {
writeConfig({
provider: 'deepseek',
selectedCodeLanguage: 'Rust',
providers: {
deepseek: { apiKey: 'deep-key', model: 'deepseek-v4-pro' },
},
})

expect(getActiveSettings()).toEqual({
apiKey: 'deep-key',
provider: 'deepseek',
model: 'deepseek-v4-pro',
language: 'zh-CN',
selectedCodeLanguage: 'Rust',
})
})

Expand All @@ -93,6 +114,21 @@ describe('settingsStore', () => {
provider: 'deepseek',
model: PROVIDER_SETTINGS.deepseek.defaultModel,
language: 'zh-CN',
selectedCodeLanguage: 'Python',
})
})

it('falls back invalid stored output code languages to Python', () => {
writeConfig({
provider: 'deepseek',
selectedCodeLanguage: 'TypeScript',
providers: {
deepseek: { apiKey: 'deep-key', model: 'deepseek-v4-flash' },
},
})

expect(getActiveSettings()).toMatchObject({
selectedCodeLanguage: 'Python',
})
})

Expand All @@ -111,6 +147,7 @@ describe('settingsStore', () => {
provider: 'kimi',
model: PROVIDER_SETTINGS.kimi.defaultModel,
language: 'en-US',
selectedCodeLanguage: 'Python',
})
})

Expand All @@ -127,36 +164,53 @@ describe('settingsStore', () => {
provider: 'glm',
model: 'glm-5-turbo',
language: 'en-US',
selectedCodeLanguage: 'Python',
})
})

it('saves active provider settings and preserves provider-specific keys', () => {
expect(saveSettingsPatch({ apiKey: 'deep-key', language: 'en-US' })).toEqual({
expect(saveSettingsPatch({ apiKey: 'deep-key', language: 'en-US', selectedCodeLanguage: 'Go' })).toEqual({
apiKey: 'deep-key',
provider: 'deepseek',
model: PROVIDER_SETTINGS.deepseek.defaultModel,
language: 'en-US',
selectedCodeLanguage: 'Go',
})

expect(saveSettingsPatch({ provider: 'kimi' })).toEqual({
apiKey: '',
provider: 'kimi',
model: PROVIDER_SETTINGS.kimi.defaultModel,
language: 'en-US',
selectedCodeLanguage: 'Go',
})

expect(saveSettingsPatch({ apiKey: 'kimi-key', model: 'kimi-k2.5' })).toEqual({
apiKey: 'kimi-key',
provider: 'kimi',
model: 'kimi-k2.5',
language: 'en-US',
selectedCodeLanguage: 'Go',
})

expect(saveSettingsPatch({ provider: 'deepseek' })).toEqual({
apiKey: 'deep-key',
provider: 'deepseek',
model: PROVIDER_SETTINGS.deepseek.defaultModel,
language: 'en-US',
selectedCodeLanguage: 'Go',
})
})

it('saves selected output code language patches and normalizes unsupported values', () => {
expect(saveSettingsPatch({ selectedCodeLanguage: 'MATLAB' })).toMatchObject({
selectedCodeLanguage: 'MATLAB',
})

expect(readStoredConfig().selectedCodeLanguage).toBe('MATLAB')

expect(saveSettingsPatch({ selectedCodeLanguage: 'CUDA' })).toMatchObject({
selectedCodeLanguage: 'Python',
})
})

Expand Down
7 changes: 5 additions & 2 deletions src/main/backend/paperAnalyzer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,8 +94,11 @@ export async function analyzePaper(
signal?: AbortSignal
): Promise<AnalysisResult> {
let language = 'zh-CN'
let selectedCodeLanguage = 'Python'
try {
language = getActiveSettings().language || 'zh-CN'
const settings = getActiveSettings()
language = settings.language || 'zh-CN'
selectedCodeLanguage = settings.selectedCodeLanguage || 'Python'
} catch {}

const msg = (zh: string, en: string) => language === 'en-US' ? en : zh
Expand All @@ -116,7 +119,7 @@ export async function analyzePaper(
onProgress({ stage: 'parsing', message: msg(`PDF 解析完成 (${pageCount} 页)`, `PDF parsed (${pageCount} pages)`) })

onProgress({ stage: 'summarizing', message: msg('正在分析论文结构并生成总结...', 'Analyzing paper structure and generating summary...') })
const analysisPrompt = buildCombinedAnalysisPrompt(text, language)
const analysisPrompt = buildCombinedAnalysisPrompt(text, language, selectedCodeLanguage)
let rawOutput = ''
let streamedSummary = ''
let summaryClosed = false
Expand Down
72 changes: 36 additions & 36 deletions src/main/backend/promptBuilder.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,24 @@
function getCodeExtension(selectedCodeLanguage: string): string {
switch (selectedCodeLanguage) {
case 'C':
return 'c'
case 'C++':
return 'cpp'
case 'Java':
return 'java'
case 'Go':
return 'go'
case 'Rust':
return 'rs'
case 'MATLAB':
return 'm'
case 'R':
return 'R'
default:
return 'py'
}
}

export function buildSummaryPrompt(text: string, language: string = 'zh-CN'): { system: string; user: string } {
const isEn = language === 'en-US'

Expand Down Expand Up @@ -71,8 +92,9 @@ $$`
}
}

export function buildCodePrompt(text: string, summaryContext: string, language: string = 'zh-CN'): { system: string; user: string } {
export function buildCodePrompt(text: string, summaryContext: string, language: string = 'zh-CN', selectedCodeLanguage: string = 'Python'): { system: string; user: string } {
const isEn = language === 'en-US'
const codeExtension = getCodeExtension(selectedCodeLanguage)

const system = `You are an expert AI research paper analyst. Your task is to analyze academic papers and produce structured summaries and, when applicable, extract core algorithmic implementations.

Expand All @@ -97,9 +119,9 @@ ${summaryContext}
Your task:
1. Identify the smallest implementable core method from the paper.
2. Implement it as multiple focused files inside a folder-style project structure.
3. Prefer Python for ML/algorithm papers.
3. Generate all demo/core code in ${selectedCodeLanguage}. Do NOT switch to Python unless the selected output code language is Python.
4. Keep files componentized and minimal: configuration, model/algorithm, loss/metrics if needed, training or inference, and an example entry point.
5. Include a requirements.txt file when dependencies are needed.
5. Use file extensions and dependency notes appropriate for ${selectedCodeLanguage}.

Output format:
Return ONLY strict JSON. Do not wrap it in Markdown fences. Do not add prose before or after the JSON.
Expand All @@ -108,35 +130,7 @@ The JSON schema MUST be:
{
"files": [
{
"path": "requirements.txt",
"content": "..."
},
{
"path": "core_code/__init__.py",
"content": "..."
},
{
"path": "core_code/config.py",
"content": "..."
},
{
"path": "core_code/model.py",
"content": "..."
},
{
"path": "core_code/losses.py",
"content": "..."
},
{
"path": "core_code/train.py",
"content": "..."
},
{
"path": "core_code/inference.py",
"content": "..."
},
{
"path": "core_code/example.py",
"path": "core_code/descriptive_file_name.${codeExtension}",
"content": "..."
}
]
Expand All @@ -157,6 +151,7 @@ Rules:
- If the paper uses pseudo-code, translate it to real code.
- If the paper describes an architecture, implement the forward pass.
- If the paper has a training procedure, implement the training loop.
- Demo Code MUST use ${selectedCodeLanguage}.
- Keep code APIs, filenames, classes, and variables in English even when the UI language is Chinese.`

return {
Expand All @@ -165,15 +160,17 @@ Rules:
}
}

export function buildCombinedAnalysisPrompt(text: string, language: string = 'zh-CN'): { system: string; user: string } {
export function buildCombinedAnalysisPrompt(text: string, language: string = 'zh-CN', selectedCodeLanguage: string = 'Python'): { system: string; user: string } {
const isEn = language === 'en-US'
const codeExtension = getCodeExtension(selectedCodeLanguage)

const system = `You are an expert AI research paper analyst. Analyze the paper, write a README.md-style summary, decide whether core component code is needed, and generate code only when applicable.

Rules:
- Be precise and factual. Do not fabricate details not present in the paper.
- Use ${isEn ? 'English' : 'Chinese'} for paper summary and decision reason.
- Keep generated code APIs, filenames, classes, and variables in English.
- Demo Code and core code files MUST be generated in ${selectedCodeLanguage}.
- Format mathematical expressions in the summary with Markdown LaTeX delimiters: inline math as $...$ and block math as $$...$$.
- Follow the exact tagged output protocol. Do not add prose before, between, or after the required tags.`

Expand Down Expand Up @@ -251,7 +248,7 @@ Use "needed": true only if the paper clearly describes at least one implementabl
"minimalImplementationBoundary": "exactly what the generated code should implement, and what it should not implement",
"files": [
{
"path": "core_code/descriptive_file_name.py",
"path": "core_code/descriptive_file_name.${codeExtension}",
"purpose": "why this file is necessary for the minimal core contribution",
"mainSymbols": ["function_or_class_name"],
"mustInclude": ["specific method elements that must appear in this file"],
Expand All @@ -277,6 +274,9 @@ Use "needed": true only if the paper clearly describes at least one implementabl
</P2CC_CODE_BLUEPRINT>

Blueprint rules:
- The selected output code language is ${selectedCodeLanguage}. Every generated source file MUST use ${selectedCodeLanguage}.
- Do NOT switch to Python unless the selected output code language is Python.
- Choose file extensions appropriate for ${selectedCodeLanguage}.
- Do NOT assume the paper is about AI, machine learning, or software engineering. Infer the domain from the paper.
- Do NOT use a fixed project template. The file list must be designed from the paper's smallest implementable computational contribution.
- Each file must be justified by the core contribution. If a file is not necessary for that contribution, omit it.
Expand All @@ -288,7 +288,7 @@ Blueprint rules:

5. Then output code files inside these exact tags. The code bundle MUST contain exactly the files listed in P2CC_CODE_BLUEPRINT.files, no more and no fewer:
<P2CC_CODE_BUNDLE>
<P2CC_FILE path="core_code/descriptive_file_name.py">
<P2CC_FILE path="core_code/descriptive_file_name.${codeExtension}">
...
</P2CC_FILE>
</P2CC_CODE_BUNDLE>
Expand All @@ -301,7 +301,7 @@ Code generation rules:
- Do NOT encode file contents as JSON strings. Write raw file content directly inside the file block.
- Use the exact paths declared in P2CC_CODE_BLUEPRINT.files.
- Do not generate extra helper files outside the blueprint.
- Prefer Python for algorithmic papers, but choose simple, dependency-light code that best fits the paper's method.
- Demo Code MUST use ${selectedCodeLanguage}; choose simple, dependency-light code that best fits the paper's method within that language.
- Keep the implementation reusable as a core component, not as a full experiment reproduction project.
- Use comments to explain key implementation decisions only where helpful.`

Expand Down
Loading
Loading