Files
ragflow/internal/cli/user_command.go

3252 lines
91 KiB
Go
Raw Normal View History

Refactor Go server model provider reading and access (#13831) ### What problem does this PR solve? 1. Refactor model provider json file format 2. Use memory data structure to replace database 3. Add CLI command to access ``` RAGFlow(user)> list pool models from 'xai'; +-------------------------------------------------------------------------------------+------------+-------------+-----------------------+ | features | max_tokens | model_types | name | +-------------------------------------------------------------------------------------+------------+-------------+-----------------------+ | map[] | 256000 | [llm] | grok-4 | | map[] | 131072 | [llm] | grok-3 | | map[] | 131072 | [llm] | grok-3-fast | | map[] | 131072 | [llm] | grok-3-mini | | map[] | 131072 | [llm] | grok-3-mini-mini-fast | | map[multimodal:map[enabled:true input_modalities:[image] output_modalities:[text]]] | 32768 | [vlm] | grok-2-vision | +-------------------------------------------------------------------------------------+------------+-------------+-----------------------+ RAGFlow(user)> show pool model 'grok-2-vision' from 'xai'; +-------------------------------------------------------------------------------------+------------+-------------+---------------+ | features | max_tokens | model_types | name | +-------------------------------------------------------------------------------------+------------+-------------+---------------+ | map[multimodal:map[enabled:true input_modalities:[image] output_modalities:[text]]] | 32768 | [vlm] | grok-2-vision | +-------------------------------------------------------------------------------------+------------+-------------+---------------+ RAGFlow(user)> list pool providers; +--------+------------------------------------------------------------+---------------------------+ | name | tags | url | +--------+------------------------------------------------------------+---------------------------+ | OpenAI | LLM,TEXT EMBEDDING,TTS,TEXT RE-RANK,SPEECH2TEXT,MODERATION | https://api.openai.com/v1 | | xAI | LLM | https://api.x.ai/v1 | +--------+------------------------------------------------------------+---------------------------+ RAGFlow(user)> show pool provider 'openai'; +---------------------------+--------+------------------------------------------------------------+--------------+ | base_url | name | tags | total_models | +---------------------------+--------+------------------------------------------------------------+--------------+ | https://api.openai.com/v1 | OpenAI | LLM,TEXT EMBEDDING,TTS,TEXT RE-RANK,SPEECH2TEXT,MODERATION | 27 | +---------------------------+--------+------------------------------------------------------------+--------------+ ``` ### Type of change - [x] New Feature (non-breaking change which adds functionality) - [x] Refactoring --------- Signed-off-by: Jin Hai <haijin.chn@gmail.com>
2026-03-30 12:00:49 +08:00
//
// 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 cli
import (
"bufio"
Refactor Go server model provider reading and access (#13831) ### What problem does this PR solve? 1. Refactor model provider json file format 2. Use memory data structure to replace database 3. Add CLI command to access ``` RAGFlow(user)> list pool models from 'xai'; +-------------------------------------------------------------------------------------+------------+-------------+-----------------------+ | features | max_tokens | model_types | name | +-------------------------------------------------------------------------------------+------------+-------------+-----------------------+ | map[] | 256000 | [llm] | grok-4 | | map[] | 131072 | [llm] | grok-3 | | map[] | 131072 | [llm] | grok-3-fast | | map[] | 131072 | [llm] | grok-3-mini | | map[] | 131072 | [llm] | grok-3-mini-mini-fast | | map[multimodal:map[enabled:true input_modalities:[image] output_modalities:[text]]] | 32768 | [vlm] | grok-2-vision | +-------------------------------------------------------------------------------------+------------+-------------+-----------------------+ RAGFlow(user)> show pool model 'grok-2-vision' from 'xai'; +-------------------------------------------------------------------------------------+------------+-------------+---------------+ | features | max_tokens | model_types | name | +-------------------------------------------------------------------------------------+------------+-------------+---------------+ | map[multimodal:map[enabled:true input_modalities:[image] output_modalities:[text]]] | 32768 | [vlm] | grok-2-vision | +-------------------------------------------------------------------------------------+------------+-------------+---------------+ RAGFlow(user)> list pool providers; +--------+------------------------------------------------------------+---------------------------+ | name | tags | url | +--------+------------------------------------------------------------+---------------------------+ | OpenAI | LLM,TEXT EMBEDDING,TTS,TEXT RE-RANK,SPEECH2TEXT,MODERATION | https://api.openai.com/v1 | | xAI | LLM | https://api.x.ai/v1 | +--------+------------------------------------------------------------+---------------------------+ RAGFlow(user)> show pool provider 'openai'; +---------------------------+--------+------------------------------------------------------------+--------------+ | base_url | name | tags | total_models | +---------------------------+--------+------------------------------------------------------------+--------------+ | https://api.openai.com/v1 | OpenAI | LLM,TEXT EMBEDDING,TTS,TEXT RE-RANK,SPEECH2TEXT,MODERATION | 27 | +---------------------------+--------+------------------------------------------------------------+--------------+ ``` ### Type of change - [x] New Feature (non-breaking change which adds functionality) - [x] Refactoring --------- Signed-off-by: Jin Hai <haijin.chn@gmail.com>
2026-03-30 12:00:49 +08:00
"context"
Go: CLI chat with text, image, video (#14573) ### What problem does this PR solve? ``` RAGFlow(user)> chat with 'glm-4.6v-flash@test@zhipu-ai' message 'What are the pics talk about?' image 'https://cdn.bigmodel.cn/static/logo/register.png' 'https://cdn.bigmodel.cn/static/logo/api-key.png' Answer: The first picture shows a login/register modal with options for phone number login, account login, and WeChat QR code login, along with a prompt for new users to get a 20 million tokens experience package. The second picture displays the API keys management page of a platform, including a warning about API key security and a table listing existing API keys with details like creation time and usage history. Time: 31.600545 RAGFlow(user)> chat with 'glm-4.6v-flash@test@zhipu-ai' message 'What are the video talk about?' video 'https://cdn.bigmodel.cn/agent-demos/lark/113123.mov' Answer: Based on the sequence of frames provided, the video is a demonstration of a web search and navigation process. 1. The video starts with a blank Google search page. 2. The user types "智谱" (which is the Chinese name for the company Zhipu AI) into the search box. 3. The search is initiated and the page shows "About 0 results". 4. The search results load, showing information about Zhipu AI, including its website. 5. The user clicks on the main website link (www.zhipuai.cn). 6. The video ends by showing the homepage of Zhipu AI's website, titled "Z.ai GLM Large Model Open Platform". In summary, the video is about searching for the company "智谱" (Zhipu AI) on Google and then navigating to its official website. Time: 76.582520 ``` ### Type of change - [x] New Feature (non-breaking change which adds functionality) Signed-off-by: Jin Hai <haijin.chn@gmail.com>
2026-05-05 18:14:39 +08:00
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"io"
"net"
Go: CLI chat with text, image, video (#14573) ### What problem does this PR solve? ``` RAGFlow(user)> chat with 'glm-4.6v-flash@test@zhipu-ai' message 'What are the pics talk about?' image 'https://cdn.bigmodel.cn/static/logo/register.png' 'https://cdn.bigmodel.cn/static/logo/api-key.png' Answer: The first picture shows a login/register modal with options for phone number login, account login, and WeChat QR code login, along with a prompt for new users to get a 20 million tokens experience package. The second picture displays the API keys management page of a platform, including a warning about API key security and a table listing existing API keys with details like creation time and usage history. Time: 31.600545 RAGFlow(user)> chat with 'glm-4.6v-flash@test@zhipu-ai' message 'What are the video talk about?' video 'https://cdn.bigmodel.cn/agent-demos/lark/113123.mov' Answer: Based on the sequence of frames provided, the video is a demonstration of a web search and navigation process. 1. The video starts with a blank Google search page. 2. The user types "智谱" (which is the Chinese name for the company Zhipu AI) into the search box. 3. The search is initiated and the page shows "About 0 results". 4. The search results load, showing information about Zhipu AI, including its website. 5. The user clicks on the main website link (www.zhipuai.cn). 6. The video ends by showing the homepage of Zhipu AI's website, titled "Z.ai GLM Large Model Open Platform". In summary, the video is about searching for the company "智谱" (Zhipu AI) on Google and then navigating to its official website. Time: 76.582520 ``` ### Type of change - [x] New Feature (non-breaking change which adds functionality) Signed-off-by: Jin Hai <haijin.chn@gmail.com>
2026-05-05 18:14:39 +08:00
netUrl "net/url"
"os"
Go: implement TTS for MiniMax provider and CLI testing for TTS (#14911) ### What problem does this PR solve? This PR implement TTS for MiniMax provider and CLI testing for TTS **The following functionalities are now supported:** **MiniMax:** - [x] Chat / Stream Chat - [x] Embedding - [x] Rerank - [x] Model listing - [x] Provider connection checking - [x] Text To Speech - [ ] OCRFile - [ ] ~~Audio To Text~~ - [ ] ~~Balance~~ **Verified examples from the CLI:** ```plaintext RAGFlow(user)> tts with 'speech-2.8-hd@test@minimax' text 'He who desires but acts not, breeds pestilence.' play format 'wav' save './internal' param '{"voice_setting": {"voice_id": "English_radiant_girl", "speed": 1, "vol": 1, "pitch": 0}, "audio_setting": {"sample_rate": 32000, "bitrate": 128000, "format": "wav", "channel": 1}, "output_format": "hex"}' Saved to directory: /home/infiniflow/Documents/development/ragflow/internal/speech-2.8-hd_output.wav SUCCESS RAGFlow(user)> stream tts with 'speech-2.8-hd@test@minimax' text 'He who desires but acts not, breeds pestilence.' play format 'wav' save './internal' param '{"voice_setting": {"voice_id": "English_radiant_girl", "speed": 1, "vol": 1, "pitch": 0}, "audio_setting": {"sample_rate": 32000, "bitrate": 128000, "format": "wav", "channel": 1}, "output_format": "hex"}' Saved to directory: /home/infiniflow/Documents/development/ragflow/internal/speech-2.8-hd_output.wav SUCCESS ``` Set `Play` to play audio in CLI Set `Save` `PATH_TO_SAVE` to save file Set `format` to save file in wav or mp3 Set `Param` align with official request body ### Type of change - [x] New Feature (non-breaking change which adds functionality)
2026-05-14 13:19:31 +08:00
"os/exec"
"path/filepath"
ce "ragflow/internal/cli/filesystem"
"strings"
"time"
)
// PingServer pings the server to check if it's alive
// Returns benchmark result map if iterations > 1, otherwise prints status
func (c *RAGFlowClient) PingServer(cmd *Command) (ResponseIf, error) {
// Get iterations from command params (for benchmark)
iterations := 1
if val, ok := cmd.Params["iterations"].(int); ok && val > 1 {
iterations = val
}
if iterations > 1 {
// Benchmark mode: multiple iterations
return c.HTTPClient.RequestWithIterations("GET", "/system/ping", "web", nil, nil, iterations)
}
// Single mode
resp, err := c.HTTPClient.Request("GET", "/system/ping", "web", nil, nil)
if err != nil {
fmt.Printf("Error: %v\n", err)
fmt.Println("Server is down")
return nil, err
}
if resp.StatusCode != 200 {
return nil, fmt.Errorf("failed to ping: HTTP %d, body: %s", resp.StatusCode, string(resp.Body))
}
var result SimpleResponse
result.Message = string(resp.Body)
result.Code = 0
return &result, nil
}
// Show server version to show RAGFlow server version
// Returns benchmark result map if iterations > 1, otherwise prints status
func (c *RAGFlowClient) ShowServerVersion(cmd *Command) (ResponseIf, error) {
// Get iterations from command params (for benchmark)
iterations := 1
if val, ok := cmd.Params["iterations"].(int); ok && val > 1 {
iterations = val
}
if iterations > 1 {
// Benchmark mode: multiple iterations
return c.HTTPClient.RequestWithIterations("GET", "/system/version", "web", nil, nil, iterations)
}
// Single mode
resp, err := c.HTTPClient.Request("GET", "/system/version", "web", nil, nil)
if err != nil {
return nil, fmt.Errorf("failed to show version: %w", err)
}
if resp.StatusCode != 200 {
return nil, fmt.Errorf("failed to show version: HTTP %d, body: %s", resp.StatusCode, string(resp.Body))
}
var result KeyValueResponse
if err = json.Unmarshal(resp.Body, &result); err != nil {
return nil, fmt.Errorf("show version failed: invalid JSON (%w)", err)
}
result.Key = "version"
result.Duration = resp.Duration
return &result, nil
}
func (c *RAGFlowClient) ListConfigs(cmd *Command) (ResponseIf, error) {
if c.ServerType != "user" {
return nil, fmt.Errorf("this command is only allowed in ADMIN mode")
}
// Get iterations from command params (for benchmark)
iterations := 1
if val, ok := cmd.Params["iterations"].(int); ok && val > 1 {
iterations = val
}
if iterations > 1 {
// Benchmark mode: multiple iterations
return c.HTTPClient.RequestWithIterations("GET", "/system/configs", "web", nil, nil, iterations)
}
// Single mode
resp, err := c.HTTPClient.Request("GET", "/system/configs", "web", nil, nil)
if err != nil {
return nil, fmt.Errorf("failed to list configs: %w", err)
}
if resp.StatusCode != 200 {
return nil, fmt.Errorf("failed to list configs: HTTP %d, body: %s", resp.StatusCode, string(resp.Body))
}
var response CommonDataResponse
if err = json.Unmarshal(resp.Body, &response); err != nil {
return nil, fmt.Errorf("list configs failed: invalid JSON (%w)", err)
}
var result CommonResponse
result.Code = 0
result.Data, err = GetConfigs(&response.Data)
if err != nil {
return nil, fmt.Errorf("failed to list configs: %w", err)
}
result.Duration = resp.Duration
return &result, nil
}
func GetConfigs(config *map[string]interface{}) ([]map[string]interface{}, error) {
if config == nil {
return nil, fmt.Errorf("config is nil")
}
result := []map[string]interface{}{}
{
redisHost := GetHost(config, "Redis", "Host", "Port")
result = append(result, map[string]interface{}{
"key": "redis_host",
"value": redisHost})
}
{
if docEngine, ok := (*config)["DocEngine"].(map[string]interface{}); ok {
engineType, _ := docEngine["Type"].(string)
result = append(result, map[string]interface{}{
"key": "doc_engine",
"value": engineType})
if engineType == "elasticsearch" {
esCfg, _ := docEngine["ES"].(map[string]interface{})
esHost, _ := esCfg["Hosts"].(string)
result = append(result, map[string]interface{}{
"key": "elasticsearch_host",
"value": esHost})
} else if engineType == "Infinity" {
infinityCfg, _ := docEngine["Infinity"].(map[string]interface{})
infinityHost, _ := infinityCfg["URI"]
result = append(result, map[string]interface{}{
"key": "infinity_host",
"value": infinityHost})
} else {
return nil, fmt.Errorf("unknown doc engine: %s", engineType)
}
}
}
{
if logConfig, ok := (*config)["Log"].(map[string]interface{}); ok {
level, _ := logConfig["Level"].(string)
result = append(result, map[string]interface{}{
"key": "log_level",
"value": level})
}
}
{
if databaseConfig, ok := (*config)["Database"].(map[string]interface{}); ok {
driver, _ := databaseConfig["Driver"].(string)
result = append(result, map[string]interface{}{
"key": "database",
"value": driver})
driverAddr, _ := databaseConfig["Host"].(string)
driverPort, _ := databaseConfig["Port"].(float64)
driverHost := fmt.Sprintf("%s:%0.f", driverAddr, driverPort)
result = append(result, map[string]interface{}{
"key": "database_host",
"value": driverHost})
}
}
{
if language, ok := (*config)["Language"].(map[string]interface{}); ok {
result = append(result, map[string]interface{}{
"key": "language",
"value": language})
}
}
{
if adminConfig, ok := (*config)["Admin"].(map[string]interface{}); ok {
adminAddr, _ := adminConfig["Host"].(string)
adminPort, _ := adminConfig["Port"].(float64)
adminHost := fmt.Sprintf("%s:%0.f", adminAddr, adminPort)
result = append(result, map[string]interface{}{
"key": "admin",
"value": adminHost})
}
}
{
if storageEngineConfig, ok := (*config)["StorageEngine"].(map[string]interface{}); ok {
engineType, _ := storageEngineConfig["Type"].(string)
result = append(result, map[string]interface{}{
"key": "storage_engine",
"value": engineType})
if engineType == "minio" {
minioCfg, _ := storageEngineConfig["Minio"].(map[string]interface{})
miniHost, _ := minioCfg["Host"].(string)
result = append(result, map[string]interface{}{
"key": "minio_host",
"value": miniHost})
} else {
return nil, fmt.Errorf("unknown storage engine: %s", engineType)
}
}
}
return result, nil
}
func GetHost(config *map[string]interface{}, serverType, address, port string) string {
if config == nil {
return ""
}
result := ""
if redis, ok := (*config)[serverType].(map[string]interface{}); ok {
serverAddr, hostOk := redis[address].(string)
serverPort, portOk := redis[port].(float64)
if hostOk && portOk {
result = fmt.Sprintf("%s:%.0f", serverAddr, serverPort)
}
}
return result
}
func (c *RAGFlowClient) SetLogLevel(cmd *Command) (ResponseIf, error) {
if c.ServerType != "user" {
return nil, fmt.Errorf("this command is only allowed in ADMIN mode")
}
if logLevel, ok := cmd.Params["level"].(string); ok {
payload := map[string]interface{}{
"level": logLevel,
}
resp, err := c.HTTPClient.Request("PUT", "/system/log", "admin", nil, payload)
if err != nil {
return nil, fmt.Errorf("failed to change log level: %w", err)
}
if resp.StatusCode != 200 {
return nil, fmt.Errorf("failed to register user: HTTP %d, body: %s", resp.StatusCode, string(resp.Body))
}
var result SimpleResponse
if err = json.Unmarshal(resp.Body, &result); err != nil {
return nil, fmt.Errorf("change log level failed: invalid JSON (%w)", err)
}
result.Code = 0
result.Duration = resp.Duration
return &result, nil
}
return nil, fmt.Errorf("no log level")
}
func (c *RAGFlowClient) RegisterUser(cmd *Command) (ResponseIf, error) {
if c.ServerType != "user" {
return nil, fmt.Errorf("this command is only allowed in ADMIN mode")
}
// Check for benchmark iterations
var ok bool
_, ok = cmd.Params["iterations"].(int)
if ok {
return nil, fmt.Errorf("failed to register user in benchmark statement")
}
var email string
email, ok = cmd.Params["user_name"].(string)
if !ok {
return nil, fmt.Errorf("no email")
}
var password string
password, ok = cmd.Params["password"].(string)
if !ok {
return nil, fmt.Errorf("no password")
}
// Encrypt password using RSA
encryptedPassword, err := EncryptPassword(password)
if err != nil {
return nil, fmt.Errorf("failed to encrypt password: %w", err)
}
var nickname string
nickname, ok = cmd.Params["nickname"].(string)
if !ok {
return nil, fmt.Errorf("no nickname")
}
payload := map[string]interface{}{
"email": email,
"password": encryptedPassword,
"nickname": nickname,
}
resp, err := c.HTTPClient.Request("POST", "/users", "web", nil, payload)
if err != nil {
return nil, fmt.Errorf("failed to register user: %w", err)
}
if resp.StatusCode != 200 {
return nil, fmt.Errorf("failed to register user: HTTP %d, body: %s", resp.StatusCode, string(resp.Body))
}
var result RegisterResponse
if err = json.Unmarshal(resp.Body, &result); err != nil {
return nil, fmt.Errorf("register user failed: invalid JSON (%w)", err)
}
if result.Code != 0 {
return nil, fmt.Errorf("%s", result.Message)
}
result.Duration = resp.Duration
return &result, nil
}
// ListDatasets lists datasets for current user (user mode)
// Returns (result_map, error) - result_map is non-nil for benchmark mode
func (c *RAGFlowClient) ListDatasets(cmd *Command) (ResponseIf, error) {
if c.ServerType != "user" {
return nil, fmt.Errorf("this command is only allowed in USER mode")
}
// Check for benchmark iterations
iterations := 1
if val, ok := cmd.Params["iterations"].(int); ok && val > 1 {
iterations = val
}
// Determine auth kind based on whether API token is being used
if c.HTTPClient.LoginToken == "" && !c.HTTPClient.useAPIToken {
return nil, fmt.Errorf("no authorization")
}
authKind := "web"
if c.HTTPClient.useAPIToken {
authKind = "api"
}
if c.HTTPClient.LoginToken != "" {
authKind = "web"
}
if iterations > 1 {
// Benchmark mode - return raw result for benchmark stats
return c.HTTPClient.RequestWithIterations("GET", "/datasets", authKind, nil, nil, iterations)
}
// Normal mode
resp, err := c.HTTPClient.Request("GET", "/datasets", authKind, nil, nil)
if err != nil {
return nil, fmt.Errorf("failed to list datasets: %w", err)
}
if resp.StatusCode != 200 {
return nil, fmt.Errorf("failed to list datasets: HTTP %d, body: %s", resp.StatusCode, string(resp.Body))
}
var result CommonResponse
if err = json.Unmarshal(resp.Body, &result); err != nil {
return nil, fmt.Errorf("list datasets failed: invalid JSON (%w)", err)
}
if result.Code != 0 {
return nil, fmt.Errorf("%s", result.Message)
}
result.Duration = resp.Duration
return &result, nil
}
// ListDatasetDocumentUserCommand lists dataset documents
func (c *RAGFlowClient) ListDatasetDocumentUserCommand(cmd *Command) (ResponseIf, error) {
if c.ServerType != "user" {
return nil, fmt.Errorf("this command is only allowed in USER mode")
}
// Check for benchmark iterations
iterations := 1
if val, ok := cmd.Params["iterations"].(int); ok && val > 1 {
iterations = val
}
// Determine auth kind based on whether API token is being used
if c.HTTPClient.LoginToken == "" && !c.HTTPClient.useAPIToken {
return nil, fmt.Errorf("no authorization")
}
datasetID, ok := cmd.Params["dataset_id"].(string)
if !ok {
return nil, fmt.Errorf("no dataset id")
}
page := 1
pageSize := 10
keywords := ""
returnEmptyMetadata := "true"
url := fmt.Sprintf("/datasets/%s/documents?page=%d&page_size=%d&keywords=%s&return_empty_metadata=%s", datasetID, page, pageSize, keywords, returnEmptyMetadata)
if iterations > 1 {
// Benchmark mode - return raw result for benchmark stats
return c.HTTPClient.RequestWithIterations("GET", url, "web", nil, nil, iterations)
}
// Normal mode
resp, err := c.HTTPClient.Request("GET", url, "web", nil, nil)
if err != nil {
return nil, fmt.Errorf("failed to list documents: %w", err)
}
if resp.StatusCode != 200 {
return nil, fmt.Errorf("failed to list documents: HTTP %d, body: %s", resp.StatusCode, string(resp.Body))
}
var result ListDocumentsResponse
if err = json.Unmarshal(resp.Body, &result); err != nil {
return nil, fmt.Errorf("list documents failed: invalid JSON (%w)", err)
}
if result.Code != 0 {
return nil, fmt.Errorf("%s", result.Message)
}
result.Duration = resp.Duration
return &result, nil
}
// getDatasetID gets dataset ID by name
func (c *RAGFlowClient) getDatasetID(datasetName string) (string, error) {
resp, err := c.HTTPClient.Request("GET", "/datasets", "web", nil, nil)
if err != nil {
return "", fmt.Errorf("failed to list datasets: %w", err)
}
if resp.StatusCode != 200 {
return "", fmt.Errorf("failed to list datasets: HTTP %d, body: %s", resp.StatusCode, string(resp.Body))
}
resJSON, err := resp.JSON()
if err != nil {
return "", fmt.Errorf("invalid JSON response: %w", err)
}
code, ok := resJSON["code"].(float64)
if !ok || code != 0 {
msg, _ := resJSON["message"].(string)
return "", fmt.Errorf("failed to list datasets: %s", msg)
}
data, ok := resJSON["data"].([]interface{})
if !ok {
return "", fmt.Errorf("invalid response format")
}
for _, kb := range data {
if kbMap, ok := kb.(map[string]interface{}); ok {
if name, _ := kbMap["name"].(string); name == datasetName {
if id, _ := kbMap["id"].(string); id != "" {
return id, nil
}
}
}
}
return "", fmt.Errorf("dataset '%s' not found", datasetName)
}
// ListMetadata lists metadata for datasets
func (c *RAGFlowClient) ListMetadata(cmd *Command) (ResponseIf, error) {
if c.ServerType != "user" {
return nil, fmt.Errorf("this command is only allowed in USER mode")
}
datasetNames, ok := cmd.Params["dataset_names"].([]string)
if !ok || len(datasetNames) == 0 {
return nil, fmt.Errorf("dataset_names not provided")
}
// Convert dataset names to IDs
datasetIDs := make([]string, 0, len(datasetNames))
for _, name := range datasetNames {
id, err := c.getDatasetID(name)
if err != nil {
return nil, err
}
datasetIDs = append(datasetIDs, id)
}
// Build comma-separated dataset_ids for query param
datasetIDsStr := strings.Join(datasetIDs, ",")
resp, err := c.HTTPClient.Request("GET", "/datasets/metadata/flattened?dataset_ids="+datasetIDsStr, "web", nil, nil)
if err != nil {
return nil, fmt.Errorf("failed to list metadata: %w", err)
}
if resp.StatusCode != 200 {
return nil, fmt.Errorf("failed to list metadata: HTTP %d, body: %s", resp.StatusCode, string(resp.Body))
}
var result MetadataResponse
if err = json.Unmarshal(resp.Body, &result); err != nil {
return nil, fmt.Errorf("list metadata failed: invalid JSON (%w)", err)
}
if result.Code != 0 {
return nil, fmt.Errorf("%s", result.Message)
}
result.Duration = resp.Duration
return &result, nil
}
// formatEmptyArray converts empty arrays to "[]" string
func formatEmptyArray(v interface{}) string {
if v == nil {
return "[]"
}
switch val := v.(type) {
case []interface{}:
if len(val) == 0 {
return "[]"
}
case []string:
if len(val) == 0 {
return "[]"
}
case []int:
if len(val) == 0 {
return "[]"
}
}
return fmt.Sprintf("%v", v)
}
// SearchOnDatasets searches for chunks in specified datasets
// Returns (result_map, error) - result_map is non-nil for benchmark mode
func (c *RAGFlowClient) SearchOnDatasets(cmd *Command) (ResponseIf, error) {
if c.ServerType != "user" {
return nil, fmt.Errorf("this command is only allowed in USER mode")
}
question, ok := cmd.Params["question"].(string)
if !ok {
return nil, fmt.Errorf("question not provided")
}
datasets, ok := cmd.Params["datasets"].(string)
if !ok {
return nil, fmt.Errorf("datasets not provided")
}
// Parse dataset names (comma-separated) and convert to IDs
datasetNames := strings.Split(datasets, ",")
datasetIDs := make([]string, 0, len(datasetNames))
for _, name := range datasetNames {
name = strings.TrimSpace(name)
id, err := c.getDatasetID(name)
if err != nil {
return nil, err
}
datasetIDs = append(datasetIDs, id)
}
// Check for benchmark iterations
iterations := 1
if val, ok := cmd.Params["iterations"].(int); ok && val > 1 {
iterations = val
}
payload := map[string]interface{}{
"dataset_ids": datasetIDs,
"question": question,
"similarity_threshold": 0.2,
"vector_similarity_weight": 0.3,
}
if iterations > 1 {
// Benchmark mode - return raw result for benchmark stats
return c.HTTPClient.RequestWithIterations("POST", "/datasets/search", "web", nil, payload, iterations)
}
// Normal mode
resp, err := c.HTTPClient.Request("POST", "/datasets/search", "web", nil, payload)
if err != nil {
return nil, fmt.Errorf("failed to search on datasets: %w", err)
}
if resp.StatusCode != 200 {
return nil, fmt.Errorf("failed to search on datasets: HTTP %d, body: %s", resp.StatusCode, string(resp.Body))
}
resJSON, err := resp.JSON()
if err != nil {
return nil, fmt.Errorf("invalid JSON response: %w", err)
}
code, ok := resJSON["code"].(float64)
if !ok || code != 0 {
msg, _ := resJSON["message"].(string)
return nil, fmt.Errorf("failed to search on datasets: %s", msg)
}
data, ok := resJSON["data"].(map[string]interface{})
if !ok {
return nil, fmt.Errorf("invalid response format")
}
chunks, ok := data["chunks"].([]interface{})
if !ok {
return nil, fmt.Errorf("invalid response format: chunks not found")
}
// Convert to slice of maps for printing
tableData := make([]map[string]interface{}, 0, len(chunks))
for _, chunk := range chunks {
if chunkMap, ok := chunk.(map[string]interface{}); ok {
row := map[string]interface{}{
"id": chunkMap["chunk_id"],
"content": chunkMap["content_with_weight"],
"document_id": chunkMap["doc_id"],
"dataset_id": chunkMap["kb_id"],
"docnm_kwd": chunkMap["docnm_kwd"],
"image_id": chunkMap["image_id"],
"similarity": chunkMap["similarity"],
"term_similarity": chunkMap["term_similarity"],
"vector_similarity": chunkMap["vector_similarity"],
}
// Add optional fields that may be empty arrays
if v, ok := chunkMap["doc_type_kwd"]; ok {
row["doc_type_kwd"] = formatEmptyArray(v)
}
if v, ok := chunkMap["important_kwd"]; ok {
row["important_kwd"] = formatEmptyArray(v)
}
if v, ok := chunkMap["mom_id"]; ok {
row["mom_id"] = formatEmptyArray(v)
}
if v, ok := chunkMap["positions"]; ok {
row["positions"] = formatEmptyArray(v)
}
if v, ok := chunkMap["content_ltks"]; ok {
row["content_ltks"] = v
}
tableData = append(tableData, row)
}
}
PrintTableSimple(tableData)
return nil, nil
}
// CreateToken creates a new API token
func (c *RAGFlowClient) CreateToken(cmd *Command) (ResponseIf, error) {
if c.ServerType != "user" {
return nil, fmt.Errorf("this command is only allowed in USER mode")
}
resp, err := c.HTTPClient.Request("POST", "/system/tokens", "web", nil, nil)
if err != nil {
return nil, fmt.Errorf("failed to create token: %w", err)
}
if resp.StatusCode != 200 {
return nil, fmt.Errorf("failed to create token: HTTP %d, body: %s", resp.StatusCode, string(resp.Body))
}
var createResult CommonDataResponse
if err = json.Unmarshal(resp.Body, &createResult); err != nil {
return nil, fmt.Errorf("create token failed: invalid JSON (%w)", err)
}
if createResult.Code != 0 {
return nil, fmt.Errorf("%s", createResult.Message)
}
var result SimpleResponse
result.Code = 0
result.Message = "Token created successfully"
result.Duration = resp.Duration
return &result, nil
}
// ListTokens lists all API tokens for the current user
func (c *RAGFlowClient) ListTokens(cmd *Command) (ResponseIf, error) {
if c.ServerType != "user" {
return nil, fmt.Errorf("this command is only allowed in USER mode")
}
resp, err := c.HTTPClient.Request("GET", "/system/tokens", "web", nil, nil)
if err != nil {
return nil, fmt.Errorf("failed to list tokens: %w", err)
}
if resp.StatusCode != 200 {
return nil, fmt.Errorf("failed to list tokens: HTTP %d, body: %s", resp.StatusCode, string(resp.Body))
}
var result CommonResponse
if err = json.Unmarshal(resp.Body, &result); err != nil {
return nil, fmt.Errorf("list tokens failed: invalid JSON (%w)", err)
}
if result.Code != 0 {
return nil, fmt.Errorf("%s", result.Message)
}
result.Duration = resp.Duration
return &result, nil
}
// DropToken deletes an API token
func (c *RAGFlowClient) DropToken(cmd *Command) (ResponseIf, error) {
if c.ServerType != "user" {
return nil, fmt.Errorf("this command is only allowed in USER mode")
}
token, ok := cmd.Params["token"].(string)
if !ok {
return nil, fmt.Errorf("token not provided")
}
resp, err := c.HTTPClient.Request("DELETE", fmt.Sprintf("/system/tokens/%s", token), "web", nil, nil)
if err != nil {
return nil, fmt.Errorf("failed to drop token: %w", err)
}
if resp.StatusCode != 200 {
return nil, fmt.Errorf("failed to drop token: HTTP %d, body: %s", resp.StatusCode, string(resp.Body))
}
var result SimpleResponse
if err = json.Unmarshal(resp.Body, &result); err != nil {
return nil, fmt.Errorf("drop token failed: invalid JSON (%w)", err)
}
if result.Code != 0 {
return nil, fmt.Errorf("%s", result.Message)
}
result.Duration = resp.Duration
return &result, nil
}
// SetToken sets the API token after validating it
func (c *RAGFlowClient) SetToken(cmd *Command) (ResponseIf, error) {
if c.ServerType != "user" {
return nil, fmt.Errorf("this command is only allowed in USER mode")
}
token, ok := cmd.Params["token"].(string)
if !ok {
return nil, fmt.Errorf("token not provided")
}
// Save current token to restore if validation fails
savedToken := c.HTTPClient.APIToken
savedUseAPIToken := c.HTTPClient.useAPIToken
// Set the new token temporarily for validation
c.HTTPClient.APIToken = token
c.HTTPClient.useAPIToken = true
// Validate token by calling list tokens API
resp, err := c.HTTPClient.Request("GET", "/tokens", "api", nil, nil)
if err != nil {
// Restore original token on error
c.HTTPClient.APIToken = savedToken
c.HTTPClient.useAPIToken = savedUseAPIToken
return nil, fmt.Errorf("failed to validate token: %w", err)
}
if resp.StatusCode != 200 {
// Restore original token on error
c.HTTPClient.APIToken = savedToken
c.HTTPClient.useAPIToken = savedUseAPIToken
return nil, fmt.Errorf("token validation failed: HTTP %d, body: %s", resp.StatusCode, string(resp.Body))
}
var result CommonResponse
if err = json.Unmarshal(resp.Body, &result); err != nil {
// Restore original token on error
c.HTTPClient.APIToken = savedToken
c.HTTPClient.useAPIToken = savedUseAPIToken
return nil, fmt.Errorf("token validation failed: invalid JSON (%w)", err)
}
if result.Code != 0 {
// Restore original token on error
c.HTTPClient.APIToken = savedToken
c.HTTPClient.useAPIToken = savedUseAPIToken
return nil, fmt.Errorf("token validation failed: %s", result.Message)
}
// Token is valid, keep it set
var successResult SimpleResponse
successResult.Code = 0
successResult.Message = "API token set successfully"
successResult.Duration = resp.Duration
return &successResult, nil
}
// ShowToken displays the current API token
func (c *RAGFlowClient) ShowToken(cmd *Command) (ResponseIf, error) {
if c.ServerType != "user" {
return nil, fmt.Errorf("this command is only allowed in USER mode")
}
if c.HTTPClient.APIToken == "" {
return nil, fmt.Errorf("no API token is currently set")
}
//fmt.Printf("Token: %s\n", c.HTTPClient.APIToken)
var result CommonResponse
result.Code = 0
result.Message = ""
result.Data = []map[string]interface{}{
{
"token": c.HTTPClient.APIToken,
},
}
result.Duration = 0
return &result, nil
}
// UnsetToken removes the current API token
func (c *RAGFlowClient) UnsetToken(cmd *Command) (ResponseIf, error) {
if c.ServerType != "user" {
return nil, fmt.Errorf("this command is only allowed in USER mode")
}
if c.HTTPClient.APIToken == "" {
return nil, fmt.Errorf("no API token is currently set")
}
c.HTTPClient.APIToken = ""
c.HTTPClient.useAPIToken = false
var result SimpleResponse
result.Code = 0
result.Message = "API token unset successfully"
result.Duration = 0
return &result, nil
}
// CreateDataset creates a table for a dataset
func (c *RAGFlowClient) CreateDataset(cmd *Command) (ResponseIf, error) {
if c.ServerType != "user" {
return nil, fmt.Errorf("this command is only allowed in USER mode")
}
datasetName, ok := cmd.Params["dataset_name"].(string)
if !ok {
return nil, fmt.Errorf("dataset_name not provided")
}
vectorSize, ok := cmd.Params["vector_size"].(int)
if !ok {
return nil, fmt.Errorf("vector_size not provided")
}
// Get dataset ID by name
datasetID, err := c.getDatasetID(datasetName)
if err != nil {
return nil, err
}
payload := map[string]interface{}{
"kb_id": datasetID,
"vector_size": vectorSize,
}
resp, err := c.HTTPClient.Request("POST", "/kb/doc_engine_table", "web", nil, payload)
if err != nil {
return nil, fmt.Errorf("failed to create table: %w", err)
}
if resp.StatusCode != 200 {
return nil, fmt.Errorf("failed to create table: HTTP %d, body: %s", resp.StatusCode, string(resp.Body))
}
resJSON, err := resp.JSON()
if err != nil {
return nil, fmt.Errorf("invalid JSON response: %w", err)
}
code, ok := resJSON["code"].(float64)
if !ok {
return nil, fmt.Errorf("invalid response format: code is not a number")
}
var result SimpleResponse
result.Code = int(code)
if result.Code == 0 {
result.Message = fmt.Sprintf("Success to create table for dataset: %s", datasetName)
} else {
result.Message = fmt.Sprintf("Failed to create table: %v", resJSON)
}
result.Duration = 0
return &result, nil
}
// CreateDatasetInDocEngine creates a table for a dataset in doc engine
func (c *RAGFlowClient) CreateDatasetInDocEngine(cmd *Command) (ResponseIf, error) {
if c.ServerType != "user" {
return nil, fmt.Errorf("this command is only allowed in USER mode")
}
datasetName, ok := cmd.Params["dataset_name"].(string)
if !ok {
return nil, fmt.Errorf("dataset_name not provided")
}
vectorSize, ok := cmd.Params["vector_size"].(int)
if !ok {
return nil, fmt.Errorf("vector_size not provided")
}
// Get dataset ID by name
datasetID, err := c.getDatasetID(datasetName)
if err != nil {
return nil, err
}
payload := map[string]interface{}{
"kb_id": datasetID,
"vector_size": vectorSize,
}
resp, err := c.HTTPClient.Request("POST", "/kb/doc_engine_table", "web", nil, payload)
if err != nil {
return nil, fmt.Errorf("failed to create table: %w", err)
}
if resp.StatusCode != 200 {
return nil, fmt.Errorf("failed to create table: HTTP %d, body: %s", resp.StatusCode, string(resp.Body))
}
resJSON, err := resp.JSON()
if err != nil {
return nil, fmt.Errorf("invalid JSON response: %w", err)
}
code, ok := resJSON["code"].(float64)
if !ok {
return nil, fmt.Errorf("invalid response format: code is not a number")
}
var result SimpleResponse
result.Code = int(code)
if result.Code == 0 {
result.Message = fmt.Sprintf("Success to create table for dataset: %s", datasetName)
} else {
result.Message = fmt.Sprintf("Failed to create table: %v", resJSON)
}
result.Duration = 0
return &result, nil
}
// DropDatasetInDocEngine drops a table for a dataset in doc engine
func (c *RAGFlowClient) DropDatasetInDocEngine(cmd *Command) (ResponseIf, error) {
if c.ServerType != "user" {
return nil, fmt.Errorf("this command is only allowed in USER mode")
}
datasetName, ok := cmd.Params["dataset_name"].(string)
if !ok {
return nil, fmt.Errorf("dataset_name not provided")
}
// Get dataset ID by name
datasetID, err := c.getDatasetID(datasetName)
if err != nil {
return nil, err
}
payload := map[string]interface{}{
"kb_id": datasetID,
}
resp, err := c.HTTPClient.Request("DELETE", "/kb/doc_engine_table", "web", nil, payload)
if err != nil {
return nil, fmt.Errorf("failed to drop dataset: %w", err)
}
if resp.StatusCode != 200 {
return nil, fmt.Errorf("failed to drop dataset: HTTP %d, body: %s", resp.StatusCode, string(resp.Body))
}
resJSON, err := resp.JSON()
if err != nil {
return nil, fmt.Errorf("invalid JSON response: %w", err)
}
code, ok := resJSON["code"].(float64)
if !ok {
return nil, fmt.Errorf("invalid response format: code is not a number")
}
var result SimpleResponse
result.Code = int(code)
if result.Code == 0 {
result.Message = fmt.Sprintf("Success to drop table for dataset: %s", datasetName)
} else {
result.Message = fmt.Sprintf("Failed to drop table for dataset: %s: %v", datasetName, resJSON)
}
result.Duration = 0
return &result, nil
}
// CreateMetadataInDocEngine creates the document metadata table for the tenant
func (c *RAGFlowClient) CreateMetadataInDocEngine(cmd *Command) (ResponseIf, error) {
if c.ServerType != "user" {
return nil, fmt.Errorf("this command is only allowed in USER mode")
}
resp, err := c.HTTPClient.Request("POST", "/tenant/doc_engine_metadata_table", "web", nil, nil)
if err != nil {
return nil, fmt.Errorf("failed to create metadata table: %w", err)
}
if resp.StatusCode != 200 {
return nil, fmt.Errorf("failed to create metadata table: HTTP %d, body: %s", resp.StatusCode, string(resp.Body))
}
resJSON, err := resp.JSON()
if err != nil {
return nil, fmt.Errorf("invalid JSON response: %w", err)
}
code, ok := resJSON["code"].(float64)
if !ok {
return nil, fmt.Errorf("invalid response format: code is not a number")
}
var result SimpleResponse
result.Code = int(code)
if result.Code == 0 {
result.Message = "Success to create metadata table"
} else {
result.Message = fmt.Sprintf("Failed to create metadata table: %v", resJSON)
}
result.Duration = 0
return &result, nil
}
// DropMetadataInDocEngine drops the document metadata table for the tenant
func (c *RAGFlowClient) DropMetadataInDocEngine(cmd *Command) (ResponseIf, error) {
if c.ServerType != "user" {
return nil, fmt.Errorf("this command is only allowed in USER mode")
}
resp, err := c.HTTPClient.Request("DELETE", "/tenant/doc_engine_metadata_table", "web", nil, nil)
if err != nil {
return nil, fmt.Errorf("failed to drop metadata table: %w", err)
}
if resp.StatusCode != 200 {
return nil, fmt.Errorf("failed to drop metadata table: HTTP %d, body: %s", resp.StatusCode, string(resp.Body))
}
resJSON, err := resp.JSON()
if err != nil {
return nil, fmt.Errorf("invalid JSON response: %w", err)
}
code, ok := resJSON["code"].(float64)
if !ok {
return nil, fmt.Errorf("invalid response format: code is not a number")
}
var result SimpleResponse
result.Code = int(code)
if result.Code == 0 {
result.Message = "Success to drop metadata table"
} else {
result.Message = fmt.Sprintf("Failed to drop metadata table: %v", resJSON)
}
result.Duration = 0
return &result, nil
}
Refactor Go server model provider reading and access (#13831) ### What problem does this PR solve? 1. Refactor model provider json file format 2. Use memory data structure to replace database 3. Add CLI command to access ``` RAGFlow(user)> list pool models from 'xai'; +-------------------------------------------------------------------------------------+------------+-------------+-----------------------+ | features | max_tokens | model_types | name | +-------------------------------------------------------------------------------------+------------+-------------+-----------------------+ | map[] | 256000 | [llm] | grok-4 | | map[] | 131072 | [llm] | grok-3 | | map[] | 131072 | [llm] | grok-3-fast | | map[] | 131072 | [llm] | grok-3-mini | | map[] | 131072 | [llm] | grok-3-mini-mini-fast | | map[multimodal:map[enabled:true input_modalities:[image] output_modalities:[text]]] | 32768 | [vlm] | grok-2-vision | +-------------------------------------------------------------------------------------+------------+-------------+-----------------------+ RAGFlow(user)> show pool model 'grok-2-vision' from 'xai'; +-------------------------------------------------------------------------------------+------------+-------------+---------------+ | features | max_tokens | model_types | name | +-------------------------------------------------------------------------------------+------------+-------------+---------------+ | map[multimodal:map[enabled:true input_modalities:[image] output_modalities:[text]]] | 32768 | [vlm] | grok-2-vision | +-------------------------------------------------------------------------------------+------------+-------------+---------------+ RAGFlow(user)> list pool providers; +--------+------------------------------------------------------------+---------------------------+ | name | tags | url | +--------+------------------------------------------------------------+---------------------------+ | OpenAI | LLM,TEXT EMBEDDING,TTS,TEXT RE-RANK,SPEECH2TEXT,MODERATION | https://api.openai.com/v1 | | xAI | LLM | https://api.x.ai/v1 | +--------+------------------------------------------------------------+---------------------------+ RAGFlow(user)> show pool provider 'openai'; +---------------------------+--------+------------------------------------------------------------+--------------+ | base_url | name | tags | total_models | +---------------------------+--------+------------------------------------------------------------+--------------+ | https://api.openai.com/v1 | OpenAI | LLM,TEXT EMBEDDING,TTS,TEXT RE-RANK,SPEECH2TEXT,MODERATION | 27 | +---------------------------+--------+------------------------------------------------------------+--------------+ ``` ### Type of change - [x] New Feature (non-breaking change which adds functionality) - [x] Refactoring --------- Signed-off-by: Jin Hai <haijin.chn@gmail.com>
2026-03-30 12:00:49 +08:00
// AddProvider creates a new model provider
// ADD PROVIDER <name>
// ADD PROVIDER <name> <api_key>
func (c *RAGFlowClient) AddProvider(cmd *Command) (ResponseIf, error) {
if c.ServerType != "user" {
return nil, fmt.Errorf("this command is only allowed in USER mode")
}
providerName, ok := cmd.Params["provider_name"].(string)
if !ok {
return nil, fmt.Errorf("provider name not provided")
}
// Build payload
payload := map[string]interface{}{
"provider_name": providerName,
}
resp, err := c.HTTPClient.Request("PUT", "/providers", "web", nil, payload)
if err != nil {
return nil, fmt.Errorf("failed to add provider: %w", err)
}
if resp.StatusCode != 200 {
return nil, fmt.Errorf("failed to add provider: HTTP %d, body: %s", resp.StatusCode, string(resp.Body))
}
var result SimpleResponse
if err = json.Unmarshal(resp.Body, &result); err != nil {
return nil, fmt.Errorf("add provider failed: invalid JSON (%w)", err)
}
if result.Code != 0 {
return nil, fmt.Errorf("%s", result.Message)
}
result.Duration = resp.Duration
return &result, nil
}
// ListProviders lists all providers
// LIST PROVIDERS
func (c *RAGFlowClient) ListProviders(cmd *Command) (ResponseIf, error) {
if c.ServerType != "user" {
return nil, fmt.Errorf("this command is only allowed in USER mode")
}
resp, err := c.HTTPClient.Request("GET", "/providers", "web", nil, nil)
if err != nil {
return nil, fmt.Errorf("failed to list providers: %w", err)
}
if resp.StatusCode != 200 {
return nil, fmt.Errorf("failed to list providers: HTTP %d, body: %s", resp.StatusCode, string(resp.Body))
}
var result CommonResponse
if err = json.Unmarshal(resp.Body, &result); err != nil {
return nil, fmt.Errorf("list providers failed: invalid JSON (%w)", err)
}
if result.Code != 0 {
return nil, fmt.Errorf("%s", result.Message)
}
result.Duration = resp.Duration
return &result, nil
}
// DeleteProvider deletes a provider
// DELETE PROVIDER <name>
func (c *RAGFlowClient) DeleteProvider(cmd *Command) (ResponseIf, error) {
if c.ServerType != "user" {
return nil, fmt.Errorf("this command is only allowed in USER mode")
}
providerName, ok := cmd.Params["provider_name"].(string)
if !ok {
return nil, fmt.Errorf("provider name not provided")
}
url := fmt.Sprintf("/providers/%s", providerName)
// Build payload
payload := map[string]interface{}{
"llm_factory": providerName,
}
resp, err := c.HTTPClient.Request("DELETE", url, "web", nil, payload)
if err != nil {
return nil, fmt.Errorf("failed to delete provider: %w", err)
}
if resp.StatusCode != 200 {
return nil, fmt.Errorf("failed to delete provider: HTTP %d, body: %s", resp.StatusCode, string(resp.Body))
}
var result SimpleResponse
if err = json.Unmarshal(resp.Body, &result); err != nil {
return nil, fmt.Errorf("delete provider failed: invalid JSON (%w)", err)
}
if result.Code != 0 {
return nil, fmt.Errorf("%s", result.Message)
}
result.Duration = resp.Duration
return &result, nil
}
// CreateProviderInstance creates a new provider instance
// CREATE PROVIDER <name> INSTANCE <instance_name> <api_key>
func (c *RAGFlowClient) CreateProviderInstance(cmd *Command) (ResponseIf, error) {
if c.ServerType != "user" {
return nil, fmt.Errorf("this command is only allowed in USER mode")
}
providerName, ok := cmd.Params["provider_name"].(string)
if !ok {
return nil, fmt.Errorf("provider name not provided")
}
instanceName, ok := cmd.Params["instance_name"].(string)
if !ok {
return nil, fmt.Errorf("instance name not provided")
}
apiKey, ok := cmd.Params["api_key"].(string)
if !ok {
return nil, fmt.Errorf("API key not provided")
}
baseUrl, ok := cmd.Params["base_url"].(string)
if !ok {
baseUrl = ""
}
region, ok := cmd.Params["region"].(string)
if !ok {
region = ""
}
url := fmt.Sprintf("/providers/%s/instances", providerName)
payload := map[string]interface{}{
"instance_name": instanceName,
"api_key": apiKey,
"base_url": baseUrl,
"region": region,
}
resp, err := c.HTTPClient.Request("POST", url, "web", nil, payload)
if err != nil {
return nil, fmt.Errorf("failed to create provider instance: %w", err)
}
if resp.StatusCode != 200 {
return nil, fmt.Errorf("failed to create provider instance: HTTP %d, body: %s", resp.StatusCode, string(resp.Body))
}
var result SimpleResponse
if err = json.Unmarshal(resp.Body, &result); err != nil {
return nil, fmt.Errorf("create provider instance failed: invalid JSON (%w)", err)
}
if result.Code != 0 {
return nil, fmt.Errorf("%s", result.Message)
}
result.Duration = resp.Duration
return &result, nil
}
// ListProviderInstances lists all instances of a provider
// LIST INSTANCES FROM PROVIDER <name>
func (c *RAGFlowClient) ListProviderInstances(cmd *Command) (ResponseIf, error) {
if c.ServerType != "user" {
return nil, fmt.Errorf("this command is only allowed in USER mode")
}
providerName, ok := cmd.Params["provider_name"].(string)
if !ok {
return nil, fmt.Errorf("provider name not provided")
}
url := fmt.Sprintf("/providers/%s/instances", providerName)
resp, err := c.HTTPClient.Request("GET", url, "web", nil, nil)
if err != nil {
return nil, fmt.Errorf("failed to list instances: %w", err)
}
if resp.StatusCode != 200 {
return nil, fmt.Errorf("failed to list instances: HTTP %d, body: %s", resp.StatusCode, string(resp.Body))
}
var result CommonResponse
if err = json.Unmarshal(resp.Body, &result); err != nil {
return nil, fmt.Errorf("list instances failed: invalid JSON (%w)", err)
}
if result.Code != 0 {
return nil, fmt.Errorf("%s", result.Message)
}
result.Duration = resp.Duration
return &result, nil
}
// ShowProviderInstance shows details of a specific instance
// SHOW INSTANCE <name> FROM PROVIDER <name>
func (c *RAGFlowClient) ShowProviderInstance(cmd *Command) (ResponseIf, error) {
if c.ServerType != "user" {
return nil, fmt.Errorf("this command is only allowed in USER mode")
}
instanceName, ok := cmd.Params["instance_name"].(string)
if !ok {
return nil, fmt.Errorf("instance name not provided")
}
providerName, ok := cmd.Params["provider_name"].(string)
if !ok {
return nil, fmt.Errorf("provider name not provided")
}
url := fmt.Sprintf("/providers/%s/instances/%s", providerName, instanceName)
resp, err := c.HTTPClient.Request("GET", url, "web", nil, nil)
if err != nil {
return nil, fmt.Errorf("failed to show instance: %w", err)
}
if resp.StatusCode != 200 {
return nil, fmt.Errorf("failed to show instance: HTTP %d, body: %s", resp.StatusCode, string(resp.Body))
}
var result CommonDataResponse
if err = json.Unmarshal(resp.Body, &result); err != nil {
return nil, fmt.Errorf("show instance failed: invalid JSON (%w)", err)
}
if result.Code != 0 {
return nil, fmt.Errorf("%s", result.Message)
}
result.Duration = resp.Duration
return &result, nil
}
// ShowInstanceBalance shows balance of a specific instance
// SHOW BALANCE FROM PROVIDER <provider_name> <instance_name>
func (c *RAGFlowClient) ShowInstanceBalance(cmd *Command) (ResponseIf, error) {
if c.ServerType != "user" {
return nil, fmt.Errorf("this command is only allowed in USER mode")
}
instanceName, ok := cmd.Params["instance_name"].(string)
if !ok {
return nil, fmt.Errorf("instance name not provided")
}
providerName, ok := cmd.Params["provider_name"].(string)
if !ok {
return nil, fmt.Errorf("provider name not provided")
}
url := fmt.Sprintf("/providers/%s/instances/%s/balance", providerName, instanceName)
resp, err := c.HTTPClient.Request("GET", url, "web", nil, nil)
if err != nil {
return nil, fmt.Errorf("failed to show instance: %w", err)
}
if resp.StatusCode != 200 {
return nil, fmt.Errorf("failed to show instance: HTTP %d, body: %s", resp.StatusCode, string(resp.Body))
}
var result CommonDataResponse
if err = json.Unmarshal(resp.Body, &result); err != nil {
return nil, fmt.Errorf("show instance failed: invalid JSON (%w)", err)
}
if result.Code != 0 {
return nil, fmt.Errorf("%s", result.Message)
}
result.Duration = resp.Duration
return &result, nil
}
// AlterProviderInstance renames a provider instance
// ALTER INSTANCE <name> NAME <new_name> FROM PROVIDER <name>
func (c *RAGFlowClient) AlterProviderInstance(cmd *Command) (ResponseIf, error) {
if c.ServerType != "user" {
return nil, fmt.Errorf("this command is only allowed in USER mode")
}
instanceName, ok := cmd.Params["instance_name"].(string)
if !ok {
return nil, fmt.Errorf("instance name not provided")
}
newName, ok := cmd.Params["new_name"].(string)
if !ok {
return nil, fmt.Errorf("new name not provided")
}
providerName, ok := cmd.Params["provider_name"].(string)
if !ok {
return nil, fmt.Errorf("provider name not provided")
}
url := fmt.Sprintf("/providers/%s/instances/%s", providerName, instanceName)
payload := map[string]interface{}{
"llm_name": newName,
}
resp, err := c.HTTPClient.Request("PUT", url, "web", nil, payload)
if err != nil {
return nil, fmt.Errorf("failed to alter instance: %w", err)
}
if resp.StatusCode != 200 {
return nil, fmt.Errorf("failed to alter instance: HTTP %d, body: %s", resp.StatusCode, string(resp.Body))
}
var result SimpleResponse
if err = json.Unmarshal(resp.Body, &result); err != nil {
return nil, fmt.Errorf("alter instance failed: invalid JSON (%w)", err)
}
if result.Code != 0 {
return nil, fmt.Errorf("%s", result.Message)
}
result.Duration = resp.Duration
return &result, nil
}
// DropProviderInstance deletes a provider instance
// DROP INSTANCE <name> FROM PROVIDER <name>
func (c *RAGFlowClient) DropProviderInstance(cmd *Command) (ResponseIf, error) {
if c.ServerType != "user" {
return nil, fmt.Errorf("this command is only allowed in USER mode")
}
instanceName, ok := cmd.Params["instance_name"].(string)
if !ok {
return nil, fmt.Errorf("instance name not provided")
}
providerName, ok := cmd.Params["provider_name"].(string)
if !ok {
return nil, fmt.Errorf("provider name not provided")
}
Fix go cli models command and api (#14166) ### What problem does this PR solve? ``` RAGFlow(user)> list providers; +--------------------------------------+----------+-------------------------------------------+--------------+ | base_url | name | tags | total_models | +--------------------------------------+----------+-------------------------------------------+--------------+ | https://open.bigmodel.cn/api/paas/v4 | ZHIPU-AI | LLM,TEXT EMBEDDING,SPEECH2TEXT,MODERATION | 21 | | https://api.x.ai/v1 | xAI | LLM | 6 | +--------------------------------------+----------+-------------------------------------------+--------------+ RAGFlow(user)> show provider 'zhipu-ai'; +--------------------------------------+----------+-------------------------------------------+--------------+ | base_url | name | tags | total_models | +--------------------------------------+----------+-------------------------------------------+--------------+ | https://open.bigmodel.cn/api/paas/v4 | ZHIPU-AI | LLM,TEXT EMBEDDING,SPEECH2TEXT,MODERATION | 21 | +--------------------------------------+----------+-------------------------------------------+--------------+ RAGFlow(user)> delete provider 'zhipu-ai'; SUCCESS RAGFlow(user)> add provider 'zhipu-ai'; SUCCESS RAGFlow(user)> create provider 'zhipu-ai' instance 'ccc' 'ccxxccxx'; SUCCESS RAGFlow(user)> list instances from 'zhipu-ai'; +---------------------------------------------------+----------------------------------+--------------+----------------------------------+--------+ | apiKey | id | instanceName | providerID | status | +---------------------------------------------------+----------------------------------+--------------+----------------------------------+--------+ | ccxxccxx | 640dd7ee398711f1bdd838a74640adcc | ccc | d1d59de5398411f1bdd838a74640adcc | active | +---------------------------------------------------+----------------------------------+--------------+----------------------------------+--------+ RAGFlow(user)> list models from 'zhipu-ai'; +----------+------------+---------------+---------------+ | features | max_tokens | model_types | name | +----------+------------+---------------+---------------+ | map[] | 128000 | [chat] | glm-4.7 | | map[] | 128000 | [chat] | glm-4.5 | | map[] | 128000 | [chat] | glm-4.5-x | | map[] | 128000 | [chat] | glm-4.5-air | | map[] | 128000 | [chat] | glm-4.5-airx | | map[] | 128000 | [chat] | glm-4.5-flash | | map[] | 64000 | [image2text] | glm-4.5v | | map[] | 128000 | [chat] | glm-4-plus | | map[] | 128000 | [chat] | glm-4-0520 | | map[] | 128000 | [chat] | glm-4 | | map[] | 8000 | [chat] | glm-4-airx | | map[] | 128000 | [chat] | glm-4-air | | map[] | 128000 | [chat] | glm-4-flash | | map[] | 128000 | [chat] | glm-4-flashx | | map[] | 1000000 | [chat] | glm-4-long | | map[] | 128000 | [chat] | glm-3-turbo | | map[] | 2000 | [image2text] | glm-4v | | map[] | 8192 | [chat] | glm-4-9b | | map[] | 512 | [embedding] | embedding-2 | | map[] | 512 | [embedding] | embedding-3 | | map[] | 4096 | [speech2text] | glm-asr | +----------+------------+---------------+---------------+ RAGFlow(user)> disable model 'glm-4.5-flash' from 'zhipu-ai' 'ccc'; SUCCESS RAGFlow(user)> drop instance 'ccc' from 'zhipu-ai'; SUCCESS RAGFlow(user)> list instances from 'zhipu-ai'; No data to print ``` Signed-off-by: Jin Hai <haijin.chn@gmail.com>
2026-04-17 09:55:25 +08:00
payload := map[string]interface{}{
"instances": []string{instanceName},
}
Fix go cli models command and api (#14166) ### What problem does this PR solve? ``` RAGFlow(user)> list providers; +--------------------------------------+----------+-------------------------------------------+--------------+ | base_url | name | tags | total_models | +--------------------------------------+----------+-------------------------------------------+--------------+ | https://open.bigmodel.cn/api/paas/v4 | ZHIPU-AI | LLM,TEXT EMBEDDING,SPEECH2TEXT,MODERATION | 21 | | https://api.x.ai/v1 | xAI | LLM | 6 | +--------------------------------------+----------+-------------------------------------------+--------------+ RAGFlow(user)> show provider 'zhipu-ai'; +--------------------------------------+----------+-------------------------------------------+--------------+ | base_url | name | tags | total_models | +--------------------------------------+----------+-------------------------------------------+--------------+ | https://open.bigmodel.cn/api/paas/v4 | ZHIPU-AI | LLM,TEXT EMBEDDING,SPEECH2TEXT,MODERATION | 21 | +--------------------------------------+----------+-------------------------------------------+--------------+ RAGFlow(user)> delete provider 'zhipu-ai'; SUCCESS RAGFlow(user)> add provider 'zhipu-ai'; SUCCESS RAGFlow(user)> create provider 'zhipu-ai' instance 'ccc' 'ccxxccxx'; SUCCESS RAGFlow(user)> list instances from 'zhipu-ai'; +---------------------------------------------------+----------------------------------+--------------+----------------------------------+--------+ | apiKey | id | instanceName | providerID | status | +---------------------------------------------------+----------------------------------+--------------+----------------------------------+--------+ | ccxxccxx | 640dd7ee398711f1bdd838a74640adcc | ccc | d1d59de5398411f1bdd838a74640adcc | active | +---------------------------------------------------+----------------------------------+--------------+----------------------------------+--------+ RAGFlow(user)> list models from 'zhipu-ai'; +----------+------------+---------------+---------------+ | features | max_tokens | model_types | name | +----------+------------+---------------+---------------+ | map[] | 128000 | [chat] | glm-4.7 | | map[] | 128000 | [chat] | glm-4.5 | | map[] | 128000 | [chat] | glm-4.5-x | | map[] | 128000 | [chat] | glm-4.5-air | | map[] | 128000 | [chat] | glm-4.5-airx | | map[] | 128000 | [chat] | glm-4.5-flash | | map[] | 64000 | [image2text] | glm-4.5v | | map[] | 128000 | [chat] | glm-4-plus | | map[] | 128000 | [chat] | glm-4-0520 | | map[] | 128000 | [chat] | glm-4 | | map[] | 8000 | [chat] | glm-4-airx | | map[] | 128000 | [chat] | glm-4-air | | map[] | 128000 | [chat] | glm-4-flash | | map[] | 128000 | [chat] | glm-4-flashx | | map[] | 1000000 | [chat] | glm-4-long | | map[] | 128000 | [chat] | glm-3-turbo | | map[] | 2000 | [image2text] | glm-4v | | map[] | 8192 | [chat] | glm-4-9b | | map[] | 512 | [embedding] | embedding-2 | | map[] | 512 | [embedding] | embedding-3 | | map[] | 4096 | [speech2text] | glm-asr | +----------+------------+---------------+---------------+ RAGFlow(user)> disable model 'glm-4.5-flash' from 'zhipu-ai' 'ccc'; SUCCESS RAGFlow(user)> drop instance 'ccc' from 'zhipu-ai'; SUCCESS RAGFlow(user)> list instances from 'zhipu-ai'; No data to print ``` Signed-off-by: Jin Hai <haijin.chn@gmail.com>
2026-04-17 09:55:25 +08:00
url := fmt.Sprintf("/providers/%s/instances", providerName)
resp, err := c.HTTPClient.Request("DELETE", url, "web", nil, payload)
if err != nil {
return nil, fmt.Errorf("failed to drop instance: %w", err)
}
if resp.StatusCode != 200 {
return nil, fmt.Errorf("failed to drop instance: HTTP %d, body: %s", resp.StatusCode, string(resp.Body))
}
var result SimpleResponse
if err = json.Unmarshal(resp.Body, &result); err != nil {
return nil, fmt.Errorf("drop instance failed: invalid JSON (%w)", err)
}
if result.Code != 0 {
return nil, fmt.Errorf("%s", result.Message)
}
result.Duration = resp.Duration
return &result, nil
}
// DropInstanceModel deletes a provider instance, only works for local deployed model
// DROP MODEL <name> FROM <provider_name> <instance_name>
func (c *RAGFlowClient) DropInstanceModel(cmd *Command) (ResponseIf, error) {
if c.ServerType != "user" {
return nil, fmt.Errorf("this command is only allowed in USER mode")
}
instanceName, ok := cmd.Params["instance_name"].(string)
if !ok {
return nil, fmt.Errorf("instance name not provided")
}
providerName, ok := cmd.Params["provider_name"].(string)
if !ok {
return nil, fmt.Errorf("provider name not provided")
}
modelName, ok := cmd.Params["model_name"].(string)
if !ok {
return nil, fmt.Errorf("model name not provided")
}
payload := map[string]interface{}{
"models": []string{modelName},
}
url := fmt.Sprintf("/providers/%s/instances/%s/models", providerName, instanceName)
resp, err := c.HTTPClient.Request("DELETE", url, "web", nil, payload)
if err != nil {
return nil, fmt.Errorf("failed to drop instance: %w", err)
}
if resp.StatusCode != 200 {
return nil, fmt.Errorf("failed to drop instance: HTTP %d, body: %s", resp.StatusCode, string(resp.Body))
}
var result SimpleResponse
if err = json.Unmarshal(resp.Body, &result); err != nil {
return nil, fmt.Errorf("drop instance failed: invalid JSON (%w)", err)
}
if result.Code != 0 {
return nil, fmt.Errorf("%s", result.Message)
}
result.Duration = resp.Duration
return &result, nil
}
func (c *RAGFlowClient) ListInstanceModels(cmd *Command) (ResponseIf, error) {
if c.ServerType != "user" {
return nil, fmt.Errorf("this command is only allowed in USER mode")
}
providerName, ok := cmd.Params["provider_name"].(string)
if !ok {
return nil, fmt.Errorf("provider_name not provided")
}
instanceName, ok := cmd.Params["instance_name"].(string)
if !ok {
return nil, fmt.Errorf("instance_name not provided")
}
var endPoint string
endPoint = fmt.Sprintf("/providers/%s/instances/%s/models", providerName, instanceName)
resp, err := c.HTTPClient.Request("GET", endPoint, "web", nil, nil)
if err != nil {
return nil, fmt.Errorf("failed to list instance models: %w", err)
}
if resp.StatusCode != 200 {
return nil, fmt.Errorf("failed to list instance models: HTTP %d, body: %s", resp.StatusCode, string(resp.Body))
}
var result CommonResponse
if err = json.Unmarshal(resp.Body, &result); err != nil {
return nil, fmt.Errorf("failed to list instance models: invalid JSON (%w)", err)
}
if result.Code != 0 {
return nil, fmt.Errorf("%s", result.Message)
}
result.Duration = resp.Duration
return &result, nil
}
func (c *RAGFlowClient) EnableOrDisableModel(cmd *Command, status string) (ResponseIf, error) {
if c.ServerType != "user" {
return nil, fmt.Errorf("this command is only allowed in USER mode")
}
modelName, ok := cmd.Params["model_name"].(string)
if !ok {
return nil, fmt.Errorf("model name not provided")
}
instanceName, ok := cmd.Params["instance_name"].(string)
if !ok {
return nil, fmt.Errorf("instance name not provided")
}
providerName, ok := cmd.Params["provider_name"].(string)
if !ok {
return nil, fmt.Errorf("provider name not provided")
}
url := fmt.Sprintf("/providers/%s/instances/%s/models/%s", providerName, instanceName, modelName)
payload := map[string]interface{}{
"status": status,
}
resp, err := c.HTTPClient.Request("PATCH", url, "web", nil, payload)
if err != nil {
return nil, fmt.Errorf("failed to enable/disable model: %w", err)
}
if resp.StatusCode != 200 {
return nil, fmt.Errorf("failed to enable/disable model: HTTP %d, body: %s", resp.StatusCode, string(resp.Body))
}
var result SimpleResponse
if err = json.Unmarshal(resp.Body, &result); err != nil {
return nil, fmt.Errorf("enable/disable model failed: invalid JSON (%w)", err)
}
if result.Code != 0 {
return nil, fmt.Errorf("%s", result.Message)
}
result.Duration = resp.Duration
return &result, nil
}
Go: CLI chat with text, image, video (#14573) ### What problem does this PR solve? ``` RAGFlow(user)> chat with 'glm-4.6v-flash@test@zhipu-ai' message 'What are the pics talk about?' image 'https://cdn.bigmodel.cn/static/logo/register.png' 'https://cdn.bigmodel.cn/static/logo/api-key.png' Answer: The first picture shows a login/register modal with options for phone number login, account login, and WeChat QR code login, along with a prompt for new users to get a 20 million tokens experience package. The second picture displays the API keys management page of a platform, including a warning about API key security and a table listing existing API keys with details like creation time and usage history. Time: 31.600545 RAGFlow(user)> chat with 'glm-4.6v-flash@test@zhipu-ai' message 'What are the video talk about?' video 'https://cdn.bigmodel.cn/agent-demos/lark/113123.mov' Answer: Based on the sequence of frames provided, the video is a demonstration of a web search and navigation process. 1. The video starts with a blank Google search page. 2. The user types "智谱" (which is the Chinese name for the company Zhipu AI) into the search box. 3. The search is initiated and the page shows "About 0 results". 4. The search results load, showing information about Zhipu AI, including its website. 5. The user clicks on the main website link (www.zhipuai.cn). 6. The video ends by showing the homepage of Zhipu AI's website, titled "Z.ai GLM Large Model Open Platform". In summary, the video is about searching for the company "智谱" (Zhipu AI) on Google and then navigating to its official website. Time: 76.582520 ``` ### Type of change - [x] New Feature (non-breaking change which adds functionality) Signed-off-by: Jin Hai <haijin.chn@gmail.com>
2026-05-05 18:14:39 +08:00
func isValidURL(str string) bool {
u, err := netUrl.Parse(str)
if err != nil {
return false
}
return u.Scheme != "" && u.Host != ""
}
func (c *RAGFlowClient) ChatToModel(cmd *Command) (ResponseIf, error) {
if c.ServerType != "user" {
return nil, fmt.Errorf("this command is only allowed in USER mode")
}
var providerName, instanceName, modelName string
// Check if composite_model_name is provided in command
if compositeModelName, ok := cmd.Params["composite_model_name"].(string); ok && compositeModelName != "" {
names := strings.Split(compositeModelName, "@")
if len(names) != 3 {
return nil, fmt.Errorf("model name must be in format 'model@instance@provider'")
}
providerName = names[2]
instanceName = names[1]
modelName = names[0]
} else if c.CurrentModel != nil {
// Use current model if set
providerName = c.CurrentModel.Provider
instanceName = c.CurrentModel.Instance
modelName = c.CurrentModel.Model
} else {
return nil, fmt.Errorf("model name not provided and no current model set. Use 'use model' command first")
}
Go: CLI chat with text, image, video (#14573) ### What problem does this PR solve? ``` RAGFlow(user)> chat with 'glm-4.6v-flash@test@zhipu-ai' message 'What are the pics talk about?' image 'https://cdn.bigmodel.cn/static/logo/register.png' 'https://cdn.bigmodel.cn/static/logo/api-key.png' Answer: The first picture shows a login/register modal with options for phone number login, account login, and WeChat QR code login, along with a prompt for new users to get a 20 million tokens experience package. The second picture displays the API keys management page of a platform, including a warning about API key security and a table listing existing API keys with details like creation time and usage history. Time: 31.600545 RAGFlow(user)> chat with 'glm-4.6v-flash@test@zhipu-ai' message 'What are the video talk about?' video 'https://cdn.bigmodel.cn/agent-demos/lark/113123.mov' Answer: Based on the sequence of frames provided, the video is a demonstration of a web search and navigation process. 1. The video starts with a blank Google search page. 2. The user types "智谱" (which is the Chinese name for the company Zhipu AI) into the search box. 3. The search is initiated and the page shows "About 0 results". 4. The search results load, showing information about Zhipu AI, including its website. 5. The user clicks on the main website link (www.zhipuai.cn). 6. The video ends by showing the homepage of Zhipu AI's website, titled "Z.ai GLM Large Model Open Platform". In summary, the video is about searching for the company "智谱" (Zhipu AI) on Google and then navigating to its official website. Time: 76.582520 ``` ### Type of change - [x] New Feature (non-breaking change which adds functionality) Signed-off-by: Jin Hai <haijin.chn@gmail.com>
2026-05-05 18:14:39 +08:00
formattedMessages := []map[string]interface{}{}
messages, ok := cmd.Params["messages"].([]string)
if !ok {
return nil, fmt.Errorf("messages not provided")
}
contents := []map[string]interface{}{}
if len(messages) > 0 {
for _, message := range messages {
contents = append(contents, map[string]interface{}{
"type": "text",
"text": message,
})
}
}
images, ok := cmd.Params["images"].([]string)
if !ok {
return nil, fmt.Errorf("images not provided")
}
if len(images) > 0 {
for _, image := range images {
if isValidURL(image) {
contents = append(contents, map[string]interface{}{
"type": "image_url",
"image_url": map[string]string{
"url": image,
},
})
} else {
// image is a path, read the file and turn it into base64
imageContent, err := os.ReadFile(image)
if err != nil {
return nil, fmt.Errorf("failed to read image: %w", err)
}
contents = append(contents, map[string]interface{}{
"type": "image_file",
"image_file": map[string]interface{}{
"content": base64.StdEncoding.EncodeToString(imageContent),
},
})
}
}
}
videos, ok := cmd.Params["videos"].([]string)
if !ok {
return nil, fmt.Errorf("images not provided")
}
if len(videos) > 0 {
for _, video := range videos {
if isValidURL(video) {
contents = append(contents, map[string]interface{}{
"type": "video_url",
"video_url": map[string]interface{}{
"url": video,
},
})
} else {
return nil, fmt.Errorf("invalid video URL: %s", video)
}
}
}
audios, ok := cmd.Params["audios"].([]string)
if !ok {
return nil, fmt.Errorf("images not provided")
}
if len(audios) > 0 {
if len(audios) != 1 {
return nil, fmt.Errorf("only one audio file is supported")
}
audioFile := audios[0]
audioContent, err := os.ReadFile(audioFile)
if err != nil {
return nil, fmt.Errorf("failed to read audio: %w", err)
}
// file type: wav or mp3
format := filepath.Ext(audioFile) // file type: wav or mp3
format = strings.TrimPrefix(format, ".")
contents = append(contents, map[string]interface{}{
"type": "input_audio",
"input_audio": map[string]interface{}{
"data": base64.StdEncoding.EncodeToString(audioContent),
"format": format,
},
})
}
Go: CLI chat with text, image, video (#14573) ### What problem does this PR solve? ``` RAGFlow(user)> chat with 'glm-4.6v-flash@test@zhipu-ai' message 'What are the pics talk about?' image 'https://cdn.bigmodel.cn/static/logo/register.png' 'https://cdn.bigmodel.cn/static/logo/api-key.png' Answer: The first picture shows a login/register modal with options for phone number login, account login, and WeChat QR code login, along with a prompt for new users to get a 20 million tokens experience package. The second picture displays the API keys management page of a platform, including a warning about API key security and a table listing existing API keys with details like creation time and usage history. Time: 31.600545 RAGFlow(user)> chat with 'glm-4.6v-flash@test@zhipu-ai' message 'What are the video talk about?' video 'https://cdn.bigmodel.cn/agent-demos/lark/113123.mov' Answer: Based on the sequence of frames provided, the video is a demonstration of a web search and navigation process. 1. The video starts with a blank Google search page. 2. The user types "智谱" (which is the Chinese name for the company Zhipu AI) into the search box. 3. The search is initiated and the page shows "About 0 results". 4. The search results load, showing information about Zhipu AI, including its website. 5. The user clicks on the main website link (www.zhipuai.cn). 6. The video ends by showing the homepage of Zhipu AI's website, titled "Z.ai GLM Large Model Open Platform". In summary, the video is about searching for the company "智谱" (Zhipu AI) on Google and then navigating to its official website. Time: 76.582520 ``` ### Type of change - [x] New Feature (non-breaking change which adds functionality) Signed-off-by: Jin Hai <haijin.chn@gmail.com>
2026-05-05 18:14:39 +08:00
files, ok := cmd.Params["files"].([]string)
if !ok {
return nil, fmt.Errorf("images not provided")
}
if len(files) > 0 {
for _, file := range files {
if isValidURL(file) {
contents = append(contents, map[string]interface{}{
"type": "file_url",
"file_url": map[string]interface{}{
"url": file,
},
})
} else {
return nil, fmt.Errorf("invalid file URL: %s", file)
}
}
}
formattedText := map[string]interface{}{
"role": "user",
"content": contents,
}
formattedMessages = append(formattedMessages, formattedText)
thinking := cmd.Params["thinking"].(bool)
stream := cmd.Params["stream"].(bool)
effort := cmd.Params["effort"].(string)
verbosity := cmd.Params["verbosity"].(string)
url := "/chat/completions"
payload := map[string]interface{}{
"provider_name": providerName,
"instance_name": instanceName,
"model_name": modelName,
Go: CLI chat with text, image, video (#14573) ### What problem does this PR solve? ``` RAGFlow(user)> chat with 'glm-4.6v-flash@test@zhipu-ai' message 'What are the pics talk about?' image 'https://cdn.bigmodel.cn/static/logo/register.png' 'https://cdn.bigmodel.cn/static/logo/api-key.png' Answer: The first picture shows a login/register modal with options for phone number login, account login, and WeChat QR code login, along with a prompt for new users to get a 20 million tokens experience package. The second picture displays the API keys management page of a platform, including a warning about API key security and a table listing existing API keys with details like creation time and usage history. Time: 31.600545 RAGFlow(user)> chat with 'glm-4.6v-flash@test@zhipu-ai' message 'What are the video talk about?' video 'https://cdn.bigmodel.cn/agent-demos/lark/113123.mov' Answer: Based on the sequence of frames provided, the video is a demonstration of a web search and navigation process. 1. The video starts with a blank Google search page. 2. The user types "智谱" (which is the Chinese name for the company Zhipu AI) into the search box. 3. The search is initiated and the page shows "About 0 results". 4. The search results load, showing information about Zhipu AI, including its website. 5. The user clicks on the main website link (www.zhipuai.cn). 6. The video ends by showing the homepage of Zhipu AI's website, titled "Z.ai GLM Large Model Open Platform". In summary, the video is about searching for the company "智谱" (Zhipu AI) on Google and then navigating to its official website. Time: 76.582520 ``` ### Type of change - [x] New Feature (non-breaking change which adds functionality) Signed-off-by: Jin Hai <haijin.chn@gmail.com>
2026-05-05 18:14:39 +08:00
"messages": formattedMessages,
"stream": stream,
"thinking": thinking,
}
if thinking {
payload["effort"] = effort
payload["verbosity"] = verbosity
}
if stream {
// Call stream http api
startTime := time.Now()
reader, err := c.HTTPClient.RequestStream("POST", url, "web", nil, payload)
if err != nil {
return nil, fmt.Errorf("failed to chat model: %w", err)
}
defer reader.Close()
// Parse SSE and output to console
scanner := bufio.NewScanner(reader)
var fullMessage strings.Builder
reasoningPrint := true
messagePrint := true
for scanner.Scan() {
line := scanner.Text()
if strings.HasPrefix(line, "data:") {
data := strings.TrimPrefix(line, "data:")
data = strings.TrimSpace(data)
if strings.HasPrefix(data, "[REASONING]") {
data = strings.TrimPrefix(data, "[REASONING]")
if reasoningPrint {
fmt.Print("Thinking: ")
reasoningPrint = false
thinking = true
} else {
fmt.Print(data)
}
os.Stdout.Sync()
}
if strings.HasPrefix(data, "[MESSAGE]") {
data = strings.TrimPrefix(data, "[MESSAGE]")
if messagePrint {
if thinking {
fmt.Println()
}
fmt.Print("Answer: ")
messagePrint = false
} else {
fmt.Print(data)
os.Stdout.Sync()
fullMessage.WriteString(data)
}
}
} else if strings.HasPrefix(line, "event:error") {
// error event
if scanner.Scan() {
errData := strings.TrimPrefix(scanner.Text(), "data:")
errData = strings.TrimSpace(errData)
return nil, fmt.Errorf("chat error: %s", errData)
}
// If there's an error, return a generic error
return nil, fmt.Errorf("chat error: received error event from server")
}
}
duration := time.Since(startTime).Seconds()
if err := scanner.Err(); err != nil {
return nil, fmt.Errorf("error reading stream: %w", err)
}
fmt.Println()
result := &StreamMessageResponse{
Code: 0,
Message: fullMessage.String(),
Duration: duration,
}
return result, nil
}
resp, err := c.HTTPClient.Request("POST", url, "web", nil, payload)
if err != nil {
return nil, formatRequestError("Chat request", err)
}
if resp.StatusCode != 200 {
return nil, fmt.Errorf("failed to list instance models: HTTP %d, body: %s", resp.StatusCode, string(resp.Body))
}
var result NonStreamResponse
if err = json.Unmarshal(resp.Body, &result); err != nil {
return nil, fmt.Errorf("failed to list instance models: invalid JSON (%w)", err)
}
if result.Code != 0 {
return nil, fmt.Errorf("%s", result.Message)
}
result.Duration = resp.Duration
return &result, nil
}
func (c *RAGFlowClient) EmbedUserText(cmd *Command) (ResponseIf, error) {
if c.HTTPClient.APIToken == "" && c.HTTPClient.LoginToken == "" {
return nil, fmt.Errorf("API token not set. Please login first")
}
if c.ServerType != "user" {
return nil, fmt.Errorf("this command is only allowed in USER mode")
}
var providerName, instanceName, modelName string
// Check if composite_model_name is provided in command
if compositeModelName, ok := cmd.Params["composite_model_name"].(string); ok && compositeModelName != "" {
names := strings.Split(compositeModelName, "@")
if len(names) != 3 {
return nil, fmt.Errorf("model name must be in format 'model@instance@provider'")
}
providerName = names[2]
instanceName = names[1]
modelName = names[0]
} else if c.CurrentModel != nil {
// Use current model if set
providerName = c.CurrentModel.Provider
instanceName = c.CurrentModel.Instance
modelName = c.CurrentModel.Model
} else {
return nil, fmt.Errorf("model name not provided and no current model set. Use 'use model' command first")
}
texts, ok := cmd.Params["texts"].([]string)
if !ok {
return nil, fmt.Errorf("texts not provided")
}
dimension, ok := cmd.Params["dimension"].(int)
if !ok {
dimension = 0
}
payload := map[string]interface{}{
"provider_name": providerName,
"instance_name": instanceName,
"model_name": modelName,
"texts": texts,
"dimension": dimension,
}
url := "/embeddings"
resp, err := c.HTTPClient.Request("POST", url, "web", nil, payload)
if err != nil {
return nil, fmt.Errorf("failed to embed text: %w", err)
}
if resp.StatusCode != 200 {
return nil, fmt.Errorf("failed to embed text: HTTP %d, body: %s", resp.StatusCode, string(resp.Body))
}
var result EmbeddingsResponse
if err = json.Unmarshal(resp.Body, &result); err != nil {
return nil, fmt.Errorf("embed text failed: invalid JSON (%w)", err)
}
if result.Code != 0 {
return nil, fmt.Errorf("%s", result.Message)
}
result.Duration = resp.Duration
return &result, nil
}
func (c *RAGFlowClient) RerankUserDocument(cmd *Command) (ResponseIf, error) {
if c.HTTPClient.APIToken == "" && c.HTTPClient.LoginToken == "" {
return nil, fmt.Errorf("API token not set. Please login first")
}
if c.ServerType != "user" {
return nil, fmt.Errorf("this command is only allowed in USER mode")
}
var providerName, instanceName, modelName string
// Check if composite_model_name is provided in command
if compositeModelName, ok := cmd.Params["composite_model_name"].(string); ok && compositeModelName != "" {
names := strings.Split(compositeModelName, "@")
if len(names) != 3 {
return nil, fmt.Errorf("model name must be in format 'model@instance@provider'")
}
providerName = names[2]
instanceName = names[1]
modelName = names[0]
} else if c.CurrentModel != nil {
// Use current model if set
providerName = c.CurrentModel.Provider
instanceName = c.CurrentModel.Instance
modelName = c.CurrentModel.Model
} else {
return nil, fmt.Errorf("model name not provided and no current model set. Use 'use model' command first")
}
query, ok := cmd.Params["query"].(string)
if !ok {
return nil, fmt.Errorf("query not provided")
}
documents, ok := cmd.Params["documents"].([]string)
if !ok {
return nil, fmt.Errorf("documents not provided")
}
topN, ok := cmd.Params["top_n"].(int)
if !ok {
return nil, fmt.Errorf("top n not provided")
}
payload := map[string]interface{}{
"provider_name": providerName,
"instance_name": instanceName,
"model_name": modelName,
"query": query,
"documents": documents,
"top_n": topN,
}
url := "/rerank"
resp, err := c.HTTPClient.Request("POST", url, "web", nil, payload)
if err != nil {
return nil, fmt.Errorf("failed to rerank document: %w", err)
}
if resp.StatusCode != 200 {
return nil, fmt.Errorf("failed to rerank document: HTTP %d, body: %s", resp.StatusCode, string(resp.Body))
}
var result CommonResponse
if err = json.Unmarshal(resp.Body, &result); err != nil {
return nil, fmt.Errorf("rerank document failed: invalid JSON (%w)", err)
}
if result.Code != 0 {
return nil, fmt.Errorf("%s", result.Message)
}
result.Duration = resp.Duration
return &result, nil
}
func (c *RAGFlowClient) TTSUserCommand(cmd *Command) (ResponseIf, error) {
if c.HTTPClient.APIToken == "" && c.HTTPClient.LoginToken == "" {
return nil, fmt.Errorf("API token not set. Please login first")
}
if c.ServerType != "user" {
return nil, fmt.Errorf("this command is only allowed in USER mode")
}
var providerName, instanceName, modelName string
// Check if composite_model_name is provided in command
if compositeModelName, ok := cmd.Params["composite_model_name"].(string); ok && compositeModelName != "" {
names := strings.Split(compositeModelName, "@")
if len(names) != 3 {
return nil, fmt.Errorf("model name must be in format 'model@instance@provider'")
}
providerName = names[2]
instanceName = names[1]
modelName = names[0]
} else if c.CurrentModel != nil {
// Use current model if set
providerName = c.CurrentModel.Provider
instanceName = c.CurrentModel.Instance
modelName = c.CurrentModel.Model
} else {
return nil, fmt.Errorf("model name not provided and no current model set. Use 'use model' command first")
}
text, ok := cmd.Params["text"].(string)
if !ok {
return nil, fmt.Errorf("text not provided")
}
//fileToSave, ok := cmd.Params["file"].(string)
//if !ok {
// return nil, fmt.Errorf("file not provided")
//}
payload := map[string]interface{}{
"provider_name": providerName,
"instance_name": instanceName,
"model_name": modelName,
"text": text,
}
Go: implement TTS for MiniMax provider and CLI testing for TTS (#14911) ### What problem does this PR solve? This PR implement TTS for MiniMax provider and CLI testing for TTS **The following functionalities are now supported:** **MiniMax:** - [x] Chat / Stream Chat - [x] Embedding - [x] Rerank - [x] Model listing - [x] Provider connection checking - [x] Text To Speech - [ ] OCRFile - [ ] ~~Audio To Text~~ - [ ] ~~Balance~~ **Verified examples from the CLI:** ```plaintext RAGFlow(user)> tts with 'speech-2.8-hd@test@minimax' text 'He who desires but acts not, breeds pestilence.' play format 'wav' save './internal' param '{"voice_setting": {"voice_id": "English_radiant_girl", "speed": 1, "vol": 1, "pitch": 0}, "audio_setting": {"sample_rate": 32000, "bitrate": 128000, "format": "wav", "channel": 1}, "output_format": "hex"}' Saved to directory: /home/infiniflow/Documents/development/ragflow/internal/speech-2.8-hd_output.wav SUCCESS RAGFlow(user)> stream tts with 'speech-2.8-hd@test@minimax' text 'He who desires but acts not, breeds pestilence.' play format 'wav' save './internal' param '{"voice_setting": {"voice_id": "English_radiant_girl", "speed": 1, "vol": 1, "pitch": 0}, "audio_setting": {"sample_rate": 32000, "bitrate": 128000, "format": "wav", "channel": 1}, "output_format": "hex"}' Saved to directory: /home/infiniflow/Documents/development/ragflow/internal/speech-2.8-hd_output.wav SUCCESS ``` Set `Play` to play audio in CLI Set `Save` `PATH_TO_SAVE` to save file Set `format` to save file in wav or mp3 Set `Param` align with official request body ### Type of change - [x] New Feature (non-breaking change which adds functionality)
2026-05-14 13:19:31 +08:00
ttsConfigPayload := make(map[string]interface{})
explicitFormat, hasExplicitFormat := cmd.Params["format"].(string)
if paramStr, ok := cmd.Params["param_str"].(string); ok && paramStr != "" {
var dynamicParams map[string]interface{}
if err := json.Unmarshal([]byte(paramStr), &dynamicParams); err != nil {
return nil, fmt.Errorf("param string must be valid JSON. Error: %w", err)
}
ttsConfigPayload["params"] = dynamicParams
if !hasExplicitFormat {
var findFormat func(map[string]interface{}) string
findFormat = func(m map[string]interface{}) string {
if val, ok := m["format"]; ok {
return fmt.Sprintf("%v", val)
}
if val, ok := m["response_format"]; ok {
return fmt.Sprintf("%v", val)
}
for _, v := range m {
if subMap, ok := v.(map[string]interface{}); ok {
if res := findFormat(subMap); res != "" {
return res
}
}
}
return ""
}
if ext := findFormat(dynamicParams); ext != "" {
explicitFormat = ext
}
}
}
if explicitFormat != "" {
ttsConfigPayload["format"] = explicitFormat
} else {
Go: implement TTS for fishaudio, openrouter and asr for fishaudio (#14926) ### What problem does this PR solve? This PR implement TTS for FishAudio and MiniMax provider and ASR for FishAudio **The following functionalities are now supported:** **FishAudio:** - [x] Text To Speech - [x] Stream Text To Speech - [x] Audio To Text **OpenRouter:** - [x] Text To Speech **Verified examples from the CLI:** ```plaintext **FishAudio** RAGFlow(user)> tts with 's1@test@fishaudio' text 'He who desires but acts not, breeds pestilence.' play format 'wav' save './internal' param '{"reference_id": "90e65eaaf50e4470b8e6d43ee6afd7d5", "temperature": 0.7, "top_p": 0.7, "prosody": {"speed": 1, "volume": 0, "normalize_loudness": true}, "chunk_length": 300, "normalize": true, "sample_rate": 44100, "mp3_bitrate": 128, "latency": "normal", "max_new_tokens": 1024, "repetition_penalty": 1.2, "min_chunk_length": 50, "condition_on_previous_chunks": true, "early_stop_threshold": 1}' Saved to directory: /home/infiniflow/Documents/development/ragflow/internal/s1_output.wav SUCCESS RAGFlow(user)> stream tts with 's1@test@fishaudio' text 'He who desires but acts not, breeds pestilence.' play format 'wav' save './internal' param '{"reference_id": "90e65eaaf50e4470b8e6d43ee6afd7d5", "temperature": 0.7, "top_p": 0.7, "prosody": {"speed": 1, "volume": 0, "normalize_loudness": true}, "chunk_length": 300, "normalize": true, "sample_rate": 44100, "mp3_bitrate": 128, "latency": "normal", "max_new_tokens": 1024, "repetition_penalty": 1.2, "min_chunk_length": 50, "condition_on_previous_chunks": true, "early_stop_threshold": 1}' Saved to directory: /home/infiniflow/Documents/development/ragflow/internal/s1_output.wav SUCCESS RAGFlow(user)> asr with 'transcribe-1@test@fishaudio' audio './internal/test.wav' param '{"language": "en", "ignore_timestamps": true}' +----------------------------------------------------------------------------------------------------------------------+ | text | +----------------------------------------------------------------------------------------------------------------------+ | The examination and testimony of the experts enabled the commission to conclude that five shots may have been fired. | +----------------------------------------------------------------------------------------------------------------------+ ``` ### Type of change - [x] Bug Fix (non-breaking change which fixes an issue) - [x] New Feature (non-breaking change which adds functionality) - [x] Refactoring
2026-05-14 18:58:00 +08:00
ttsConfigPayload["format"] = "mp3"
Go: implement TTS for MiniMax provider and CLI testing for TTS (#14911) ### What problem does this PR solve? This PR implement TTS for MiniMax provider and CLI testing for TTS **The following functionalities are now supported:** **MiniMax:** - [x] Chat / Stream Chat - [x] Embedding - [x] Rerank - [x] Model listing - [x] Provider connection checking - [x] Text To Speech - [ ] OCRFile - [ ] ~~Audio To Text~~ - [ ] ~~Balance~~ **Verified examples from the CLI:** ```plaintext RAGFlow(user)> tts with 'speech-2.8-hd@test@minimax' text 'He who desires but acts not, breeds pestilence.' play format 'wav' save './internal' param '{"voice_setting": {"voice_id": "English_radiant_girl", "speed": 1, "vol": 1, "pitch": 0}, "audio_setting": {"sample_rate": 32000, "bitrate": 128000, "format": "wav", "channel": 1}, "output_format": "hex"}' Saved to directory: /home/infiniflow/Documents/development/ragflow/internal/speech-2.8-hd_output.wav SUCCESS RAGFlow(user)> stream tts with 'speech-2.8-hd@test@minimax' text 'He who desires but acts not, breeds pestilence.' play format 'wav' save './internal' param '{"voice_setting": {"voice_id": "English_radiant_girl", "speed": 1, "vol": 1, "pitch": 0}, "audio_setting": {"sample_rate": 32000, "bitrate": 128000, "format": "wav", "channel": 1}, "output_format": "hex"}' Saved to directory: /home/infiniflow/Documents/development/ragflow/internal/speech-2.8-hd_output.wav SUCCESS ``` Set `Play` to play audio in CLI Set `Save` `PATH_TO_SAVE` to save file Set `format` to save file in wav or mp3 Set `Param` align with official request body ### Type of change - [x] New Feature (non-breaking change which adds functionality)
2026-05-14 13:19:31 +08:00
}
if len(ttsConfigPayload) > 0 {
payload["tts_config"] = ttsConfigPayload
}
url := "/audio/speech"
resp, err := c.HTTPClient.Request("POST", url, "web", nil, payload)
if err != nil {
return nil, fmt.Errorf("failed to TTS document: %w", err)
}
if resp.StatusCode != 200 {
return nil, fmt.Errorf("failed to TTS document: HTTP %d, body: %s", resp.StatusCode, string(resp.Body))
}
Go: implement TTS for MiniMax provider and CLI testing for TTS (#14911) ### What problem does this PR solve? This PR implement TTS for MiniMax provider and CLI testing for TTS **The following functionalities are now supported:** **MiniMax:** - [x] Chat / Stream Chat - [x] Embedding - [x] Rerank - [x] Model listing - [x] Provider connection checking - [x] Text To Speech - [ ] OCRFile - [ ] ~~Audio To Text~~ - [ ] ~~Balance~~ **Verified examples from the CLI:** ```plaintext RAGFlow(user)> tts with 'speech-2.8-hd@test@minimax' text 'He who desires but acts not, breeds pestilence.' play format 'wav' save './internal' param '{"voice_setting": {"voice_id": "English_radiant_girl", "speed": 1, "vol": 1, "pitch": 0}, "audio_setting": {"sample_rate": 32000, "bitrate": 128000, "format": "wav", "channel": 1}, "output_format": "hex"}' Saved to directory: /home/infiniflow/Documents/development/ragflow/internal/speech-2.8-hd_output.wav SUCCESS RAGFlow(user)> stream tts with 'speech-2.8-hd@test@minimax' text 'He who desires but acts not, breeds pestilence.' play format 'wav' save './internal' param '{"voice_setting": {"voice_id": "English_radiant_girl", "speed": 1, "vol": 1, "pitch": 0}, "audio_setting": {"sample_rate": 32000, "bitrate": 128000, "format": "wav", "channel": 1}, "output_format": "hex"}' Saved to directory: /home/infiniflow/Documents/development/ragflow/internal/speech-2.8-hd_output.wav SUCCESS ``` Set `Play` to play audio in CLI Set `Save` `PATH_TO_SAVE` to save file Set `format` to save file in wav or mp3 Set `Param` align with official request body ### Type of change - [x] New Feature (non-breaking change which adds functionality)
2026-05-14 13:19:31 +08:00
var ttsResult struct {
Code int `json:"code"`
Message string `json:"message"`
Data struct {
Audio string `json:"audio"`
} `json:"data"`
}
if err = json.Unmarshal(resp.Body, &ttsResult); err != nil {
return nil, fmt.Errorf("TTS document failed: invalid JSON (%w)", err)
}
Go: implement TTS for MiniMax provider and CLI testing for TTS (#14911) ### What problem does this PR solve? This PR implement TTS for MiniMax provider and CLI testing for TTS **The following functionalities are now supported:** **MiniMax:** - [x] Chat / Stream Chat - [x] Embedding - [x] Rerank - [x] Model listing - [x] Provider connection checking - [x] Text To Speech - [ ] OCRFile - [ ] ~~Audio To Text~~ - [ ] ~~Balance~~ **Verified examples from the CLI:** ```plaintext RAGFlow(user)> tts with 'speech-2.8-hd@test@minimax' text 'He who desires but acts not, breeds pestilence.' play format 'wav' save './internal' param '{"voice_setting": {"voice_id": "English_radiant_girl", "speed": 1, "vol": 1, "pitch": 0}, "audio_setting": {"sample_rate": 32000, "bitrate": 128000, "format": "wav", "channel": 1}, "output_format": "hex"}' Saved to directory: /home/infiniflow/Documents/development/ragflow/internal/speech-2.8-hd_output.wav SUCCESS RAGFlow(user)> stream tts with 'speech-2.8-hd@test@minimax' text 'He who desires but acts not, breeds pestilence.' play format 'wav' save './internal' param '{"voice_setting": {"voice_id": "English_radiant_girl", "speed": 1, "vol": 1, "pitch": 0}, "audio_setting": {"sample_rate": 32000, "bitrate": 128000, "format": "wav", "channel": 1}, "output_format": "hex"}' Saved to directory: /home/infiniflow/Documents/development/ragflow/internal/speech-2.8-hd_output.wav SUCCESS ``` Set `Play` to play audio in CLI Set `Save` `PATH_TO_SAVE` to save file Set `format` to save file in wav or mp3 Set `Param` align with official request body ### Type of change - [x] New Feature (non-breaking change which adds functionality)
2026-05-14 13:19:31 +08:00
if ttsResult.Code != 0 {
return nil, fmt.Errorf("%s", ttsResult.Message)
}
Go: implement TTS for MiniMax provider and CLI testing for TTS (#14911) ### What problem does this PR solve? This PR implement TTS for MiniMax provider and CLI testing for TTS **The following functionalities are now supported:** **MiniMax:** - [x] Chat / Stream Chat - [x] Embedding - [x] Rerank - [x] Model listing - [x] Provider connection checking - [x] Text To Speech - [ ] OCRFile - [ ] ~~Audio To Text~~ - [ ] ~~Balance~~ **Verified examples from the CLI:** ```plaintext RAGFlow(user)> tts with 'speech-2.8-hd@test@minimax' text 'He who desires but acts not, breeds pestilence.' play format 'wav' save './internal' param '{"voice_setting": {"voice_id": "English_radiant_girl", "speed": 1, "vol": 1, "pitch": 0}, "audio_setting": {"sample_rate": 32000, "bitrate": 128000, "format": "wav", "channel": 1}, "output_format": "hex"}' Saved to directory: /home/infiniflow/Documents/development/ragflow/internal/speech-2.8-hd_output.wav SUCCESS RAGFlow(user)> stream tts with 'speech-2.8-hd@test@minimax' text 'He who desires but acts not, breeds pestilence.' play format 'wav' save './internal' param '{"voice_setting": {"voice_id": "English_radiant_girl", "speed": 1, "vol": 1, "pitch": 0}, "audio_setting": {"sample_rate": 32000, "bitrate": 128000, "format": "wav", "channel": 1}, "output_format": "hex"}' Saved to directory: /home/infiniflow/Documents/development/ragflow/internal/speech-2.8-hd_output.wav SUCCESS ``` Set `Play` to play audio in CLI Set `Save` `PATH_TO_SAVE` to save file Set `format` to save file in wav or mp3 Set `Param` align with official request body ### Type of change - [x] New Feature (non-breaking change which adds functionality)
2026-05-14 13:19:31 +08:00
// Convert Base64 back to the original audio byte stream
audioBytes, err := base64.StdEncoding.DecodeString(ttsResult.Data.Audio)
if err != nil {
return nil, fmt.Errorf("failed to decode audio base64: %w", err)
}
shouldPlay, _ := cmd.Params["play"].(bool)
shouldSave, _ := cmd.Params["save"].(bool)
saveDir, _ := cmd.Params["save_path"].(string)
Go: implement TTS, ASR for Siliconflow and TTs for StepFun (#14944) ### What problem does this PR solve? This PRimplement TTS, ASR for Siliconflow and TTs for StepFun **The following functionalities are now supported:** **SiliConFlow:** - [x] Text To Speech - [x] Audio To Text - [x] Stream Audio To Text **StrepFun:** - [x] Audio To Text - [x] Stream Audio To Text **Verified examples from the CLI:** ```plaintext # SiliconFlow RAGFlow(user)> tts with 'FunAudioLLM/CosyVoice2-0.5B@test@Siliconflow' text 'hello? show yourself' play format 'wav' param '{"voice": "fnlp/MOSS-TTSD-v0.5:alex"}' SUCCESS RAGFlow(user)> asr with 'FunAudioLLM/SenseVoiceSmall@test@siliconflow' audio './internal/test.wav' param '' +----------------------------------------------------------------------------------------------------------------------+ | text | +----------------------------------------------------------------------------------------------------------------------+ | The examination and testimony of the experts enabled the commission to conclude that five shots may have been fired. | +----------------------------------------------------------------------------------------------------------------------+ RAGFlow(user)> stream asr with 'FunAudioLLM/SenseVoiceSmall@test@siliconflow' audio './internal/test.wav' param '' +----------------------------------------------------------------------------------------------------------------------+ | text | +----------------------------------------------------------------------------------------------------------------------+ | The examination and testimony of the experts enabled the commission to conclude that five shots may have been fired. | +----------------------------------------------------------------------------------------------------------------------+ ``` ### Type of change - [x] Bug Fix (non-breaking change which fixes an issue) - [x] New Feature (non-breaking change which adds functionality)
2026-05-15 14:03:33 +08:00
// format file name
safeModelName := strings.ReplaceAll(modelName, "/", "_")
safeModelName = strings.ReplaceAll(safeModelName, ":", "-")
fileName := fmt.Sprintf("%s_output.%s", safeModelName, explicitFormat)
Go: implement TTS for MiniMax provider and CLI testing for TTS (#14911) ### What problem does this PR solve? This PR implement TTS for MiniMax provider and CLI testing for TTS **The following functionalities are now supported:** **MiniMax:** - [x] Chat / Stream Chat - [x] Embedding - [x] Rerank - [x] Model listing - [x] Provider connection checking - [x] Text To Speech - [ ] OCRFile - [ ] ~~Audio To Text~~ - [ ] ~~Balance~~ **Verified examples from the CLI:** ```plaintext RAGFlow(user)> tts with 'speech-2.8-hd@test@minimax' text 'He who desires but acts not, breeds pestilence.' play format 'wav' save './internal' param '{"voice_setting": {"voice_id": "English_radiant_girl", "speed": 1, "vol": 1, "pitch": 0}, "audio_setting": {"sample_rate": 32000, "bitrate": 128000, "format": "wav", "channel": 1}, "output_format": "hex"}' Saved to directory: /home/infiniflow/Documents/development/ragflow/internal/speech-2.8-hd_output.wav SUCCESS RAGFlow(user)> stream tts with 'speech-2.8-hd@test@minimax' text 'He who desires but acts not, breeds pestilence.' play format 'wav' save './internal' param '{"voice_setting": {"voice_id": "English_radiant_girl", "speed": 1, "vol": 1, "pitch": 0}, "audio_setting": {"sample_rate": 32000, "bitrate": 128000, "format": "wav", "channel": 1}, "output_format": "hex"}' Saved to directory: /home/infiniflow/Documents/development/ragflow/internal/speech-2.8-hd_output.wav SUCCESS ``` Set `Play` to play audio in CLI Set `Save` `PATH_TO_SAVE` to save file Set `format` to save file in wav or mp3 Set `Param` align with official request body ### Type of change - [x] New Feature (non-breaking change which adds functionality)
2026-05-14 13:19:31 +08:00
cwd, err := os.Getwd()
if err != nil {
cwd = "."
}
localPath := filepath.Join(cwd, fileName)
if err := os.WriteFile(localPath, audioBytes, 0644); err != nil {
return nil, fmt.Errorf("failed to write local audio file: %w", err)
}
if shouldPlay {
cmdExec := exec.Command("aplay", localPath)
if err := cmdExec.Run(); err != nil {
fmt.Printf("Play error: %v (Hint: did you use 'format: wav' in your params?)\n", err)
}
}
var finalMessage string
if shouldSave {
if saveDir == "" {
saveDir = cwd
} else {
absSaveDir, err := filepath.Abs(saveDir)
if err == nil {
saveDir = absSaveDir
}
if err := os.MkdirAll(saveDir, 0755); err != nil {
return nil, fmt.Errorf("failed to create save directory: %w", err)
}
finalPath := filepath.Join(saveDir, fileName)
if err := os.WriteFile(finalPath, audioBytes, 0644); err != nil {
return nil, fmt.Errorf("failed to save file to target directory: %w", err)
}
if saveDir != cwd {
os.Remove(localPath)
}
finalMessage = fmt.Sprintf("Saved to directory: %s", finalPath)
}
} else {
defer os.Remove(localPath)
finalMessage = "TTS Task Completed (Audio not saved)"
}
if finalMessage != "" && shouldSave {
fmt.Println(finalMessage)
}
var result SimpleResponse
result.Code = 0
result.Message = "SUCCESS"
result.Duration = resp.Duration
return &result, nil
}
func (c *RAGFlowClient) ASRUserCommand(cmd *Command) (ResponseIf, error) {
if c.HTTPClient.APIToken == "" && c.HTTPClient.LoginToken == "" {
return nil, fmt.Errorf("API token not set. Please login first")
}
if c.ServerType != "user" {
return nil, fmt.Errorf("this command is only allowed in USER mode")
}
var providerName, instanceName, modelName string
// Check if composite_model_name is provided in command
if compositeModelName, ok := cmd.Params["composite_model_name"].(string); ok && compositeModelName != "" {
names := strings.Split(compositeModelName, "@")
if len(names) != 3 {
return nil, fmt.Errorf("model name must be in format 'model@instance@provider'")
}
providerName = names[2]
instanceName = names[1]
modelName = names[0]
} else if c.CurrentModel != nil {
// Use current model if set
providerName = c.CurrentModel.Provider
instanceName = c.CurrentModel.Instance
modelName = c.CurrentModel.Model
} else {
return nil, fmt.Errorf("model name not provided and no current model set. Use 'use model' command first")
}
audioFile, ok := cmd.Params["audio_file"].(string)
if !ok {
Go: implement TTS for fishaudio, openrouter and asr for fishaudio (#14926) ### What problem does this PR solve? This PR implement TTS for FishAudio and MiniMax provider and ASR for FishAudio **The following functionalities are now supported:** **FishAudio:** - [x] Text To Speech - [x] Stream Text To Speech - [x] Audio To Text **OpenRouter:** - [x] Text To Speech **Verified examples from the CLI:** ```plaintext **FishAudio** RAGFlow(user)> tts with 's1@test@fishaudio' text 'He who desires but acts not, breeds pestilence.' play format 'wav' save './internal' param '{"reference_id": "90e65eaaf50e4470b8e6d43ee6afd7d5", "temperature": 0.7, "top_p": 0.7, "prosody": {"speed": 1, "volume": 0, "normalize_loudness": true}, "chunk_length": 300, "normalize": true, "sample_rate": 44100, "mp3_bitrate": 128, "latency": "normal", "max_new_tokens": 1024, "repetition_penalty": 1.2, "min_chunk_length": 50, "condition_on_previous_chunks": true, "early_stop_threshold": 1}' Saved to directory: /home/infiniflow/Documents/development/ragflow/internal/s1_output.wav SUCCESS RAGFlow(user)> stream tts with 's1@test@fishaudio' text 'He who desires but acts not, breeds pestilence.' play format 'wav' save './internal' param '{"reference_id": "90e65eaaf50e4470b8e6d43ee6afd7d5", "temperature": 0.7, "top_p": 0.7, "prosody": {"speed": 1, "volume": 0, "normalize_loudness": true}, "chunk_length": 300, "normalize": true, "sample_rate": 44100, "mp3_bitrate": 128, "latency": "normal", "max_new_tokens": 1024, "repetition_penalty": 1.2, "min_chunk_length": 50, "condition_on_previous_chunks": true, "early_stop_threshold": 1}' Saved to directory: /home/infiniflow/Documents/development/ragflow/internal/s1_output.wav SUCCESS RAGFlow(user)> asr with 'transcribe-1@test@fishaudio' audio './internal/test.wav' param '{"language": "en", "ignore_timestamps": true}' +----------------------------------------------------------------------------------------------------------------------+ | text | +----------------------------------------------------------------------------------------------------------------------+ | The examination and testimony of the experts enabled the commission to conclude that five shots may have been fired. | +----------------------------------------------------------------------------------------------------------------------+ ``` ### Type of change - [x] Bug Fix (non-breaking change which fixes an issue) - [x] New Feature (non-breaking change which adds functionality) - [x] Refactoring
2026-05-14 18:58:00 +08:00
return nil, fmt.Errorf("audio file not provided")
}
payload := map[string]interface{}{
"provider_name": providerName,
"instance_name": instanceName,
"model_name": modelName,
Go: implement TTS for fishaudio, openrouter and asr for fishaudio (#14926) ### What problem does this PR solve? This PR implement TTS for FishAudio and MiniMax provider and ASR for FishAudio **The following functionalities are now supported:** **FishAudio:** - [x] Text To Speech - [x] Stream Text To Speech - [x] Audio To Text **OpenRouter:** - [x] Text To Speech **Verified examples from the CLI:** ```plaintext **FishAudio** RAGFlow(user)> tts with 's1@test@fishaudio' text 'He who desires but acts not, breeds pestilence.' play format 'wav' save './internal' param '{"reference_id": "90e65eaaf50e4470b8e6d43ee6afd7d5", "temperature": 0.7, "top_p": 0.7, "prosody": {"speed": 1, "volume": 0, "normalize_loudness": true}, "chunk_length": 300, "normalize": true, "sample_rate": 44100, "mp3_bitrate": 128, "latency": "normal", "max_new_tokens": 1024, "repetition_penalty": 1.2, "min_chunk_length": 50, "condition_on_previous_chunks": true, "early_stop_threshold": 1}' Saved to directory: /home/infiniflow/Documents/development/ragflow/internal/s1_output.wav SUCCESS RAGFlow(user)> stream tts with 's1@test@fishaudio' text 'He who desires but acts not, breeds pestilence.' play format 'wav' save './internal' param '{"reference_id": "90e65eaaf50e4470b8e6d43ee6afd7d5", "temperature": 0.7, "top_p": 0.7, "prosody": {"speed": 1, "volume": 0, "normalize_loudness": true}, "chunk_length": 300, "normalize": true, "sample_rate": 44100, "mp3_bitrate": 128, "latency": "normal", "max_new_tokens": 1024, "repetition_penalty": 1.2, "min_chunk_length": 50, "condition_on_previous_chunks": true, "early_stop_threshold": 1}' Saved to directory: /home/infiniflow/Documents/development/ragflow/internal/s1_output.wav SUCCESS RAGFlow(user)> asr with 'transcribe-1@test@fishaudio' audio './internal/test.wav' param '{"language": "en", "ignore_timestamps": true}' +----------------------------------------------------------------------------------------------------------------------+ | text | +----------------------------------------------------------------------------------------------------------------------+ | The examination and testimony of the experts enabled the commission to conclude that five shots may have been fired. | +----------------------------------------------------------------------------------------------------------------------+ ``` ### Type of change - [x] Bug Fix (non-breaking change which fixes an issue) - [x] New Feature (non-breaking change which adds functionality) - [x] Refactoring
2026-05-14 18:58:00 +08:00
"file": audioFile,
}
asrConfigPayload := make(map[string]interface{})
if paramStr, ok := cmd.Params["param_str"].(string); ok && paramStr != "" {
var dynamicParams map[string]interface{}
if err := json.Unmarshal([]byte(paramStr), &dynamicParams); err != nil {
return nil, fmt.Errorf("param string must be valid JSON. Error: %w", err)
}
asrConfigPayload["params"] = dynamicParams
}
if len(asrConfigPayload) > 0 {
payload["asr_config"] = asrConfigPayload
}
url := "/audio/transcriptions"
resp, err := c.HTTPClient.Request("POST", url, "web", nil, payload)
if err != nil {
return nil, fmt.Errorf("failed to ASR document: %w", err)
}
if resp.StatusCode != 200 {
return nil, fmt.Errorf("failed to ASR document: HTTP %d, body: %s", resp.StatusCode, string(resp.Body))
}
Go: implement TTS for fishaudio, openrouter and asr for fishaudio (#14926) ### What problem does this PR solve? This PR implement TTS for FishAudio and MiniMax provider and ASR for FishAudio **The following functionalities are now supported:** **FishAudio:** - [x] Text To Speech - [x] Stream Text To Speech - [x] Audio To Text **OpenRouter:** - [x] Text To Speech **Verified examples from the CLI:** ```plaintext **FishAudio** RAGFlow(user)> tts with 's1@test@fishaudio' text 'He who desires but acts not, breeds pestilence.' play format 'wav' save './internal' param '{"reference_id": "90e65eaaf50e4470b8e6d43ee6afd7d5", "temperature": 0.7, "top_p": 0.7, "prosody": {"speed": 1, "volume": 0, "normalize_loudness": true}, "chunk_length": 300, "normalize": true, "sample_rate": 44100, "mp3_bitrate": 128, "latency": "normal", "max_new_tokens": 1024, "repetition_penalty": 1.2, "min_chunk_length": 50, "condition_on_previous_chunks": true, "early_stop_threshold": 1}' Saved to directory: /home/infiniflow/Documents/development/ragflow/internal/s1_output.wav SUCCESS RAGFlow(user)> stream tts with 's1@test@fishaudio' text 'He who desires but acts not, breeds pestilence.' play format 'wav' save './internal' param '{"reference_id": "90e65eaaf50e4470b8e6d43ee6afd7d5", "temperature": 0.7, "top_p": 0.7, "prosody": {"speed": 1, "volume": 0, "normalize_loudness": true}, "chunk_length": 300, "normalize": true, "sample_rate": 44100, "mp3_bitrate": 128, "latency": "normal", "max_new_tokens": 1024, "repetition_penalty": 1.2, "min_chunk_length": 50, "condition_on_previous_chunks": true, "early_stop_threshold": 1}' Saved to directory: /home/infiniflow/Documents/development/ragflow/internal/s1_output.wav SUCCESS RAGFlow(user)> asr with 'transcribe-1@test@fishaudio' audio './internal/test.wav' param '{"language": "en", "ignore_timestamps": true}' +----------------------------------------------------------------------------------------------------------------------+ | text | +----------------------------------------------------------------------------------------------------------------------+ | The examination and testimony of the experts enabled the commission to conclude that five shots may have been fired. | +----------------------------------------------------------------------------------------------------------------------+ ``` ### Type of change - [x] Bug Fix (non-breaking change which fixes an issue) - [x] New Feature (non-breaking change which adds functionality) - [x] Refactoring
2026-05-14 18:58:00 +08:00
var rawResult struct {
Code int `json:"code"`
Message string `json:"message"`
Data map[string]interface{} `json:"data"`
}
if err = json.Unmarshal(resp.Body, &rawResult); err != nil {
return nil, fmt.Errorf("ASR document failed: invalid JSON (%w)", err)
}
Go: implement TTS for fishaudio, openrouter and asr for fishaudio (#14926) ### What problem does this PR solve? This PR implement TTS for FishAudio and MiniMax provider and ASR for FishAudio **The following functionalities are now supported:** **FishAudio:** - [x] Text To Speech - [x] Stream Text To Speech - [x] Audio To Text **OpenRouter:** - [x] Text To Speech **Verified examples from the CLI:** ```plaintext **FishAudio** RAGFlow(user)> tts with 's1@test@fishaudio' text 'He who desires but acts not, breeds pestilence.' play format 'wav' save './internal' param '{"reference_id": "90e65eaaf50e4470b8e6d43ee6afd7d5", "temperature": 0.7, "top_p": 0.7, "prosody": {"speed": 1, "volume": 0, "normalize_loudness": true}, "chunk_length": 300, "normalize": true, "sample_rate": 44100, "mp3_bitrate": 128, "latency": "normal", "max_new_tokens": 1024, "repetition_penalty": 1.2, "min_chunk_length": 50, "condition_on_previous_chunks": true, "early_stop_threshold": 1}' Saved to directory: /home/infiniflow/Documents/development/ragflow/internal/s1_output.wav SUCCESS RAGFlow(user)> stream tts with 's1@test@fishaudio' text 'He who desires but acts not, breeds pestilence.' play format 'wav' save './internal' param '{"reference_id": "90e65eaaf50e4470b8e6d43ee6afd7d5", "temperature": 0.7, "top_p": 0.7, "prosody": {"speed": 1, "volume": 0, "normalize_loudness": true}, "chunk_length": 300, "normalize": true, "sample_rate": 44100, "mp3_bitrate": 128, "latency": "normal", "max_new_tokens": 1024, "repetition_penalty": 1.2, "min_chunk_length": 50, "condition_on_previous_chunks": true, "early_stop_threshold": 1}' Saved to directory: /home/infiniflow/Documents/development/ragflow/internal/s1_output.wav SUCCESS RAGFlow(user)> asr with 'transcribe-1@test@fishaudio' audio './internal/test.wav' param '{"language": "en", "ignore_timestamps": true}' +----------------------------------------------------------------------------------------------------------------------+ | text | +----------------------------------------------------------------------------------------------------------------------+ | The examination and testimony of the experts enabled the commission to conclude that five shots may have been fired. | +----------------------------------------------------------------------------------------------------------------------+ ``` ### Type of change - [x] Bug Fix (non-breaking change which fixes an issue) - [x] New Feature (non-breaking change which adds functionality) - [x] Refactoring
2026-05-14 18:58:00 +08:00
if rawResult.Code != 0 {
return nil, fmt.Errorf("%s", rawResult.Message)
}
Go: implement TTS for fishaudio, openrouter and asr for fishaudio (#14926) ### What problem does this PR solve? This PR implement TTS for FishAudio and MiniMax provider and ASR for FishAudio **The following functionalities are now supported:** **FishAudio:** - [x] Text To Speech - [x] Stream Text To Speech - [x] Audio To Text **OpenRouter:** - [x] Text To Speech **Verified examples from the CLI:** ```plaintext **FishAudio** RAGFlow(user)> tts with 's1@test@fishaudio' text 'He who desires but acts not, breeds pestilence.' play format 'wav' save './internal' param '{"reference_id": "90e65eaaf50e4470b8e6d43ee6afd7d5", "temperature": 0.7, "top_p": 0.7, "prosody": {"speed": 1, "volume": 0, "normalize_loudness": true}, "chunk_length": 300, "normalize": true, "sample_rate": 44100, "mp3_bitrate": 128, "latency": "normal", "max_new_tokens": 1024, "repetition_penalty": 1.2, "min_chunk_length": 50, "condition_on_previous_chunks": true, "early_stop_threshold": 1}' Saved to directory: /home/infiniflow/Documents/development/ragflow/internal/s1_output.wav SUCCESS RAGFlow(user)> stream tts with 's1@test@fishaudio' text 'He who desires but acts not, breeds pestilence.' play format 'wav' save './internal' param '{"reference_id": "90e65eaaf50e4470b8e6d43ee6afd7d5", "temperature": 0.7, "top_p": 0.7, "prosody": {"speed": 1, "volume": 0, "normalize_loudness": true}, "chunk_length": 300, "normalize": true, "sample_rate": 44100, "mp3_bitrate": 128, "latency": "normal", "max_new_tokens": 1024, "repetition_penalty": 1.2, "min_chunk_length": 50, "condition_on_previous_chunks": true, "early_stop_threshold": 1}' Saved to directory: /home/infiniflow/Documents/development/ragflow/internal/s1_output.wav SUCCESS RAGFlow(user)> asr with 'transcribe-1@test@fishaudio' audio './internal/test.wav' param '{"language": "en", "ignore_timestamps": true}' +----------------------------------------------------------------------------------------------------------------------+ | text | +----------------------------------------------------------------------------------------------------------------------+ | The examination and testimony of the experts enabled the commission to conclude that five shots may have been fired. | +----------------------------------------------------------------------------------------------------------------------+ ``` ### Type of change - [x] Bug Fix (non-breaking change which fixes an issue) - [x] New Feature (non-breaking change which adds functionality) - [x] Refactoring
2026-05-14 18:58:00 +08:00
var result CommonResponse
result.Code = rawResult.Code
Go: implement TTS, ASR for Siliconflow and TTs for StepFun (#14944) ### What problem does this PR solve? This PRimplement TTS, ASR for Siliconflow and TTs for StepFun **The following functionalities are now supported:** **SiliConFlow:** - [x] Text To Speech - [x] Audio To Text - [x] Stream Audio To Text **StrepFun:** - [x] Audio To Text - [x] Stream Audio To Text **Verified examples from the CLI:** ```plaintext # SiliconFlow RAGFlow(user)> tts with 'FunAudioLLM/CosyVoice2-0.5B@test@Siliconflow' text 'hello? show yourself' play format 'wav' param '{"voice": "fnlp/MOSS-TTSD-v0.5:alex"}' SUCCESS RAGFlow(user)> asr with 'FunAudioLLM/SenseVoiceSmall@test@siliconflow' audio './internal/test.wav' param '' +----------------------------------------------------------------------------------------------------------------------+ | text | +----------------------------------------------------------------------------------------------------------------------+ | The examination and testimony of the experts enabled the commission to conclude that five shots may have been fired. | +----------------------------------------------------------------------------------------------------------------------+ RAGFlow(user)> stream asr with 'FunAudioLLM/SenseVoiceSmall@test@siliconflow' audio './internal/test.wav' param '' +----------------------------------------------------------------------------------------------------------------------+ | text | +----------------------------------------------------------------------------------------------------------------------+ | The examination and testimony of the experts enabled the commission to conclude that five shots may have been fired. | +----------------------------------------------------------------------------------------------------------------------+ ``` ### Type of change - [x] Bug Fix (non-breaking change which fixes an issue) - [x] New Feature (non-breaking change which adds functionality)
2026-05-15 14:03:33 +08:00
result.Data = []map[string]interface{}{
{"text": rawResult.Data["text"].(string)},
}
result.Duration = resp.Duration
return &result, nil
}
func (c *RAGFlowClient) OCRUserCommand(cmd *Command) (ResponseIf, error) {
if c.HTTPClient.APIToken == "" && c.HTTPClient.LoginToken == "" {
return nil, fmt.Errorf("API token not set. Please login first")
}
if c.ServerType != "user" {
return nil, fmt.Errorf("this command is only allowed in USER mode")
}
var providerName, instanceName, modelName string
// Check if composite_model_name is provided in command
if compositeModelName, ok := cmd.Params["composite_model_name"].(string); ok && compositeModelName != "" {
names := strings.Split(compositeModelName, "@")
if len(names) != 3 {
return nil, fmt.Errorf("model name must be in format 'model@instance@provider'")
}
providerName = names[2]
instanceName = names[1]
modelName = names[0]
} else if c.CurrentModel != nil {
// Use current model if set
providerName = c.CurrentModel.Provider
instanceName = c.CurrentModel.Instance
modelName = c.CurrentModel.Model
} else {
return nil, fmt.Errorf("model name not provided and no current model set. Use 'use model' command first")
}
var filename string
var fileURL string
var ok bool
var fileContent []byte
filename, ok = cmd.Params["file"].(string)
if ok {
// read file and convert to base64
var err error
fileContent, err = os.ReadFile(filename)
if err != nil {
return nil, fmt.Errorf("failed to read file: %w", err)
}
} else {
fileURL, ok = cmd.Params["url"].(string)
if !ok {
return nil, fmt.Errorf("file or url not provided")
}
}
payload := map[string]interface{}{
"provider_name": providerName,
"instance_name": instanceName,
"model_name": modelName,
}
if fileContent != nil {
payload["content"] = fileContent
} else {
payload["url"] = fileURL
}
url := "/file/ocr"
resp, err := c.HTTPClient.Request("POST", url, "web", nil, payload)
if err != nil {
return nil, fmt.Errorf("failed to OCR document: %w", err)
}
if resp.StatusCode != 200 {
return nil, fmt.Errorf("failed to OCR document: HTTP %d, body: %s", resp.StatusCode, string(resp.Body))
}
var result CommonDataResponse
if err = json.Unmarshal(resp.Body, &result); err != nil {
return nil, fmt.Errorf("OCR document failed: invalid JSON (%w)", err)
}
if result.Code != 0 {
return nil, fmt.Errorf("%s", result.Message)
}
result.Duration = resp.Duration
return &result, nil
}
Go: add file parse command (#14892) ### What problem does this PR solve? ``` RAGFlow(user)> ocr with 'hunyuanocr@test@gitee' file './picture.png' +----------------------------------------------------------+ | text | +----------------------------------------------------------+ | 生活不是等待风暴过去,而是学会在雨中翩翩起舞。 ——佚名 | +----------------------------------------------------------+ RAGFlow(user)> list 'test@gitee' tasks; +---------+----------------------------------+ | status | task_id | +---------+----------------------------------+ | success | C3FX4MQNKY5MGC6ZFMIXIAMJKHCEBQB5 | +---------+----------------------------------+ RAGFlow(user)> show 'test@gitee' task 'C3FX4MQNKY5MGC6ZFMIXIAMJKHCEBQB5'; +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+-------+ | content | index | +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+-------+ | # PDF 1: Purpose of RAGFlow RAGFlow is an open source Retrieval-Augmented Generation (RAG) engine designed to turn raw documents into reliable context for large language models.Its purpose is to make it practical to build an Al assistant that can ans... | 1 | +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+-------+ ``` ### Type of change - [x] New Feature (non-breaking change which adds functionality) --------- Signed-off-by: Jin Hai <haijin.chn@gmail.com>
2026-05-15 12:29:52 +08:00
func (c *RAGFlowClient) ParseFileUserCommand(cmd *Command) (ResponseIf, error) {
if c.HTTPClient.APIToken == "" && c.HTTPClient.LoginToken == "" {
return nil, fmt.Errorf("API token not set. Please login first")
}
if c.ServerType != "user" {
return nil, fmt.Errorf("this command is only allowed in USER mode")
}
var providerName, instanceName, modelName string
// Check if composite_model_name is provided in command
if compositeModelName, ok := cmd.Params["composite_model_name"].(string); ok && compositeModelName != "" {
names := strings.Split(compositeModelName, "@")
if len(names) != 3 {
return nil, fmt.Errorf("model name must be in format 'model@instance@provider'")
}
providerName = names[2]
instanceName = names[1]
modelName = names[0]
} else if c.CurrentModel != nil {
// Use current model if set
providerName = c.CurrentModel.Provider
instanceName = c.CurrentModel.Instance
modelName = c.CurrentModel.Model
} else {
return nil, fmt.Errorf("model name not provided and no current model set. Use 'use model' command first")
}
var filename string
var fileURL string
var ok bool
var fileContent []byte
filename, ok = cmd.Params["file"].(string)
if ok {
Go: implement provider: MinerU (#14990) ### What problem does this PR solve? Implement MinerU Provider **The following functionalities are now supported:** **MinerU** ---- - [x] Parse file - [x] Show task - [ ] ~~List tasks~~ **Verified examples from the CLI:** ```plaintext RAGFlow(user)> parse with 'vlm@test@mineru' file 'https://arxiv.org/pdf/2505.09358' +--------------------------------------+ | task_id | +--------------------------------------+ | 142ac8ea-d9d0-4a68-a2d1-d3af67635dc9 | +--------------------------------------+ RAGFlow(user)> show 'test@mineru' task '142ac8ea-d9d0-4a68-a2d1-d3af67635dc9' +--------------------------------------------+-------+ | content | index | +--------------------------------------------+-------+ | Task is running... Progress: 17 / 18 pages | 0 | +--------------------------------------------+-------+ RAGFlow(user)> show 'test@mineru' task '142ac8ea-d9d0-4a68-a2d1-d3af67635dc9' +--------------------------------------------------------------------------------------------+-------+ | content | index | +--------------------------------------------------------------------------------------------+-------+ | https://cdn-mineru.openxlab.org.cn/pdf/2026-05-18/142ac8ea-d9d0-4a68-a2d1-d3af67635dc9.zip | 0 | +--------------------------------------------------------------------------------------------+-------+ ``` ### Type of change - [x] Bug Fix (non-breaking change which fixes an issue) - [x] New Feature (non-breaking change which adds functionality) - [x] Refactoring
2026-05-19 10:49:33 +08:00
// For online file
if strings.HasPrefix(filename, "http://") || strings.HasPrefix(filename, "https://") {
fileURL = filename
} else {
// read file and convert to base64
var err error
fileContent, err = os.ReadFile(filename)
if err != nil {
return nil, fmt.Errorf("failed to read file: %w", err)
}
Go: add file parse command (#14892) ### What problem does this PR solve? ``` RAGFlow(user)> ocr with 'hunyuanocr@test@gitee' file './picture.png' +----------------------------------------------------------+ | text | +----------------------------------------------------------+ | 生活不是等待风暴过去,而是学会在雨中翩翩起舞。 ——佚名 | +----------------------------------------------------------+ RAGFlow(user)> list 'test@gitee' tasks; +---------+----------------------------------+ | status | task_id | +---------+----------------------------------+ | success | C3FX4MQNKY5MGC6ZFMIXIAMJKHCEBQB5 | +---------+----------------------------------+ RAGFlow(user)> show 'test@gitee' task 'C3FX4MQNKY5MGC6ZFMIXIAMJKHCEBQB5'; +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+-------+ | content | index | +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+-------+ | # PDF 1: Purpose of RAGFlow RAGFlow is an open source Retrieval-Augmented Generation (RAG) engine designed to turn raw documents into reliable context for large language models.Its purpose is to make it practical to build an Al assistant that can ans... | 1 | +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+-------+ ``` ### Type of change - [x] New Feature (non-breaking change which adds functionality) --------- Signed-off-by: Jin Hai <haijin.chn@gmail.com>
2026-05-15 12:29:52 +08:00
}
} else {
fileURL, ok = cmd.Params["url"].(string)
if !ok {
return nil, fmt.Errorf("file or url not provided")
}
}
payload := map[string]interface{}{
"provider_name": providerName,
"instance_name": instanceName,
"model_name": modelName,
}
if fileContent != nil {
payload["content"] = fileContent
} else {
payload["url"] = fileURL
}
url := "/file/parse"
resp, err := c.HTTPClient.Request("POST", url, "web", nil, payload)
if err != nil {
return nil, fmt.Errorf("failed to PARSE document: %w", err)
}
if resp.StatusCode != 200 {
return nil, fmt.Errorf("failed to PARSE document: HTTP %d, body: %s", resp.StatusCode, string(resp.Body))
}
var result CommonDataResponse
if err = json.Unmarshal(resp.Body, &result); err != nil {
return nil, fmt.Errorf("PARSE document failed: invalid JSON (%w)", err)
}
if result.Code != 0 {
return nil, fmt.Errorf("%s", result.Message)
}
result.Duration = resp.Duration
return &result, nil
}
func (c *RAGFlowClient) ListTasksUserCommand(cmd *Command) (ResponseIf, error) {
if c.HTTPClient.APIToken == "" && c.HTTPClient.LoginToken == "" {
return nil, fmt.Errorf("API token not set. Please login first")
}
if c.ServerType != "user" {
return nil, fmt.Errorf("this command is only allowed in USER mode")
}
var providerName, instanceName string
// Check if composite_instance_name is provided in command
if compositeModelName, ok := cmd.Params["composite_instance_name"].(string); ok && compositeModelName != "" {
names := strings.Split(compositeModelName, "@")
if len(names) != 2 {
return nil, fmt.Errorf("model name must be in format 'instance@provider'")
}
providerName = names[1]
instanceName = names[0]
} else {
return nil, fmt.Errorf("no provider name or instance name")
}
url := fmt.Sprintf("/providers/%s/instances/%s/tasks", providerName, instanceName)
resp, err := c.HTTPClient.Request("GET", url, "web", nil, nil)
if err != nil {
return nil, fmt.Errorf("failed to list tasks: %w", err)
}
if resp.StatusCode != 200 {
return nil, fmt.Errorf("failed to list tasks: HTTP %d, body: %s", resp.StatusCode, string(resp.Body))
}
var result CommonResponse
if err = json.Unmarshal(resp.Body, &result); err != nil {
return nil, fmt.Errorf("list tasks failed: invalid JSON (%w)", err)
}
if result.Code != 0 {
return nil, fmt.Errorf("%s", result.Message)
}
result.Duration = resp.Duration
return &result, nil
}
func (c *RAGFlowClient) ShowTaskUserCommand(cmd *Command) (ResponseIf, error) {
if c.HTTPClient.APIToken == "" && c.HTTPClient.LoginToken == "" {
return nil, fmt.Errorf("API token not set. Please login first")
}
if c.ServerType != "user" {
return nil, fmt.Errorf("this command is only allowed in USER mode")
}
var providerName, instanceName string
// Check if composite_instance_name is provided in command
if compositeModelName, ok := cmd.Params["composite_instance_name"].(string); ok && compositeModelName != "" {
names := strings.Split(compositeModelName, "@")
if len(names) != 2 {
return nil, fmt.Errorf("model name must be in format 'instance@provider'")
}
providerName = names[1]
instanceName = names[0]
} else {
return nil, fmt.Errorf("no provider name or instance name")
}
taskID, ok := cmd.Params["task_id"].(string)
if !ok {
return nil, fmt.Errorf("task id not provided")
}
url := fmt.Sprintf("/providers/%s/instances/%s/tasks/%s", providerName, instanceName, taskID)
resp, err := c.HTTPClient.Request("GET", url, "web", nil, nil)
if err != nil {
return nil, fmt.Errorf("failed to get task: %w", err)
}
if resp.StatusCode != 200 {
return nil, fmt.Errorf("failed to get task: HTTP %d, body: %s", resp.StatusCode, string(resp.Body))
}
var result TaskResponse
if err = json.Unmarshal(resp.Body, &result); err != nil {
return nil, fmt.Errorf("get task failed: invalid JSON (%w)", err)
}
if result.Code != 0 {
return nil, fmt.Errorf("%s", result.Message)
}
result.Duration = resp.Duration
return &result, nil
}
func (c *RAGFlowClient) CheckProviderConnection(cmd *Command) (ResponseIf, error) {
if c.HTTPClient.APIToken == "" && c.HTTPClient.LoginToken == "" {
return nil, fmt.Errorf("API token not set. Please login first")
}
if c.ServerType != "user" {
return nil, fmt.Errorf("this command is only allowed in USER mode")
}
instanceName, ok := cmd.Params["instance_name"].(string)
if !ok {
return nil, fmt.Errorf("instance name not provided")
}
providerName, ok := cmd.Params["provider_name"].(string)
if !ok {
return nil, fmt.Errorf("provider name not provided")
}
url := fmt.Sprintf("/providers/%s/instances/%s/connection", providerName, instanceName)
resp, err := c.HTTPClient.Request("GET", url, "web", nil, nil)
if err != nil {
return nil, fmt.Errorf("failed to check provider connection: %w", err)
}
if resp.StatusCode != 200 {
return nil, fmt.Errorf("failed to check provider connection: HTTP %d, body: %s", resp.StatusCode, string(resp.Body))
}
var result SimpleResponse
if err = json.Unmarshal(resp.Body, &result); err != nil {
return nil, fmt.Errorf("check provider connection failed: invalid JSON (%w)", err)
}
if result.Code != 0 {
return nil, fmt.Errorf("%s", result.Message)
}
result.Duration = resp.Duration
return &result, nil
}
// UseModel sets the current model for chat
func (c *RAGFlowClient) UseModel(cmd *Command) (ResponseIf, error) {
if c.HTTPClient.APIToken == "" && c.HTTPClient.LoginToken == "" {
return nil, fmt.Errorf("API token not set. Please login first")
}
if c.ServerType != "user" {
return nil, fmt.Errorf("this command is only allowed in USER mode")
}
compositeModelName, ok := cmd.Params["composite_model_name"].(string)
if !ok || compositeModelName == "" {
return nil, fmt.Errorf("model identifier not provided")
}
names := strings.Split(compositeModelName, "@")
if len(names) != 3 {
return nil, fmt.Errorf("model identifier must be in format 'model@instance@provider'")
}
c.CurrentModel = &CurrentModel{
Provider: names[2],
Instance: names[1],
Model: names[0],
}
var result SimpleResponse
result.Code = 0
result.Message = fmt.Sprintf("Current model set to: %s/%s/%s", c.CurrentModel.Provider, c.CurrentModel.Instance, c.CurrentModel.Model)
return &result, nil
}
// ShowCurrentModel displays the current model configuration
func (c *RAGFlowClient) ShowCurrentModel(cmd *Command) (ResponseIf, error) {
if c.ServerType != "user" {
return nil, fmt.Errorf("this command is only allowed in USER mode")
}
if c.CurrentModel == nil {
return nil, fmt.Errorf("no current model set. Use 'use model' command first")
}
var result CommonResponse
result.Code = 0
result.Data = []map[string]interface{}{
{
"provider": c.CurrentModel.Provider,
"instance": c.CurrentModel.Instance,
"model": c.CurrentModel.Model,
},
}
return &result, nil
}
func (c *RAGFlowClient) AddCustomModel(cmd *Command) (ResponseIf, error) {
if c.HTTPClient.APIToken == "" && c.HTTPClient.LoginToken == "" {
return nil, fmt.Errorf("API token not set. Please login first")
}
if c.ServerType != "user" {
return nil, fmt.Errorf("this command is only allowed in USER mode")
}
providerName, ok := cmd.Params["provider_name"].(string)
if !ok {
return nil, fmt.Errorf("provider name not provided")
}
instanceName, ok := cmd.Params["instance_name"].(string)
if !ok {
return nil, fmt.Errorf("instance name not provided")
}
modelName, ok := cmd.Params["model_name"].(string)
if !ok {
return nil, fmt.Errorf("model name not provided")
}
// chat, vision, embedding, rerank, tts, asr, ocr
modelTypes, ok := cmd.Params["model_types"].([]string)
if !ok {
return nil, fmt.Errorf("model type not provided")
}
maxTokens, ok := cmd.Params["max_tokens"].(int)
if !ok {
return nil, fmt.Errorf("max tokens not provided")
}
url := fmt.Sprintf("/providers/%s/instances/%s/models", providerName, instanceName)
payload := map[string]interface{}{
"provider_name": providerName,
"instance_name": instanceName,
"model_name": modelName,
"model_types": modelTypes,
"max_tokens": maxTokens,
}
supportThink, ok := cmd.Params["support_think"].(bool)
if ok {
payload["thinking"] = supportThink
}
resp, err := c.HTTPClient.Request("POST", url, "web", nil, payload)
if err != nil {
return nil, fmt.Errorf("failed to add custom model: %w", err)
}
if resp.StatusCode != 200 {
return nil, fmt.Errorf("failed to add custom model: HTTP %d, body: %s", resp.StatusCode, string(resp.Body))
}
var result SimpleResponse
if err = json.Unmarshal(resp.Body, &result); err != nil {
return nil, fmt.Errorf("add custom model failed: invalid JSON (%w)", err)
}
if result.Code != 0 {
return nil, fmt.Errorf("%s", result.Message)
}
result.Duration = resp.Duration
return &result, nil
}
Refactor Go server model provider reading and access (#13831) ### What problem does this PR solve? 1. Refactor model provider json file format 2. Use memory data structure to replace database 3. Add CLI command to access ``` RAGFlow(user)> list pool models from 'xai'; +-------------------------------------------------------------------------------------+------------+-------------+-----------------------+ | features | max_tokens | model_types | name | +-------------------------------------------------------------------------------------+------------+-------------+-----------------------+ | map[] | 256000 | [llm] | grok-4 | | map[] | 131072 | [llm] | grok-3 | | map[] | 131072 | [llm] | grok-3-fast | | map[] | 131072 | [llm] | grok-3-mini | | map[] | 131072 | [llm] | grok-3-mini-mini-fast | | map[multimodal:map[enabled:true input_modalities:[image] output_modalities:[text]]] | 32768 | [vlm] | grok-2-vision | +-------------------------------------------------------------------------------------+------------+-------------+-----------------------+ RAGFlow(user)> show pool model 'grok-2-vision' from 'xai'; +-------------------------------------------------------------------------------------+------------+-------------+---------------+ | features | max_tokens | model_types | name | +-------------------------------------------------------------------------------------+------------+-------------+---------------+ | map[multimodal:map[enabled:true input_modalities:[image] output_modalities:[text]]] | 32768 | [vlm] | grok-2-vision | +-------------------------------------------------------------------------------------+------------+-------------+---------------+ RAGFlow(user)> list pool providers; +--------+------------------------------------------------------------+---------------------------+ | name | tags | url | +--------+------------------------------------------------------------+---------------------------+ | OpenAI | LLM,TEXT EMBEDDING,TTS,TEXT RE-RANK,SPEECH2TEXT,MODERATION | https://api.openai.com/v1 | | xAI | LLM | https://api.x.ai/v1 | +--------+------------------------------------------------------------+---------------------------+ RAGFlow(user)> show pool provider 'openai'; +---------------------------+--------+------------------------------------------------------------+--------------+ | base_url | name | tags | total_models | +---------------------------+--------+------------------------------------------------------------+--------------+ | https://api.openai.com/v1 | OpenAI | LLM,TEXT EMBEDDING,TTS,TEXT RE-RANK,SPEECH2TEXT,MODERATION | 27 | +---------------------------+--------+------------------------------------------------------------+--------------+ ``` ### Type of change - [x] New Feature (non-breaking change which adds functionality) - [x] Refactoring --------- Signed-off-by: Jin Hai <haijin.chn@gmail.com>
2026-03-30 12:00:49 +08:00
// Context related commands
// CECat handles the cat command - shows content using Context Engine
func (c *RAGFlowClient) CECat(cmd *Command) (ResponseIf, error) {
if c.HTTPClient.APIToken == "" && c.HTTPClient.LoginToken == "" {
return nil, fmt.Errorf("API token not set. Please login first")
}
if c.ServerType != "user" {
return nil, fmt.Errorf("this command is only allowed in USER mode")
}
path, ok := cmd.Params["path"].(string)
if !ok {
return nil, fmt.Errorf("fail to convert 'path' to string")
}
// Execute cat command through Filesystem Engine
ctx := context.Background()
content, err := c.ContextEngine.Cat(ctx, path)
if err != nil {
return nil, err
}
// Convert to response
var response ContextCatResponse
response.OutputFormat = c.OutputFormat
response.Code = 0
response.Content = string(content)
return &response, nil
}
Refactor Go server model provider reading and access (#13831) ### What problem does this PR solve? 1. Refactor model provider json file format 2. Use memory data structure to replace database 3. Add CLI command to access ``` RAGFlow(user)> list pool models from 'xai'; +-------------------------------------------------------------------------------------+------------+-------------+-----------------------+ | features | max_tokens | model_types | name | +-------------------------------------------------------------------------------------+------------+-------------+-----------------------+ | map[] | 256000 | [llm] | grok-4 | | map[] | 131072 | [llm] | grok-3 | | map[] | 131072 | [llm] | grok-3-fast | | map[] | 131072 | [llm] | grok-3-mini | | map[] | 131072 | [llm] | grok-3-mini-mini-fast | | map[multimodal:map[enabled:true input_modalities:[image] output_modalities:[text]]] | 32768 | [vlm] | grok-2-vision | +-------------------------------------------------------------------------------------+------------+-------------+-----------------------+ RAGFlow(user)> show pool model 'grok-2-vision' from 'xai'; +-------------------------------------------------------------------------------------+------------+-------------+---------------+ | features | max_tokens | model_types | name | +-------------------------------------------------------------------------------------+------------+-------------+---------------+ | map[multimodal:map[enabled:true input_modalities:[image] output_modalities:[text]]] | 32768 | [vlm] | grok-2-vision | +-------------------------------------------------------------------------------------+------------+-------------+---------------+ RAGFlow(user)> list pool providers; +--------+------------------------------------------------------------+---------------------------+ | name | tags | url | +--------+------------------------------------------------------------+---------------------------+ | OpenAI | LLM,TEXT EMBEDDING,TTS,TEXT RE-RANK,SPEECH2TEXT,MODERATION | https://api.openai.com/v1 | | xAI | LLM | https://api.x.ai/v1 | +--------+------------------------------------------------------------+---------------------------+ RAGFlow(user)> show pool provider 'openai'; +---------------------------+--------+------------------------------------------------------------+--------------+ | base_url | name | tags | total_models | +---------------------------+--------+------------------------------------------------------------+--------------+ | https://api.openai.com/v1 | OpenAI | LLM,TEXT EMBEDDING,TTS,TEXT RE-RANK,SPEECH2TEXT,MODERATION | 27 | +---------------------------+--------+------------------------------------------------------------+--------------+ ``` ### Type of change - [x] New Feature (non-breaking change which adds functionality) - [x] Refactoring --------- Signed-off-by: Jin Hai <haijin.chn@gmail.com>
2026-03-30 12:00:49 +08:00
// CEList handles the ls command - lists nodes using Context Engine
func (c *RAGFlowClient) CEList(cmd *Command) (ResponseIf, error) {
// Get path from command params, default to "datasets"
path, _ := cmd.Params["path"].(string)
if path == "" {
path = "datasets"
}
// Parse options
opts := &ce.ListOptions{}
if recursive, ok := cmd.Params["recursive"].(bool); ok {
opts.Recursive = recursive
}
if limit, ok := cmd.Params["limit"].(int); ok {
opts.Limit = limit
}
if offset, ok := cmd.Params["offset"].(int); ok {
opts.Offset = offset
}
// Execute list command through Filesystem Engine
Refactor Go server model provider reading and access (#13831) ### What problem does this PR solve? 1. Refactor model provider json file format 2. Use memory data structure to replace database 3. Add CLI command to access ``` RAGFlow(user)> list pool models from 'xai'; +-------------------------------------------------------------------------------------+------------+-------------+-----------------------+ | features | max_tokens | model_types | name | +-------------------------------------------------------------------------------------+------------+-------------+-----------------------+ | map[] | 256000 | [llm] | grok-4 | | map[] | 131072 | [llm] | grok-3 | | map[] | 131072 | [llm] | grok-3-fast | | map[] | 131072 | [llm] | grok-3-mini | | map[] | 131072 | [llm] | grok-3-mini-mini-fast | | map[multimodal:map[enabled:true input_modalities:[image] output_modalities:[text]]] | 32768 | [vlm] | grok-2-vision | +-------------------------------------------------------------------------------------+------------+-------------+-----------------------+ RAGFlow(user)> show pool model 'grok-2-vision' from 'xai'; +-------------------------------------------------------------------------------------+------------+-------------+---------------+ | features | max_tokens | model_types | name | +-------------------------------------------------------------------------------------+------------+-------------+---------------+ | map[multimodal:map[enabled:true input_modalities:[image] output_modalities:[text]]] | 32768 | [vlm] | grok-2-vision | +-------------------------------------------------------------------------------------+------------+-------------+---------------+ RAGFlow(user)> list pool providers; +--------+------------------------------------------------------------+---------------------------+ | name | tags | url | +--------+------------------------------------------------------------+---------------------------+ | OpenAI | LLM,TEXT EMBEDDING,TTS,TEXT RE-RANK,SPEECH2TEXT,MODERATION | https://api.openai.com/v1 | | xAI | LLM | https://api.x.ai/v1 | +--------+------------------------------------------------------------+---------------------------+ RAGFlow(user)> show pool provider 'openai'; +---------------------------+--------+------------------------------------------------------------+--------------+ | base_url | name | tags | total_models | +---------------------------+--------+------------------------------------------------------------+--------------+ | https://api.openai.com/v1 | OpenAI | LLM,TEXT EMBEDDING,TTS,TEXT RE-RANK,SPEECH2TEXT,MODERATION | 27 | +---------------------------+--------+------------------------------------------------------------+--------------+ ``` ### Type of change - [x] New Feature (non-breaking change which adds functionality) - [x] Refactoring --------- Signed-off-by: Jin Hai <haijin.chn@gmail.com>
2026-03-30 12:00:49 +08:00
ctx := context.Background()
result, err := c.ContextEngine.List(ctx, path, opts)
if err != nil {
return nil, err
}
// Convert to response
var response ContextListResponse
response.OutputFormat = c.OutputFormat
Refactor Go server model provider reading and access (#13831) ### What problem does this PR solve? 1. Refactor model provider json file format 2. Use memory data structure to replace database 3. Add CLI command to access ``` RAGFlow(user)> list pool models from 'xai'; +-------------------------------------------------------------------------------------+------------+-------------+-----------------------+ | features | max_tokens | model_types | name | +-------------------------------------------------------------------------------------+------------+-------------+-----------------------+ | map[] | 256000 | [llm] | grok-4 | | map[] | 131072 | [llm] | grok-3 | | map[] | 131072 | [llm] | grok-3-fast | | map[] | 131072 | [llm] | grok-3-mini | | map[] | 131072 | [llm] | grok-3-mini-mini-fast | | map[multimodal:map[enabled:true input_modalities:[image] output_modalities:[text]]] | 32768 | [vlm] | grok-2-vision | +-------------------------------------------------------------------------------------+------------+-------------+-----------------------+ RAGFlow(user)> show pool model 'grok-2-vision' from 'xai'; +-------------------------------------------------------------------------------------+------------+-------------+---------------+ | features | max_tokens | model_types | name | +-------------------------------------------------------------------------------------+------------+-------------+---------------+ | map[multimodal:map[enabled:true input_modalities:[image] output_modalities:[text]]] | 32768 | [vlm] | grok-2-vision | +-------------------------------------------------------------------------------------+------------+-------------+---------------+ RAGFlow(user)> list pool providers; +--------+------------------------------------------------------------+---------------------------+ | name | tags | url | +--------+------------------------------------------------------------+---------------------------+ | OpenAI | LLM,TEXT EMBEDDING,TTS,TEXT RE-RANK,SPEECH2TEXT,MODERATION | https://api.openai.com/v1 | | xAI | LLM | https://api.x.ai/v1 | +--------+------------------------------------------------------------+---------------------------+ RAGFlow(user)> show pool provider 'openai'; +---------------------------+--------+------------------------------------------------------------+--------------+ | base_url | name | tags | total_models | +---------------------------+--------+------------------------------------------------------------+--------------+ | https://api.openai.com/v1 | OpenAI | LLM,TEXT EMBEDDING,TTS,TEXT RE-RANK,SPEECH2TEXT,MODERATION | 27 | +---------------------------+--------+------------------------------------------------------------+--------------+ ``` ### Type of change - [x] New Feature (non-breaking change which adds functionality) - [x] Refactoring --------- Signed-off-by: Jin Hai <haijin.chn@gmail.com>
2026-03-30 12:00:49 +08:00
response.Code = 0
response.Data = ce.FormatNodes(result.Nodes, string(c.OutputFormat))
return &response, nil
}
// CESearch handles the search command using Context Engine
func (c *RAGFlowClient) CESearch(cmd *Command) (ResponseIf, error) {
// Get path and query from command params
path, _ := cmd.Params["path"].(string)
if path == "" {
path = "datasets"
}
query, _ := cmd.Params["query"].(string)
// Parse options
opts := &ce.SearchOptions{
Query: query,
}
if limit, ok := cmd.Params["limit"].(int); ok {
opts.Limit = limit
}
if offset, ok := cmd.Params["offset"].(int); ok {
opts.Offset = offset
}
if recursive, ok := cmd.Params["recursive"].(bool); ok {
opts.Recursive = recursive
}
// Execute search command through Filesystem Engine
Refactor Go server model provider reading and access (#13831) ### What problem does this PR solve? 1. Refactor model provider json file format 2. Use memory data structure to replace database 3. Add CLI command to access ``` RAGFlow(user)> list pool models from 'xai'; +-------------------------------------------------------------------------------------+------------+-------------+-----------------------+ | features | max_tokens | model_types | name | +-------------------------------------------------------------------------------------+------------+-------------+-----------------------+ | map[] | 256000 | [llm] | grok-4 | | map[] | 131072 | [llm] | grok-3 | | map[] | 131072 | [llm] | grok-3-fast | | map[] | 131072 | [llm] | grok-3-mini | | map[] | 131072 | [llm] | grok-3-mini-mini-fast | | map[multimodal:map[enabled:true input_modalities:[image] output_modalities:[text]]] | 32768 | [vlm] | grok-2-vision | +-------------------------------------------------------------------------------------+------------+-------------+-----------------------+ RAGFlow(user)> show pool model 'grok-2-vision' from 'xai'; +-------------------------------------------------------------------------------------+------------+-------------+---------------+ | features | max_tokens | model_types | name | +-------------------------------------------------------------------------------------+------------+-------------+---------------+ | map[multimodal:map[enabled:true input_modalities:[image] output_modalities:[text]]] | 32768 | [vlm] | grok-2-vision | +-------------------------------------------------------------------------------------+------------+-------------+---------------+ RAGFlow(user)> list pool providers; +--------+------------------------------------------------------------+---------------------------+ | name | tags | url | +--------+------------------------------------------------------------+---------------------------+ | OpenAI | LLM,TEXT EMBEDDING,TTS,TEXT RE-RANK,SPEECH2TEXT,MODERATION | https://api.openai.com/v1 | | xAI | LLM | https://api.x.ai/v1 | +--------+------------------------------------------------------------+---------------------------+ RAGFlow(user)> show pool provider 'openai'; +---------------------------+--------+------------------------------------------------------------+--------------+ | base_url | name | tags | total_models | +---------------------------+--------+------------------------------------------------------------+--------------+ | https://api.openai.com/v1 | OpenAI | LLM,TEXT EMBEDDING,TTS,TEXT RE-RANK,SPEECH2TEXT,MODERATION | 27 | +---------------------------+--------+------------------------------------------------------------+--------------+ ``` ### Type of change - [x] New Feature (non-breaking change which adds functionality) - [x] Refactoring --------- Signed-off-by: Jin Hai <haijin.chn@gmail.com>
2026-03-30 12:00:49 +08:00
ctx := context.Background()
result, err := c.ContextEngine.Search(ctx, path, opts)
if err != nil {
return nil, err
}
// Convert to response
var response ContextSearchResponse
response.OutputFormat = c.OutputFormat
Refactor Go server model provider reading and access (#13831) ### What problem does this PR solve? 1. Refactor model provider json file format 2. Use memory data structure to replace database 3. Add CLI command to access ``` RAGFlow(user)> list pool models from 'xai'; +-------------------------------------------------------------------------------------+------------+-------------+-----------------------+ | features | max_tokens | model_types | name | +-------------------------------------------------------------------------------------+------------+-------------+-----------------------+ | map[] | 256000 | [llm] | grok-4 | | map[] | 131072 | [llm] | grok-3 | | map[] | 131072 | [llm] | grok-3-fast | | map[] | 131072 | [llm] | grok-3-mini | | map[] | 131072 | [llm] | grok-3-mini-mini-fast | | map[multimodal:map[enabled:true input_modalities:[image] output_modalities:[text]]] | 32768 | [vlm] | grok-2-vision | +-------------------------------------------------------------------------------------+------------+-------------+-----------------------+ RAGFlow(user)> show pool model 'grok-2-vision' from 'xai'; +-------------------------------------------------------------------------------------+------------+-------------+---------------+ | features | max_tokens | model_types | name | +-------------------------------------------------------------------------------------+------------+-------------+---------------+ | map[multimodal:map[enabled:true input_modalities:[image] output_modalities:[text]]] | 32768 | [vlm] | grok-2-vision | +-------------------------------------------------------------------------------------+------------+-------------+---------------+ RAGFlow(user)> list pool providers; +--------+------------------------------------------------------------+---------------------------+ | name | tags | url | +--------+------------------------------------------------------------+---------------------------+ | OpenAI | LLM,TEXT EMBEDDING,TTS,TEXT RE-RANK,SPEECH2TEXT,MODERATION | https://api.openai.com/v1 | | xAI | LLM | https://api.x.ai/v1 | +--------+------------------------------------------------------------+---------------------------+ RAGFlow(user)> show pool provider 'openai'; +---------------------------+--------+------------------------------------------------------------+--------------+ | base_url | name | tags | total_models | +---------------------------+--------+------------------------------------------------------------+--------------+ | https://api.openai.com/v1 | OpenAI | LLM,TEXT EMBEDDING,TTS,TEXT RE-RANK,SPEECH2TEXT,MODERATION | 27 | +---------------------------+--------+------------------------------------------------------------+--------------+ ``` ### Type of change - [x] New Feature (non-breaking change which adds functionality) - [x] Refactoring --------- Signed-off-by: Jin Hai <haijin.chn@gmail.com>
2026-03-30 12:00:49 +08:00
response.Code = 0
response.Total = result.Total
response.Data = ce.FormatNodes(result.Nodes, string(c.OutputFormat))
return &response, nil
}
// InsertDatasetFromFile inserts dataset chunks from a JSON file
func (c *RAGFlowClient) InsertDatasetFromFile(cmd *Command) (ResponseIf, error) {
if c.ServerType != "user" {
return nil, fmt.Errorf("this command is only allowed in USER mode")
}
filePath, ok := cmd.Params["file_path"].(string)
if !ok {
return nil, fmt.Errorf("file_path not provided")
}
payload := map[string]interface{}{
"file_path": filePath,
}
resp, err := c.HTTPClient.Request("POST", "/kb/insert_from_file", "web", nil, payload)
if err != nil {
return nil, fmt.Errorf("failed to insert dataset from file: %w", err)
}
if resp.StatusCode != 200 {
return nil, fmt.Errorf("failed to insert dataset from file: HTTP %d, body: %s", resp.StatusCode, string(resp.Body))
}
resJSON, err := resp.JSON()
if err != nil {
return nil, fmt.Errorf("invalid JSON response: %w", err)
}
code, ok := resJSON["code"].(float64)
if !ok {
return nil, fmt.Errorf("invalid response format: code is not a number")
}
var result SimpleResponse
result.Code = int(code)
if result.Code == 0 {
result.Message = fmt.Sprintf("Success to insert dataset from file: %s", filePath)
} else {
result.Message = fmt.Sprintf("Failed to insert dataset from file: %v", resJSON)
}
result.Duration = 0
return &result, nil
}
// InsertMetadataFromFile inserts metadata from a JSON file
func (c *RAGFlowClient) InsertMetadataFromFile(cmd *Command) (ResponseIf, error) {
if c.ServerType != "user" {
return nil, fmt.Errorf("this command is only allowed in USER mode")
}
filePath, ok := cmd.Params["file_path"].(string)
if !ok {
return nil, fmt.Errorf("file_path not provided")
}
payload := map[string]interface{}{
"file_path": filePath,
}
resp, err := c.HTTPClient.Request("POST", "/tenant/insert_metadata_from_file", "web", nil, payload)
if err != nil {
return nil, fmt.Errorf("failed to insert metadata from file: %w", err)
}
if resp.StatusCode != 200 {
return nil, fmt.Errorf("failed to insert metadata from file: HTTP %d, body: %s", resp.StatusCode, string(resp.Body))
}
resJSON, err := resp.JSON()
if err != nil {
return nil, fmt.Errorf("invalid JSON response: %w", err)
}
code, ok := resJSON["code"].(float64)
if !ok {
return nil, fmt.Errorf("invalid response format: code is not a number")
}
var result SimpleResponse
result.Code = int(code)
if result.Code == 0 {
result.Message = fmt.Sprintf("Success to insert metadata from file: %s", filePath)
} else {
result.Message = fmt.Sprintf("Failed to insert metadata from file: %v", resJSON)
}
result.Duration = 0
return &result, nil
}
// UpdateChunk updates a chunk in a dataset
func (c *RAGFlowClient) UpdateChunk(cmd *Command) (ResponseIf, error) {
if c.ServerType != "user" {
return nil, fmt.Errorf("this command is only allowed in USER mode")
}
chunkID, ok := cmd.Params["chunk_id"].(string)
if !ok {
return nil, fmt.Errorf("chunk_id not provided")
}
datasetName, ok := cmd.Params["dataset_name"].(string)
if !ok {
return nil, fmt.Errorf("dataset_name not provided")
}
jsonBody, ok := cmd.Params["json_body"].(string)
if !ok {
return nil, fmt.Errorf("json_body not provided")
}
// Look up dataset_id from dataset_name
datasetID, err := c.getDatasetID(datasetName)
if err != nil {
return nil, fmt.Errorf("failed to get dataset ID: %w", err)
}
// Try to get doc_id from the chunk retrieval endpoint
getResp, err := c.HTTPClient.Request("GET", "/chunk/get?chunk_id="+chunkID, "web", nil, nil)
if err != nil {
return nil, fmt.Errorf("failed to get chunk info: %w", err)
}
var docID string
if getResp.StatusCode == 200 {
getJSON, err := getResp.JSON()
if err == nil {
if data, ok := getJSON["data"].(map[string]interface{}); ok {
if d, ok := data["doc_id"].(string); ok {
docID = d
}
}
}
}
if docID == "" {
return nil, fmt.Errorf("could not find document_id for chunk %s. Please provide document_id explicitly", chunkID)
}
// Parse the JSON body
var payload map[string]interface{}
if err := json.Unmarshal([]byte(jsonBody), &payload); err != nil {
return nil, fmt.Errorf("invalid JSON body: %w", err)
}
// Add IDs to payload
payload["dataset_id"] = datasetID
payload["document_id"] = docID
payload["chunk_id"] = chunkID
resp, err := c.HTTPClient.Request("POST", "/chunk/update", "web", nil, payload)
if err != nil {
return nil, fmt.Errorf("failed to update chunk: %w", err)
}
if resp.StatusCode != 200 {
return nil, fmt.Errorf("failed to update chunk: HTTP %d, body: %s", resp.StatusCode, string(resp.Body))
}
resJSON, err := resp.JSON()
if err != nil {
return nil, fmt.Errorf("invalid JSON response: %w", err)
}
code, ok := resJSON["code"].(float64)
if !ok {
return nil, fmt.Errorf("invalid response format: code is not a number")
}
var result SimpleResponse
result.Code = int(code)
if result.Code == 0 {
result.Message = fmt.Sprintf("Success to update chunk: %s", chunkID)
} else {
result.Message = fmt.Sprintf("Failed to update chunk: %v", resJSON)
}
result.Duration = 0
return &result, nil
}
// GetChunk retrieves a chunk by ID
func (c *RAGFlowClient) GetChunk(cmd *Command) (ResponseIf, error) {
if c.ServerType != "user" {
return nil, fmt.Errorf("this command is only allowed in USER mode")
}
chunkID, ok := cmd.Params["chunk_id"].(string)
if !ok {
return nil, fmt.Errorf("chunk_id not provided")
}
datasetName, ok := cmd.Params["dataset_name"].(string)
if !ok {
return nil, fmt.Errorf("dataset_name not provided")
}
datasetID, err := c.getDatasetID(datasetName)
if err != nil {
return nil, err
}
docID, ok := cmd.Params["doc_id"].(string)
if !ok {
return nil, fmt.Errorf("doc_id not provided")
}
resp, err := c.HTTPClient.Request("GET", fmt.Sprintf("/datasets/%s/documents/%s/chunks/%s", datasetID, docID, chunkID), "web", nil, nil)
if err != nil {
return nil, fmt.Errorf("failed to get chunk: %w", err)
}
if resp.StatusCode != 200 {
return nil, fmt.Errorf("failed to get chunk: HTTP %d, body: %s", resp.StatusCode, string(resp.Body))
}
var result ChunkResponse
if err = json.Unmarshal(resp.Body, &result); err != nil {
return nil, fmt.Errorf("get chunk failed: invalid JSON (%w)", err)
}
if result.Code != 0 {
return nil, fmt.Errorf("%s", result.Message)
}
result.Duration = resp.Duration
return &result, nil
}
// SetMeta sets metadata for a document
func (c *RAGFlowClient) SetMeta(cmd *Command) (ResponseIf, error) {
if c.ServerType != "user" {
return nil, fmt.Errorf("this command is only allowed in USER mode")
}
docID, ok := cmd.Params["doc_id"].(string)
if !ok {
return nil, fmt.Errorf("doc_id not provided")
}
metaJSON, ok := cmd.Params["meta"].(string)
if !ok {
return nil, fmt.Errorf("meta not provided")
}
payload := map[string]interface{}{
"doc_id": docID,
"meta": metaJSON,
}
resp, err := c.HTTPClient.Request("POST", "/document/set_meta", "web", nil, payload)
if err != nil {
return nil, fmt.Errorf("failed to set metadata: %w", err)
}
if resp.StatusCode != 200 {
return nil, fmt.Errorf("failed to set metadata: HTTP %d, body: %s", resp.StatusCode, string(resp.Body))
}
resJSON, err := resp.JSON()
if err != nil {
return nil, fmt.Errorf("invalid JSON response: %w", err)
}
code, ok := resJSON["code"].(float64)
if !ok {
return nil, fmt.Errorf("invalid response format: code is not a number")
}
var result SimpleResponse
result.Code = int(code)
if result.Code == 0 {
result.Message = fmt.Sprintf("Success to set metadata for document: %s", docID)
} else {
result.Message = fmt.Sprintf("Failed to set metadata: %v", resJSON)
}
result.Duration = 0
return &result, nil
}
// RmTags removes tags from chunks in a dataset
func (c *RAGFlowClient) RmTags(cmd *Command) (ResponseIf, error) {
if c.ServerType != "user" {
return nil, fmt.Errorf("this command is only allowed in USER mode")
}
datasetName, ok := cmd.Params["dataset_name"].(string)
if !ok {
return nil, fmt.Errorf("dataset_name not provided")
}
kbID, err := c.getDatasetID(datasetName)
if err != nil {
return nil, err
}
tags, ok := cmd.Params["tags"].([]string)
if !ok {
return nil, fmt.Errorf("tags not provided")
}
payload := map[string]interface{}{
"tags": tags,
}
resp, err := c.HTTPClient.Request("DELETE", "/datasets/"+kbID+"/tags", "web", nil, payload)
if err != nil {
return nil, fmt.Errorf("failed to remove tags: %w", err)
}
if resp.StatusCode != 200 {
return nil, fmt.Errorf("failed to remove tags: HTTP %d, body: %s", resp.StatusCode, string(resp.Body))
}
resJSON, err := resp.JSON()
if err != nil {
return nil, fmt.Errorf("invalid JSON response: %w", err)
}
code, ok := resJSON["code"].(float64)
if !ok {
return nil, fmt.Errorf("invalid response format: code is not a number")
}
var result SimpleResponse
result.Code = int(code)
if result.Code == 0 {
result.Message = fmt.Sprintf("Success to remove tags from dataset: %s", kbID)
} else {
result.Message = fmt.Sprintf("Failed to remove tags: %v", resJSON)
}
result.Duration = 0
return &result, nil
}
// RemoveChunks removes chunks from a document
func (c *RAGFlowClient) RemoveChunks(cmd *Command) (ResponseIf, error) {
if c.ServerType != "user" {
return nil, fmt.Errorf("this command is only allowed in USER mode")
}
docID, ok := cmd.Params["doc_id"].(string)
if !ok {
return nil, fmt.Errorf("doc_id not provided")
}
payload := map[string]interface{}{
"doc_id": docID,
}
// Check if delete_all is set
if deleteAll, ok := cmd.Params["delete_all"].(bool); ok && deleteAll {
payload["delete_all"] = true
} else if chunkIDs, ok := cmd.Params["chunk_ids"].([]string); ok {
payload["chunk_ids"] = chunkIDs
}
resp, err := c.HTTPClient.Request("POST", "/chunk/rm", "web", nil, payload)
if err != nil {
return nil, fmt.Errorf("failed to remove chunks: %w", err)
}
if resp.StatusCode != 200 {
return nil, fmt.Errorf("failed to remove chunks: HTTP %d, body: %s", resp.StatusCode, string(resp.Body))
}
resJSON, err := resp.JSON()
if err != nil {
return nil, fmt.Errorf("invalid JSON response: %w", err)
}
code, ok := resJSON["code"].(float64)
if !ok {
return nil, fmt.Errorf("invalid response format: code is not a number")
}
var result SimpleResponse
result.Code = int(code)
if result.Code == 0 {
deletedCount := int64(0)
switch data := resJSON["data"].(type) {
case float64:
deletedCount = int64(data)
case map[string]interface{}:
if count, ok := data["deleted_count"].(float64); ok {
deletedCount = int64(count)
}
}
result.Message = fmt.Sprintf("Success to remove chunks from document %s: %d chunks deleted", docID, deletedCount)
} else {
result.Message = fmt.Sprintf("Failed to remove chunks: %v", resJSON)
}
result.Duration = 0
return &result, nil
}
// formatRequestError Uniformly handle and format network errors in HTTP requests
func formatRequestError(action string, err error) error {
if err == nil {
return nil
}
var netErr net.Error
switch {
case errors.Is(err, io.EOF), errors.Is(err, io.ErrUnexpectedEOF):
return fmt.Errorf("%s failed - connection closed (EOF): upstream overloaded or proxy timeout: %w", action, err)
case errors.As(err, &netErr) && netErr.Timeout():
return fmt.Errorf("%s failed - request timeout: server took too long to respond: %w", action, err)
default:
return fmt.Errorf("%s failed: %w", action, err)
}
}