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), }, } }