Signed-off-by: Andrey Sobolev <haiodo@gmail.com>
This commit is contained in:
Andrey Sobolev
2023-02-15 09:14:20 +06:00
committed by GitHub
parent aa8530d678
commit 199da5077a
71 changed files with 888 additions and 312 deletions
+4 -4
View File
@@ -107,7 +107,7 @@ export class OpenAIEmbeddingsStage implements FullTextPipelineStage {
updateSummary (summary: FullSummaryStage): void {
summary.fieldFilter.push((attr, value) => {
const tMarkup = attr.type._class === core.class.TypeMarkup
const lowerCase = value.toLocaleLowerCase()
const lowerCase: string = value.toLocaleLowerCase()
if (tMarkup && (lowerCase.includes('gpt:') || lowerCase.includes('gpt Answer:'))) {
return false
}
@@ -253,11 +253,11 @@ export class OpenAIEmbeddingsStage implements FullTextPipelineStage {
const docs = await this.adapter.searchEmbedding(_classes, query, embedding, {
size,
from,
minScore: -100,
embeddingBoost: 100,
minScore: -9,
embeddingBoost: 10,
field: this.field,
field_enable: this.field_enabled,
fulltextBoost: 10
fulltextBoost: 1
})
return {
docs,
+2 -2
View File
@@ -17,7 +17,7 @@ import type { Plugin, Resource } from '@hcengineering/platform'
import { plugin } from '@hcengineering/platform'
import type { Account, Class, Ref } from '@hcengineering/core'
import { TriggerFunc } from '@hcengineering/server-core'
import { AsyncTriggerFunc } from '@hcengineering/server-core'
import type { OpenAIConfiguration } from './types'
export * from './types'
@@ -31,7 +31,7 @@ export const openAIId = 'openai' as Plugin
*/
const openaiPlugin = plugin(openAIId, {
trigger: {
OnGPTRequest: '' as Resource<TriggerFunc>
AsyncOnGPTRequest: '' as Resource<AsyncTriggerFunc>
},
class: {
OpenAIConfiguration: '' as Ref<Class<OpenAIConfiguration>>
+97 -49
View File
@@ -26,18 +26,18 @@ import core, {
TxCUD,
TxProcessor
} from '@hcengineering/core'
import type { TriggerControl } from '@hcengineering/server-core'
import recruit, { ApplicantMatch } from '@hcengineering/recruit'
import type { AsyncTriggerControl } from '@hcengineering/server-core'
import got from 'got'
import { convert } from 'html-to-text'
import { chunks, encode } from './encoder/encoder'
import { chunks } from './encoder/encoder'
import openai, { OpenAIConfiguration, openAIRatelimitter } from './plugin'
import recruit, { ApplicantMatch } from '@hcengineering/recruit'
const model = 'text-davinci-003'
const defaultOptions = {
max_tokens: 4000,
temperature: 0.9,
temperature: 0.2,
top_p: 1,
n: 1,
stop: null as string | null
@@ -46,16 +46,31 @@ const defaultOptions = {
async function performCompletion (
prompt: string,
options: typeof defaultOptions,
config: OpenAIConfiguration
config: OpenAIConfiguration,
maxLen: number
): Promise<any> {
const ep = config.endpoint + '/completions'
const chunkedPrompt = chunks(prompt, options.max_tokens - 250)[0]
const tokens = encode(chunkedPrompt).length
const chunkedPrompt = chunks(prompt, options.max_tokens - maxLen)[0]
let response: any
let timeout = 50
const st = Date.now()
const request: Record<string, any> = {
model,
prompt: chunkedPrompt,
max_tokens: maxLen,
temperature: options.temperature,
top_p: options.top_p,
n: options.n,
stream: false
}
if (options.stop != null) {
request.stop = options.stop
}
while (true) {
try {
console.info('Sending request to OpenAI')
response = await openAIRatelimitter.exec(
async () =>
await got
@@ -64,18 +79,8 @@ async function performCompletion (
'Content-Type': 'application/json',
Authorization: `Bearer ${config.token}`
},
json: {
model,
prompt: chunkedPrompt,
max_tokens: options.max_tokens - tokens,
temperature: options.temperature,
top_p: options.top_p,
n: options.n,
stream: false,
logprobs: null,
stop: options.stop
},
timeout: 180000
json: request,
timeout: 60000
})
.json()
)
@@ -84,15 +89,21 @@ async function performCompletion (
const msg = (e.message as string) ?? ''
if (
msg.includes('Response code 429 (Too Many Requests)') ||
msg.includes('Response code 503 (Service Unavailable)')
msg.includes('Response code 503 (Service Unavailable)') ||
msg.includes('Response code 400 (Bad Request)')
) {
timeout += 100
console.info('Too many requests, Waiting 1sec to retry.')
await new Promise((resolve) => {
setTimeout(resolve, 1000)
setTimeout(resolve, timeout)
})
continue
}
if (Date.now() - st > 60000) {
return {}
}
console.error(e)
return []
return {}
}
}
return response
@@ -100,7 +111,7 @@ async function performCompletion (
/**
* @public
*/
export async function OnGPTRequest (tx: Tx, tc: TriggerControl): Promise<Tx[]> {
export async function AsyncOnGPTRequest (tx: Tx, tc: AsyncTriggerControl): Promise<Tx[]> {
const actualTx = TxProcessor.extractTx(tx)
if (tc.hierarchy.isDerived(actualTx._class, core.class.TxCUD) && actualTx.modifiedBy !== openai.account.GPT) {
@@ -116,7 +127,7 @@ export async function OnGPTRequest (tx: Tx, tc: TriggerControl): Promise<Tx[]> {
return []
}
async function handleComment (tx: Tx, tc: TriggerControl): Promise<Tx[]> {
async function handleComment (tx: Tx, tc: AsyncTriggerControl): Promise<Tx[]> {
const actualTx = TxProcessor.extractTx(tx)
const cud: TxCUD<Doc> = actualTx as TxCUD<Doc>
@@ -178,7 +189,7 @@ async function handleComment (tx: Tx, tc: TriggerControl): Promise<Tx[]> {
const options = parseOptions(split)
const response = await performCompletion(prompt, options, config)
const response = await performCompletion(prompt, options, config, 1024)
const result: Tx[] = []
let finalMsg = msg + '</br>'
@@ -205,19 +216,60 @@ async function handleComment (tx: Tx, tc: TriggerControl): Promise<Tx[]> {
)
// col.modifiedBy = openai.account.GPT
result.push(col)
// Store response transactions
await tc.txFx(async (st) => {
for (const t of result) {
await st.tx(t)
}
})
return result
}
}
return []
}
async function handleApplicantMatch (tx: Tx, tc: TriggerControl): Promise<Tx[]> {
function getText (response: any): string | undefined {
let result = ''
for (const choices of response?.choices ?? []) {
let val = (choices.text as string).trim()
// Add new line before Reason:
val = val.split('\n\n').join('\n')
val = val.replace('Reason:', '\nReason:')
val = val.replace('Candidate is', '\nCandidate is')
val = val.replace(/Match score: (\d+\/\d+|\d+%) /gi, (val) => val + '\n')
val = val.split('\n').join('\n<br/>')
result += val.trim()
}
if (result.length === 0) {
return undefined
}
return result
}
async function summarizeCandidate (config: OpenAIConfiguration, chunks: string[], maxLen: number): Promise<string> {
const options: typeof defaultOptions = {
...defaultOptions,
temperature: 0.1
}
if (chunks.length === 1) {
return chunks[0]
}
const candidateSummaryRequest = `I want you to act as a recruiter.
I will provide some information about candidate, and it will be your job to come up with short and essential summary describing resume.
My first request is "I need help to summarize my CV.” ${chunks.join(' ')}`
return getText(await performCompletion(candidateSummaryRequest, options, config, maxLen)) ?? chunks[0]
}
async function summarizeVacancy (config: OpenAIConfiguration, chunks: string[], maxLen: number): Promise<string> {
const options: typeof defaultOptions = {
...defaultOptions,
temperature: 0.1
}
if (chunks.length === 1) {
return chunks[0]
}
const candidateSummaryRequest = `I want you to act as a recruiter.
I will provide some information about vacancy, and it will be your job to come up with short and essential summary describing vacancy.
My first request is "I need help to summarize my Vacancy description.” ${chunks.join(' ')}`
return getText(await performCompletion(candidateSummaryRequest, options, config, maxLen)) ?? chunks[0]
}
async function handleApplicantMatch (tx: Tx, tc: AsyncTriggerControl): Promise<Tx[]> {
const [config] = await tc.findAll(openai.class.OpenAIConfiguration, {})
if (!(config?.enabled ?? false)) {
@@ -236,7 +288,7 @@ async function handleApplicantMatch (tx: Tx, tc: TriggerControl): Promise<Tx[]>
temperature: 0.1
}
const maxAnswerTokens = 500
const maxAnswerTokens = 256
const maxVacancyTokens = options.max_tokens - maxAnswerTokens / 2
const maxCandidateTokens = maxVacancyTokens
@@ -247,7 +299,9 @@ async function handleApplicantMatch (tx: Tx, tc: TriggerControl): Promise<Tx[]>
selectors: [{ selector: 'img', format: 'skip' }]
})
candidateText = chunks(candidateText, maxCandidateTokens)[0]
const candidateTextC = chunks(candidateText, maxCandidateTokens)
candidateText = await summarizeCandidate(config, candidateTextC, maxCandidateTokens)
let vacancyText = cud.attributes.vacancy
@@ -255,18 +309,19 @@ async function handleApplicantMatch (tx: Tx, tc: TriggerControl): Promise<Tx[]>
preserveNewlines: true,
selectors: [{ selector: 'img', format: 'skip' }]
})
vacancyText = chunks(vacancyText, maxVacancyTokens)[0]
vacancyText = await summarizeVacancy(config, chunks(vacancyText, maxVacancyTokens), maxVacancyTokens)
// Enabled, we could complete.
const text = `'I want you to act as a recruiter. I will provide some information about vacancy and resume, and it will be your job to come up with solution why candidate is matching vacancy. Please considering following vacancy:\n ${vacancyText}\n and please write if following candidate good match for vacancy and why:\n ${candidateText}\n`
// const text = `I want you to act as a recruiter.
// I will provide some information about vacancy and resume, and it will be your job to come up with solution why candidate is matching vacancy.
// My first request is "I need help to match vacancy ${vacancyText} and CV: ${candidateText}”`
const text = `'Considering following vacancy:\n ${vacancyText}\n write if following candidate good for vacancy and why:\n ${candidateText}\n`
const response = await performCompletion(text, options, config)
const response = await performCompletion(text, options, config, maxAnswerTokens)
const result: Tx[] = []
let finalMsg = ''
for (const choices of response.choices) {
for (const choices of response?.choices ?? []) {
let val = (choices.text as string).trim()
// Add new line before Reason:
val = val.split('\n\n').join('\n')
@@ -291,13 +346,6 @@ async function handleApplicantMatch (tx: Tx, tc: TriggerControl): Promise<Tx[]>
)
// col.modifiedBy = openai.account.GPT
result.push(col)
// Store response transactions
await tc.txFx(async (st) => {
for (const t of result) {
await st.tx(t)
}
})
return result
}
@@ -306,7 +354,7 @@ async function handleApplicantMatch (tx: Tx, tc: TriggerControl): Promise<Tx[]>
*/
export const openAIPluginImpl = async () => ({
trigger: {
OnGPTRequest
AsyncOnGPTRequest
}
})
function parseOptions (split: string[]): typeof defaultOptions {