new(security.gblib): SMS Auth.

This commit is contained in:
Rodrigo Rodriguez 2024-03-11 13:30:11 -03:00
parent 158c1603c9
commit a7f345592a
12 changed files with 341 additions and 96 deletions

View file

@ -116,7 +116,7 @@
"botframework-connector": "4.18.0",
"botlib": "3.0.11",
"c3-chart-maker": "0.2.8",
"cd": "0.3.3",
"cd": "^0.3.3",
"chalk-animation": "^2.0.3",
"chatgpt": "2.4.2",
"chrome-remote-interface": "0.31.3",

View file

@ -466,7 +466,7 @@ export class GBVMService extends GBService {
}
} while (include);
let { code, map, metadata, tasks } = await this.convert(filename, mainName, basicCode);
let { code, map, metadata, tasks, systemPrompt } = await this.convert(filename, mainName, basicCode);
// Generates function JSON metadata to be used later.
@ -474,7 +474,6 @@ export class GBVMService extends GBService {
Fs.writeFileSync(jsonFile, JSON.stringify(metadata));
const mapFile = `${filename}.map`;
Fs.writeFileSync(mapFile, JSON.stringify(map));
// Execute off-line code tasks
@ -836,6 +835,7 @@ export class GBVMService extends GBService {
let description;
let table = null; // Used for TABLE keyword.
let talk = null;
let systemPrompt = null;
let connection = null;
const tasks = [];
let fields = {};
@ -851,7 +851,7 @@ export class GBVMService extends GBService {
line = line.replace(/^\s*\d+\s*/gi, '');
if (!table && !talk) {
if (!table && !talk && !systemPrompt) {
for (let j = 0; j < keywords.length; j++) {
line = line.replace(keywords[j][0], keywords[j][1]); // TODO: Investigate delay here.
}
@ -874,6 +874,15 @@ export class GBVMService extends GBService {
emmit = false;
}
const endSystemPromptKeyword = /^\s*END SYSTEM PROMPT\s*/gim;
let endSystemPromptReg = endSystemPromptKeyword.exec(line);
if (endSystemPromptReg && systemPrompt) {
line = systemPrompt + '`})';
systemPrompt = null;
emmit = true;
}
const endTalkKeyword = /^\s*END TALK\s*/gim;
let endTalkReg = endTalkKeyword.exec(line);
if (endTalkReg && talk) {
@ -883,7 +892,6 @@ export class GBVMService extends GBService {
emmit = true;
}
const endTableKeyword = /^\s*END TABLE\s*/gim;
let endTableReg = endTableKeyword.exec(line);
if (endTableReg && table) {
@ -906,6 +914,13 @@ export class GBVMService extends GBService {
emmit = false;
}
// Inside BEGIN SYSTEM PROMPT
if (systemPrompt) {
systemPrompt += line + '\\n';
emmit = false;
}
// Inside BEGIN/END table pair containing FIELDS.
if (table && line.trim() !== '') {
@ -929,6 +944,13 @@ export class GBVMService extends GBService {
emmit = false;
}
const systemPromptKeyword = /^\s*BEGIN SYSTEM PROMPT\s*/gim;
let systemPromptReg = systemPromptKeyword.exec(line);
if (systemPromptReg && !systemPrompt) {
systemPrompt = "await sys.setSystemPrompt ({pid: pid, text: `";
emmit = false;
}
// Add additional lines returned from replacement.
let add = emmit ? line.split(/\r\n|\r|\n/).length : 0;
@ -952,7 +974,7 @@ export class GBVMService extends GBService {
let metadata = GBVMService.getMetadata(mainName, properties, description);
return { code, map, metadata, tasks };
return { code, map, metadata, tasks, systemPrompt };
}
/**

View file

@ -1440,6 +1440,20 @@ export class SystemKeywords {
return ret;
}
public static async setSystemPrompt({ pid, systemPrompt }) {
let { min, user } = await DialogKeywords.getProcessInfo(pid);
const sec = new SecService();
if (user) {
user['systemPrompt'] = systemPrompt;
const path = DialogKeywords.getGBAIPath(min.botId);
const systemPromptFile = urlJoin(process.cwd(), 'work', path, 'users',user.userSystemId, 'systemPrompt.txt');
Fs.writeFileSync(systemPromptFile, systemPrompt);
}
}
/**
* Creates a folder in the bot instance drive.
*

View file

@ -68,6 +68,11 @@ import { GBUtil } from '../../../src/util.js';
* services like NLP or Search.
*/
export class GBConversationalService {
public async getNewMobileCode(){
throw new Error('Method removed.');
}
/**
* Reference to the core service.
*/
@ -280,20 +285,6 @@ export class GBConversationalService {
return str;
}
public getNewMobileCode() {
const passwordGenerator = new PasswordGenerator();
const options = {
upperCaseAlpha: false,
lowerCaseAlpha: false,
number: true,
specialCharacter: false,
minimumLength: 4,
maximumLength: 4
};
let code = passwordGenerator.generatePassword(options);
return code;
}
public getCurrentLanguage(step: GBDialogStep) {
return step.context.activity.locale;
}

View file

@ -768,9 +768,9 @@ export class GBMinService {
min['scheduleMap'] = {};
min['conversationWelcomed'] = {};
if (process.env.OPENAI_API_KEY) {
min['vectorStore'] = await this.deployer.loadOrCreateEmptyVectorStore(min);
const gbkbPath = DialogKeywords.getGBAIPath(min.botId, 'gbkb');
min['vectorStorePath'] = Path.join('work', gbkbPath, 'docs-vectorized');
min['vectorStore'] = await this.deployer.loadOrCreateEmptyVectorStore(min);
}
min['apiConversations'] = {};
min.packages = sysPackages;

View file

@ -30,30 +30,81 @@
'use strict';
import { RunnableSequence } from "@langchain/core/runnables";
import { ChatOpenAI } from "@langchain/openai";
import { GBMinInstance } from 'botlib';
import * as Fs from 'fs';
import { formatXml } from "langchain/agents/format_scratchpad/xml";
import { jsonSchemaToZod } from "json-schema-to-zod";
import { renderTextDescription } from "langchain/tools/render";
import { AgentExecutor, AgentStep } from "langchain/agents";
import { BufferWindowMemory } from 'langchain/memory';
import { AIMessagePromptTemplate, ChatPromptTemplate, HumanMessagePromptTemplate, MessagesPlaceholder } from '@langchain/core/prompts';
import { Tool } from "langchain/tools";
import { HNSWLib } from '@langchain/community/vectorstores/hnswlib';
import { StringOutputParser } from "@langchain/core/output_parsers";
import { AIMessagePromptTemplate, ChatPromptTemplate, HumanMessagePromptTemplate, MessagesPlaceholder } from '@langchain/core/prompts';
import { RunnableSequence } from "@langchain/core/runnables";
import { convertToOpenAITool } from "@langchain/core/utils/function_calling";
import { ChatOpenAI } from "@langchain/openai";
import { GBLog, GBMinInstance } from 'botlib';
import * as Fs from 'fs';
import { jsonSchemaToZod } from "json-schema-to-zod";
import { BufferWindowMemory } from 'langchain/memory';
import Path from 'path';
import { CollectionUtil } from 'pragmatismo-io-framework';
import { DialogKeywords } from '../../basic.gblib/services/DialogKeywords.js';
import { GBVMService } from '../../basic.gblib/services/GBVMService.js';
import { GBConfigService } from '../../core.gbapp/services/GBConfigService.js';
import { GuaribasSubject } from '../../kb.gbapp/models/index.js';
import { XMLAgentOutputParser } from "langchain/agents/xml/output_parser";
import { StringOutputParser } from "@langchain/core/output_parsers";
import { convertToOpenAITool } from "@langchain/core/utils/function_calling";
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"];
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;
if (isChatGeneration(llmOutputs[0])) {
parsedOutput = llmOutputs[0].message.content;
} else {
parsedOutput = llmOutputs[0].text;
}
let parsedText;
parsedText = parsedOutput;
return parsedText;
}
}
export class ChatServices {
@ -62,6 +113,10 @@ export class ChatServices {
sanitizedQuestion: string,
numDocuments: number
): Promise<string> {
if (sanitizedQuestion === '') {
return '';
}
const documents = await vectorStore.similaritySearch(sanitizedQuestion, numDocuments);
return documents
.map((doc) => doc.pageContent)
@ -78,13 +133,13 @@ export class ChatServices {
* result = CONTINUE text
*
*/
public static async continue(min: GBMinInstance, text: string, chatId) {
public static async continue(min: GBMinInstance, question: string, chatId) {
}
public static async answerByGPT(min: GBMinInstance, pid,
query: string,
public static async answerByGPT(min: GBMinInstance, user, pid,
question: string,
searchScore: number,
subjects: GuaribasSubject[]
) {
@ -93,6 +148,7 @@ export class ChatServices {
return { answer: undefined, questionId: 0 };
}
const contentLocale = min.core.getParam(
min.instance,
'Default Content Language',
@ -102,6 +158,10 @@ export class ChatServices {
let tools = await ChatServices.getTools(min);
let toolsAsText = ChatServices.getToolsAsText(tools);
const toolMap: Record<string, any> = {
multiply: tools[0]
};
const memory = new BufferWindowMemory({
returnMessages: true,
memoryKey: 'chat_history',
@ -115,46 +175,55 @@ export class ChatServices {
temperature: 0,
});
const contextVectorStore = min['vectorStore'];
let promptTemplate = `Answer in ${contentLocale}.
You have access to the context (RELEVANTDOCS) provided by the user.
When answering think about whether the question in RELEVANTDOCS, but never mention
to user about the source.
Dont justify your answers. Don't refer to yourself in any of the created content.
Don´t prefix RESPONSE: when answering the user.
RELEVANTDOCS: {context}
QUESTION: """{input}"""
You have the following tools that you can invoke based on the user inquiry.
Tools:
${toolsAsText}
`;
const toolMap: Record<string, any> = {
multiply: ()=>{},
};
const context = min['vectorStore'];
const modelWithTools = model.bind({
tools: tools.map(convertToOpenAITool),
tool_choice: {
type: "function",
function: { name: "multiply" },
},
});
// 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,
});
},
);
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"),
AIMessagePromptTemplate.fromTemplate(`Follow Up Input: {question} Standalone question:`),
AIMessagePromptTemplate.fromTemplate(`Follow Up Input: {question}
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.\n\n{context}\n\n"
`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}`
),
new MessagesPlaceholder("chat_history"),
HumanMessagePromptTemplate.fromTemplate("Question: {question}"),
@ -168,13 +237,13 @@ export class ChatServices {
return chat_history;
},
context: async (output: string) => {
return await ChatServices.getRelevantContext(context, output, 1);
return await this.getRelevantContext(contextVectorStore, output, 1);
},
},
combineDocumentsPrompt,
modelWithTools,
new StringOutputParser(),
new CustomLLMOutputParser(),
]);
const conversationalQaChain = RunnableSequence.from([
@ -191,19 +260,30 @@ export class ChatServices {
combineDocumentsChain,
]);
const question = "How can I initialize a ReAct agent?";
const systemPrompt = user['systemPrompt'];
let result = await conversationalQaChain.invoke({
question,
});
return { answer: result.toString() , questionId: 0 };
// await memory.saveContext(
// {
// input: query,
// },
// {
// output: result,
// }
// );
GBLog.info(`GPT Result: ${result.toString()}`);
return { answer: result.toString(), questionId: 0 };
}
private static getToolsAsText(tools) {
return Object.keys(tools)
.map((toolname) => `${tools[toolname].function.name}: ${tools[toolname].function.description}`)
.map((toolname) => `${tools[toolname].name}: ${tools[toolname].description}`)
.join("\n");
}
@ -218,11 +298,11 @@ export class ChatServices {
if (Fs.existsSync(functionJSON)) {
const func = JSON.parse(Fs.readFileSync(functionJSON, 'utf8'));
func.schema = jsonSchemaToZod(func.properties, { module: "esm" });
func.func = async ()=>{
func.func = async () => {
const name = '';
const pid = 1;
const text = ''; // TODO:
const result = await GBVMService.callVM(name, min, false, pid,false, [text]);
const result = await GBVMService.callVM(name, min, false, pid, false, [text]);
}
@ -230,6 +310,22 @@ export class ChatServices {
}
});
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;
}
}

View file

@ -228,7 +228,14 @@ export class AskDialog extends IGBDialog {
min.instance.searchScore ? min.instance.searchScore : minBoot.instance.searchScore
);
const results = await service.ask(min,step.context.activity['pid'], text, searchScore, null /* user.subjects */);
// Tries to answer by NLP.
let handled = await min.conversationalService.routeNLP(step, min, text);
if (handled) {
return;
}
const results = await service.ask(min, user, step, step.context.activity['pid'], text, searchScore, null /* user.subjects */);
// If there is some result, answer immediately.

View file

@ -269,6 +269,8 @@ export class KBService implements IGBKBService {
public async ask(
min: GBMinInstance,
user,
step,
pid,
query: string,
searchScore: number,
@ -372,11 +374,11 @@ export class KBService implements IGBKBService {
}
}
GBLog.info(
`SEARCH returned LOW level score, calling GPT
`SEARCH returned LOW level score, calling NLP if any,
returnedScore: ${returnedScore} < required (searchScore): ${searchScore}`
);
return await ChatServices.answerByGPT(min,pid,
return await ChatServices.answerByGPT(min, user, pid,
query,
searchScore,
subjects
@ -384,10 +386,6 @@ export class KBService implements IGBKBService {
}
public async getSubjectItems(instanceId: number, parentId: number): Promise<GuaribasSubject[]> {
const where = { parentSubjectId: parentId, instanceId: instanceId };

View file

@ -0,0 +1,103 @@
/*****************************************************************************\
| ® |
| |
| |
| |
| |
| |
| General Bots Copyright (c) pragmatismo.com.br. All rights reserved. |
| 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. |
| |
| "General Bots" is a registered trademark of pragmatismo.com.br. |
| 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. |
| |
\*****************************************************************************/
/**
* @fileoverview Dialog for handling OAuth scenarios.
*/
'use strict';
import { TokenResponse } from 'botbuilder';
import { GBLog, GBMinInstance, IGBDialog } from 'botlib';
import { Messages } from '../strings.js';
import { GBAdminService } from '../../admin.gbapp/services/GBAdminService.js';
import { SecService } from '../services/SecService.js';
/**
* Dialogs for handling Menu control.
*/
export class SMSAuthDialog extends IGBDialog {
public static getSMSAuthDialog(min: GBMinInstance) {
return {
id: '/smsauth',
waterfall: [
async (step) => {
const msg = 'Por favor, qual o seu celular? Ex: 55 21 99999-0000.';
step.activeDialog.state.resetInfo = {};
return await min.conversationalService.prompt(min, step, msg);
},
async (step) => {
await step.context.sendActivity('Por favor, digite o código enviado para seu celular.');
const mobile = step.result.replace(/\+|\s|\-/g, '');
const locale = step.context.activity.locale;
step.activeDialog.state.resetInfo.mobile = mobile;
// Generates a new mobile code.
let code = GBAdminService.getMobileCode();
GBLog.info(`SMS Auth: Generated new code: ${code} is being sent.`);
step.activeDialog.state.resetInfo.sentCode = code;
step.activeDialog.state.resetInfo.mobile = mobile;
// Sends a confirmation SMS.
await min.whatsAppDirectLine.sendToDevice(
mobile,
Messages[locale].please_use_code(code)
);
return await min.conversationalService.prompt(min, step, Messages[locale].confirm_mobile);
},
async (step) => {
const typed = step.result;
const locale = step.context.activity.locale;
// Checks if the typed code is equal to the one
// sent to the registered mobile.
if (typed == step.activeDialog.state.resetInfo.sentCode) {
let sec = new SecService();
const member = step.context.activity.from;
GBLog.info(`SMS Auth: User Authenticated.`);
await step.context.sendActivity(Messages[locale].authenticated);
return await step.endDialog(step.activeDialog.state.resetInfo.mobile);
}
else {
await step.context.sendActivity(Messages[locale].not_authorized);
return await step.endDialog(false);
}
}
]
};
}
}

View file

@ -39,6 +39,7 @@ import { Sequelize } from 'sequelize-typescript';
import { OAuthDialog } from './dialogs/OAuthDialog.js';
import { ProfileDialog } from './dialogs/ProfileDialog.js';
import { GuaribasGroup, GuaribasUser, GuaribasUserGroup } from './models/index.js';
import { SMSAuthDialog } from './dialogs/SMSAuthDialog.js';
/**
* Package for the security module.
@ -50,7 +51,8 @@ export class GBSecurityPackage implements IGBPackage {
ProfileDialog.getNameDialog(min),
ProfileDialog.getEmailDialog(min),
ProfileDialog.getMobileDialog(min),
ProfileDialog.getMobileConfirmDialog(min)
ProfileDialog.getMobileConfirmDialog(min),
SMSAuthDialog.getSMSAuthDialog(min)
];
if (process.env.ENABLE_AUTH) {

View file

@ -6,6 +6,8 @@ import { FindOptions } from 'sequelize';
import { DialogKeywords } from '../../../packages/basic.gblib/services/DialogKeywords.js';
import * as Fs from 'fs';
import mkdirp from 'mkdirp';
import urlJoin from 'url-join';
/**
* Security service layer.
@ -26,17 +28,19 @@ export class SecService extends GBService {
}
});
const gbaiPath = DialogKeywords.getGBAIPath(min.botId);
const dir = urlJoin ('work',gbaiPath, 'users', userSystemId);
if (!user) {
user = GuaribasUser.build();
const gbaiPath = DialogKeywords.getGBAIPath(min.botId);
const dir = `work/${gbaiPath}/users/${userSystemId}`;
if (!Fs.existsSync(dir)) {
mkdirp.sync(dir);
}
}
const systemPromptFile = urlJoin(dir, 'systemPrompt.txt');
if (Fs.existsSync(systemPromptFile)) {
user[ 'systemPrompt'] = Fs.readFileSync(systemPromptFile);
}
user.instanceId = min.instance.instanceId;

View file

@ -6,7 +6,11 @@ export const Messages = {
whats_email: "What's your E-mail address?",
validation_enter_name: 'Please enter your full name.',
validation_enter_valid_mobile: 'Please enter a valid mobile number.',
validation_enter_valid_email: 'Please enter a valid e-mail.'
validation_enter_valid_email: 'Please enter a valid e-mail.',
authenticated: 'You are now authenticated.',
not_authorized: 'Wrong verification code. Not authenticated yet. Try again, please.',
please_use_code:(code)=> `Please, answer the Bot with the code: ${code}.`
},
'pt-BR': {
whats_name: 'Qual o seu nome?',
@ -18,6 +22,10 @@ export const Messages = {
código enviado para seu celular.`,
validation_enter_valid_email: 'Por favor, digite um e-mail válido no formato nome@domínio.com.br.',
validation_enter_name: 'Por favor, digite seu nome completo',
validation_enter_valid_mobile: 'Por favor, insira um número de celular válido (ex.: +55 21 98888-7766).'
validation_enter_valid_mobile: 'Por favor, insira um número de celular válido (ex.: +55 21 98888-7766).',
authenticated: 'Você está autenticada(o).',
not_authorized: 'Código de identificação inválido. Não autorizado, tente novamente, por favor.',
please_use_code:(code)=> `Por favor, responda ao bot com o código: ${code}.`
}
};