mirror of
https://github.com/infiniflow/ragflow.git
synced 2026-07-13 08:28:22 +08:00
### What problem does this PR solve? Implement retrieval_test in GO ### Type of change - [x] Refactoring
381 lines
10 KiB
Go
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,
|
|
}
|
|
})
|
|
}
|