2025-05-07 17:10:09 +00:00
|
|
|
|
package gigachat
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"context"
|
|
|
|
|
"errors"
|
|
|
|
|
"fmt"
|
|
|
|
|
"gitea.pena/SQuiz/common/model"
|
2025-06-18 13:17:08 +00:00
|
|
|
|
"gitea.pena/SQuiz/worker/internal/senders"
|
2025-05-07 17:10:09 +00:00
|
|
|
|
"github.com/go-redis/redis/v8"
|
|
|
|
|
"github.com/go-resty/resty/v2"
|
|
|
|
|
"github.com/google/uuid"
|
|
|
|
|
"go.uber.org/zap"
|
|
|
|
|
"time"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
type Deps struct {
|
|
|
|
|
Logger *zap.Logger
|
|
|
|
|
Client *resty.Client
|
|
|
|
|
BaseURL string
|
|
|
|
|
AuthKey string
|
|
|
|
|
RedisClient *redis.Client
|
2025-06-18 13:17:08 +00:00
|
|
|
|
TgSender *senders.TgSender
|
|
|
|
|
TgChatID int64
|
2025-05-07 17:10:09 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type GigaChatClient struct {
|
|
|
|
|
logger *zap.Logger
|
|
|
|
|
client *resty.Client
|
|
|
|
|
baseURL string
|
|
|
|
|
authKey string
|
|
|
|
|
redisClient *redis.Client
|
2025-06-18 13:17:08 +00:00
|
|
|
|
tgSender *senders.TgSender
|
|
|
|
|
tgChatID int64
|
2025-05-07 17:10:09 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func NewGigaChatClient(ctx context.Context, deps Deps) (*GigaChatClient, error) {
|
|
|
|
|
client := &GigaChatClient{
|
|
|
|
|
logger: deps.Logger,
|
|
|
|
|
client: deps.Client,
|
|
|
|
|
baseURL: deps.BaseURL,
|
|
|
|
|
authKey: deps.AuthKey,
|
|
|
|
|
redisClient: deps.RedisClient,
|
2025-06-18 13:17:08 +00:00
|
|
|
|
|
|
|
|
|
tgSender: deps.TgSender,
|
|
|
|
|
tgChatID: deps.TgChatID,
|
2025-05-07 17:10:09 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if err := client.updateToken(ctx); err != nil {
|
|
|
|
|
return nil, fmt.Errorf("failed to get access token: %w", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return client, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (r *GigaChatClient) SendMsg(ctx context.Context, audience model.GigaChatAudience, question model.Question) (string, error) {
|
2025-06-06 14:27:43 +00:00
|
|
|
|
gender := ""
|
|
|
|
|
|
|
|
|
|
switch audience.Sex {
|
|
|
|
|
case 0:
|
|
|
|
|
gender = "женский"
|
|
|
|
|
case 1:
|
2025-05-10 10:22:36 +00:00
|
|
|
|
gender = "мужской"
|
2025-06-06 14:27:43 +00:00
|
|
|
|
case 2:
|
|
|
|
|
gender = "не имеет значения"
|
2025-05-10 10:22:36 +00:00
|
|
|
|
}
|
2025-06-06 14:27:43 +00:00
|
|
|
|
|
2025-05-10 10:22:36 +00:00
|
|
|
|
userInput := fmt.Sprintf(model.ReworkQuestionPrompt, audience.Age, gender, question.Title, question.Description)
|
2025-05-07 17:10:09 +00:00
|
|
|
|
|
|
|
|
|
token, err := r.redisClient.Get(ctx, "gigachat_token").Result()
|
|
|
|
|
if err != nil {
|
|
|
|
|
r.logger.Error("failed to get token from redis", zap.Error(err))
|
|
|
|
|
return "", err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
reqBody := model.GigaChatRequest{
|
|
|
|
|
Model: "GigaChat-2-Max",
|
|
|
|
|
Stream: false,
|
|
|
|
|
UpdateInterval: 0,
|
|
|
|
|
Messages: []model.GigaChatMessage{
|
|
|
|
|
{Role: "system", Content: model.CreatePrompt},
|
|
|
|
|
{Role: "user", Content: userInput},
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var response model.GigaChatResponse
|
|
|
|
|
|
|
|
|
|
resp, err := r.client.R().
|
|
|
|
|
SetHeader("Content-Type", "application/json").
|
|
|
|
|
SetHeader("Authorization", "Bearer "+token).
|
|
|
|
|
SetBody(reqBody).
|
|
|
|
|
SetResult(&response).
|
|
|
|
|
Post(r.baseURL + "/chat/completions")
|
|
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
r.logger.Error("failed send request to GigaChat", zap.Error(err))
|
|
|
|
|
return "", err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if resp.IsError() {
|
|
|
|
|
errMsg := fmt.Sprintf("error GigaChat API: %s", resp.Status())
|
|
|
|
|
r.logger.Error(errMsg)
|
|
|
|
|
return "", errors.New(errMsg)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if len(response.Choices) == 0 || response.Choices[0].Message.Content == "" {
|
|
|
|
|
// когда возникает такая ошибка то значит еще траим отправить запрос
|
|
|
|
|
return "", model.EmptyResponseErrorGigaChat
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return response.Choices[0].Message.Content, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (r *GigaChatClient) TokenResearch(ctx context.Context) {
|
2025-06-05 14:35:12 +00:00
|
|
|
|
ticker := time.NewTicker(time.Minute)
|
2025-05-07 17:10:09 +00:00
|
|
|
|
defer ticker.Stop()
|
|
|
|
|
|
|
|
|
|
for {
|
|
|
|
|
select {
|
|
|
|
|
case <-ticker.C:
|
|
|
|
|
ttl, err := r.redisClient.TTL(ctx, "gigachat_token").Result()
|
2025-06-18 13:17:08 +00:00
|
|
|
|
fmt.Println("GGCHATtoken", ttl, err, ttl < 2*time.Minute)
|
2025-05-07 17:10:09 +00:00
|
|
|
|
if err != nil || ttl < 2*time.Minute {
|
|
|
|
|
if err := r.updateToken(ctx); err != nil {
|
|
|
|
|
r.logger.Error("failed to update GigaChat token", zap.Error(err))
|
|
|
|
|
} else {
|
|
|
|
|
r.logger.Info("successfully updated GigaChat token")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
case <-ctx.Done():
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (r *GigaChatClient) updateToken(ctx context.Context) error {
|
2025-05-28 22:13:31 +00:00
|
|
|
|
formData := "scope=GIGACHAT_API_B2B"
|
2025-05-07 17:10:09 +00:00
|
|
|
|
|
|
|
|
|
var respData struct {
|
|
|
|
|
AccessToken string `json:"access_token"`
|
|
|
|
|
ExpiresAt int64 `json:"expires_at"`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
resp, err := r.client.R().
|
|
|
|
|
SetHeader("Authorization", "Basic "+r.authKey).
|
|
|
|
|
SetHeader("Content-Type", "application/x-www-form-urlencoded").
|
|
|
|
|
SetBody(formData).
|
|
|
|
|
SetHeader("RqUID", uuid.New().String()).
|
|
|
|
|
SetResult(&respData).
|
|
|
|
|
Post("https://ngw.devices.sberbank.ru:9443/api/v2/oauth")
|
|
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
if resp.IsError() {
|
|
|
|
|
return fmt.Errorf("token request failed: %s", resp.Status())
|
|
|
|
|
}
|
|
|
|
|
|
2025-06-13 11:13:20 +00:00
|
|
|
|
ttl := time.Until(time.Unix(int64(respData.ExpiresAt/1000), 0))
|
2025-06-18 13:17:08 +00:00
|
|
|
|
fmt.Println("GGCTOKENEXP", respData.ExpiresAt, ttl, ttl < 2*time.Minute, time.Now())
|
2025-05-07 17:10:09 +00:00
|
|
|
|
err = r.redisClient.Set(ctx, "gigachat_token", respData.AccessToken, ttl).Err()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return fmt.Errorf("failed to save token to redis: %w", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
}
|
2025-06-18 13:17:08 +00:00
|
|
|
|
|
|
|
|
|
func (r *GigaChatClient) getBalance(ctx context.Context) (int, error) {
|
|
|
|
|
var respData struct {
|
|
|
|
|
Balance []struct {
|
|
|
|
|
Usage string `json:"usage"`
|
|
|
|
|
Value int `json:"value"`
|
|
|
|
|
} `json:"balance"`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
token, err := r.redisClient.Get(ctx, "gigachat_token").Result()
|
|
|
|
|
if err != nil {
|
|
|
|
|
r.logger.Error("failed to get token from redis", zap.Error(err))
|
|
|
|
|
return 0, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
resp, err := r.client.R().
|
|
|
|
|
SetHeader("Authorization", "Bearer "+token).
|
|
|
|
|
SetResult(&respData).
|
|
|
|
|
Get(r.baseURL + "/balance")
|
|
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
return 0, fmt.Errorf("failed to fetch balance: %w", err)
|
|
|
|
|
}
|
|
|
|
|
if resp.IsError() {
|
|
|
|
|
return 0, fmt.Errorf("balance request failed: %s", resp.Status())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// прверяем то что используем для переформулирования
|
|
|
|
|
for _, item := range respData.Balance {
|
|
|
|
|
if item.Usage == "GigaChat-Max" {
|
|
|
|
|
return item.Value, nil
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return 0, errors.New("no used models found")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (r *GigaChatClient) MonitorTokenBalance(ctx context.Context) {
|
|
|
|
|
ticker := time.NewTicker(5 * time.Minute)
|
|
|
|
|
defer ticker.Stop()
|
|
|
|
|
|
|
|
|
|
alert := false // чтоб не спамить каждые 5 минут
|
|
|
|
|
|
|
|
|
|
for {
|
|
|
|
|
select {
|
|
|
|
|
case <-ticker.C:
|
|
|
|
|
balance, err := r.getBalance(ctx)
|
|
|
|
|
if err != nil {
|
|
|
|
|
r.logger.Error("failed to get GigaChat token", zap.Error(err))
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if balance < 500_000 && !alert {
|
|
|
|
|
msg := fmt.Sprintf("Остаток токенов в GigaChat упал ниже 500000.\nТекущий баланс: %d токенов.", balance)
|
|
|
|
|
if err := r.tgSender.SendMessage(r.tgChatID, msg); err != nil {
|
|
|
|
|
r.logger.Error("failed to send Telegram alert", zap.Error(err))
|
|
|
|
|
} else {
|
|
|
|
|
alert = true
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if balance >= 500_000 && alert {
|
|
|
|
|
alert = false
|
|
|
|
|
}
|
|
|
|
|
case <-ctx.Done():
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|