Files
ragflow/internal/service/related_question.go

214 lines
5.6 KiB
Go

//
// Copyright 2026 The InfiniFlow Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
package service
import (
"encoding/json"
"fmt"
"regexp"
"strings"
"ragflow/internal/entity"
modelModule "ragflow/internal/entity/models"
)
// GenerateRelatedQuestions generates related search questions for chat/searchbot endpoints.
func GenerateRelatedQuestions(tenantID, question, searchID string, searchSvc *SearchService, tenantSvc *TenantService, modelProviderSvc *ModelProviderService) ([]string, error) {
if modelProviderSvc == nil {
return nil, fmt.Errorf("model provider service not configured")
}
searchConfig := relatedQuestionsSearchConfig(searchID, searchSvc)
modelID := relatedQuestionsModelID(tenantID, searchConfig, tenantSvc)
prompt, err := LoadPrompt("related_question")
if err != nil {
return nil, err
}
messages := []modelModule.Message{
{Role: "system", Content: prompt},
{Role: "user", Content: "\nKeywords: " + question + "\nRelated search terms:\n "},
}
response, err := modelProviderSvc.Chat(tenantID, modelID, messages, relatedQuestionsConfig(searchConfig))
if err != nil {
return nil, err
}
if response != nil && response.Answer != nil {
return parseRelatedQuestions(*response.Answer), nil
}
return []string{}, nil
}
func relatedQuestionsSearchConfig(searchID string, searchSvc *SearchService) map[string]interface{} {
if searchID == "" || searchSvc == nil {
return map[string]interface{}{}
}
if detail, err := searchSvc.GetDetail(searchID); err == nil && detail != nil {
return relatedQuestionsSearchConfigFromDetail(detail)
}
return map[string]interface{}{}
}
func relatedQuestionsSearchConfigFromDetail(detail map[string]interface{}) map[string]interface{} {
if sc, ok := detail["search_config"].(map[string]interface{}); ok && sc != nil {
return sc
}
if sc, ok := detail["search_config"].(entity.JSONMap); ok && sc != nil {
return map[string]interface{}(sc)
}
return map[string]interface{}{}
}
func relatedQuestionsModelID(tenantID string, searchConfig map[string]interface{}, tenantSvc *TenantService) string {
modelID, _ := searchConfig["chat_id"].(string)
if modelID != "" || tenantSvc == nil {
return modelID
}
defaultModel, err := tenantSvc.GetDefaultModelName(tenantID, entity.ModelTypeChat)
if err == nil {
modelID = defaultModel
}
return modelID
}
func relatedQuestionsConfig(searchConfig map[string]interface{}) *modelModule.ChatConfig {
var genConf map[string]interface{}
switch v := searchConfig["llm_setting"].(type) {
case map[string]interface{}:
genConf = v
case entity.JSONMap:
genConf = map[string]interface{}(v)
}
if genConf == nil {
return &modelModule.ChatConfig{Temperature: float64Ptr(0.9)}
}
cfg := &modelModule.ChatConfig{}
for key, value := range genConf {
if key == "parameter" {
continue
}
switch key {
case "stream":
if v, ok := value.(bool); ok {
cfg.Stream = &v
}
case "thinking":
if v, ok := value.(bool); ok {
cfg.Thinking = &v
}
case "max_tokens":
if v, ok := intFromRelatedQuestionConfig(value); ok {
cfg.MaxTokens = &v
}
case "temperature":
if v, ok := floatFromRelatedQuestionConfig(value); ok {
cfg.Temperature = &v
}
case "top_p":
if v, ok := floatFromRelatedQuestionConfig(value); ok && v > 0 {
cfg.TopP = &v
}
case "do_sample":
if v, ok := value.(bool); ok {
cfg.DoSample = &v
}
case "stop":
if stops := stringSliceFromRelatedQuestionConfig(value); len(stops) > 0 {
cfg.Stop = &stops
}
case "model_class":
if v, ok := value.(string); ok {
cfg.ModelClass = &v
}
case "effort":
if v, ok := value.(string); ok {
cfg.Effort = &v
}
case "verbosity":
if v, ok := value.(string); ok {
cfg.Verbosity = &v
}
}
}
return cfg
}
func intFromRelatedQuestionConfig(value interface{}) (int, bool) {
switch v := value.(type) {
case int:
return v, true
case int64:
return int(v), true
case float64:
return int(v), true
case json.Number:
if n, err := v.Int64(); err == nil {
return int(n), true
}
}
return 0, false
}
func floatFromRelatedQuestionConfig(value interface{}) (float64, bool) {
switch v := value.(type) {
case float64:
return v, true
case float32:
return float64(v), true
case int:
return float64(v), true
case int64:
return float64(v), true
case json.Number:
if n, err := v.Float64(); err == nil {
return n, true
}
}
return 0, false
}
func stringSliceFromRelatedQuestionConfig(value interface{}) []string {
switch v := value.(type) {
case []string:
return v
case []interface{}:
result := make([]string, 0, len(v))
for _, item := range v {
if s, ok := item.(string); ok {
result = append(result, s)
}
}
return result
}
return nil
}
var relatedQuestionLineRe = regexp.MustCompile(`^\d+\.\s`)
func parseRelatedQuestions(text string) []string {
var result []string
for _, line := range strings.Split(text, "\n") {
if relatedQuestionLineRe.MatchString(line) {
result = append(result, relatedQuestionLineRe.ReplaceAllString(line, ""))
}
}
if result == nil {
return []string{}
}
return result
}
func float64Ptr(v float64) *float64 { return &v }