13a2c69d5e
记忆嵌入修复: - 新增 memory.APIEmbedder 使用 text-embedding-3-small 替代 SimpleEmbedder - Extractor 保存记忆时自动生成向量嵌入 - Embedder 接口增加 IsAvailable() 方法 跨群聊上下文隔离: - Thinker 新增 thinkSessionID 字段,performThink 启动时绑定会话 - storeThought 优先使用绑定的 session 推送思考结果 - 防止思考过程中其他群消息改变 activeSessionID 导致串台 Co-Authored-By: Claude <noreply@anthropic.com>
105 lines
2.3 KiB
Go
105 lines
2.3 KiB
Go
package memory
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"time"
|
|
)
|
|
|
|
// APIEmbedder generates text embeddings via OpenAI-compatible API.
|
|
type APIEmbedder struct {
|
|
baseURL string
|
|
apiKey string
|
|
model string
|
|
httpClient *http.Client
|
|
}
|
|
|
|
// NewAPIEmbedder creates a new embedding service.
|
|
func NewAPIEmbedder(baseURL, apiKey, model string) *APIEmbedder {
|
|
return &APIEmbedder{
|
|
baseURL: baseURL,
|
|
apiKey: apiKey,
|
|
model: model,
|
|
httpClient: &http.Client{
|
|
Timeout: 30 * time.Second,
|
|
},
|
|
}
|
|
}
|
|
|
|
type embRequest struct {
|
|
Input []string `json:"input"`
|
|
Model string `json:"model"`
|
|
}
|
|
|
|
type embResponse struct {
|
|
Data []embData `json:"data"`
|
|
Error *embError `json:"error,omitempty"`
|
|
}
|
|
|
|
type embData struct {
|
|
Embedding []float64 `json:"embedding"`
|
|
}
|
|
|
|
type embError struct {
|
|
Message string `json:"message"`
|
|
}
|
|
|
|
// Embed generates an embedding vector for the given text.
|
|
func (e *APIEmbedder) Embed(ctx context.Context, text string) ([]float64, error) {
|
|
if !e.IsAvailable() {
|
|
return nil, fmt.Errorf("embedding service not available")
|
|
}
|
|
|
|
reqBody := embRequest{
|
|
Input: []string{text},
|
|
Model: e.model,
|
|
}
|
|
|
|
jsonBody, err := json.Marshal(reqBody)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("marshal embedding request: %w", err)
|
|
}
|
|
|
|
req, err := http.NewRequestWithContext(ctx, "POST", e.baseURL+"/embeddings", bytes.NewReader(jsonBody))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("create embedding request: %w", err)
|
|
}
|
|
req.Header.Set("Content-Type", "application/json")
|
|
req.Header.Set("Authorization", "Bearer "+e.apiKey)
|
|
|
|
resp, err := e.httpClient.Do(req)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("embedding request failed: %w", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
body, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("read embedding response: %w", err)
|
|
}
|
|
|
|
var embResp embResponse
|
|
if err := json.Unmarshal(body, &embResp); err != nil {
|
|
return nil, fmt.Errorf("parse embedding response: %w", err)
|
|
}
|
|
|
|
if embResp.Error != nil {
|
|
return nil, fmt.Errorf("embedding API error: %s", embResp.Error.Message)
|
|
}
|
|
|
|
if len(embResp.Data) == 0 {
|
|
return nil, fmt.Errorf("no embedding returned")
|
|
}
|
|
|
|
return embResp.Data[0].Embedding, nil
|
|
}
|
|
|
|
// IsAvailable checks if the embedding service is configured.
|
|
func (e *APIEmbedder) IsAvailable() bool {
|
|
return e.apiKey != "" && e.baseURL != ""
|
|
}
|