mirror of
https://github.com/infiniflow/ragflow.git
synced 2026-06-29 15:31:05 +08:00
fix(Go): rewrite chat, listmodels, embed for Ollama (#15213)
### What problem does this PR solve? IDK how to implement **`Ollama`** on #14580 but it's totally wrong. This is the rewrite version for **`Ollama`** **Verified from CLI** ``` # Embed RAGFlow(user)> embed text 'what is rag' 'who are you' with 'nomic-embed-text:latest@test12@ollama' dimension 1024; +-----------+-------+ | dimension | index | +-----------+-------+ | 768 | 0 | | 768 | 1 | +-----------+-------+ # Chat RAGFlow(user)> think chat with 'qwen3:0.6b@test12@ollama' message 'who r u' Thinking: Okay, the user asked, "Who r u?" I need to respond appropriately. First, I should acknowledge their question. Since I'm an AI, I don't have a physical form, but I can confirm that I'm a large language model. I should keep the response friendly and offer help. Let me make sure I'm not making up any information and that the response is natural. Also, I should check for any typos and ensure clarity. Alright, that should cover it. Answer: I'm an AI language model, and I don't have a physical form. However, I can tell you that I'm designed to assist with questions and tasks. How can I help you today? Time: 2.914285 RAGFlow(user)> stream think chat with 'qwen3:0.6b@test12@ollama' message 'who r u' Thinking: , the user asked, "Who are you?" I need to respond appropriately. Since I'm an AI assistant, I should mention that I don't have a physical form or a mind. I should also clarify that I can help with various tasks like answering questions or providing information. It's important to keep the response friendly and informative while maintaining the correct tone. Answer: don't have a physical form or a mind, but I'm here to help with your questions or tasks! What can I do for you today? Time: 1.740047 # LisyModels RAGFlow(user)> list supported models from 'ollama' 'test12' +-------------------------+ | model_name | +-------------------------+ | nomic-embed-text:latest | | qwen3:0.6b | +-------------------------+ ``` ### Type of change - [x] Bug Fix (non-breaking change which fixes an issue) - [x] Refactoring
This commit is contained in:
@@ -1,9 +1,9 @@
|
||||
{
|
||||
"name": "ollama",
|
||||
"url_suffix": {
|
||||
"chat": "chat/completions",
|
||||
"models": "models",
|
||||
"embedding": "embeddings"
|
||||
"chat": "api/chat",
|
||||
"models": "api/ps",
|
||||
"embedding": "api/embed"
|
||||
},
|
||||
"class": "local"
|
||||
}
|
||||
@@ -4,7 +4,9 @@
|
||||
"chat": "v1/chat/completions",
|
||||
"embedding": "v1/embeddings",
|
||||
"models": "v1/models",
|
||||
"rerank": "v1/rerank"
|
||||
"rerank": "v1/rerank",
|
||||
"asr": "v1/audio/transcriptions",
|
||||
"tts": "v1/audio/speech"
|
||||
},
|
||||
"class": "local"
|
||||
}
|
||||
|
||||
@@ -8,7 +8,6 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"ragflow/internal/common"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
@@ -78,9 +77,15 @@ func (o *OllamaModel) ChatWithMessages(modelName string, messages []Message, api
|
||||
// Convert messages to API format
|
||||
apiMessages := make([]map[string]interface{}, len(messages))
|
||||
for i, msg := range messages {
|
||||
arr, _ := msg.Content.([]interface{})
|
||||
|
||||
first, _ := arr[0].(map[string]interface{})
|
||||
|
||||
text, _ := first["text"].(string)
|
||||
|
||||
apiMessages[i] = map[string]interface{}{
|
||||
"role": msg.Role,
|
||||
"content": msg.Content,
|
||||
"content": text,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -113,15 +118,13 @@ func (o *OllamaModel) ChatWithMessages(modelName string, messages []Message, api
|
||||
reqBody["stop"] = *chatModelConfig.Stop
|
||||
}
|
||||
|
||||
if chatModelConfig.Thinking != nil {
|
||||
if chatModelConfig.Effort != nil && *chatModelConfig.Effort != "" {
|
||||
if strings.HasPrefix(strings.ToLower(modelName), "gpt-oss") {
|
||||
reqBody["think"] = *chatModelConfig.Effort
|
||||
}
|
||||
} else if chatModelConfig.Thinking != nil {
|
||||
if *chatModelConfig.Thinking {
|
||||
reqBody["thinking"] = map[string]interface{}{
|
||||
"type": "enabled",
|
||||
}
|
||||
} else {
|
||||
reqBody["thinking"] = map[string]interface{}{
|
||||
"type": "disabled",
|
||||
}
|
||||
reqBody["think"] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -137,7 +140,6 @@ func (o *OllamaModel) ChatWithMessages(modelName string, messages []Message, api
|
||||
}
|
||||
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", *apiConfig.ApiKey))
|
||||
|
||||
resp, err := o.httpClient.Do(req)
|
||||
if err != nil {
|
||||
@@ -160,35 +162,19 @@ func (o *OllamaModel) ChatWithMessages(modelName string, messages []Message, api
|
||||
return nil, fmt.Errorf("failed to parse response: %w", err)
|
||||
}
|
||||
|
||||
choices, ok := result["choices"].([]interface{})
|
||||
if !ok || len(choices) == 0 {
|
||||
return nil, fmt.Errorf("no choices in response")
|
||||
}
|
||||
|
||||
firstChoice, ok := choices[0].(map[string]interface{})
|
||||
message, ok := result["message"].(map[string]interface{})
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("invalid choice format")
|
||||
return nil, fmt.Errorf("failed to parse response: message not found")
|
||||
}
|
||||
|
||||
messageMap, ok := firstChoice["message"].(map[string]interface{})
|
||||
content, ok := message["content"].(string)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("invalid message format")
|
||||
return nil, fmt.Errorf("failed to parse response: content not found")
|
||||
}
|
||||
|
||||
content, ok := messageMap["content"].(string)
|
||||
reasonContent, ok := message["thinking"].(string)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("invalid content format")
|
||||
}
|
||||
|
||||
var reasonContent string
|
||||
if chatModelConfig != nil && chatModelConfig.Thinking != nil && *chatModelConfig.Thinking {
|
||||
reasonContent, ok = messageMap["reasoning_content"].(string)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("invalid content format")
|
||||
}
|
||||
if reasonContent != "" && reasonContent[0] == '\n' {
|
||||
reasonContent = reasonContent[1:]
|
||||
}
|
||||
return nil, fmt.Errorf("failed to parse response: thinking not found")
|
||||
}
|
||||
|
||||
chatResponse := &ChatResponse{
|
||||
@@ -218,9 +204,15 @@ func (o *OllamaModel) ChatStreamlyWithSender(modelName string, messages []Messag
|
||||
// Convert messages to API format (supporting multimodal content)
|
||||
apiMessages := make([]map[string]interface{}, len(messages))
|
||||
for i, msg := range messages {
|
||||
arr, _ := msg.Content.([]interface{})
|
||||
|
||||
first, _ := arr[0].(map[string]interface{})
|
||||
|
||||
text, _ := first["text"].(string)
|
||||
|
||||
apiMessages[i] = map[string]interface{}{
|
||||
"role": msg.Role,
|
||||
"content": msg.Content,
|
||||
"content": text,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -255,15 +247,13 @@ func (o *OllamaModel) ChatStreamlyWithSender(modelName string, messages []Messag
|
||||
reqBody["stop"] = *modelConfig.Stop
|
||||
}
|
||||
|
||||
if modelConfig.Thinking != nil {
|
||||
if modelConfig.Effort != nil && *modelConfig.Effort != "" {
|
||||
if strings.HasPrefix(strings.ToLower(modelName), "gpt-oss") {
|
||||
reqBody["think"] = *modelConfig.Effort
|
||||
}
|
||||
} else if modelConfig.Thinking != nil {
|
||||
if *modelConfig.Thinking {
|
||||
reqBody["thinking"] = map[string]interface{}{
|
||||
"type": "enabled",
|
||||
}
|
||||
} else {
|
||||
reqBody["thinking"] = map[string]interface{}{
|
||||
"type": "disabled",
|
||||
}
|
||||
reqBody["think"] = true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -278,7 +268,6 @@ func (o *OllamaModel) ChatStreamlyWithSender(modelName string, messages []Messag
|
||||
}
|
||||
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", *apiConfig.ApiKey))
|
||||
|
||||
resp, err := o.httpClient.Do(req)
|
||||
if err != nil {
|
||||
@@ -294,66 +283,40 @@ func (o *OllamaModel) ChatStreamlyWithSender(modelName string, messages []Messag
|
||||
// SSE parsing: read line by line
|
||||
scanner := bufio.NewScanner(resp.Body)
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
common.Info(line)
|
||||
line := strings.TrimSpace(scanner.Text())
|
||||
|
||||
// SSE data line starts with "data:"
|
||||
if !strings.HasPrefix(line, "data:") {
|
||||
// ignore the blank
|
||||
if line == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
// Extract JSON after "data:"
|
||||
data := strings.TrimSpace(line[5:])
|
||||
|
||||
// [DONE] marks the end of stream
|
||||
if data == "[DONE]" {
|
||||
break
|
||||
}
|
||||
|
||||
// Parse the JSON event
|
||||
var event map[string]interface{}
|
||||
if err = json.Unmarshal([]byte(data), &event); err != nil {
|
||||
if err = json.Unmarshal([]byte(line), &event); err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
choices, ok := event["choices"].([]interface{})
|
||||
if !ok || len(choices) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
firstChoice, ok := choices[0].(map[string]interface{})
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
delta, ok := firstChoice["delta"].(map[string]interface{})
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
reasoningContent, ok := delta["reasoning_content"].(string)
|
||||
if ok && reasoningContent != "" {
|
||||
if err := sender(nil, &reasoningContent); err != nil {
|
||||
return err
|
||||
if messageMap, ok := event["message"].(map[string]interface{}); ok {
|
||||
if reasoningContent, exists := messageMap["thinking"].(string); exists && reasoningContent != "" {
|
||||
if err := sender(nil, &reasoningContent); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if content, exists := messageMap["content"].(string); exists && content != "" {
|
||||
if err := sender(&content, nil); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
content, ok := delta["content"].(string)
|
||||
if ok && content != "" {
|
||||
if err := sender(&content, nil); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
finishReason, ok := firstChoice["finish_reason"].(string)
|
||||
if ok && finishReason != "" {
|
||||
if done, ok := event["done"].(bool); ok && done {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Send [DONE] marker for OpenAI compatibility
|
||||
// Send [DONE] marker for OpenAI compatibility with RAGFlow frontend
|
||||
endOfStream := "[DONE]"
|
||||
if err = sender(&endOfStream, nil); err != nil {
|
||||
if err := sender(&endOfStream, nil); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -425,17 +388,30 @@ func (o *OllamaModel) Embed(modelName *string, texts []string, apiConfig *APICon
|
||||
return nil, fmt.Errorf("Ollama embeddings API error: %s, body: %s", resp.Status, string(body))
|
||||
}
|
||||
|
||||
var parsed openaiEmbeddingResponse
|
||||
if err = json.Unmarshal(body, &parsed); err != nil {
|
||||
return nil, fmt.Errorf("failed to parse response: %w", err)
|
||||
var embedResp struct {
|
||||
Model string `json:"model"`
|
||||
Embeddings [][]float64 `json:"embeddings"`
|
||||
}
|
||||
|
||||
var embeddings []EmbeddingData
|
||||
for _, dataElem := range parsed.Data {
|
||||
var embeddingData EmbeddingData
|
||||
embeddingData.Embedding = dataElem.Embedding
|
||||
embeddingData.Index = dataElem.Index
|
||||
embeddings = append(embeddings, embeddingData)
|
||||
if err = json.Unmarshal(body, &embedResp); err != nil {
|
||||
return nil, fmt.Errorf("failed to unmarshal response: %w", err)
|
||||
}
|
||||
|
||||
if len(embedResp.Embeddings) == 0 {
|
||||
return nil, fmt.Errorf("no embeddings returned")
|
||||
}
|
||||
|
||||
embeddings := make([]EmbeddingData, 0, len(embedResp.Embeddings))
|
||||
|
||||
for i, emb := range embedResp.Embeddings {
|
||||
if len(emb) == 0 {
|
||||
return nil, fmt.Errorf("empty embedding at index %d", i)
|
||||
}
|
||||
|
||||
embeddings = append(embeddings, EmbeddingData{
|
||||
Embedding: emb,
|
||||
Index: i,
|
||||
})
|
||||
}
|
||||
|
||||
return embeddings, nil
|
||||
@@ -489,7 +465,6 @@ func (o *OllamaModel) ListModels(apiConfig *APIConfig) ([]string, error) {
|
||||
}
|
||||
|
||||
url := fmt.Sprintf("%s/%s", baseURL, o.URLSuffix.Models)
|
||||
|
||||
reqBody := map[string]interface{}{}
|
||||
|
||||
jsonData, err := json.Marshal(reqBody)
|
||||
@@ -503,12 +478,6 @@ func (o *OllamaModel) ListModels(apiConfig *APIConfig) ([]string, error) {
|
||||
}
|
||||
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
// Ollama is a local provider and the API key is optional. Only set
|
||||
// the Authorization header when a non-empty key was supplied. This
|
||||
// also avoids a nil-pointer dereference on apiConfig or ApiKey.
|
||||
if apiConfig != nil && apiConfig.ApiKey != nil && *apiConfig.ApiKey != "" {
|
||||
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", *apiConfig.ApiKey))
|
||||
}
|
||||
|
||||
resp, err := o.httpClient.Do(req)
|
||||
if err != nil {
|
||||
@@ -533,9 +502,9 @@ func (o *OllamaModel) ListModels(apiConfig *APIConfig) ([]string, error) {
|
||||
|
||||
// convert result["data"] to []map[string]interface{}
|
||||
models := make([]string, 0)
|
||||
for _, model := range result["data"].([]interface{}) {
|
||||
for _, model := range result["models"].([]interface{}) {
|
||||
modelMap := model.(map[string]interface{})
|
||||
modelName := modelMap["id"].(string)
|
||||
modelName := modelMap["name"].(string)
|
||||
models = append(models, modelName)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user