Support GPT-4-0613 function calls

This commit is contained in:
bobo.yang 2023-07-24 00:11:56 +08:00
parent 6650a5f253
commit cda646f830
11 changed files with 211 additions and 79 deletions

View File

@ -1,9 +1,12 @@
import { Action, CustomActions } from './customAction';
import fs from 'fs';
import { Action, CustomActions, getActionInstruction } from './customAction';
import { CommandResult } from '../util/commonUtil';
import { logger } from '../util/logger';
import { SymbolRefAction } from './symbolRefAction';
import { SymbolDefAction } from './symbolDefAction';
// extend Action
@ -29,7 +32,7 @@ export class CommandRunAction implements Action {
async handlerAction(args: {[key: string]: any}): Promise<CommandResult> {
try {
const commandData = JSON.parse(args.content);
const result = await ActionManager.getInstance().applyCommandAction(commandData.command, commandData.args);
const result = await ActionManager.getInstance().applyCommandAction(commandData.name, commandData.arguments);
return result;
} catch (error) {
logger.channel()?.error('Failed to parse code file content: ' + error);
@ -51,6 +54,9 @@ export default class ActionManager {
}
ActionManager.instance.registerAction(new CommandRunAction());
ActionManager.instance.registerAction(new SymbolRefAction());
ActionManager.instance.registerAction(new SymbolDefAction());
return ActionManager.instance;
}
@ -160,4 +166,23 @@ export default class ActionManager {
this.registerAction(chatAction);
}
}
public actionInstruction(): string {
let functionsDefList = []
for (const action of this.actions) {
functionsDefList.push(getActionInstruction(action));
}
// return as json string
return JSON.stringify(functionsDefList, null, 4);
}
public saveActionInstructionFile(tarFile: string): void {
try {
fs.writeFileSync(tarFile, this.actionInstruction());
} catch (error) {
logger.channel()?.error(`Failed to save action instruction file: ${error}`);
logger.channel()?.show();
}
}
}

View File

