mirror of
https://github.com/hcengineering/platform.git
synced 2026-09-13 13:17:45 +02:00
@@ -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,
|
||||
|
||||
@@ -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>>
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user