mirror of
https://github.com/infiniflow/ragflow.git
synced 2026-07-16 20:57:21 +08:00
### What problem does this PR solve? This PR completes the Jina provider **The following functionalities are now supported:** **Jina:** - [ ] Chat / Stream Chat (Not available for now: [(Jina chat API docs)](https://api.jina.ai/docs#/Search%20Foundation%20Models/chat_completions_v1_chat_completions_post)) - [x] Embedding - [x] Rerank - [x] Model listing - [x] Provider connection checking - [ ] ~~Balance~~ **Verified examples from the CLI:** ```plaintext RAGFlow(user)> embed text 'walkerwhat' 'jumperwho' with 'jina-embeddings-v2-base-en@test@jina' dimension 16 +-----------+-------+ | dimension | index | +-----------+-------+ | 768 | 0 | | 768 | 1 | +-----------+-------+ RAGFlow(user)> rerank query 'what is rag' document 'rag is retrieval augment generation' 'rag need llm' 'famous rag project includes ragflow' with 'jina-reranker-v2-base-multilingual@test@jina' top 3; +-------+-----------------+ | index | relevance_score | +-------+-----------------+ | 0 | 0.74316794 | | 2 | 0.18713269 | | 1 | 0.15817434 | +-------+-----------------+ RAGFlow(user)> list supported models from 'jina' 'test' +---------------------------------------------+ | model_name | +---------------------------------------------+ | Jina AI: Jina VLM | | Jina AI: Jina Reranker v3 | | Jina AI: Jina Code Embeddings 0.5b | | Jina AI: Jina Code Embeddings 1.5b | | Jina AI: Jina Embeddings v4 | | Jina AI: Jina Reranker M0 | | Jina AI: ReaderLM v2 | | Jina AI: Jina Clip v2 | | Jina AI: Jina Embeddings v3 | | Jina AI: Jina Colbert v2 | | Jina AI: Reader LM 0.5b | | Jina AI: Reader LM 1.5b | | Jina AI: Jina Reranker v2 Base Multilingual | | Jina AI: Jina Clip v1 | | Jina AI: Jina Reranker v1 Tiny EN | | Jina AI: Jina Reranker v1 Turbo EN | | Jina AI: Jina Reranker v1 Base EN | | Jina AI: Jina Colbert v1 EN | | Jina AI: Jina Embeddings v2 Base ES | | Jina AI: Jina Embeddings v2 Base Code | | Jina AI: Jina Embeddings v2 Base DE | | Jina AI: Jina Embeddings v2 Base ZH | | Jina AI: Jina Embeddings v2 Base EN | | Jina AI: Jina Embedding B EN v1 | | Jina AI: Jina Embeddings v5 Text Small | | Jina AI: Jina Embeddings v5 Omni Small | | Jina AI: Jina Embeddings v5 Omni Nano | | Jina AI: Jina Embeddings v5 Text Nano | +---------------------------------------------+ RAGFlow(user)> check instance 'test' from 'jina' SUCCESS ``` ### Type of change - [x] New Feature (non-breaking change which adds functionality)
253 lines
6.8 KiB
Go
253 lines
6.8 KiB
Go
package models
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"time"
|
|
)
|
|
|
|
type JinaModel struct {
|
|
BaseURL map[string]string
|
|
URLSuffix URLSuffix
|
|
httpClient *http.Client
|
|
}
|
|
|
|
func NewJinaModel(baseURL map[string]string, urlSuffix URLSuffix) *JinaModel {
|
|
return &JinaModel{
|
|
BaseURL: baseURL,
|
|
URLSuffix: urlSuffix,
|
|
httpClient: &http.Client{
|
|
Timeout: time.Second * 90,
|
|
},
|
|
}
|
|
}
|
|
|
|
func (j *JinaModel) NewInstance(baseURL map[string]string) ModelDriver {
|
|
return &JinaModel{
|
|
BaseURL: baseURL,
|
|
URLSuffix: j.URLSuffix,
|
|
httpClient: &http.Client{
|
|
Timeout: time.Second * 90,
|
|
},
|
|
}
|
|
}
|
|
|
|
func (j *JinaModel) Name() string {
|
|
return "jina"
|
|
}
|
|
|
|
func (j *JinaModel) ChatWithMessages(modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig) (*ChatResponse, error) {
|
|
//TODO implement me: https://api.jina.ai/docs#/Search%20Foundation%20Models/chat_completions_v1_chat_completions_post
|
|
return nil, fmt.Errorf("jina does not implement ChatWithMessages(not available for now)")
|
|
}
|
|
|
|
func (j *JinaModel) ChatStreamlyWithSender(modelName string, messages []Message, apiConfig *APIConfig, modelConfig *ChatConfig, sender func(*string, *string) error) error {
|
|
//TODO implement me: https://api.jina.ai/docs#/Search%20Foundation%20Models/chat_completions_v1_chat_completions_post
|
|
return fmt.Errorf("jina does not implement ChatStreamlyWithSender(not available for now)")
|
|
}
|
|
|
|
func (j *JinaModel) Embed(modelName *string, texts []string, apiConfig *APIConfig, embeddingConfig *EmbeddingConfig) ([]EmbeddingData, error) {
|
|
if len(texts) == 0 {
|
|
return []EmbeddingData{}, nil
|
|
}
|
|
|
|
var region = "default"
|
|
if apiConfig != nil && apiConfig.Region != nil && *apiConfig.Region != "" {
|
|
region = *apiConfig.Region
|
|
}
|
|
|
|
url := fmt.Sprintf("%s/%s", j.BaseURL[region], j.URLSuffix.Embedding)
|
|
|
|
reqBody := map[string]interface{}{
|
|
"model": *modelName,
|
|
"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", url, bytes.NewBuffer(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", fmt.Sprintf("Bearer %s", *apiConfig.ApiKey))
|
|
|
|
resp, err := j.httpClient.Do(req)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to send request: %w", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
body, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to read response: %w", err)
|
|
}
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
return nil, fmt.Errorf("Jina embedding API error: status %d, body: %s", resp.StatusCode, string(body))
|
|
}
|
|
|
|
var parsedResponse struct {
|
|
Data []struct {
|
|
Embedding []float64 `json:"embedding"`
|
|
Index int `json:"index"`
|
|
} `json:"data"`
|
|
}
|
|
|
|
if err = json.Unmarshal(body, &parsedResponse); err != nil {
|
|
return nil, fmt.Errorf("failed to decode response: %w", err)
|
|
}
|
|
|
|
if len(parsedResponse.Data) == 0 {
|
|
return nil, fmt.Errorf("Jina embedding response contains no data: %s", string(body))
|
|
}
|
|
|
|
var embeddings []EmbeddingData
|
|
for _, dataElem := range parsedResponse.Data {
|
|
embeddings = append(embeddings, EmbeddingData{
|
|
Embedding: dataElem.Embedding,
|
|
Index: dataElem.Index,
|
|
})
|
|
}
|
|
|
|
return embeddings, nil
|
|
}
|
|
|
|
func (j *JinaModel) Rerank(modelName *string, query string, documents []string, apiConfig *APIConfig, rerankConfig *RerankConfig) (*RerankResponse, error) {
|
|
if len(documents) == 0 {
|
|
return &RerankResponse{}, nil
|
|
}
|
|
|
|
var region = "default"
|
|
if apiConfig != nil && apiConfig.Region != nil && *apiConfig.Region != "" {
|
|
region = *apiConfig.Region
|
|
}
|
|
|
|
url := fmt.Sprintf("%s/%s", j.BaseURL[region], j.URLSuffix.Rerank)
|
|
|
|
var topN = rerankConfig.TopN
|
|
if rerankConfig.TopN != 0 {
|
|
topN = rerankConfig.TopN
|
|
}
|
|
|
|
reqBody := map[string]interface{}{
|
|
"model": *modelName,
|
|
"query": query,
|
|
"documents": documents,
|
|
"top_n": topN,
|
|
}
|
|
|
|
jsonData, err := json.Marshal(reqBody)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to marshal request: %w", err)
|
|
}
|
|
|
|
req, err := http.NewRequest("POST", url, bytes.NewBuffer(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", fmt.Sprintf("Bearer %s", *apiConfig.ApiKey))
|
|
|
|
resp, err := j.httpClient.Do(req)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to send request: %w", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
body, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to read response: %w", err)
|
|
}
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
return nil, fmt.Errorf("Jina Rerank API error: status %d, body: %s", resp.StatusCode, string(body))
|
|
}
|
|
|
|
var rerankResp struct {
|
|
Results []struct {
|
|
Index int `json:"index"`
|
|
RelevanceScore float64 `json:"relevance_score"`
|
|
} `json:"results"`
|
|
}
|
|
|
|
if err = json.Unmarshal(body, &rerankResp); err != nil {
|
|
return nil, fmt.Errorf("failed to decode response: %w", err)
|
|
}
|
|
|
|
var rerankResponse RerankResponse
|
|
for _, result := range rerankResp.Results {
|
|
rerankResult := RerankResult{
|
|
Index: result.Index,
|
|
RelevanceScore: result.RelevanceScore,
|
|
}
|
|
rerankResponse.Data = append(rerankResponse.Data, rerankResult)
|
|
}
|
|
|
|
return &rerankResponse, nil
|
|
}
|
|
|
|
func (j *JinaModel) ListModels(apiConfig *APIConfig) ([]string, error) {
|
|
var region = "default"
|
|
if apiConfig != nil && apiConfig.Region != nil && *apiConfig.Region != "" {
|
|
region = *apiConfig.Region
|
|
}
|
|
|
|
url := fmt.Sprintf("%s/%s", j.BaseURL[region], j.URLSuffix.Models)
|
|
|
|
req, err := http.NewRequest("GET", url, nil)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to create request: %w", err)
|
|
}
|
|
|
|
req.Header.Set("Content-Type", "application/json")
|
|
|
|
resp, err := j.httpClient.Do(req)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to send request: %w", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
body, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to read response body: %w", err)
|
|
}
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
return nil, fmt.Errorf("API request failed with status %d: %s", resp.StatusCode, string(body))
|
|
}
|
|
|
|
// Parse response
|
|
var result map[string]interface{}
|
|
if err = json.Unmarshal(body, &result); err != nil {
|
|
return nil, fmt.Errorf("failed to parse response: %w", err)
|
|
}
|
|
|
|
// convert result["data"] to []map[string]interface{}
|
|
models := make([]string, 0)
|
|
for _, model := range result["data"].([]interface{}) {
|
|
modelMap := model.(map[string]interface{})
|
|
modelName := modelMap["name"].(string)
|
|
models = append(models, modelName)
|
|
}
|
|
|
|
return models, nil
|
|
}
|
|
|
|
func (j *JinaModel) Balance(apiConfig *APIConfig) (map[string]interface{}, error) {
|
|
return nil, fmt.Errorf("no such method")
|
|
}
|
|
|
|
func (j *JinaModel) CheckConnection(apiConfig *APIConfig) error {
|
|
_, err := j.ListModels(apiConfig)
|
|
return err
|
|
}
|