Skip to content

Commit 28942db

Browse files
committed
feat(ai-code-assistant): Enhance FIM completion with dynamic tokens and context-aware stop sequences
- Add dynamic token calculation based on cursor context (expressions, statements, functions, classes) - Implement context-aware stop sequences builder with support for comments, objects, and functions - Create lowcodeFormatter utility to optimize low-code metadata and reduce token consumption - Extract code context metadata (function names, class names) and inject as comments for better completion - Add createLowcodeInstruction, extractCodeContext, and buildCodeMetaComment helpers to FIM prompts - Enhance QwenClient to support stop sequences parameter in API requests - Introduce TOKEN_LIMITS, STOP_SEQUENCES, and CONTEXT_STOP_SEQUENCES configuration constants - Improve ModelAdapter to use formatted low-code context and dynamic parameters for better completion quality - Limit stop sequences to maximum 16 items per API requirements
1 parent 6720828 commit 28942db

5 files changed

Lines changed: 448 additions & 19 deletions

File tree

packages/plugins/script/src/ai-code-assistant/api/ModelAdapter.ts

Lines changed: 93 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,73 @@
44
*/
55

66
import { getMetaApi, META_SERVICE } from '@opentiny/tiny-engine-meta-register'
7-
import { FIMPromptBuilder, FIM_SYSTEM_PROMPT } from '../prompts/fim'
7+
import {
8+
FIMPromptBuilder,
9+
FIM_SYSTEM_PROMPT,
10+
createLowcodeInstruction,
11+
extractCodeContext,
12+
buildCodeMetaComment
13+
} from '../prompts/fim'
814
import { buildNESUserPrompt, parseNESResponse } from '../prompts/nes-builder'
915
import { NES_SYSTEM_PROMPT } from '../prompts/nes-system'
1016
import { buildLowcodeMetadata, cleanCompletion } from '../utils/code'
17+
import { formatLowcodeContext } from '../utils/lowcodeFormatter'
1118
import { callQwenChat } from './QwenClient'
12-
import { QWEN_CONFIG, FIXED_MODEL_CONFIG, HTTP_CONFIG } from '../config'
19+
import {
20+
QWEN_CONFIG,
21+
FIXED_MODEL_CONFIG,
22+
HTTP_CONFIG,
23+
TOKEN_LIMITS,
24+
STOP_SEQUENCES,
25+
CONTEXT_STOP_SEQUENCES
26+
} from '../config'
27+
28+
// ==================== 辅助函数 ====================
29+
30+
/**
31+
* 根据光标上下文动态计算 max_tokens
32+
*/
33+
function calculateDynamicTokens(cursorContext: any): number {
34+
if (!cursorContext) return TOKEN_LIMITS.DEFAULT
35+
36+
if (cursorContext.needsExpression) return TOKEN_LIMITS.EXPRESSION
37+
if (cursorContext.needsStatement) return TOKEN_LIMITS.STATEMENT
38+
if (cursorContext.inFunction) return TOKEN_LIMITS.FUNCTION
39+
if (cursorContext.inClass) return TOKEN_LIMITS.CLASS
40+
41+
return TOKEN_LIMITS.DEFAULT
42+
}
43+
44+
/**
45+
* 根据光标上下文构建 stop sequences(最多 16 个)
46+
*/
47+
function buildStopSequences(cursorContext: any): string[] {
48+
const stops: string[] = [...STOP_SEQUENCES.CORE]
49+
50+
if (cursorContext) {
51+
if (cursorContext.inBlockComment || cursorContext.inLineComment) {
52+
stops.push(...CONTEXT_STOP_SEQUENCES.COMMENT)
53+
} else if (cursorContext.needsExpression) {
54+
stops.push(...CONTEXT_STOP_SEQUENCES.EXPRESSION)
55+
stops.push(...STOP_SEQUENCES.NEW_SCOPE)
56+
} else if (cursorContext.inObject) {
57+
stops.push(...CONTEXT_STOP_SEQUENCES.OBJECT)
58+
stops.push(...STOP_SEQUENCES.NEW_SCOPE)
59+
} else if (cursorContext.inFunction) {
60+
stops.push(...CONTEXT_STOP_SEQUENCES.FUNCTION)
61+
stops.push(...STOP_SEQUENCES.BLOCK_END)
62+
} else {
63+
stops.push(...STOP_SEQUENCES.NEW_SCOPE)
64+
stops.push(...STOP_SEQUENCES.BLOCK_END)
65+
}
66+
} else {
67+
stops.push(...STOP_SEQUENCES.NEW_SCOPE)
68+
stops.push(...STOP_SEQUENCES.BLOCK_END)
69+
}
70+
71+
const unique = [...new Set(stops)]
72+
return unique.length > 16 ? unique.slice(0, 16) : unique
73+
}
1374

