chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,13 @@
|
||||
import rootConfig from '../../eslint.config.mjs'
|
||||
|
||||
export default [
|
||||
...rootConfig,
|
||||
{
|
||||
languageOptions: {
|
||||
parserOptions: {
|
||||
project: ['./tsconfig.json'],
|
||||
tsconfigRootDir: import.meta.dirname,
|
||||
},
|
||||
},
|
||||
},
|
||||
]
|
||||
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"name": "@botpresshub/knowledge",
|
||||
"scripts": {
|
||||
"check:type": "tsc --noEmit",
|
||||
"build": "bp add -y && bp build",
|
||||
"test": "vitest --run"
|
||||
},
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@botpress/client": "workspace:*",
|
||||
"@botpress/sdk": "workspace:*",
|
||||
"json5": "^2.2.3",
|
||||
"jsonrepair": "^3.10.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@botpress/cli": "workspace:*",
|
||||
"@botpress/common": "workspace:*",
|
||||
"@botpress/sdk": "workspace:*",
|
||||
"@botpresshub/llm": "workspace:*",
|
||||
"@bpinternal/genenv": "0.0.1",
|
||||
"@types/semver": "^7.3.11",
|
||||
"semver": "^7.3.8"
|
||||
},
|
||||
"bpDependencies": {
|
||||
"llm": "../../interfaces/llm"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import * as sdk from '@botpress/sdk'
|
||||
import llm from './bp_modules/llm'
|
||||
|
||||
export default new sdk.PluginDefinition({
|
||||
name: 'knowledge',
|
||||
version: '1.0.0',
|
||||
configuration: { schema: sdk.z.object({}) },
|
||||
interfaces: {
|
||||
llm: sdk.version.allWithinMajorOf(llm),
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,31 @@
|
||||
import JSON5 from 'json5'
|
||||
import { jsonrepair } from 'jsonrepair'
|
||||
import * as bp from '.botpress'
|
||||
|
||||
export type LLMInput = bp.interfaces.llm.actions.generateContent.input.Input
|
||||
export type LLMOutput = bp.interfaces.llm.actions.generateContent.output.Output
|
||||
|
||||
export type LLMMessage = LLMInput['messages'][number]
|
||||
export type LLMChoice = LLMOutput['choices'][number]
|
||||
|
||||
type PredictResponse = {
|
||||
success: boolean
|
||||
json: object
|
||||
}
|
||||
|
||||
const tryParseJson = (str: string) => {
|
||||
try {
|
||||
return JSON5.parse(jsonrepair(str))
|
||||
} catch {
|
||||
return str
|
||||
}
|
||||
}
|
||||
|
||||
export const parseLLMOutput = (output: LLMOutput): PredictResponse => {
|
||||
const mappedChoices: LLMChoice['content'][] = output.choices.map((choice) => choice.content)
|
||||
const firstChoice = mappedChoices[0]!
|
||||
return {
|
||||
success: true,
|
||||
json: tryParseJson(firstChoice as string),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
import * as gen from './generate-content'
|
||||
import * as questions from './question-prompt'
|
||||
import * as bp from '.botpress'
|
||||
|
||||
const plugin = new bp.Plugin({
|
||||
actions: {},
|
||||
})
|
||||
|
||||
plugin.on.beforeIncomingMessage('*', async ({ data: message, client, ctx, actions }) => {
|
||||
if (message.type !== 'text') {
|
||||
console.debug('Ignoring non-text message')
|
||||
return
|
||||
}
|
||||
|
||||
const text: string = message.payload.text
|
||||
if (!text) {
|
||||
console.debug('Ignoring empty message')
|
||||
return
|
||||
}
|
||||
|
||||
console.debug('Extracting questions from:', text)
|
||||
|
||||
const llmInput = questions.prompt({ text, line: 'L1' })
|
||||
const llmOutput = await actions.llm.generateContent(llmInput)
|
||||
|
||||
const { success, json } = gen.parseLLMOutput(llmOutput)
|
||||
if (!success) {
|
||||
console.debug('Failed to extract questions')
|
||||
return
|
||||
}
|
||||
|
||||
const parsedResult = questions.OutputFormat.safeParse(json)
|
||||
if (!parsedResult.success) {
|
||||
console.debug('Failed to extract questions')
|
||||
return
|
||||
}
|
||||
|
||||
const { data } = parsedResult
|
||||
if (!data.hasQuestions || !data.questions?.length) {
|
||||
console.debug('No questions extracted')
|
||||
return
|
||||
}
|
||||
|
||||
const canonicalQuestion = data.questions.map((question) => question.resolved_question).join(' ')
|
||||
|
||||
console.debug('Searching for:', canonicalQuestion)
|
||||
const { passages } = await client.searchFiles({
|
||||
query: canonicalQuestion,
|
||||
})
|
||||
|
||||
if (!passages.length) {
|
||||
console.debug('No passages found')
|
||||
return
|
||||
}
|
||||
|
||||
// TODO: replace by proper answer generation
|
||||
const answer = passages.map((p) => p.content).join('\n')
|
||||
|
||||
await client.createMessage({
|
||||
conversationId: message.conversationId,
|
||||
userId: ctx.botId,
|
||||
payload: {
|
||||
text: answer,
|
||||
},
|
||||
tags: {},
|
||||
type: 'text',
|
||||
})
|
||||
|
||||
return { stop: true }
|
||||
})
|
||||
|
||||
export default plugin
|
||||
@@ -0,0 +1,255 @@
|
||||
import { z } from '@botpress/sdk'
|
||||
import { LLMInput, LLMMessage } from './generate-content'
|
||||
|
||||
export type ExtractedQuestion = z.infer<typeof ExtractedQuestion>
|
||||
export const ExtractedQuestion = z.object({
|
||||
line: z.string().describe('The line number of the question (must be prefixed with "L")'),
|
||||
raw_question: z.string().describe('The raw question extracted from the user message'),
|
||||
resolved_question: z.string().describe('The resolved question with any missing context filled in'),
|
||||
search_query: z.string().describe('The search query that would be used to find the answer in a search engine'),
|
||||
})
|
||||
|
||||
export type OutputFormat = z.infer<typeof OutputFormat>
|
||||
export const OutputFormat = z.object({
|
||||
hasQuestions: z.boolean().describe('Whether or not questions were found in the user message'),
|
||||
questions: z
|
||||
.array(ExtractedQuestion)
|
||||
.describe('List of extracted questions, or an empty array if no questions are found')
|
||||
.optional(),
|
||||
})
|
||||
|
||||
type Example = {
|
||||
output: Array<Omit<ExtractedQuestion, 'line'>>
|
||||
input: {
|
||||
context: string
|
||||
user_message: string
|
||||
}
|
||||
}
|
||||
|
||||
const formatQuestionExtractorMessage = (
|
||||
text: string,
|
||||
context: string
|
||||
): { user_message_line: string; message: string } => {
|
||||
const contextLines = context.split('\n').map((line, index) => `[L${index + 1}]\t ${line}`)
|
||||
const user_message_line = `L${contextLines.length + 1}`
|
||||
|
||||
return {
|
||||
user_message_line,
|
||||
message: `
|
||||
<CONTEXT>
|
||||
${contextLines.join('\n')}
|
||||
</CONTEXT>
|
||||
|
||||
<USER MESSAGE>
|
||||
[${user_message_line}]\t ${text}
|
||||
</USER MESSAGE>`.trim(),
|
||||
}
|
||||
}
|
||||
|
||||
const makeExample = (props: Example): LLMMessage[] => {
|
||||
const { message, user_message_line } = formatQuestionExtractorMessage(props.input.user_message, props.input.context)
|
||||
return [
|
||||
{
|
||||
role: 'user',
|
||||
content: message,
|
||||
},
|
||||
{
|
||||
role: 'assistant',
|
||||
content: JSON.stringify({
|
||||
hasQuestions: props.output.length > 0,
|
||||
questions: props.output.map((o) => ({
|
||||
line: user_message_line,
|
||||
raw_question: o.raw_question,
|
||||
resolved_question: o.resolved_question,
|
||||
search_query: o.search_query,
|
||||
})),
|
||||
} satisfies OutputFormat),
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
export type PromptArgs = {
|
||||
text: string
|
||||
line: string
|
||||
}
|
||||
export const prompt = (args: PromptArgs): LLMInput => ({
|
||||
responseFormat: 'json_object',
|
||||
temperature: 0,
|
||||
systemPrompt: `
|
||||
You are a question extractor.
|
||||
You will be given a USER MESSAGE and a CONTEXT of the conversation so far.
|
||||
The CONTEXT should not be analyzed, only the USER MESSAGE. The purpose of the CONTEXT is to provide context for the USER MESSAGE.
|
||||
|
||||
Your goal is to respond with the a list of question(s) found in the USER MESSAGE, if any.
|
||||
For the purpose of this task, a question is defined as any sentence that is asking for information or is seeking an answer. This includes direct questions, indirect questions, rhetorical questions, search engine queries etc.
|
||||
|
||||
Include the raw_question (with no modifications), the resolved_question (with any missing context filled in), and the search_query (the query that would be used to find the answer in a search engine). If no relevant context is found, the resolved_question should be the same as the raw_question.
|
||||
Questions should be extracted with the following JSON format:
|
||||
|
||||
[
|
||||
{
|
||||
"line": 'L34',
|
||||
"raw_question": "how old is he",
|
||||
"resolved_question": "how old is he (Justin Timberlake)?",
|
||||
"search_query": "\"Justin Timberlake\" age",
|
||||
}
|
||||
]
|
||||
|
||||
If there are no questions in the USER MESSAGE, return an empty array.
|
||||
|
||||
Always respond in JSON with the following format:
|
||||
type OutputFormat = ${ExtractedQuestion.toTypescriptType({ treatDefaultAsOptional: true })}
|
||||
|
||||
The below examples are for illustrative purposes only. Your responses will be evaluated based on the quality of the questions extracted.
|
||||
Please extract the questions found on line L${args.line} only. If there are no questions, return an empty array.
|
||||
`.trim(),
|
||||
messages: [
|
||||
...makeExample({
|
||||
input: {
|
||||
context: `
|
||||
Summary:
|
||||
"""
|
||||
The conversation so far is about the population of the United States.
|
||||
"""
|
||||
|
||||
Transcript:
|
||||
"""
|
||||
user: how many people live in the united states?
|
||||
bot: The population of the United States is 331 million people.
|
||||
"""`,
|
||||
user_message: 'tell me the same for canada, japan and china',
|
||||
},
|
||||
output: [
|
||||
{
|
||||
raw_question: 'tell me the same for canada, japan and china',
|
||||
resolved_question: 'tell me the population for Canada, Japan and China',
|
||||
search_query: 'current population of Canada, Japan, China',
|
||||
},
|
||||
],
|
||||
}),
|
||||
...makeExample({
|
||||
input: {
|
||||
context: `
|
||||
Summary:
|
||||
"""
|
||||
The conversation so far is about organizing an event for a company called Blue Bridge.
|
||||
"""
|
||||
|
||||
Transcript:
|
||||
"""
|
||||
user: the name of the event is Night of the Stars
|
||||
bot: Got it, who should I send invitations to?
|
||||
"""`,
|
||||
user_message: "hmmm i'm not sure, list people in marketing and sales and i'll tell you",
|
||||
},
|
||||
output: [
|
||||
{
|
||||
raw_question: 'list people in marketing and sales',
|
||||
resolved_question: 'list people in marketing and sales (at Blue Bridge)',
|
||||
search_query: '"Blue Bridge" sales and marketing employee directory',
|
||||
},
|
||||
],
|
||||
}),
|
||||
...makeExample({
|
||||
input: {
|
||||
context: '',
|
||||
user_message: 'what is ptow',
|
||||
},
|
||||
output: [
|
||||
{
|
||||
raw_question: 'what is ptow',
|
||||
resolved_question: 'what is ptow?',
|
||||
search_query: 'ptow meaning',
|
||||
},
|
||||
],
|
||||
}),
|
||||
...makeExample({
|
||||
input: {
|
||||
context: '',
|
||||
user_message: 'sure? lets do it',
|
||||
},
|
||||
output: [],
|
||||
}),
|
||||
...makeExample({
|
||||
input: {
|
||||
context: `
|
||||
User Information:
|
||||
"""
|
||||
Name: Alex
|
||||
Language: English
|
||||
Location: United States
|
||||
City: New York
|
||||
"""
|
||||
|
||||
Transcript:
|
||||
"""
|
||||
user: hello!
|
||||
bot: Hello! How can I help you today?
|
||||
"""`,
|
||||
user_message: 'what is the weather?',
|
||||
},
|
||||
output: [
|
||||
{
|
||||
raw_question: 'what is the weather?',
|
||||
resolved_question: 'what is the weather (in New York)?',
|
||||
search_query: 'current weather in New York',
|
||||
},
|
||||
],
|
||||
}),
|
||||
...makeExample({
|
||||
input: {
|
||||
context: `
|
||||
Transcript:
|
||||
"""
|
||||
user: hello!
|
||||
bot: Hello! How can I help you today?
|
||||
user: can you give me a list of the top 10 movies?
|
||||
"""`,
|
||||
user_message: 'sorry, make it 3 after all',
|
||||
},
|
||||
output: [],
|
||||
}),
|
||||
...makeExample({
|
||||
input: {
|
||||
context: `
|
||||
Transcript:
|
||||
"""
|
||||
"""`,
|
||||
user_message: 'How tall is Patrick?',
|
||||
},
|
||||
output: [
|
||||
{
|
||||
raw_question: 'How tall is Patrick',
|
||||
resolved_question: 'How tall is Patrick',
|
||||
search_query: 'How tall is Patrick',
|
||||
},
|
||||
],
|
||||
}),
|
||||
...makeExample({
|
||||
input: {
|
||||
context: `
|
||||
Transcript:
|
||||
"""
|
||||
user: tell me about cindy
|
||||
bot: Cindy is 45 years old and is currently a software engineer at Google.
|
||||
user: what is her salary?
|
||||
bot: She earned $142,000 per year in 2022.
|
||||
user: what about her husband?
|
||||
bot: Her husband, Danny, is a doctor.
|
||||
"""`,
|
||||
user_message: 'I see! I think I met him once at a party. I wonder if he make more than her',
|
||||
},
|
||||
output: [
|
||||
{
|
||||
raw_question: 'I wonder if he make more than her',
|
||||
resolved_question: 'I wonder if Danny makes more than 142,000 per year?',
|
||||
search_query: 'Danny (doctor) salary comparison to Cindy (software engineer)',
|
||||
},
|
||||
],
|
||||
}),
|
||||
{
|
||||
role: 'user',
|
||||
content: args.text,
|
||||
},
|
||||
],
|
||||
})
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"paths": { "*": ["./*"] },
|
||||
"outDir": "dist"
|
||||
},
|
||||
"include": [".botpress/**/*", "definitions/**/*", "src/**/*", "*.ts"]
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
import config from '../../vitest.config'
|
||||
export default config
|
||||
Reference in New Issue
Block a user