Files
DeepRes_bot/internal/research/synthesize/synthesize.go
T

146 lines
4.8 KiB
Go

package synthesize
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"strings"
"time"
)
type LLMClient struct {
apiKey string
model string
baseURL string
client *http.Client
}
type Message struct {
Role string `json:"role"`
Content string `json:"content"`
}
type LLMRequest struct {
Model string `json:"model"`
Messages []Message `json:"messages"`
Temperature float32 `json:"temperature,omitempty"`
MaxTokens int `json:"max_tokens,omitempty"`
}
type LLMResponse struct {
Choices []struct {
Message Message `json:"message"`
} `json:"choices"`
Error *struct {
Message string `json:"message"`
Code int `json:"code"`
} `json:"error,omitempty"`
}
func NewLLMClient(baseURL, apiKey, model string) *LLMClient {
return &LLMClient{
apiKey: apiKey,
model: model,
baseURL: baseURL,
client: &http.Client{Timeout: 120 * time.Second},
}
}
func (c *LLMClient) Chat(ctx context.Context, messages []Message) (string, error) {
reqBody := LLMRequest{
Model: c.model,
Messages: messages,
Temperature: 0.7,
MaxTokens: 3000,
}
payload, err := json.Marshal(reqBody)
if err != nil {
return "", fmt.Errorf("marshal llm request: %w", err)
}
req, err := http.NewRequestWithContext(ctx, "POST", c.baseURL+"/v1/chat/completions", bytes.NewReader(payload))
if err != nil {
return "", fmt.Errorf("llm request: %w", err)
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+c.apiKey)
resp, err := c.client.Do(req)
if err != nil {
return "", fmt.Errorf("llm call: %w", err)
}
defer resp.Body.Close()
respBody, err := io.ReadAll(resp.Body)
if err != nil {
return "", fmt.Errorf("read llm response: %w", err)
}
if resp.StatusCode != http.StatusOK {
return "", fmt.Errorf("llm returned %d: %s", resp.StatusCode, string(respBody))
}
var lr LLMResponse
if err := json.Unmarshal(respBody, &lr); err != nil {
return "", fmt.Errorf("parse llm response: %w", err)
}
if lr.Error != nil {
return "", fmt.Errorf("llm error %d: %s", lr.Error.Code, lr.Error.Message)
}
if len(lr.Choices) == 0 {
return "", fmt.Errorf("empty llm response: %s", string(respBody))
}
return lr.Choices[0].Message.Content, nil
}
// ---- Промпты ----
func BuildFastResearchPrompt(query string, sources []string) []Message {
srcText := strings.Join(sources, "\n\n---\n\n")
return []Message{
{
Role: "system",
Content: "Ты — DeepRes, AI-исследователь. Твоя задача: на основе предоставленных источников написать структурированную выжимку по теме. Выдели ключевые факты, противоположные точки зрения, практические выводы. Если источники противоречат друг другу — укажи это. Пиши на русском, если тема на русском. Формат: кратко, ёмко, по делу.",
},
{
Role: "user",
Content: fmt.Sprintf("Тема: %s\n\nИсточники:\n%s\n\nНапиши выжимку на 3-5 абзацев с ключевыми выводами и ссылками на источники.", query, srcText),
},
}
}
func BuildDeepResearchPrompt(query string, sources []string, followUpQ string) []Message {
srcText := strings.Join(sources, "\n\n---\n\n")
messages := []Message{
{
Role: "system",
Content: "Ты — DeepRes, AI-исследователь. Напиши ПОЛНЫЙ отчёт по теме на основе источников. Структура: введение, ключевые находки, анализ, противоположные точки зрения, выводы и рекомендации. Отметь области, по которым недостаточно информации.",
},
{
Role: "user",
Content: fmt.Sprintf("Тема: %s\n\nИсточники:\n%s", query, srcText),
},
}
if followUpQ != "" {
messages = append(messages, Message{Role: "user", Content: followUpQ})
}
return messages
}
func BuildGapPrompt(query string, sources []string) []Message {
srcText := strings.Join(sources, "\n\n---\n\n")
return []Message{
{
Role: "system",
Content: "Ты — аналитик, который ищет неизведанные области. Проанализируй предоставленные источники и найди:\n1) Какие аспекты темы НЕ освещены?\n2) Где источники противоречат друг другу?\n3) Какие вопросы остаются открытыми?\n4) Что было бы важно изучить дальше?",
},
{
Role: "user",
Content: fmt.Sprintf("Тема: %s\n\nИсточники:\n%s", query, srcText),
},
}
}