1475
/**
1576
* 统一的模型适配器(固定使用 Qwen)
@@ -59,29 +120,42 @@ export class ModelAdapter {
59120

60121
const { apiKey, baseUrl } = config
61122
const lowcodeMetadata = buildLowcodeMetadata()
123+
const language = 'javascript'
124+
125+
// 格式化低代码元数据(提取签名/键名/类型,减少 token 消耗)
126+
const formattedContext = formatLowcodeContext(lowcodeMetadata)
62127

63128
const fimMetadata = {
64-
language: 'javascript',
129+
language,
65130
isComment: prefix.trim().endsWith('//') || prefix.includes('/*'),
66-
lowcodeContext: lowcodeMetadata
67-
? {
68-
dataSource: lowcodeMetadata.dataSource || [],
69-
utils: lowcodeMetadata.utils || [],
70-
globalState: lowcodeMetadata.globalState || [],
71-
state: lowcodeMetadata.state || {},
72-
methods: lowcodeMetadata.methods || {},
73-
currentSchema: lowcodeMetadata.currentSchema || null
74-
}
75-
: null
131+
lowcodeContext: formattedContext
76132
}
77133

78-
// 构建 FIM Prompt
134+
// 提取代码上下文元信息(函数名、类名等)
135+
const codeContext = extractCodeContext(prefix)
136+
const metaComment = buildCodeMetaComment(language, codeContext)
137+
138+
// 构建 FIM Prompt(在 prefix 前注入元信息注释)
79139
const { fimPrompt, cursorContext } = this.fimBuilder.buildOptimizedFIMPrompt(
80-
`${prefix}[CURSOR]${suffix}`,
140+
`${metaComment}${prefix}[CURSOR]${suffix}`,
81141
fimMetadata
82142
)
83143

84-
const instruction = this.fimBuilder.selectInstruction(cursorContext, fimMetadata.language || 'javascript')
144+
// 选择 instruction(低代码增强 or 普通 or 注释)
145+
let instruction: string
146+
if (cursorContext.inBlockComment || cursorContext.inLineComment) {
147+
instruction = this.fimBuilder.selectInstruction(cursorContext, language)
148+
} else if (fimMetadata.lowcodeContext) {
149+
instruction = createLowcodeInstruction(language, fimMetadata.lowcodeContext)
150+
} else {
151+
instruction = this.fimBuilder.selectInstruction(cursorContext, language)
152+
}
153+
154+
// 动态 Token 计算
155+
const maxTokens = calculateDynamicTokens(cursorContext)
156+
157+
// 动态 Stop Sequences
158+
const stop = buildStopSequences(cursorContext)
85159

86160
// 合并外部 signal、去重 signal 和超时 signal
87161
const timeoutSignal = AbortSignal.timeout(HTTP_CONFIG.FIM_TIMEOUT_MS)
@@ -97,10 +171,11 @@ export class ModelAdapter {
97171
],
98172
{
99173
model: FIXED_MODEL_CONFIG.FIM.MODEL,
100-
maxTokens: FIXED_MODEL_CONFIG.FIM.MAX_TOKENS,
174+
maxTokens,
101175
temperature: FIXED_MODEL_CONFIG.FIM.TEMPERATURE,
102176
top_p: FIXED_MODEL_CONFIG.FIM.TOP_P,
103-
presence_penalty: FIXED_MODEL_CONFIG.FIM.PRESENCE_PENALTY
177+
presence_penalty: FIXED_MODEL_CONFIG.FIM.PRESENCE_PENALTY,
178+
stop
104179
},
105180
apiKey,
106181
baseUrl,

packages/plugins/script/src/ai-code-assistant/api/QwenClient.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ export async function callQwenChat(
1616
): Promise<string> {
1717
const chatUrl = `${baseUrl}${QWEN_CONFIG.CHAT_PATH}`
1818

19-
const requestBody = {
19+
const requestBody: any = {
2020
model: config.model,
2121
messages,
2222
max_tokens: config.maxTokens,
@@ -25,6 +25,11 @@ export async function callQwenChat(
2525
stream: HTTP_CONFIG.STREAM
2626
}
2727

28+
// 添加 stop sequences(如果提供)
29+
if (config.stop && config.stop.length > 0) {
30+
requestBody.stop = config.stop
31+
}
32+
2833
const response = await fetch(chatUrl, {
2934
method: HTTP_CONFIG.METHOD,
3035
headers: {

packages/plugins/script/src/ai-code-assistant/config.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,31 @@ export const ERROR_MESSAGES = {
8080
QWEN_API_ERROR: 'Qwen API 错误'
8181
}
8282

83+
// ==================== Token 动态计算 ====================
84+
85+
export const TOKEN_LIMITS = {
86+
EXPRESSION: 64,
87+
STATEMENT: 256,
88+
FUNCTION: 200,
89+
CLASS: 256,
90+
DEFAULT: 128
91+
}
92+
93+
// ==================== Stop Sequences ====================
94+
95+
export const STOP_SEQUENCES = {
96+
CORE: ['\n\n', '```'],
97+
NEW_SCOPE: ['\nfunction ', '\nclass ', '\nexport ', '\nimport '],
98+
BLOCK_END: ['\n}', '\n};']
99+
}
100+
101+
export const CONTEXT_STOP_SEQUENCES: Record<string, string[]> = {
102+
EXPRESSION: [';', ',', '\n)'],
103+
COMMENT: ['\n\n', '*/'],
104+
OBJECT: ['\n}', '\n};'],
105+
FUNCTION: ['\n}', '\nfunction ', '\nreturn ']
106+
}
107+
83108
// ==================== 提示词配置 ====================
84109

