package rag import ( "bytes" "context" "encoding/json" "fmt" "net/http" "time" ) // chatClient 是 OpenAI 兼容的非流式对话客户端,供图谱实体抽取用。 // 配置由控制面(chat kind)经 NATS 下发(与 Dispatcher 共用同一个模型)。 type chatClient struct { baseURL string apiKey string model string hc *http.Client } func newChatClient(baseURL, apiKey, model string) *chatClient { if baseURL == "" || model == "" { return nil } return &chatClient{baseURL: baseURL, apiKey: apiKey, model: model, hc: &http.Client{Timeout: 60 * time.Second}} } func (c *chatClient) ready() bool { return c != nil && c.baseURL != "" } // complete 一次性补全(非流式),返回助手回复文本。 func (c *chatClient) complete(ctx context.Context, system, user string) (string, error) { body, _ := json.Marshal(map[string]any{ "model": c.model, "messages": []map[string]string{ {"role": "system", "content": system}, {"role": "user", "content": user}, }, "stream": false, }) req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.baseURL+"/chat/completions", bytes.NewReader(body)) if err != nil { return "", err } req.Header.Set("Content-Type", "application/json") if c.apiKey != "" { req.Header.Set("Authorization", "Bearer "+c.apiKey) } resp, err := c.hc.Do(req) if err != nil { return "", err } defer resp.Body.Close() if resp.StatusCode >= 400 { buf := new(bytes.Buffer) _, _ = buf.ReadFrom(resp.Body) return "", fmt.Errorf("chat http %d: %s", resp.StatusCode, buf.String()) } var out struct { Choices []struct { Message struct { Content string `json:"content"` } `json:"message"` } `json:"choices"` } if err := json.NewDecoder(resp.Body).Decode(&out); err != nil { return "", err } if len(out.Choices) == 0 { return "", fmt.Errorf("chat: empty choices") } return out.Choices[0].Message.Content, nil }