new(gpt.gblib): GPT Tools and .gbdialog.

This commit is contained in:
Rodrigo Rodriguez 2024-03-15 07:14:21 -03:00
parent a0c3481c7d
commit 359c1beb02
7 changed files with 192 additions and 98 deletions

View file

@ -1,3 +1,4 @@
/*****************************************************************************\
| ® |
| |

View file

@ -1249,43 +1249,60 @@ export class DialogKeywords {
const { min, user } = await DialogKeywords.getProcessInfo(pid);
GBLogEx.info(min,`MESSAGE BOT: ${text}.`);
const conversation = min['apiConversations'][pid];
const { conversation, client} = min['apiConversations'][pid];
conversation.client.apis.Conversations.Conversations_PostActivity({
await client.apis.Conversations.Conversations_PostActivity({
conversationId: conversation.conversationId,
activity: {
textFormat: 'plain',
text: text,
type: 'message',
from: {
id: 'word',
name: 'word'
id: user.userSystemId,
name: user.userName
}
}
});
const watermarkMap = conversation.watermarkMap;
let messages = [];
const response = await conversation.client.apis.Conversations.Conversations_GetActivities({
conversationId: conversation.conversationId,
watermark: conversation.watermark
});
conversation.watermarkMap = response.obj.watermark;
let activities = response.obj.activites;
GBLog.info(`MessageBot: Starting message polling ${conversation.conversationId}).`);
if (activities && activities.length) {
activities = activities.filter(m => m.from.id === min.botId && m.type === 'message');
if (activities.length) {
activities.forEach(activity => {
messages.push({ text: activity.text });
GBLogEx.info(min, `MESSAGE BOT answer from bot: ${activity.text}`);
const worker = async () => {
try {
const response = await client.apis.Conversations.Conversations_GetActivities({
conversationId: conversation.conversationId,
watermark: conversation.watermark
});
conversation.watermarkMap = response.obj.watermark;
let activities = response.obj.activites;
if (activities && activities.length) {
activities = activities.filter(m => m.from.id === min.botId && m.type === 'message');
if (activities.length) {
activities.forEach(activity => {
messages.push({ text: activity.text });
GBLogEx.info(min, `MESSAGE BOT answer from bot: ${activity.text}`);
});
}
}
return messages.join('\n');
} catch (err) {
GBLog.error(
`Error calling printMessages API ${err.data === undefined ? err : err.data} ${err.errObj ? err.errObj.message : ''
}`
);
}
}
return messages.join('\n');
};
setInterval(worker, DEFAULT_HEAR_POLL_INTERVAL);
}

View file

@ -645,6 +645,8 @@ export class GBVMService extends GBService {
}
catch(e){
console.log(e);
reject ({message: e.message, name: e.name});
}
finally{
@ -652,6 +654,8 @@ export class GBVMService extends GBService {
await wa.closeHandles({pid: pid});
await sys.closeHandles({pid: pid});
resolve(true);
}
})();
`;
@ -1085,8 +1089,9 @@ export class GBVMService extends GBService {
try {
if (GBConfigService.get('GBVM') === 'false') {
return await (async () => {
return await new Promise(resolve => {
return await new Promise((resolve, reject) => {
sandbox['resolve'] = resolve;
sandbox['reject'] = reject;
const vm1 = new NodeVM({
allowAsync: true,
sandbox: sandbox,

View file

@ -714,7 +714,7 @@ export class KeywordsExpressions {
keywords[i++] = [
/\= NEW OBJECT/gim,
($0, $1, $2, $3) => {
return ` = {pid: pid}`;
return ` = {}`;
}
];

View file

@ -1440,16 +1440,19 @@ export class SystemKeywords {
return ret;
}
public static async setSystemPrompt({ pid, systemPrompt }) {
public async setSystemPrompt({ pid, text }) {
let { min, user } = await DialogKeywords.getProcessInfo(pid);
const sec = new SecService();
if (user) {
user['systemPrompt'] = systemPrompt;
ChatServices.userSystemPrompt[user.userSystemId] = text;
const path = DialogKeywords.getGBAIPath(min.botId);
const systemPromptFile = urlJoin(process.cwd(), 'work', path, 'users',user.userSystemId, 'systemPrompt.txt');
Fs.writeFileSync(systemPromptFile, systemPrompt);
Fs.writeFileSync(systemPromptFile, text);
}
}

View file

@ -455,8 +455,7 @@ export class GBMinService {
if (!res) {
return 'GeneralBots';
}
if (req.body?.AccountSid)
{
if (req.body?.AccountSid) {
return 'official';
}
return req.body.phone_id ? 'maytapi' : 'chatapi';
@ -842,8 +841,8 @@ export class GBMinService {
await min.whatsAppDirectLine.setup(true);
} else {
if (min !== minBoot && minBoot.instance.whatsappServiceKey
&& min.instance.webchatKey) {
if (min !== minBoot && minBoot.instance.whatsappServiceKey
&& min.instance.webchatKey) {
min.whatsAppDirectLine = new WhatsappDirectLine(
min,
min.botId,
@ -957,7 +956,7 @@ export class GBMinService {
req.body.channelId = req.body.from.channelIdEx === 'whatsapp' ? 'omnichannel' : req.body.channelId;
req.body.group = req.body.from.group;
// Default activity processing and handler.
const handler = async context => {
@ -1027,7 +1026,7 @@ export class GBMinService {
}
let pid = step.context.activity['pid'];
if (!pid){
if (!pid) {
pid = GBVMService.createProcessInfo(user, min, step.context.activity.channelId, null);
}
step.context.activity['pid'] = pid;
@ -1086,7 +1085,7 @@ export class GBMinService {
const startDialog = min.core.getParam(min.instance, 'Start Dialog', null);
if (context.activity.type === 'installationUpdate') {
GBLog.info(`Bot installed on Teams.`);
} else if (context.activity.type === 'conversationUpdate' && context.activity.membersAdded.length > 0) {
@ -1421,7 +1420,7 @@ export class GBMinService {
GBLog.info(
`Auto start (whatsapp) dialog is now being called: ${startDialog} for ${min.instance.instanceId}...`
);
await GBVMService.callVM(startDialog.toLowerCase(), min, step,pid);
await GBVMService.callVM(startDialog.toLowerCase(), min, step, pid);
return;
}
@ -1455,7 +1454,7 @@ export class GBMinService {
} else if (cmdOrDialogName === '/callsch') {
await GBVMService.callVM(args, min, null, pid);
} else if (cmdOrDialogName === '/calldbg') {
await GBVMService.callVM(args, min, step, pid, true);
await GBVMService.callVM(args, min, step, pid, true);
} else {
await step.beginDialog(cmdOrDialogName, { args: args });
}
@ -1613,8 +1612,40 @@ export class GBMinService {
await CollectionUtil.asyncForEach(Object.values(min.scriptMap), async script => {
dialogs[script] = async (data) => {
let params = JSON.parse(data);
return await GBVMService.callVM(script, min, null, params.pid, false, params);
let sec = new SecService();
const user = await sec.ensureUser(
min,
data.userSystemId,
data.userSystemId,
'',
'api',
data.userSystemId,
null
);
let pid = data?.pid;
if (script === 'start'){
pid = GBVMService.createProcessInfo(user, min, 'api', null);
const client = await new SwaggerClient({
spec: JSON.parse(Fs.readFileSync('directline-3.0.json', 'utf8')),
requestInterceptor: req => {
req.headers['Authorization'] = `Bearer ${min.instance.webchatKey}`;
}
});
const response = await client.apis.Conversations.Conversations_StartConversation();
min['apiConversations'][pid] = {conversation: response.obj, client: client};
}
let ret = await GBVMService.callVM(script, min, null, pid, false, data);
if (script === 'start')
{
ret = pid;
}
return ret;
}
});

View file

@ -69,7 +69,7 @@ export class CustomLLMOutputParser extends BaseLLMOutputParser<ExpectedOutput> {
private toolChain: RunnableSequence
private documentChain: RunnableSequence;
constructor( toolChain: RunnableSequence, documentChain: RunnableSequence) {
constructor(toolChain: RunnableSequence, documentChain: RunnableSequence) {
super();
this.toolChain = toolChain;
this.documentChain = documentChain;
@ -83,19 +83,19 @@ export class CustomLLMOutputParser extends BaseLLMOutputParser<ExpectedOutput> {
"Output parser did not receive any generations."
);
}
let parsedOutput;
let result;
if (llmOutputs[0]['message'].lc_kwargs.additional_kwargs.tool_calls) {
this.toolChain.invoke({func: llmOutputs[0]['message'].lc_kwargs.additional_kwargs.tool_calls});
return this.toolChain.invoke({ func: llmOutputs[0]['message'].lc_kwargs.additional_kwargs.tool_calls });
}
if (isChatGeneration(llmOutputs[0])) {
parsedOutput = llmOutputs[0].message.content;
result = llmOutputs[0].message.content;
} else {
parsedOutput = llmOutputs[0].text;
result = llmOutputs[0].text;
}
return this.documentChain.invoke(parsedOutput);
return this.documentChain ? this.documentChain.invoke(result) : result;
}
}
@ -131,6 +131,8 @@ export class ChatServices {
}
private static memoryMap = {};
public static userSystemPrompt = {};
public static async answerByGPT(min: GBMinInstance, user, pid,
question: string,
@ -142,22 +144,24 @@ export class ChatServices {
return { answer: undefined, questionId: 0 };
}
const contentLocale = min.core.getParam(
min.instance,
'Default Content Language',
GBConfigService.get('DEFAULT_CONTENT_LANGUAGE')
);
const docsContext = min['vectorStore'];
const context = min['vectorStore'];
const memory = new BufferWindowMemory({
returnMessages: true,
memoryKey: 'chat_history',
inputKey: 'input',
k: 2,
});
if (!this.memoryMap[user.userSystemId]) {
this.memoryMap[user.userSystemId] = new BufferWindowMemory({
returnMessages: true,
memoryKey: 'chat_history',
inputKey: 'input',
k: 2,
})
}
const memory = this.memoryMap[user.userSystemId];
const systemPrompt = this.userSystemPrompt[user.userSystemId];
const model = new ChatOpenAI({
openAIApiKey: process.env.OPENAI_API_KEY,
@ -168,15 +172,19 @@ export class ChatServices {
let tools = await ChatServices.getTools(min);
let toolsAsText = ChatServices.getToolsAsText(tools);
const modelWithTools = model.bind({
tools: tools.map(convertToOpenAITool)
});
const questionGeneratorTemplate = ChatPromptTemplate.fromMessages([
AIMessagePromptTemplate.fromTemplate(
`Answer the question without calling any tool, but if there is a need to call:
`
Answer the question without calling any tool, but if there is a need to call:
You have access to the following set of tools. Here are the names and descriptions for each tool:
${toolsAsText}
Do not use any previous tools output in the chat_history.
`
),
new MessagesPlaceholder("chat_history"),
@ -184,10 +192,22 @@ export class ChatServices {
Standalone question:`),
]);
const toolsResultPrompt = ChatPromptTemplate.fromMessages([
AIMessagePromptTemplate.fromTemplate(
`The tool just returned value in last call. Using {chat_history}
rephrase the answer to the user using this tool output.
`
),
new MessagesPlaceholder("chat_history"),
AIMessagePromptTemplate.fromTemplate(`Tool output: {tool_output}
Standalone question:`),
]);
const combineDocumentsPrompt = ChatPromptTemplate.fromMessages([
AIMessagePromptTemplate.fromTemplate(
`Use the following pieces of context to answer the question at the end.
`
If you don't know the answer, just say that you don't know, don't try to make up an answer.
Use the following pieces, if any, of context to answer the question at the end.
\n\n{context}\n\n
`
),
@ -197,13 +217,11 @@ export class ChatServices {
const callToolChain = RunnableSequence.from([
{
func: async (output: object) =>{
tool_output: async (output: object) => {
const name = output['func'][0].function.name;
const args = JSON.parse(output['func'][0].function.arguments);
GBLog.info(`Running .gbdialog '${name}' as GPT tool...`);
const pid = GBVMService.createProcessInfo(null, min, 'gpt', null);
return await GBVMService.callVM(name, min, false, pid, false, args);
@ -212,12 +230,13 @@ export class ChatServices {
const { chat_history } = await memory.loadMemoryVariables({});
return chat_history;
},
},
toolsResultPrompt,
model,
new StringOutputParser()
]);
const combineDocumentsChain = RunnableSequence.from([
{
question: (output: string) => output,
@ -226,7 +245,8 @@ export class ChatServices {
return chat_history;
},
context: async (output: string) => {
return await ChatServices.getRelevantContext(context, output, 1);
const c = await ChatServices.getRelevantContext(docsContext, output, 1);
return c ?? 'answer just with user question.';
},
},
@ -245,34 +265,63 @@ export class ChatServices {
},
questionGeneratorTemplate,
modelWithTools,
new CustomLLMOutputParser(callToolChain, combineDocumentsChain)
new CustomLLMOutputParser(callToolChain, docsContext?.docstore?._docs.length > 0 ? combineDocumentsChain : null),
new StringOutputParser()
]);
const systemPrompt = user['systemPrompt'];
const directChain = RunnableSequence.from([
{
question: (i: { question: string }) => {
return `
${systemPrompt}
${i.question}`
},
chat_history: async () => {
const { chat_history } = await memory.loadMemoryVariables({});
return chat_history;
},
},
modelWithTools,
new CustomLLMOutputParser(callToolChain, docsContext?.docstore?._docs.length > 0 ? combineDocumentsChain : null),
new StringOutputParser()
]);
let result = await conversationalQaChain.invoke({
question,
});
const direct = true;
let result;
if (result['name']) {
const func = result['func'];
await func.func(min, result['args']);
if (direct) {
result = await (tools.length > 0 ? modelWithTools : model).invoke(`
${systemPrompt}
${question}`);
} else {
result = result.content;
}
else {
// await memory.saveContext(
// {
// input: query,
// },
// {
// output: result,
// }
// );
GBLog.info(`GPT Result: ${result.toString()}`);
return { answer: result.toString(), questionId: 0 };
result = await (directChain ?? conversationalQaChain).invoke({
question,
});
}
await memory.saveContext(
{
input: question,
},
{
output: result,
}
);
GBLog.info(`GPT Result: ${result.toString()}`);
return { answer: result.toString(), questionId: 0 };
}
private static getToolsAsText(tools) {
@ -286,39 +335,27 @@ export class ChatServices {
// Adds .gbdialog as functions if any to GPT Functions.
await CollectionUtil.asyncForEach(Object.keys(min.scriptMap), async (script) => {
const path = DialogKeywords.getGBAIPath(min.botId, "gbdialog", null);
const jsonFile = Path.join('work', path, `${script}.json`);
if (Fs.existsSync(jsonFile)) {
if (Fs.existsSync(jsonFile) && script.toLowerCase() !== 'start.vbs') {
const funcJSON = JSON.parse(Fs.readFileSync(jsonFile, 'utf8'));
const funcObj = funcJSON?.function;
if (funcObj){
if (funcObj) {
// TODO: Use ajv.
funcObj.schema = eval(jsonSchemaToZod(funcObj.parameters));
functions.push(new DynamicStructuredTool(funcObj));
}
}
});
const multiplyTool = new DynamicStructuredTool({
name: "multiply",
description: "Multiply two integers together.",
schema: z.object({
firstInt: z.number(),
secondInt: z.number(),
}),
func: async ({ firstInt, secondInt }) => {
return (firstInt * secondInt).toString();
},
});
functions.push(multiplyTool);
return functions;
}
}