sync: migrate ai-operator to Gitea (2026-08-10)

This commit is contained in:
konturai-ops
2026-08-10 15:26:52 +00:00
commit 53652b95ad
173 changed files with 16676 additions and 0 deletions
+98
View File
@@ -0,0 +1,98 @@
package llm
import (
"bufio"
"bytes"
"context"
"encoding/json"
"errors"
"net/http"
"strings"
"ai-operator/internal/config"
)
type OpenAIStreaming struct {
cfg config.Config
client *http.Client
}
func NewOpenAIStreaming(cfg config.Config) *OpenAIStreaming {
return &OpenAIStreaming{cfg: cfg, client: &http.Client{Timeout: cfg.LLM.Timeout}}
}
func (p *OpenAIStreaming) StreamGenerate(ctx context.Context, req GenerateRequest) (<-chan Event, error) {
if p.cfg.OpenAI.APIKey == "" {
return nil, errors.New("OPENAI_API_KEY is required for streaming LLM")
}
out := make(chan Event, 64)
go p.run(ctx, req, out)
return out, nil
}
func (p *OpenAIStreaming) run(ctx context.Context, req GenerateRequest, out chan<- Event) {
defer close(out)
payload := map[string]any{
"model": p.cfg.LLM.Model,
"temperature": p.cfg.LLM.Temperature,
"max_tokens": p.cfg.LLM.MaxOutputTokens,
"stream": true,
}
msgs := []map[string]string{{"role": "system", "content": req.SystemPrompt}}
for _, m := range req.Messages {
if strings.TrimSpace(m.Content) != "" {
msgs = append(msgs, map[string]string{"role": m.Role, "content": m.Content})
}
}
payload["messages"] = msgs
body, _ := json.Marshal(payload)
httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, "https://api.openai.com/v1/chat/completions", bytes.NewReader(body))
if err != nil {
out <- Event{Type: EventError, Error: err.Error()}
return
}
httpReq.Header.Set("Authorization", "Bearer "+p.cfg.OpenAI.APIKey)
httpReq.Header.Set("Content-Type", "application/json")
resp, err := p.client.Do(httpReq)
if err != nil {
out <- Event{Type: EventError, Error: err.Error()}
return
}
defer resp.Body.Close()
if resp.StatusCode < 200 || resp.StatusCode > 299 {
out <- Event{Type: EventError, Error: "openai streaming llm returned non-2xx"}
return
}
sc := bufio.NewScanner(resp.Body)
for sc.Scan() {
line := strings.TrimSpace(sc.Text())
if !strings.HasPrefix(line, "data:") {
continue
}
data := strings.TrimSpace(strings.TrimPrefix(line, "data:"))
if data == "[DONE]" {
out <- Event{Type: EventDone}
return
}
var chunk struct {
Choices []struct {
Delta struct {
Content string `json:"content"`
} `json:"delta"`
} `json:"choices"`
}
if err := json.Unmarshal([]byte(data), &chunk); err != nil {
continue
}
for _, c := range chunk.Choices {
if c.Delta.Content != "" {
out <- Event{Type: EventTextDelta, Text: c.Delta.Content}
}
}
}
if err := sc.Err(); err != nil {
out <- Event{Type: EventError, Error: err.Error()}
return
}
out <- Event{Type: EventDone}
}
+11
View File
@@ -0,0 +1,11 @@
package llm
import "strings"
func BuildAnswerPrompt(userText string, toolAnswer string) string {
toolAnswer = strings.TrimSpace(toolAnswer)
if toolAnswer == "" {
toolAnswer = "В базе знаний нет точной информации по этому вопросу."
}
return "Сформулируй короткий голосовой ответ Жанны только по этому содержанию. Ответ 1-3 предложения, без упоминания JSON/tools/chunks. Вопрос клиента: " + userText + "\nСодержимое KB/tool result: " + toolAnswer
}
+36
View File
@@ -0,0 +1,36 @@
package llm
import "context"
type EventType string
const (
EventTextDelta EventType = "text_delta"
EventToolCallDelta EventType = "tool_call_delta"
EventToolCallDone EventType = "tool_call_done"
EventDone EventType = "done"
EventError EventType = "error"
)
type Message struct {
Role string
Content string
}
type GenerateRequest struct {
CallID string
SystemPrompt string
Messages []Message
MaxTokens int
Temperature float64
}
type Event struct {
Type EventType
Text string
Error string
}
type StreamingLLM interface {
StreamGenerate(ctx context.Context, req GenerateRequest) (<-chan Event, error)
}