sync: migrate ai-operator to Gitea (2026-08-10)
This commit is contained in:
@@ -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}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
Reference in New Issue
Block a user