stage changes

This commit is contained in:
bobo.yang 2024-04-08 22:10:56 +08:00
parent d6321c8509
commit 9924472c1a
13 changed files with 619 additions and 1 deletions

View File

@ -724,6 +724,16 @@
{
"command": "devchat.fix_chinese",
"title": "Devchat:修复此"
},
{
"command": "DevChat.codecomplete",
"title": "Codecomplete",
"category": "DevChat"
},
{
"command": "DevChat.codecomplete_callback",
"title": "Codecomplete Callback",
"category": "DevChat"
}
],
"keybindings": [
@ -731,7 +741,12 @@
"command": "devchat.openChatPanel",
"key": "ctrl+shift+/",
"mac": "cmd+shift+/"
}
},
{
"command": "DevChat.codecomplete",
"key": "ctrl+shift+'",
"mac": "cmd+shift+'"
}
],
"menus": {
"editor/title": [

View File

@ -0,0 +1,3 @@
status.ts: 代码补全状态表达接口。预期有三种状态:未就绪、就绪、代码补全中。

View File

@ -0,0 +1,74 @@
/*
使
*/
type CacheItem = {
value: any;
timestamp: number;
};
class MemoryCacheManager {
private static maxCapacity: number = 5;
private cache: Map<string, CacheItem>;
constructor() {
this.cache = new Map<string, CacheItem>();
}
/**
*
*/
set(key: string, value: any): void {
// 首先检查缓存中是否已经有了该键值对,若有,则更新;若没有,则添加
if (this.cache.has(key)) {
this.cache.set(key, { value, timestamp: Date.now() });
} else {
// 先确保缓存没有超出最大容量
if (this.cache.size >= MemoryCacheManager.maxCapacity) {
this.evict();
}
this.cache.set(key, { value, timestamp: Date.now() });
}
}
/**
*
*/
get(key: string): any | undefined {
const item = this.cache.get(key);
if (item) {
// 更新timestamp以反映最近一次访问
item.timestamp = Date.now();
return item.value;
}
return undefined;
}
/**
*
*/
delete(key: string): boolean {
return this.cache.delete(key);
}
/**
* 使
*/
private evict(): void {
let oldestKey: string | null = null;
let oldestTimestamp: number = Infinity;
for (const [key, item] of this.cache.entries()) {
if (item.timestamp < oldestTimestamp) {
oldestTimestamp = item.timestamp;
oldestKey = key;
}
}
if (oldestKey !== null) {
this.cache.delete(oldestKey);
}
}
}
export default MemoryCacheManager;

View File

@ -0,0 +1,148 @@
/*
chunk进行过滤
*/
import * as vscode from 'vscode';
import { logger } from '../../util/logger';
import { CodeCompletionChunk, streamComplete } from './llm';
// 代码补全返回结果定义
export interface CodeCompleteResult {
prompt: string;
code: string;
id: string;
}
export class LLMStreamComplete {
private token: vscode.CancellationToken;
private curlineIndent: number = 0;
constructor(token: vscode.CancellationToken, curlineIndent: number) {
this.token = token;
this.curlineIndent = curlineIndent;
}
async * chunkStopCanceled(chunks: AsyncIterable<CodeCompletionChunk>) {
for await (const chunk of chunks) {
if (this.token.isCancellationRequested) {
break;
}
yield chunk;
}
}
// 过滤第一个就是换行符的chunk避免补全时出现空行
async * stopWhenFirstCharIsNewLine(chunks: AsyncIterable<CodeCompletionChunk>) {
let isFirst = true;
for await (const chunk of chunks) {
if (chunk.text.length === 0) {
yield chunk;
}
if (isFirst && chunk.text[0] === "\n") {
break;
}
isFirst = false;
yield chunk;
}
}
// 当前chunk中字符串不是以行为单位需要重新整合为以行为单位。
async * toLines(chunks: AsyncIterable<CodeCompletionChunk>) {
let line = "";
let id = "";
for await (const chunk of chunks) {
if (chunk.id) {
id = chunk.id;
}
line += chunk.text;
while (line.indexOf("\n") !== -1) {
const index = line.indexOf("\n");
yield {
text: line.slice(0, index + 1),
id
};
line = line.slice(index + 1);
}
}
if (line.length > 0) {
yield { text: line, id };
}
}
// async * stopAtLineEnd(chunks: AsyncIterable<CodeCompletionChunk>) {
// for await (const chunk of chunks) {
// if (chunk.text.indexOf("\n") !== -1) {
// chunk.text = chunk.text.slice(0, chunk.text.indexOf("\n"));
// yield chunk;
// break;
// }
// yield chunk;
// }
// }
async * stopAtSameBlock(chunks: AsyncIterable<CodeCompletionChunk>) {
let index = 0;
let preIndent = -1;
let hasIndentBigger = false;
let sameIndentTimes = 0;
for await (const chunk of chunks) {
let lineIndent = chunk.text.search(/\S/);
if (index === 0) {
lineIndent = this.curlineIndent;
}
if (index > 0 && chunk.text.trim().length > 0 && lineIndent < this.curlineIndent ) {
break;
}
if (index > 0 && preIndent === 0 && lineIndent === 0) {
break;
}
if (index > 0 && hasIndentBigger && lineIndent === this.curlineIndent && chunk.text.trim().length > 3) {
break;
}
if (index > 0 && preIndent === lineIndent) {
sameIndentTimes += 1;
} else {
sameIndentTimes = 0;
}
if (sameIndentTimes > 1) {
break;
}
if (lineIndent > this.curlineIndent) {
hasIndentBigger = true;
}
preIndent = lineIndent;
index += 1;
yield chunk;
}
}
async llmStreamComplete(prompt: string) : Promise<CodeCompleteResult | undefined> {
// TODO
// 对LLM的异常进行捕获避免中断代码补全
const chunks = streamComplete(prompt);
const chunks2 = this.chunkStopCanceled(chunks);
const chunks3 = this.toLines(chunks2);
const chunks4 = this.stopAtSameBlock(chunks3);
let id = "";
let completionCode = "";
for await (const chunk of chunks4) {
completionCode += chunk.text;
if (chunk.id) {
id = chunk.id;
}
}
logger.channel()?.info("code:", completionCode);
return { prompt, code: completionCode, id };
}
}

View File

@ -0,0 +1,154 @@
import * as vscode from 'vscode';
import { logger } from '../../util/logger';
import Debouncer from './debouncer';
import MemoryCacheManager from './cache';
import { createPrompt } from './promptCreator';
import { CodeCompleteResult, LLMStreamComplete } from './chunkFilter';
import { nvidiaStarcoderComplete } from './llm';
export function registerCodeCompleteCallbackCommand(context: vscode.ExtensionContext) {
let disposable = vscode.commands.registerCommand(
"DevChat.codecomplete_callback",
async (callback: any) => {
callback();
}
);
context.subscriptions.push(disposable);
}
export class InlineCompletionProvider implements vscode.InlineCompletionItemProvider {
private debouncer: Debouncer;
private cache: MemoryCacheManager;
constructor() {
// TODO
// Read delay time from config
this.debouncer = new Debouncer(500);
this.cache = new MemoryCacheManager();
}
async codeComplete(document: vscode.TextDocument, position: vscode.Position, context: vscode.InlineCompletionContext, token: vscode.CancellationToken): Promise<CodeCompleteResult | undefined> {
// TODO
// create prompt
const fsPath = document.uri.fsPath;
const fileContent = document.getText();
const prompt = await createPrompt(fsPath, fileContent, position.line, position.character);
// check cache
const result = await this.cache.get(prompt);
if(result) {
return result;
}
// TODO
// call code_completion
const lines = fileContent.split('\n');
let curlineIndent = lines[position.line].search(/\S/);
if (curlineIndent === -1) {
curlineIndent = 0;
}
const completor = new LLMStreamComplete(token, curlineIndent);
const response = await completor.llmStreamComplete(prompt);
if (!response || response.code.length === 0) {
return undefined;
}
if (token.isCancellationRequested) {
return undefined;
}
// cache result
this.cache.set(prompt, response);
return response;
}
async provideInlineCompletionItems(document: vscode.TextDocument, position: vscode.Position, context: vscode.InlineCompletionContext, token: vscode.CancellationToken): Promise<vscode.InlineCompletionItem[] | null> {
const result = await this.debouncer.debounce();
if(!result) {
return [];
}
const response: CodeCompleteResult | undefined = await this.codeComplete(document, position, context, token);
if(!response) {
return [];
}
if (token.isCancellationRequested) {
return [];
}
// TODO
// 代码补全建议是否已经被用户看到,这个需要更加准确的方式来识别。
logger.channel()?.info("code complete show.");
const logRejectionTimeout: NodeJS.Timeout = setTimeout(() => {
logger.channel()?.info("code complete not accept.");
}, 10_000);
// 代码补全回调处理
const callback = () => {
logger.channel()?.info("accept:", response.id);
// delete cache
this.cache.delete(response.prompt);
// delete timer
clearTimeout(logRejectionTimeout);
};
return [
new vscode.InlineCompletionItem(
response.code,
new vscode.Range(
position,
position
),
{
title: "code complete accept",
command: "DevChat.codecomplete_callback",
arguments: [callback],
}
),
];
// // 等待时间(单位:毫秒),可根据需要调整
// const delayTime = 5000;
// // 创建一个新的Promise用于实现等待逻辑
// await new Promise((resolve) => {
// const timer = setTimeout(resolve, delayTime);
// // 如果请求在等待时间结束前被取消,则清除定时器
// token.onCancellationRequested(() => clearTimeout(timer));
// });
// logger.channel()?.info("----->");
// // 如果请求已被取消则直接返回null
// if (token.isCancellationRequested) {
// logger.channel()?.info("request cancelled before completion");
// return [];
// }
// // 根据文档和位置计算补全项(这里仅作示例,实际实现可能会有所不同)
// // 获取position前文本
// const documentText = document.getText();
// const offsetPos = document.offsetAt(position);
// // 获取position前文本
// const prefix = documentText.substring(0, offsetPos);
// const suffix = documentText.substring(offsetPos);
// const prompt = "<fim_prefix>" + prefix + "<fim_suffix>" + suffix + "<fim_middle>";
// // call code_completion
// const response = await code_completion(prompt);
// if (!response) {
// logger.channel()?.info("finish provideInlineCompletionItems");
// return [];
// }
// logger.channel()?.info("finish provideInlineCompletionItems");
// return [new vscode.InlineCompletionItem(response[0], new vscode.Range(position, position))];
}
}

View File

@ -0,0 +1,31 @@
export class Debouncer {
private debouncing = false;
private debounceTimeout?: NodeJS.Timeout;
private lastTimeStampt?: string;
constructor(private debounceDelay: number) { }
async debounce(): Promise<boolean> {
const timestampt = Date.now().toString();
this.lastTimeStampt = timestampt;
if (this.debouncing) {
this.debounceTimeout?.refresh();
const lastTimestampt = await new Promise<string | undefined>((resolve) =>
setTimeout(() => {
resolve(this.lastTimeStampt);
}, this.debounceDelay)
);
return timestampt === lastTimestampt;
} else {
this.debouncing = true;
this.lastTimeStampt = timestampt;
this.debounceTimeout = setTimeout(() => {
this.debouncing = false;
}, this.debounceDelay);
return true;
}
}
}
export default Debouncer;

View File

@ -0,0 +1,86 @@
/*
LLM模型生成代码补全
*/
import axios from 'axios';
import { logger } from "../../util/logger";
import { Chunk } from 'webpack';
// 定义代码补全chunk结构内容
export interface CodeCompletionChunk {
text: string;
id: string;
}
export async function* streamComplete(prompt: string): AsyncGenerator<CodeCompletionChunk> {
for await (const chunk of nvidiaStarcoderComplete(prompt)) {
yield chunk;
}
}
export async function * nvidiaStarcoderComplete(prompt: string) : AsyncGenerator<CodeCompletionChunk> {
const invokeUrl = 'https://api.nvcf.nvidia.com/v2/nvcf/pexec/functions/6acada03-fe2f-4e4d-9e0a-e711b9fd1b59';
const headers = {
"Authorization": "Bearer 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",
"Accept": "text/event-stream",
"Content-Type": "application/json",
};
const payload = {
"prompt": prompt,
"temperature": 0.2,
"top_p": 0.7,
"max_tokens": 1024,
"seed": 42,
"bad": null,
"stop": ["<file_sep>"],
"stream": true
};
try {
const response = await fetch(invokeUrl, {
method: 'POST',
headers,
body: JSON.stringify(payload),
});
if (response.ok && response.body) {
const stream = response.body as any;
const decoder = new TextDecoder("utf-8");
for await (const chunk of stream) {
const chunkText = decoder.decode(chunk).trim();
// data: {"id":"5d3376e0-2abc-4230-b796-c6fc9ae91cd4","choices":[{"index":0,"delta":"-","finish_reason":null}]}\n\n
if (!chunkText.startsWith("data:")) {
// log unexpected data
logger.channel()?.info("Unexpected data: " + chunkText);
return;
}
const jsonData = chunkText.substring(5).trim();
if (jsonData === "[DONE]") {
return;
}
try {
const data = JSON.parse(chunkText.substring(5).trim());
yield {
text: data.choices[0].delta,
id: data.id
};
} catch (e: any) {
logger.channel()?.info("receve:", chunkText);
logger.channel()?.error("JSON Parsing Error:", e.message);
}
}
} else {
logger.channel()?.error("Error making request:", response.statusText);
}
} catch (error: any) {
logger.channel()?.error("Error making request:", error.message);
}
}

View File

@ -0,0 +1,23 @@
export interface ModelConfigTemplate {
template: string;
stop: string[];
}
const stableCodeTemplate: ModelConfigTemplate = {
template: "<fim_prefix>{{{prefix}}}<fim_suffix>{{{suffix}}}<fim_middle>",
stop: ["<fim_prefix>", "<fim_suffix>", "<fim_middle>", "<|endoftext|>"],
};
const MODLE_COMPLETE_CONFIG = {
'starcoder': stableCodeTemplate,
'starcoder2': stableCodeTemplate,
};
export function getModelConfigTemplate(modelName: string): ModelConfigTemplate | undefined {
if (modelName in MODLE_COMPLETE_CONFIG) {
return MODLE_COMPLETE_CONFIG[modelName];
}
return undefined;
}

View File

@ -0,0 +1,69 @@
/*
prompt
prompt的好坏
AST获取相对完整的信息
*/
import { logger } from "../../util/logger";
import { log } from "console";
const PREFIX_MAX_SIZE: number = 600;
const SUFFIX_MAX_SIZE: number = 400;
// 尽量获取一个完整的代码片段作为代码补全的上下文
// 解析AST是一个好的方法但还是会有点偏重计算。先尝试通过缩进来定位合适的块。
// 整体范围保持在30行代码以内。
async function curfilePrompt(filePath: string, fileContent: string, line: number, column: number) {
// 以line, column为中心向前后扩展, 按行找出符合PREFIX_MAX_SIZE SUFFIX_MAX_SIZE长度显示的prefix, suffix
// 分割文件内容为行数组
const lines = fileContent.split('\n');
// 初始化prefix和suffix内容及长度
let prefix = '';
let suffix = '';
let prefixSize = 0;
let suffixSize = 0;
// 从光标所在行开始,向上构建前缀
for (let i = line; i >= 0; i--) {
let lineText: string = lines[i] + '\n';
if (i === line) {
lineText = lines[i].substring(0, column);
}
prefix = lineText + prefix;
prefixSize += lineText.length;
if (prefixSize > PREFIX_MAX_SIZE) {
break;
}
}
// 从光标所在行下一行开始,向下构建后缀
for (let i = line; i < lines.length; i++) {
let lineText = lines[i] + '\n';
if (i === line) {
lineText = lines[i].substring(column, lines[i].length) + '\n';
}
suffix += lineText;
suffixSize += lineText.length;
if (suffixSize > PREFIX_MAX_SIZE) {
break;
}
}
// 返回前缀和后缀
return {
prefix,
suffix
};
}
export async function createPrompt(filePath: string, fileContent: string, line: number, column: number) {
const { prefix, suffix } = await curfilePrompt(filePath, fileContent, line, column);
const prompt = "<fim_prefix>" + prefix + "<fim_suffix>" + suffix + "<fim_middle>";
return prompt;
}

View File

@ -0,0 +1,4 @@
/*
*/

View File

View File

@ -0,0 +1,5 @@
/*
*/

View File

@ -130,6 +130,12 @@ async function activate(context: vscode.ExtensionContext) {
regLanguageContext();
regDevChatView(context);
const provider = new InlineCompletionProvider();
const selector = { pattern: "**" };
context.subscriptions.push(vscode.languages.registerInlineCompletionItemProvider(selector, provider));
registerCodeCompleteCallbackCommand(context);
registerOpenChatPanelCommand(context);
registerAddContextCommand(context);