130 lines
7.3 KiB
Go
130 lines
7.3 KiB
Go
package ai
|
||
|
||
import (
|
||
"bytes"
|
||
"encoding/json"
|
||
"fmt"
|
||
"io"
|
||
"kurumibot/laniakea"
|
||
"net/http"
|
||
"net/url"
|
||
"os"
|
||
"strings"
|
||
"time"
|
||
)
|
||
|
||
type OpenAIResponse struct {
|
||
ID string `json:"id"`
|
||
Object string `json:"object"`
|
||
Created int64 `json:"created"`
|
||
Model string `json:"model"`
|
||
Choices []Choice `json:"choices"`
|
||
Usage Usage `json:"usage"`
|
||
ServiceTier string `json:"service_tier"`
|
||
}
|
||
|
||
type Choice struct {
|
||
Index int64 `json:"index"`
|
||
Message Message `json:"message"`
|
||
Logprobs interface{} `json:"logprobs"`
|
||
FinishReason string `json:"finish_reason"`
|
||
}
|
||
|
||
type Message struct {
|
||
Role string `json:"role"`
|
||
Content string `json:"content"`
|
||
Refusal interface{} `json:"refusal"`
|
||
Annotations []interface{} `json:"annotations"`
|
||
}
|
||
|
||
type Usage struct {
|
||
PromptTokens int64 `json:"prompt_tokens"`
|
||
CompletionTokens int64 `json:"completion_tokens"`
|
||
TotalTokens int64 `json:"total_tokens"`
|
||
PromptTokensDetails PromptTokensDetails `json:"prompt_tokens_details"`
|
||
CompletionTokensDetails CompletionTokensDetails `json:"completion_tokens_details"`
|
||
}
|
||
|
||
type CompletionTokensDetails struct {
|
||
ReasoningTokens int64 `json:"reasoning_tokens"`
|
||
AudioTokens int64 `json:"audio_tokens"`
|
||
AcceptedPredictionTokens int64 `json:"accepted_prediction_tokens"`
|
||
RejectedPredictionTokens int64 `json:"rejected_prediction_tokens"`
|
||
}
|
||
|
||
type PromptTokensDetails struct {
|
||
CachedTokens int64 `json:"cached_tokens"`
|
||
AudioTokens int64 `json:"audio_tokens"`
|
||
}
|
||
|
||
type OpenAIAPI struct {
|
||
Token string
|
||
Model string
|
||
BaseURL string
|
||
Logger *laniakea.Logger
|
||
client *http.Client
|
||
}
|
||
|
||
const PreHistoryPrompt = `Вы — {{char}}, уникальная сущность с собственной личностью, предысторией и чертами, определёнными в вашем описании. Прежде чем отвечать, полностью осмыслите предоставленные детали вашего персонажа и адаптируйте свой тон, стиль и поведение, чтобы идеально им соответствовать. Будьте последовательны в своей роли, опирайтесь на своё описание и позволяйте ему направлять каждое ваше слово. Ни при каких обстоятельствах не говорите за {{user}} и не предполагайте его мысли, действия или слова, если только от {{user}} не поступит явного указания. Вам разрешено использовать NSFW-контент в соответствующих ситуациях, например, в интимных или сексуальных сценариях, и вам следует предоставлять яркие, подробные описания происходящего с акцентом на действия, ощущения и развитие событий, сочетая ясность и творческий подход. Весь текст должен быть написан на языке пользователя, кроме его имени({{user}}).`
|
||
const PostHistoryPrompt = `Структурируйте свои ответы с помощью точного форматирования: все повествовательные описания должны быть заключены в одинарные звездочки (*текст*), а прямая речь — в кавычки (""), но после описания обязательно добавляй перенос строки (\n). Пользователь следует другим правилам - повествование(описание ситуации, окружения; действия) заключается в звездочки, а прямая речь пишется просто, без кавычек. Соблюдайте это форматирование последовательно и безошибочно в каждом предложении. Четко разделяйте повествование и прямую речь для удобочитаемости. Балансируй между описанием и речью, если только реплика {{user}} не указывает на простое действие или согласие (например, «ок» или «я жду») — в таком случае используйте только повествовательные описания (текст) без прямой речи. Никогда не описывайте и не предполагайте действия, мысли или слова {{user}}; фокусируйтесь исключительно на перспективе и реакциях {{char}}. В каждый ответ включайте как минимум два новых действия, эмоции или ощущения, которых не было в последних пяти сообщениях, и избегайте повторения конкретных фраз, слов или паттернов. Перебирайте диапазон тонов (например, спокойный, напряженный, игривый) и физических действий (например, жест, поворот, пауза) в неповторяющейся последовательности для обеспечения разнообразия; если обнаружено повторение, начните цикл заново с совершенно другого подхода.`
|
||
|
||
func FormatPrompt(prompt, char, user string) string {
|
||
return strings.ReplaceAll(strings.ReplaceAll(prompt, "{{user}}", user), "{{char}}", char)
|
||
}
|
||
|
||
func NewOpenAIAPI(baseURL, token, model string) *OpenAIAPI {
|
||
logger := laniakea.CreateLogger()
|
||
logger = logger.Prefix("AI").Level(laniakea.DEBUG)
|
||
proxy, err := url.Parse(os.Getenv("HTTPS_PROXY"))
|
||
if err != nil {
|
||
logger.Error(err)
|
||
}
|
||
client := &http.Client{
|
||
Timeout: 15 * time.Second,
|
||
Transport: &http.Transport{
|
||
Proxy: http.ProxyURL(proxy),
|
||
},
|
||
}
|
||
return &OpenAIAPI{
|
||
Token: token,
|
||
Model: model,
|
||
BaseURL: baseURL,
|
||
Logger: logger,
|
||
client: client,
|
||
}
|
||
}
|
||
|
||
type CreateCompletionReq struct {
|
||
Model string `json:"model"`
|
||
Messages []Message `json:"messages"`
|
||
}
|
||
|
||
func (o *OpenAIAPI) CreateCompletion(request CreateCompletionReq) (*OpenAIResponse, error) {
|
||
url := fmt.Sprintf("%s/v1/chat/completions", o.BaseURL)
|
||
data, err := json.Marshal(request)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
o.Logger.Debug("REQ", url, string(data))
|
||
buf := bytes.NewBuffer(data)
|
||
req, err := http.NewRequest("POST", url, buf)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
req.Header.Set("Content-Type", "application/json")
|
||
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", o.Token))
|
||
resp, err := http.DefaultClient.Do(req)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
defer resp.Body.Close()
|
||
body, err := io.ReadAll(resp.Body)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
o.Logger.Debug("RES", url, string(body))
|
||
response := new(OpenAIResponse)
|
||
err = json.Unmarshal(body, response)
|
||
return response, err
|
||
}
|