@ -15,6 +15,27 @@ export interface Action {
handlerAction: (args: { [key: string]: string }) => Promise<CommandResult>;
}
// generate instruction for action
export function getActionInstruction(action: Action): any {
const actionSchema = {
name: action.name,
description: action.description,
parameters: {
type: "object",
properties: action.args.reduce((obj: any, arg: any) => {
obj[arg.name] = {
type: arg.type,
description: arg.description
};
return obj;
}, {}),
required: action.args.filter((arg: any) => arg.required).map((arg: any) => arg.name)
}
};
return actionSchema;
}
export class CustomActions {
private static instance: CustomActions | null = null;
private actions: Action[] = [];
@ -29,44 +50,6 @@ export class CustomActions {
return CustomActions.instance;
}
public actionInstruction(): string {
let instruction = 'As an AI bot, you replay user with "command" block, don\'t replay any other words.\n' +
'"command" block is like this:\n' +
'``` command\n' +
'{\n' +
' "command": "xxx",\n' +
' "args" {\n' +
' "var name": "xxx"\n' +
' }\n' +
'}\n' +
'```\n' +
'You can split task into small sub tasks, after each command I will give you the result of command executed. so, the next command can depend pre command\'s output.\n' +
'\n' +
'Supported commands are:\n';
let index = 1;
for (const action of this.actions) {
instruction += String(index) + ". " + this.getActionInstruction(action.name) + "\n";
index += 1;
}
instruction += 'Restriction for output:\n' +
'1. Only reponse "command" block.\n' +
'2. Don\'t include any other text exclude command.\n' +
'3. Only supported extension commands can be used to complete the response.\n' +
'4. When update file, old_content must include at least three lines.';
return instruction;
}
public saveActionInstructionFile(tarFile: string): void {
try {
fs.writeFileSync(tarFile, this.actionInstruction());
} catch (error) {
logger.channel()?.error(`Failed to save action instruction file: ${error}`);
logger.channel()?.show();
}
}
public parseActions(workflowsDir: string): void {
this.actions = [];
@ -162,23 +145,4 @@ export class CustomActions {
public getActions(): Action[] {
return this.actions;
}
// generate instruction for action
public getActionInstruction(actionName: string): string {
const action = this.actions.find(action => action.name.trim() === actionName.trim());
if (!action) {
return '';
}
let instruction = `${action.name}: ${action.description}\n`;
// if args is not undefined and has values, then visit args
if (action.args !== undefined && action.args.length > 0) {
instruction += `Args:\n`;
for (const arg of action.args) {
instruction += ` name: ${arg.name} type: (${arg.type}) description: ${arg.description}\n`;
}
}
return instruction;
}
}

View File

@ -0,0 +1,40 @@
import { Action, CustomActions } from './customAction';
import { CommandResult } from '../util/commonUtil';
import { logger } from '../util/logger';
export class SymbolDefAction implements Action {
name: string;
description: string;
type: string[];
action: string;
handler: string[];
args: { "name": string, "description": string, "type": string, "as"?: string, "from": string }[];
constructor() {
this.name = 'symbol_def';
this.description = 'Retrieve the definition information related to the symbol';
this.type = ['symbol'];
this.action = 'symbol_def';
this.handler = [];
this.args = [
{"name": "symbol", "description": "The symbol variable specifies the symbol for which definition information is to be retrieved.", "type": "string", "from": "content.content.symbol"},
];
}
async handlerAction(args: {[key: string]: any}): Promise<CommandResult> {
try {
const symbolName = args.symbol;
// get reference information
return {exitCode: 0, stdout: "", stderr: ""};
} catch (error) {
logger.channel()?.error(`${this.name} handle error: ${error}`);
logger.channel()?.show();
return {exitCode: -1, stdout: '', stderr: `${this.name} handle error: ${error}`};
}
}
};

View File

@ -0,0 +1,40 @@
import { Action, CustomActions } from './customAction';
import { CommandResult } from '../util/commonUtil';
import { logger } from '../util/logger';
export class SymbolRefAction implements Action {
name: string;
description: string;
type: string[];
action: string;
handler: string[];
args: { "name": string, "description": string, "type": string, "as"?: string, "from": string }[];
constructor() {
this.name = 'symbol_ref';
this.description = 'Retrieve the reference information related to the symbol';
this.type = ['symbol'];
this.action = 'symbol_ref';
this.handler = [];
this.args = [
{"name": "symbol", "description": "The symbol variable specifies the symbol for which reference information is to be retrieved.", "type": "string", "from": "content.content.symbol"},
];
}
async handlerAction(args: {[key: string]: any}): Promise<CommandResult> {
try {
const symbolName = args.symbol;
// get reference information
return {exitCode: 0, stdout: "", stderr: ""};
} catch (error) {
logger.channel()?.error(`${this.name} handle error: ${error}`);
logger.channel()?.show();
return {exitCode: -1, stdout: '', stderr: `${this.name} handle error: ${error}`};
}
}
};

View File

@ -1,6 +1,18 @@
import * as vscode from 'vscode';
import { regInMessage, regOutMessage } from '../util/reg_messages';
import ActionManager from '../action/actionManager';
import { MessageHandler } from './messageHandler';
import { sendMessage } from './sendMessage';
import { logger } from '../util/logger';
function compressText(text: string, maxLength: number): string {
if (text.length <= maxLength) {
return text;
}
const halfLength = Math.floor(maxLength / 2);
return text.slice(0, halfLength) + " ... " + text.slice(-halfLength);
}
async function replaceFileContent(uri: vscode.Uri, newContent: string) {
try {
@ -61,8 +73,22 @@ export async function applyCodeFile(text: string, fileName: string): Promise<voi
regInMessage({command: 'code_file_apply', content: '', fileName: ''});
export async function codeFileApply(message: any, panel: vscode.WebviewPanel|vscode.WebviewView): Promise<void> {
await applyCodeFile(message.content, message.fileName);
return;
// await applyCodeFile(message.content, message.fileName);
// return;
try {
const result = await ActionManager.getInstance().applyAction("command_run", { "command": "", "fileName": message.fileName, "content": message.content });
// send error message to devchat
const commandObj = JSON.parse(message.content)
const newMessage = `{"exit_code": ${result.exitCode}, stdout: ${result.stdout}, stderr: ${compressText(result.stderr, 100)}}`;
MessageHandler.sendMessage(panel, { "command": "sendMessageSystem" });
sendMessage({command: 'sendMessage', text: newMessage}, panel, commandObj.name);
} catch (error) {
logger.channel()?.error('Failed to parse code file content: ' + error);
logger.channel()?.show();
}
}

View File

@ -15,12 +15,12 @@ regOutMessage({ command: 'receiveMessagePartial', text: 'xxxx', user: 'xxx', dat
// return message:
// { command: 'receiveMessage', text: 'xxxx', hash: 'xxx', user: 'xxx', date: 'xxx'}
// { command: 'receiveMessagePartial', text: 'xxxx', user: 'xxx', date: 'xxx'}
export async function sendMessage(message: any, panel: vscode.WebviewPanel|vscode.WebviewView): Promise<void> {
_lastMessage = message;
export async function sendMessage(message: any, panel: vscode.WebviewPanel|vscode.WebviewView, function_name: string|undefined = undefined): Promise<void> {
_lastMessage = [message, function_name];
const responseMessage = await sendMessageBase(message, (data: { command: string, text: string, user: string, date: string}) => {
const responseMessage = await sendMessageBase(message, (data: { command: string, text: string, user: string, date: string}) => {
MessageHandler.sendMessage(panel, data, false);
});
}, function_name);
if (responseMessage) {
MessageHandler.sendMessage(panel, responseMessage);
}
@ -31,7 +31,7 @@ regInMessage({command: 'regeneration'});
export async function regeneration(message: any, panel: vscode.WebviewPanel|vscode.WebviewView): Promise<void> {
// call sendMessage to send last message again
if (_lastMessage) {
sendMessage(_lastMessage, panel);
sendMessage(_lastMessage[0], panel, _lastMessage[1]);
}
}

View File

@ -102,6 +102,7 @@ export async function parseMessageAndSetOptions(message: any, chatOptions: any):
if (parsedMessage.reference.length > 0) {
chatOptions.reference = parsedMessage.reference;
}
return parsedMessage;
}
@ -135,13 +136,16 @@ export async function handlerResponseText(partialDataText: string, chatResponse:
}
}
if (chatResponse.finish_reason === "function_call") {
return '\n```command\n' + responseText + '\n```\n';
}
return responseText;
}
// 重构后的sendMessage函数
export async function sendMessageBase(message: any, handlePartialData: (data: { command: string, text: string, user: string, date: string}) => void): Promise<{ command: string, text: string, hash: string, user: string, date: string, isError: boolean }|undefined> {
export async function sendMessageBase(message: any, handlePartialData: (data: { command: string, text: string, user: string, date: string}) => void, function_name: string| undefined = undefined): Promise<{ command: string, text: string, hash: string, user: string, date: string, isError: boolean }|undefined> {
userStop = false;
const chatOptions: any = {};
const parsedMessage = await parseMessageAndSetOptions(message, chatOptions);
@ -150,9 +154,18 @@ export async function sendMessageBase(message: any, handlePartialData: (data: {
}
logger.channel()?.info(`parent hash: ${chatOptions.parent}`);
chatOptions.functions = "./.chat/functions.json";
if (function_name) {
chatOptions.function_name = function_name;
chatOptions.role = "function";
}
let partialDataText = '';
const onData = (partialResponse: ChatResponse) => {
partialDataText = partialResponse.response.replace(/```\ncommitmsg/g, "```commitmsg");
if (partialResponse.finish_reason === "function_call") {
partialDataText = '\n```command\n' + partialDataText + '\n```\n';
}
handlePartialData({ command: 'receiveMessagePartial', text: partialDataText!, user: partialResponse.user, date: partialResponse.date });
};

View File

@ -39,8 +39,8 @@ export class DevChatViewProvider implements vscode.WebviewViewProvider {
ChatContextManager.getInstance().loadCustomContexts(workflowsDir);
ActionManager.getInstance().loadCustomActions(workflowsDir);
const actionInstrucFile = path.join(UiUtilWrapper.workspaceFoldersFirstPath()!, './.chat/action.instr');
CustomActions.getInstance().saveActionInstructionFile( actionInstrucFile);
const actionInstrucFile = path.join(UiUtilWrapper.workspaceFoldersFirstPath()!, './.chat/functions.json');
ActionManager.getInstance().saveActionInstructionFile( actionInstrucFile);
}
this._view = webviewView;

View File

@ -19,6 +19,9 @@ export interface ChatOptions {
parent?: string;
reference?: string[];
header?: string[];
functions?: string;
role?: string;
function_name?: string;
context?: string[];
}
@ -46,6 +49,7 @@ export interface ChatResponse {
user: string;
date: string;
response: string;
finish_reason: string;
isError: boolean;
}
@ -80,6 +84,18 @@ class DevChat {
}
}
if (options.functions) {
args.push("-f", options.functions);
}
if (options.role) {
args.push("-R", options.role);
}
if (options.function_name) {
args.push("-n", options.function_name);
}
if (options.parent) {
args.push("-p", options.parent);
}
@ -96,6 +112,7 @@ class DevChat {
user: "",
date: "",
response: "",
finish_reason: "",
isError: isPartial ? false : true,
};
}
@ -116,16 +133,27 @@ class DevChat {
}
}
let finishReasonLine = "";
for (let i = responseLines.length - 1; i >= 0; i--) {
if (responseLines[i].startsWith("finish_reason:")) {
finishReasonLine = responseLines[i];
responseLines.splice(i, 1);
break;
}
}
if (!promptHashLine) {
return {
"prompt-hash": "",
user: user,
date: date,
response: responseLines.join("\n"),
finish_reason: "",
isError: isPartial ? false : true,
};
}
const finishReason = finishReasonLine.split(" ")[1];
const promptHash = promptHashLine.split(" ")[1];
const response = responseLines.join("\n");
@ -134,6 +162,7 @@ class DevChat {
user,
date,
response,
finish_reason: finishReason,
isError: false,
};
}
@ -218,6 +247,7 @@ class DevChat {
user: "",
date: "",
response: stderr,
finish_reason: "",
isError: true,
};
}
@ -230,6 +260,7 @@ class DevChat {
user: "",
date: "",
response: `Error: ${error.stderr}\nExit code: ${error.code}`,
finish_reason: "",
isError: true,
};
}

View File

@ -1,5 +1,5 @@
{
"name": "run_shell",
"name": "run_shell_file",
"description": "run shell script",
"type": [
"shell"

View File

@ -1,7 +0,0 @@
{
"pattern": "auto",
"description": "Generate commands for the bot to execute",
"message": "",
"default": false,
"instructions": ["./chat/action.instr"]
}