botserver/packages/gpt.gblib/services/ChatServices.ts

332 lines
11 KiB
TypeScript
Raw Normal View History

2023-07-23 10:59:59 -03:00
/*****************************************************************************\
2024-01-09 17:40:48 -03:00
| ® |
| |
| |
| |
| |
2023-07-23 10:59:59 -03:00
| |
| General Bots Copyright (c) pragmatismo.com.br. All rights reserved. |
2023-07-23 10:59:59 -03:00
| Licensed under the AGPL-3.0. |
| |
| According to our dual licensing model, this program can be used either |
| under the terms of the GNU Affero General Public License, version 3, |
| or under a proprietary license. |
| |
| The texts of the GNU Affero General Public License with an additional |
| permission and of our proprietary license can be found at and |
| in the LICENSE file you have received along with this program. |
| |
| This program is distributed in the hope that it will be useful, |
| but WITHOUT ANY WARRANTY, without even the implied warranty of |
| MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| GNU Affero General Public License for more details. |
| |
2024-01-10 14:52:01 -03:00
| "General Bots" is a registered trademark of pragmatismo.com.br. |
2023-07-23 10:59:59 -03:00
| The licensing of the program under the AGPLv3 does not imply a |
| trademark license. Therefore any rights, title and interest in |
| our trademarks remain entirely with us. |
| |
\*****************************************************************************/
'use strict';
2024-03-11 13:30:11 -03:00
import { HNSWLib } from '@langchain/community/vectorstores/hnswlib';
import { StringOutputParser } from "@langchain/core/output_parsers";
import { AIMessagePromptTemplate, ChatPromptTemplate, HumanMessagePromptTemplate, MessagesPlaceholder } from '@langchain/core/prompts';
2024-03-10 00:16:24 -03:00
import { RunnableSequence } from "@langchain/core/runnables";
2024-03-11 13:30:11 -03:00
import { convertToOpenAITool } from "@langchain/core/utils/function_calling";
2024-03-10 00:16:24 -03:00
import { ChatOpenAI } from "@langchain/openai";
2024-03-11 13:30:11 -03:00
import { GBLog, GBMinInstance } from 'botlib';
2024-03-10 00:16:24 -03:00
import * as Fs from 'fs';
import { jsonSchemaToZod } from "json-schema-to-zod";
import { BufferWindowMemory } from 'langchain/memory';
2024-03-10 00:16:24 -03:00
import Path from 'path';
import { CollectionUtil } from 'pragmatismo-io-framework';
import { DialogKeywords } from '../../basic.gblib/services/DialogKeywords.js';
2024-03-04 20:05:56 -03:00
import { GBVMService } from '../../basic.gblib/services/GBVMService.js';
2024-03-10 00:16:24 -03:00
import { GBConfigService } from '../../core.gbapp/services/GBConfigService.js';
import { GuaribasSubject } from '../../kb.gbapp/models/index.js';
2024-03-11 13:30:11 -03:00
import { z } from "zod";
import { DynamicStructuredTool } from "@langchain/core/tools";
import { JsonOutputToolsParser } from "langchain/output_parsers";
import {
RunnableLambda,
RunnablePassthrough,
} from "@langchain/core/runnables";
import {
CombiningOutputParser,
} from "langchain/output_parsers";
import {
BaseLLMOutputParser,
OutputParserException,
} from "@langchain/core/output_parsers";
import { ChatGeneration, Generation } from "@langchain/core/outputs";
export interface CustomOutputParserFields { }
// This can be more generic, like Record<string, string>
export type ExpectedOutput = {
greeting: string;
};
function isChatGeneration(
llmOutput: ChatGeneration | Generation
): llmOutput is ChatGeneration {
return "message" in llmOutput;
}
export class CustomLLMOutputParser extends BaseLLMOutputParser<ExpectedOutput> {
lc_namespace = ["langchain", "output_parsers"];
2024-03-10 00:16:24 -03:00
2024-03-11 13:30:11 -03:00
constructor(fields?: CustomOutputParserFields) {
super(fields);
}
async parseResult(
llmOutputs: ChatGeneration[] | Generation[]
): Promise<ExpectedOutput> {
if (!llmOutputs.length) {
throw new OutputParserException(
"Output parser did not receive any generations."
);
}
let parsedOutput;
2024-03-04 20:05:56 -03:00
2023-07-23 10:59:59 -03:00
2024-03-11 13:30:11 -03:00
if (isChatGeneration(llmOutputs[0])) {
parsedOutput = llmOutputs[0].message.content;
} else {
parsedOutput = llmOutputs[0].text;
}
let parsedText;
parsedText = parsedOutput;
return parsedText;
}
}
export class ChatServices {
private static async getRelevantContext(
vectorStore: HNSWLib,
sanitizedQuestion: string,
numDocuments: number
): Promise<string> {
2024-03-11 13:30:11 -03:00
if (sanitizedQuestion === '') {
return '';
}
const documents = await vectorStore.similaritySearch(sanitizedQuestion, numDocuments);
return documents
.map((doc) => doc.pageContent)
.join(', ')
.trim()
.replaceAll('\n', ' ');
}
2023-07-23 10:59:59 -03:00
/**
* Generate text
*
2023-07-23 10:59:59 -03:00
* CONTINUE keword.
*
2023-07-23 10:59:59 -03:00
* result = CONTINUE text
*
2023-07-23 10:59:59 -03:00
*/
2024-03-11 13:30:11 -03:00
public static async continue(min: GBMinInstance, question: string, chatId) {
2023-07-23 10:59:59 -03:00
}
2024-03-04 20:05:56 -03:00
2024-03-11 13:30:11 -03:00
public static async answerByGPT(min: GBMinInstance, user, pid,
question: string,
searchScore: number,
subjects: GuaribasSubject[]
) {
2024-03-06 14:38:37 -03:00
if (!process.env.OPENAI_API_KEY) {
return { answer: undefined, questionId: 0 };
}
2024-03-11 13:30:11 -03:00
const contentLocale = min.core.getParam(
min.instance,
'Default Content Language',
GBConfigService.get('DEFAULT_CONTENT_LANGUAGE')
);
2024-03-03 16:20:50 -03:00
2024-03-10 00:16:24 -03:00
let tools = await ChatServices.getTools(min);
let toolsAsText = ChatServices.getToolsAsText(tools);
2024-03-03 16:20:50 -03:00
2024-03-11 13:30:11 -03:00
const toolMap: Record<string, any> = {
multiply: tools[0]
};
2024-03-10 00:16:24 -03:00
const memory = new BufferWindowMemory({
returnMessages: true,
memoryKey: 'chat_history',
inputKey: 'input',
k: 2,
});
2024-03-04 20:05:56 -03:00
2024-03-10 00:16:24 -03:00
const model = new ChatOpenAI({
openAIApiKey: process.env.OPENAI_API_KEY,
modelName: "gpt-3.5-turbo-0125",
temperature: 0,
});
2024-03-04 20:05:56 -03:00
2024-03-11 13:30:11 -03:00
const context = min['vectorStore'];
2024-03-10 00:16:24 -03:00
const modelWithTools = model.bind({
tools: tools.map(convertToOpenAITool),
2024-03-11 13:30:11 -03:00
tool_choice: {
type: "function",
function: { name: "multiply" },
},
2024-03-10 00:16:24 -03:00
});
2024-03-11 13:30:11 -03:00
// Function for dynamically constructing the end of the chain based on the model-selected tool.
const callSelectedTool = RunnableLambda.from(
(toolInvocation: Record<string, any>) => {
const selectedTool = toolMap[toolInvocation.type];
if (!selectedTool) {
throw new Error(
`No matching tool available for requested type "${toolInvocation.type}".`
);
}
const toolCallChain = RunnableSequence.from([
(toolInvocation) => toolInvocation.args,
selectedTool,
]);
// We use `RunnablePassthrough.assign` here to return the intermediate `toolInvocation` params
// as well, but you can omit if you only care about the answer.
return RunnablePassthrough.assign({
output: toolCallChain,
});
},
);
2024-03-10 00:16:24 -03:00
const questionGeneratorTemplate = ChatPromptTemplate.fromMessages([
AIMessagePromptTemplate.fromTemplate(
"Given the following conversation about a codebase and a follow up question, rephrase the follow up question to be a standalone question."
),
new MessagesPlaceholder("chat_history"),
2024-03-11 13:30:11 -03:00
AIMessagePromptTemplate.fromTemplate(`Follow Up Input: {question}
Standalone question:`),
2024-03-10 00:16:24 -03:00
]);
const combineDocumentsPrompt = ChatPromptTemplate.fromMessages([
AIMessagePromptTemplate.fromTemplate(
2024-03-11 13:30:11 -03:00
`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.
\n\n{context}\n\n
You have the following tools to call:
${toolsAsText}`
2024-03-10 00:16:24 -03:00
),
new MessagesPlaceholder("chat_history"),
HumanMessagePromptTemplate.fromTemplate("Question: {question}"),
]);
const combineDocumentsChain = RunnableSequence.from([
{
question: (output: string) => output,
chat_history: async () => {
const { chat_history } = await memory.loadMemoryVariables({});
return chat_history;
},
context: async (output: string) => {
2024-03-11 13:30:11 -03:00
return await ChatServices.getRelevantContext(context, output, 1);
2024-03-10 00:16:24 -03:00
},
},
combineDocumentsPrompt,
modelWithTools,
2024-03-11 13:30:11 -03:00
new CustomLLMOutputParser(),
2024-03-10 00:16:24 -03:00
]);
2024-03-11 13:30:11 -03:00
2024-03-10 00:16:24 -03:00
const conversationalQaChain = RunnableSequence.from([
{
question: (i: { question: string }) => i.question,
chat_history: async () => {
const { chat_history } = await memory.loadMemoryVariables({});
return chat_history;
},
},
questionGeneratorTemplate,
modelWithTools,
new StringOutputParser(),
combineDocumentsChain,
]);
2024-03-11 13:30:11 -03:00
const systemPrompt = user['systemPrompt'];
2024-03-10 00:16:24 -03:00
let result = await conversationalQaChain.invoke({
question,
});
2024-03-11 13:30:11 -03:00
// await memory.saveContext(
// {
// input: query,
// },
// {
// output: result,
// }
// );
GBLog.info(`GPT Result: ${result.toString()}`);
return { answer: result.toString(), questionId: 0 };
2024-03-03 16:20:50 -03:00
2024-03-10 00:16:24 -03:00
}
2024-03-03 16:20:50 -03:00
2024-03-10 00:16:24 -03:00
private static getToolsAsText(tools) {
return Object.keys(tools)
2024-03-11 13:30:11 -03:00
.map((toolname) => `${tools[toolname].name}: ${tools[toolname].description}`)
2024-03-10 00:16:24 -03:00
.join("\n");
}
2024-03-03 16:20:50 -03:00
2024-03-10 00:16:24 -03:00
private static async getTools(min: GBMinInstance) {
let functions = [];
2024-03-10 00:16:24 -03:00
// 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 functionJSON = Path.join('work', path, `${script}.json`);
2024-03-04 20:05:56 -03:00
2024-03-10 00:16:24 -03:00
if (Fs.existsSync(functionJSON)) {
const func = JSON.parse(Fs.readFileSync(functionJSON, 'utf8'));
2024-03-11 13:30:11 -03:00
func.schema = jsonSchemaToZod(func.properties, { module: "esm" });
func.func = async () => {
const name = '';
const pid = 1;
const text = ''; // TODO:
const result = await GBVMService.callVM(name, min, false, pid, false, [text]);
2024-03-04 20:05:56 -03:00
2024-03-11 13:30:11 -03:00
}
2024-03-10 00:16:24 -03:00
functions.push(func);
2024-03-04 20:05:56 -03:00
}
2024-03-10 00:16:24 -03:00
});
2024-03-11 13:30:11 -03:00
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);
2024-03-10 00:16:24 -03:00
return functions;
}
}