Files
ragflow/internal/service/models/siliconflow_model.go
qinling0210 1473000135 Implement retrieval_test in GO (#14231)
### What problem does this PR solve?

Implement retrieval_test in GO

### Type of change

- [x] Refactoring
2026-04-24 15:30:14 +08:00

381 lines
10 KiB
Go

//
// Copyright 2026 The InfiniFlow Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
package models
import (
"encoding/json"
"fmt"
"io"
"net/http"
"ragflow/internal/entity"
"strings"
)
// siliconflowEmbeddingModel implements EmbeddingModel for SILICONFLOW API (OpenAI-compatible)
type siliconflowEmbeddingModel struct {
apiKey string
apiBase string
model string
httpClient *http.Client
}
// siliconflowChatModel implements ChatModel for SILICONFLOW API
type siliconflowChatModel struct {
apiKey string
apiBase string
model string
httpClient *http.Client
}
// siliconflowRerankModel implements RerankModel for SILICONFLOW API
type siliconflowRerankModel struct {
apiKey string
apiBase string
model string
httpClient *http.Client
}
// SiliconflowEmbeddingRequest represents SILICONFLOW embedding request
type SiliconflowEmbeddingRequest struct {
Model string `json:"model"`
Input []string `json:"input"`
}
// SiliconflowEmbeddingResponse represents SILICONFLOW embedding response
type SiliconflowEmbeddingResponse struct {
Data []struct {
Embedding []float64 `json:"embedding"`
Index int `json:"index"`
} `json:"data"`
}
// SiliconflowChatRequest represents SILICONFLOW chat request
type SiliconflowChatRequest struct {
Model string `json:"model"`
Messages []ChatMessage `json:"messages"`
Temperature float64 `json:"temperature,omitempty"`
MaxTokens int `json:"max_tokens,omitempty"`
Stream bool `json:"stream,omitempty"`
}
// SiliconflowChatResponse represents SILICONFLOW chat response
type SiliconflowChatResponse struct {
Choices []struct {
Message struct {
Content string `json:"content"`
} `json:"message"`
FinishReason string `json:"finish_reason"`
} `json:"choices"`
Error struct {
Message string `json:"message"`
Code string `json:"code"`
} `json:"error,omitempty"`
}
// ChatMessage represents a chat message
type ChatMessage struct {
Role string `json:"role"`
Content string `json:"content"`
}
// SiliconflowRerankRequest represents SILICONFLOW rerank request
type SiliconflowRerankRequest struct {
Model string `json:"model"`
Query string `json:"query"`
Documents []string `json:"documents"`
TopN int `json:"top_n"`
ReturnDocuments bool `json:"return_documents"`
MaxChunksPerDoc int `json:"max_chunks_per_doc"`
OverlapTokens int `json:"overlap_tokens"`
}
// SiliconflowRerankResponse represents SILICONFLOW rerank response
type SiliconflowRerankResponse struct {
Results []struct {
Index int `json:"index"`
RelevanceScore float64 `json:"relevance_score"`
} `json:"results"`
}
// Encode encodes a list of texts into embeddings using SILICONFLOW API
func (m *siliconflowEmbeddingModel) Encode(texts []string) ([][]float64, error) {
if len(texts) == 0 {
return [][]float64{}, nil
}
reqBody := SiliconflowEmbeddingRequest{
Model: m.model,
Input: texts,
}
jsonData, err := json.Marshal(reqBody)
if err != nil {
return nil, fmt.Errorf("failed to marshal request: %w", err)
}
req, err := http.NewRequest("POST", m.apiBase+"/embeddings", strings.NewReader(string(jsonData)))
if err != nil {
return nil, fmt.Errorf("failed to create request: %w", err)
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+m.apiKey)
resp, err := m.httpClient.Do(req)
if err != nil {
return nil, fmt.Errorf("failed to send request: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
return nil, fmt.Errorf("SILICONFLOW API error: %s, body: %s", resp.Status, string(body))
}
var embeddingResp SiliconflowEmbeddingResponse
if err := json.NewDecoder(resp.Body).Decode(&embeddingResp); err != nil {
return nil, fmt.Errorf("failed to decode response: %w", err)
}
// Sort embeddings by index to ensure correct order
embeddings := make([][]float64, len(texts))
for _, data := range embeddingResp.Data {
if data.Index < len(embeddings) {
embeddings[data.Index] = data.Embedding
}
}
return embeddings, nil
}
// EncodeQuery encodes a single query string into embedding
func (m *siliconflowEmbeddingModel) EncodeQuery(query string) ([]float64, error) {
embeddings, err := m.Encode([]string{query})
if err != nil {
return nil, err
}
if len(embeddings) == 0 {
return nil, fmt.Errorf("no embedding returned")
}
return embeddings[0], nil
}
// Chat sends a chat message and returns response
func (m *siliconflowChatModel) Chat(system string, history []map[string]string, genConf map[string]interface{}) (string, error) {
// Build messages array
var messages []ChatMessage
// Add system message if provided
if system != "" {
messages = append(messages, ChatMessage{Role: "system", Content: system})
}
// Add history messages
for _, msg := range history {
role := msg["role"]
content := msg["content"]
if role != "" && content != "" {
messages = append(messages, ChatMessage{Role: role, Content: content})
}
}
// Extract generation config
temperature := 0.7
if temp, ok := genConf["temperature"].(float64); ok {
temperature = temp
}
maxTokens := 1024
if mt, ok := genConf["max_tokens"].(int); ok {
maxTokens = mt
}
// Build request
reqBody := SiliconflowChatRequest{
Model: m.model,
Messages: messages,
Temperature: temperature,
MaxTokens: maxTokens,
}
jsonData, err := json.Marshal(reqBody)
if err != nil {
return "", fmt.Errorf("failed to marshal request: %w", err)
}
// Build URL - append /chat/completions if not already present
url := m.apiBase
if !strings.HasSuffix(url, "/chat/completions") {
if !strings.HasSuffix(url, "/") {
url += "/"
}
url += "chat/completions"
}
req, err := http.NewRequest("POST", url, strings.NewReader(string(jsonData)))
if err != nil {
return "", fmt.Errorf("failed to create request: %w", err)
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+m.apiKey)
resp, err := m.httpClient.Do(req)
if err != nil {
return "", fmt.Errorf("failed to send request: %w", err)
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return "", fmt.Errorf("failed to read response: %w", err)
}
if resp.StatusCode != http.StatusOK {
return "", fmt.Errorf("SILICONFLOW API error: %s, body: %s", resp.Status, string(body))
}
var chatResp SiliconflowChatResponse
if err := json.Unmarshal(body, &chatResp); err != nil {
return "", fmt.Errorf("failed to decode response: %w", err)
}
if chatResp.Error.Message != "" {
return "", fmt.Errorf("chat error: %s", chatResp.Error.Message)
}
if len(chatResp.Choices) == 0 {
return "", fmt.Errorf("no response choices returned")
}
return chatResp.Choices[0].Message.Content, nil
}
// ChatStreamly sends a chat message and streams response
func (m *siliconflowChatModel) ChatStreamly(system string, history []map[string]string, genConf map[string]interface{}) (<-chan string, error) {
// For now, return a simple non-streaming implementation
// Streaming can be implemented later with SSE support
responseChan := make(chan string)
go func() {
defer close(responseChan)
response, err := m.Chat(system, history, genConf)
if err != nil {
responseChan <- "**ERROR**: " + err.Error()
return
}
responseChan <- response
}()
return responseChan, nil
}
// Similarity calculates similarity scores between query and texts using SiliconFlow API
func (m *siliconflowRerankModel) Similarity(query string, texts []string) ([]float64, error) {
if len(texts) == 0 {
return []float64{}, nil
}
reqBody := SiliconflowRerankRequest{
Model: m.model,
Query: query,
Documents: texts,
TopN: len(texts),
ReturnDocuments: false,
MaxChunksPerDoc: 1024,
OverlapTokens: 80,
}
jsonData, err := json.Marshal(reqBody)
if err != nil {
return nil, fmt.Errorf("failed to marshal request: %w", err)
}
reqURL := m.apiBase
if !strings.Contains(reqURL, "/rerank") {
if !strings.HasSuffix(reqURL, "/") {
reqURL += "/"
}
reqURL += "rerank"
}
req, err := http.NewRequest("POST", reqURL, strings.NewReader(string(jsonData)))
if err != nil {
return nil, fmt.Errorf("failed to create request: %w", err)
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+m.apiKey)
resp, err := m.httpClient.Do(req)
if err != nil {
return nil, fmt.Errorf("failed to send request: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
return nil, fmt.Errorf("SiliconFlow Rerank API error: %s, body: %s", resp.Status, string(body))
}
body, _ := io.ReadAll(resp.Body)
var rerankResp SiliconflowRerankResponse
if err := json.Unmarshal(body, &rerankResp); err != nil {
return nil, fmt.Errorf("failed to decode response: %w", err)
}
scores := make([]float64, len(texts))
for _, result := range rerankResp.Results {
if result.Index >= 0 && result.Index < len(texts) {
scores[result.Index] = result.RelevanceScore
}
}
return scores, nil
}
// init registers the SILICONFLOW model factories
func init() {
RegisterEmbeddingModelFactory("SILICONFLOW", func(apiKey, apiBase, modelName string, httpClient *http.Client) entity.EmbeddingModel {
return &siliconflowEmbeddingModel{
apiKey: apiKey,
apiBase: apiBase,
model: modelName,
httpClient: httpClient,
}
})
RegisterChatModelFactory("SILICONFLOW", func(apiKey, apiBase, modelName string, httpClient *http.Client) entity.ChatModel {
return &siliconflowChatModel{
apiKey: apiKey,
apiBase: apiBase,
model: modelName,
httpClient: httpClient,
}
})
RegisterRerankModelFactory("SILICONFLOW", func(apiKey, apiBase, modelName string, httpClient *http.Client) entity.RerankModel {
return &siliconflowRerankModel{
apiKey: apiKey,
apiBase: apiBase,
model: modelName,
httpClient: httpClient,
}
})
}