fix: 实现Gemini客户端取代官方版本

This commit is contained in:
ikechan8370 2023-12-15 00:41:42 +08:00
parent 6769e9d3f0
commit bbe769f1aa
9 changed files with 431 additions and 2428 deletions

View file

@ -0,0 +1,251 @@
import crypto from 'crypto'
import { GoogleGeminiClient } from './GoogleGeminiClient.js'
import { newFetch } from '../utils/proxy.js'
import _ from 'lodash'
const BASEURL = 'https://generativelanguage.googleapis.com'
export const HarmCategory = {
HARM_CATEGORY_UNSPECIFIED: 'HARM_CATEGORY_UNSPECIFIED',
HARM_CATEGORY_HATE_SPEECH: 'HARM_CATEGORY_HATE_SPEECH',
HARM_CATEGORY_SEXUALLY_EXPLICIT: 'HARM_CATEGORY_SEXUALLY_EXPLICIT',
HARM_CATEGORY_HARASSMENT: 'HARM_CATEGORY_HARASSMENT',
HARM_CATEGORY_DANGEROUS_CONTENT: 'HARM_CATEGORY_DANGEROUS_CONTENT'
}
export const HarmBlockThreshold = {
HARM_BLOCK_THRESHOLD_UNSPECIFIED: 'HARM_BLOCK_THRESHOLD_UNSPECIFIED',
BLOCK_LOW_AND_ABOVE: 'BLOCK_LOW_AND_ABOVE',
BLOCK_MEDIUM_AND_ABOVE: 'BLOCK_MEDIUM_AND_ABOVE',
BLOCK_ONLY_HIGH: 'BLOCK_ONLY_HIGH',
BLOCK_NONE: 'BLOCK_NONE'
}
/**
* @typedef {{
* role: string,
* parts: Array<{
* text?: string,
* functionCall?: FunctionCall,
* functionResponse?: FunctionResponse
* }>
* }} Content
*
* Gemini消息的基本格式
*/
/**
* @typedef {{
* name: string,
* args: {}
* }} FunctionCall
*
* Gemini的FunctionCall
*/
/**
* @typedef {{
* name: string,
* response: {
* name: string,
* content: {}
* }
* }} FunctionResponse
*
* Gemini的Function执行结果包裹
* 其中response可以为任意本项目根据官方示例封装为name和content两个字段
*/
export class CustomGoogleGeminiClient extends GoogleGeminiClient {
constructor (props) {
super(props)
this.model = props.model
this.baseUrl = props.baseUrl || BASEURL
this.supportFunction = true
this.debug = props.debug
}
/**
*
* @param text
* @param {{conversationId: string?, parentMessageId: string?, stream: boolean?, onProgress: function?, functionResponse: FunctionResponse?, system: string?}} opt
* @returns {Promise<{conversationId: string?, parentMessageId: string, text: string, id: string}>}
*/
async sendMessage (text, opt) {
let history = await this.getHistory(opt.parentMessageId)
let systemMessage = opt.system
if (systemMessage) {
history = history.reverse()
history.push({
role: 'model',
parts: [
{
text: 'ok'
}
]
})
history.push({
role: 'user',
parts: [
{
text: systemMessage
}
]
})
history = history.reverse()
}
const idThis = crypto.randomUUID()
const idModel = crypto.randomUUID()
const thisMessage = opt.functionResponse
? {
role: 'function',
parts: [{
functionResponse: opt.functionResponse
}],
id: idThis,
parentMessageId: opt.parentMessageId || undefined
}
: {
role: 'user',
parts: [{ text }],
id: idThis,
parentMessageId: opt.parentMessageId || undefined
}
history.push(_.cloneDeep(thisMessage))
let url = `${this.baseUrl}/v1beta/models/gemini-pro:generateContent?key=${this._key}`
let body = {
// 不去兼容官方的简单格式了直接用免得function还要转换
/**
* @type Array<Content>
*/
contents: history,
safetySettings: [
{
category: HarmCategory.HARM_CATEGORY_DANGEROUS_CONTENT,
threshold: HarmBlockThreshold.BLOCK_NONE
},
{
category: HarmCategory.HARM_CATEGORY_HARASSMENT,
threshold: HarmBlockThreshold.BLOCK_NONE
},
{
category: HarmCategory.HARM_CATEGORY_SEXUALLY_EXPLICIT,
threshold: HarmBlockThreshold.BLOCK_NONE
},
{
category: HarmCategory.HARM_CATEGORY_HATE_SPEECH,
threshold: HarmBlockThreshold.BLOCK_NONE
}
],
generationConfig: {
maxOutputTokens: 1000,
temperature: 0.9,
topP: 0.95,
topK: 16
},
tools: [
{
functionDeclarations: this.tools.map(tool => tool.function())
}
]
}
body.contents.forEach(content => {
delete content.id
delete content.parentMessageId
delete content.conversationId
})
let result = await newFetch(url, {
method: 'POST',
body: JSON.stringify(body)
})
if (result.status !== 200) {
throw new Error(await result.text())
}
/**
* @type {Content | undefined}
*/
let responseContent
/**
* @type {{candidates: Array<{content: Content}>}}
*/
let response = await result.json()
if (this.debug) {
console.log(JSON.stringify(response))
}
responseContent = response.candidates[0].content
if (responseContent.parts[0].functionCall) {
// functionCall
const functionCall = responseContent.parts[0].functionCall
// Gemini有时候只回复一个空的functionCall,无语死了
if (functionCall.name) {
logger.info(JSON.stringify(functionCall))
const funcName = functionCall.name
let chosenTool = this.tools.find(t => t.name === funcName)
/**
* @type {FunctionResponse}
*/
let functionResponse = {
name: funcName,
response: {
name: funcName,
content: null
}
}
if (!chosenTool) {
// 根本没有这个工具!
functionResponse.response.content = {
error: `Function ${funcName} doesn't exist`
}
} else {
// execute function
try {
functionResponse.response.content = await chosenTool.func(functionCall.args, this.e)
if (this.debug) {
logger.info(JSON.stringify(functionResponse.response.content))
}
} catch (err) {
logger.error(err)
functionResponse.response.content = {
error: `Function execute error: ${err.message}`
}
}
}
let responseOpt = _.cloneDeep(opt)
responseOpt.parentMessageId = idModel
responseOpt.functionResponse = functionResponse
// 递归直到返回text
// 先把这轮的消息存下来
await this.upsertMessage(thisMessage)
const respMessage = Object.assign(responseContent, {
id: idModel,
parentMessageId: idThis
})
await this.upsertMessage(respMessage)
return await this.sendMessage('', responseOpt)
} else {
// 谷歌抽风了,瞎调函数,不保存这轮,直接返回
return {
text: '',
conversationId: '',
parentMessageId: opt.parentMessageId,
id: '',
error: true
}
}
}
if (responseContent) {
await this.upsertMessage(thisMessage)
const respMessage = Object.assign(responseContent, {
id: idModel,
parentMessageId: idThis
})
await this.upsertMessage(respMessage)
}
return {
text: responseContent.parts[0].text,
conversationId: '',
parentMessageId: idThis,
id: idModel
}
}
}