85110
/**

packages/plugins/script/src/ai-code-assistant/prompts/fim.ts

Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,169 @@ ${fileContent}
7070
Complete the code/text at the [CURSOR] position. Return ONLY the completion text.`
7171
}
7272

73+
// ==================== 低代码上下文 Prompt ====================
74+
75+
export const LOWCODE_CONTEXT_INSTRUCTION = `You are working in a low-code platform environment with specific APIs and data structures.
76+
77+
AVAILABLE RUNTIME APIS (all accessed via 'this.'):
78+
1. Data Sources (this.dataSource.xxx)
79+
- Predefined data models for the application
80+
- Access pattern: this.dataSource.<sourceName>
81+
82+
2. Utility Functions (this.utils.xxx)
83+
- Common utility methods and npm dependencies
84+
- Access pattern: this.utils.<utilityName>
85+
- May include imported libraries (check utils metadata for imports)
86+
87+
3. Global State (this.stores.xxx)
88+
- Pinia-based global state management
89+
- Access pattern: this.stores.<storeName>.<property>
90+
- Actions: this.stores.<storeName>.<actionName>()
91+
92+
4. Local State (this.state.xxx)
93+
- Component-level reactive state
94+
- Access pattern: this.state.<propertyName>
95+
96+
5. Local Methods (this.xxx)
97+
- Component-level methods
98+
- Access pattern: this.<methodName>()
99+
100+
6. Component References (this.$('refName'))
101+
- Access Vue component refs
102+
- Access pattern: this.$('<refName>')
103+
104+
IMPORTANT RULES:
105+
- ONLY use APIs that are explicitly defined in the provided metadata
106+
- DO NOT reference undefined utilities, data sources, or state properties
107+
- Follow the JSExpression/JSFunction protocol for dynamic values
108+
- Use 'function' keyword for function definitions, NOT arrow functions
109+
- Respect the component schema structure (props, events, refs)
110+
111+
PROTOCOL CONVENTIONS:
112+
- Static values: { width: '300px' }
113+
- Dynamic expressions: { width: { type: 'JSExpression', value: 'this.state.xxx' } }
114+
- Function handlers: { onClick: { type: 'JSFunction', value: 'function onClick() {}' } }`
115+
116+
/**
117+
* 构建低代码增强指令(将元数据注入 instruction)
118+
*/
119+
export function createLowcodeInstruction(language: string, lowcodeContext: any = {}): string {
120+
let instruction = createCodeInstruction(language)
121+
122+
if (!lowcodeContext || Object.keys(lowcodeContext).length === 0) {
123+
return instruction
124+
}
125+
126+
instruction += `\n\n${LOWCODE_CONTEXT_INSTRUCTION}`
127+
128+
const { dataSource, utils, globalState, state, methods, currentSchema } = lowcodeContext
129+
130+
if (dataSource?.length > 0) {
131+
instruction += `\n\nAVAILABLE DATA SOURCES:\n${JSON.stringify(dataSource, null, 2)}`
132+
}
133+
if (utils?.length > 0) {
134+
instruction += `\n\nAVAILABLE UTILITIES:\n${JSON.stringify(utils, null, 2)}`
135+
}
136+
if (globalState?.length > 0) {
137+
instruction += `\n\nGLOBAL STATE (Pinia Stores):\n${JSON.stringify(globalState, null, 2)}`
138+
}
139+
if (state && Object.keys(state).length > 0) {
140+
instruction += `\n\nLOCAL STATE:\n${JSON.stringify(state, null, 2)}`
141+
}
142+
if (methods && Object.keys(methods).length > 0) {
143+
instruction += `\n\nLOCAL METHODS:\n${JSON.stringify(methods, null, 2)}`
144+
}
145+
if (currentSchema) {
146+
instruction += `\n\nCURRENT COMPONENT: ${currentSchema.componentName || 'Unknown'}`
147+
if (currentSchema.props) {
148+
instruction += `\n- Props: Use component props as defined in schema`
149+
instruction += `\n- Events: Props starting with 'on' are event handlers`
150+
}
151+
if (currentSchema.ref) {
152+
instruction += `\nRef: this.$('${currentSchema.ref}')`
153+
}
154+
}
155+
156+
return instruction
157+
}
158+
159+
// ==================== 代码上下文提取 ====================
160+
161+
const CODE_CONTEXT_PATTERNS = {
162+
FUNCTION: /function\s+(\w+)|const\s+(\w+)\s*=.*=>|(\w+)\s*\([^)]*\)\s*{/,
163+
CLASS: /class\s+(\w+)/,
164+
INTERFACE: /interface\s+(\w+)/,
165+
TYPE: /type\s+(\w+)/
166+
}
167+
168+
/**
169+
* 从光标前文本中提取当前代码上下文(函数名、类名等)
170+
*/
171+
export function extractCodeContext(textBeforeCursor: string): {
172+
functionName: string
173+
className: string
174+
interfaceName: string
175+
typeName: string
176+
} {
177+
const lines = textBeforeCursor.split('\n')
178+
let functionName = ''
179+
let className = ''
180+
let interfaceName = ''
181+
let typeName = ''
182+
183+
const startLine = Math.max(0, lines.length - 20)
184+
185+
for (let i = lines.length - 1; i >= startLine; i--) {
186+
const line = lines[i]
187+
188+
if (!functionName) {
189+
const m = line.match(CODE_CONTEXT_PATTERNS.FUNCTION)
190+
if (m) functionName = m[1] || m[2] || m[3]
191+
}
192+
if (!className) {
193+
const m = line.match(CODE_CONTEXT_PATTERNS.CLASS)
194+
if (m) className = m[1]
195+
}
196+
if (!interfaceName) {
197+
const m = line.match(CODE_CONTEXT_PATTERNS.INTERFACE)
198+
if (m) interfaceName = m[1]
199+
}
200+
if (!typeName) {
201+
const m = line.match(CODE_CONTEXT_PATTERNS.TYPE)
202+
if (m) typeName = m[1]
203+
}
204+
205+
if (functionName && className && interfaceName && typeName) break
206+
}
207+
208+
return { functionName, className, interfaceName, typeName }
209+
}
210+
211+
/**
212+
* 构建代码元信息注释(注入到 FIM prefix 前面)
213+
*/
214+
export function buildCodeMetaComment(language: string, codeContext: ReturnType<typeof extractCodeContext>): string {
215+
let meta = `// Language: ${language}\n`
216+
217+
if (codeContext.className) {
218+
meta += `// Current Class: ${codeContext.className}\n`
219+
meta += `// IMPORTANT: Only complete code within this class\n`
220+
}
221+
if (codeContext.interfaceName) {
222+
meta += `// Current Interface: ${codeContext.interfaceName}\n`
223+
}
224+
if (codeContext.typeName) {
225+
meta += `// Current Type: ${codeContext.typeName}\n`
226+
}
227+
if (codeContext.functionName) {
228+
meta += `// Current Function: ${codeContext.functionName}\n`
229+
meta += `// IMPORTANT: Only complete code within this function scope\n`
230+
}
231+
232+
meta += `// NOTE: Do not reference variables or code from other functions\n\n`
233+
return meta
234+
}
235+
73236
// ==================== FIM Prompt Builder ====================
74237

75238
export class FIMPromptBuilder {

0 commit comments

Comments
 (0)