2026-03-04 19:17:16 +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 service
import (
"encoding/json"
"errors"
"fmt"
2026-05-06 10:41:58 +08:00
"ragflow/internal/common"
2026-03-04 19:17:16 +08:00
"strings"
"time"
2026-04-29 14:44:12 +08:00
"go.uber.org/zap"
2026-03-04 19:17:16 +08:00
"ragflow/internal/dao"
2026-03-27 19:25:18 +08:00
"ragflow/internal/entity"
2026-04-29 14:44:12 +08:00
modelModule "ragflow/internal/entity/models"
2026-03-04 19:17:16 +08:00
)
// ChatSessionService chat session (conversation) service
type ChatSessionService struct {
2026-04-29 14:44:12 +08:00
chatSessionDAO * dao . ChatSessionDAO
chatDAO * dao . ChatDAO
userTenantDAO * dao . UserTenantDAO
modelProviderSvc * ModelProviderService
2026-03-04 19:17:16 +08:00
}
// NewChatSessionService create chat session service
func NewChatSessionService ( ) * ChatSessionService {
return & ChatSessionService {
2026-04-29 14:44:12 +08:00
chatSessionDAO : dao . NewChatSessionDAO ( ) ,
chatDAO : dao . NewChatDAO ( ) ,
userTenantDAO : dao . NewUserTenantDAO ( ) ,
modelProviderSvc : NewModelProviderService ( ) ,
2026-03-04 19:17:16 +08:00
}
}
// SetChatSessionRequest set chat session request
type SetChatSessionRequest struct {
SessionID string ` json:"conversation_id,omitempty" `
DialogID string ` json:"dialog_id,omitempty" `
Name string ` json:"name,omitempty" `
IsNew bool ` json:"is_new" `
}
// SetChatSessionResponse set chat session response
type SetChatSessionResponse struct {
2026-03-27 19:25:18 +08:00
* entity . ChatSession
2026-03-04 19:17:16 +08:00
}
// SetChatSession create or update a chat session
func ( s * ChatSessionService ) SetChatSession ( userID string , req * SetChatSessionRequest ) ( * SetChatSessionResponse , error ) {
name := req . Name
if name == "" {
name = "New chat session"
}
// Limit name length to 255 characters
if len ( name ) > 255 {
name = name [ : 255 ]
}
if ! req . IsNew {
// Update existing chat session
updates := map [ string ] interface { } {
2026-05-25 14:00:08 +08:00
"name" : name ,
"user_id" : userID ,
2026-03-04 19:17:16 +08:00
}
if err := s . chatSessionDAO . UpdateByID ( req . SessionID , updates ) ; err != nil {
return nil , errors . New ( "Chat session not found" )
}
// Get updated chat session
session , err := s . chatSessionDAO . GetByID ( req . SessionID )
if err != nil {
return nil , errors . New ( "Fail to update a chat session" )
}
return & SetChatSessionResponse { ChatSession : session } , nil
}
// Create new chat session
// Check if dialog exists
dialog , err := s . chatSessionDAO . GetDialogByID ( req . DialogID )
if err != nil {
return nil , errors . New ( "Dialog not found" )
}
// Generate UUID for new chat session
2026-05-25 14:00:08 +08:00
newID := common . GenerateUUID ( )
2026-03-04 19:17:16 +08:00
// Get prologue from dialog's prompt_config
prologue := "Hi! I'm your assistant. What can I do for you?"
if dialog . PromptConfig != nil {
if p , ok := dialog . PromptConfig [ "prologue" ] . ( string ) ; ok && p != "" {
prologue = p
}
}
// Create initial message - store as JSON object with messages array
messagesObj := map [ string ] interface { } {
"messages" : [ ] map [ string ] interface { } {
{
"role" : "assistant" ,
"content" : prologue ,
} ,
} ,
}
messagesJSON , _ := json . Marshal ( messagesObj )
// Create reference - store as JSON array
referenceJSON , _ := json . Marshal ( [ ] interface { } { } )
// Create chat session
2026-03-27 19:25:18 +08:00
session := & entity . ChatSession {
2026-03-04 19:17:16 +08:00
ID : newID ,
DialogID : req . DialogID ,
Name : & name ,
Message : messagesJSON ,
UserID : & userID ,
Reference : referenceJSON ,
}
if err := s . chatSessionDAO . Create ( session ) ; err != nil {
return nil , errors . New ( "Fail to create a chat session" )
}
return & SetChatSessionResponse { ChatSession : session } , nil
}
// RemoveChatSessionRequest remove chat sessions request
type RemoveChatSessionRequest struct {
ChatSessions [ ] string ` json:"conversation_ids" binding:"required" `
}
// RemoveChatSessions removes chat sessions (hard delete)
func ( s * ChatSessionService ) RemoveChatSessions ( userID string , chatSessions [ ] string ) error {
// Get user's tenants
tenantIDs , err := s . userTenantDAO . GetTenantIDsByUserID ( userID )
if err != nil {
return err
}
// Build a set of user's tenant IDs for quick lookup
tenantIDSet := make ( map [ string ] bool )
for _ , tid := range tenantIDs {
tenantIDSet [ tid ] = true
}
tenantIDSet [ userID ] = true
// Check each chat session
for _ , convID := range chatSessions {
// Get the chat session
session , err := s . chatSessionDAO . GetByID ( convID )
if err != nil {
return fmt . Errorf ( "Chat session not found: %s" , convID )
}
// Check if user is the owner by checking dialog ownership
isOwner := false
for tenantID := range tenantIDSet {
exists , err := s . chatSessionDAO . CheckDialogExists ( tenantID , session . DialogID )
if err != nil {
return err
}
if exists {
isOwner = true
break
}
}
if ! isOwner {
return errors . New ( "Only owner of chat session authorized for this operation" )
}
// Delete the chat session
if err := s . chatSessionDAO . DeleteByID ( convID ) ; err != nil {
return err
}
}
return nil
}
// ListChatSessionsRequest list chat sessions request
type ListChatSessionsRequest struct {
DialogID string ` json:"dialog_id" binding:"required" `
}
// ListChatSessionsResponse list chat sessions response
type ListChatSessionsResponse struct {
2026-03-27 19:25:18 +08:00
Sessions [ ] * entity . ChatSession
2026-03-04 19:17:16 +08:00
}
// ListChatSessions lists chat sessions for a dialog
2026-05-07 17:14:22 +08:00
func ( s * ChatSessionService ) ListChatSessions ( userID string , chatID string ) ( * ListChatSessionsResponse , error ) {
2026-03-04 19:17:16 +08:00
// Get user's tenants
tenantIDs , err := s . userTenantDAO . GetTenantIDsByUserID ( userID )
if err != nil {
return nil , err
}
// Check if user is the owner of the dialog
isOwner := false
for _ , tenantID := range tenantIDs {
2026-05-07 17:14:22 +08:00
var exists bool
exists , err = s . chatSessionDAO . CheckDialogExists ( tenantID , chatID )
2026-03-04 19:17:16 +08:00
if err != nil {
return nil , err
}
if exists {
isOwner = true
break
}
}
// Also check with userID as tenant
if ! isOwner {
2026-05-07 17:14:22 +08:00
var exists bool
exists , err = s . chatSessionDAO . CheckDialogExists ( userID , chatID )
2026-03-04 19:17:16 +08:00
if err != nil {
return nil , err
}
isOwner = exists
}
if ! isOwner {
2026-05-07 17:14:22 +08:00
return nil , errors . New ( "only owner of dialog authorized for this operation" )
2026-03-04 19:17:16 +08:00
}
// List chat sessions
2026-05-07 17:14:22 +08:00
sessions , err := s . chatSessionDAO . ListByChatID ( chatID )
2026-03-04 19:17:16 +08:00
if err != nil {
return nil , err
}
return & ListChatSessionsResponse { Sessions : sessions } , nil
}
// Completion performs chat completion with full RAG support
func ( s * ChatSessionService ) Completion ( userID string , conversationID string , messages [ ] map [ string ] interface { } , llmID string , chatModelConfig map [ string ] interface { } , messageID string ) ( map [ string ] interface { } , error ) {
// Validate the last message is from user
if len ( messages ) == 0 {
return nil , errors . New ( "messages cannot be empty" )
}
lastRole , _ := messages [ len ( messages ) - 1 ] [ "role" ] . ( string )
if lastRole != "user" {
return nil , errors . New ( "the last content of this conversation is not from user" )
}
// Get conversation
session , err := s . chatSessionDAO . GetByID ( conversationID )
if err != nil {
return nil , errors . New ( "Conversation not found" )
}
// Get dialog
dialog , err := s . chatSessionDAO . GetDialogByID ( session . DialogID )
if err != nil {
return nil , errors . New ( "Dialog not found" )
}
// Deep copy messages to session
sessionMessages := s . buildSessionMessages ( session , messages )
// Initialize reference if empty
reference := s . initializeReference ( session )
// Check if custom LLM is specified and validate API key
isEmbedded := llmID != ""
if llmID != "" {
hasKey , err := s . checkTenantLLMAPIKey ( dialog . TenantID , llmID )
if err != nil || ! hasKey {
return nil , fmt . Errorf ( "Cannot use specified model %s" , llmID )
}
dialog . LLMID = llmID
if chatModelConfig != nil {
dialog . LLMSetting = chatModelConfig
}
}
// Perform chat completion with RAG
result , err := s . asyncChat ( dialog , session , messages , chatModelConfig , messageID , reference , false )
if err != nil {
return nil , err
}
// Update conversation if not embedded
if ! isEmbedded {
s . updateSessionMessages ( session , sessionMessages , reference )
}
return result , nil
}
// CompletionStream performs streaming chat completion with full RAG support
func ( s * ChatSessionService ) CompletionStream ( userID string , conversationID string , messages [ ] map [ string ] interface { } , llmID string , chatModelConfig map [ string ] interface { } , messageID string , streamChan chan <- string ) error {
// Validate the last message is from user
if len ( messages ) == 0 {
streamChan <- fmt . Sprintf ( "data: %s\n\n" , ` { "code": 500, "message": "messages cannot be empty", "data": { "answer": "**ERROR**: messages cannot be empty", "reference": []}} ` )
return errors . New ( "messages cannot be empty" )
}
lastRole , _ := messages [ len ( messages ) - 1 ] [ "role" ] . ( string )
if lastRole != "user" {
streamChan <- fmt . Sprintf ( "data: %s\n\n" , ` { "code": 500, "message": "the last content of this conversation is not from user", "data": { "answer": "**ERROR**: the last content of this conversation is not from user", "reference": []}} ` )
return errors . New ( "the last content of this conversation is not from user" )
}
// Get conversation
session , err := s . chatSessionDAO . GetByID ( conversationID )
if err != nil {
streamChan <- fmt . Sprintf ( "data: %s\n\n" , ` { "code": 500, "message": "Conversation not found", "data": { "answer": "**ERROR**: Conversation not found", "reference": []}} ` )
return errors . New ( "Conversation not found" )
}
// Get dialog
dialog , err := s . chatSessionDAO . GetDialogByID ( session . DialogID )
if err != nil {
streamChan <- fmt . Sprintf ( "data: %s\n\n" , ` { "code": 500, "message": "Dialog not found", "data": { "answer": "**ERROR**: Dialog not found", "reference": []}} ` )
return errors . New ( "Dialog not found" )
}
// Deep copy messages to session
sessionMessages := s . buildSessionMessages ( session , messages )
// Initialize reference if empty
reference := s . initializeReference ( session )
// Check if custom LLM is specified and validate API key
isEmbedded := llmID != ""
if llmID != "" {
hasKey , err := s . checkTenantLLMAPIKey ( dialog . TenantID , llmID )
if err != nil || ! hasKey {
errMsg := fmt . Sprintf ( ` { "code": 500, "message": "Cannot use specified model %s", "data": { "answer": "**ERROR**: Cannot use specified model", "reference": []}} ` , llmID )
streamChan <- fmt . Sprintf ( "data: %s\n\n" , errMsg )
return fmt . Errorf ( "Cannot use specified model %s" , llmID )
}
dialog . LLMID = llmID
if chatModelConfig != nil {
dialog . LLMSetting = chatModelConfig
}
}
// Perform streaming chat completion with RAG
resultChan , err := s . asyncChatStream ( dialog , session , messages , chatModelConfig , messageID , reference )
if err != nil {
streamChan <- fmt . Sprintf ( "data: %s\n\n" , fmt . Sprintf ( ` { "code": 500, "message": "%s", "data": { "answer": "**ERROR**: %s", "reference": []}} ` , err . Error ( ) , err . Error ( ) ) )
return err
}
// Stream results
for result := range resultChan {
data , _ := json . Marshal ( map [ string ] interface { } {
"code" : 0 ,
"message" : "" ,
"data" : result ,
} )
streamChan <- fmt . Sprintf ( "data: %s\n\n" , string ( data ) )
}
// Send final completion signal
finalData , _ := json . Marshal ( map [ string ] interface { } {
"code" : 0 ,
"message" : "" ,
"data" : true ,
} )
streamChan <- fmt . Sprintf ( "data: %s\n\n" , string ( finalData ) )
// Update conversation if not embedded
if ! isEmbedded {
s . updateSessionMessages ( session , sessionMessages , reference )
}
return nil
}
// Helper methods
2026-03-27 19:25:18 +08:00
func ( s * ChatSessionService ) buildSessionMessages ( session * entity . ChatSession , messages [ ] map [ string ] interface { } ) [ ] map [ string ] interface { } {
2026-03-04 19:17:16 +08:00
// Deep copy messages to session
sessionMessages := make ( [ ] map [ string ] interface { } , len ( messages ) )
for i , msg := range messages {
sessionMessages [ i ] = make ( map [ string ] interface { } )
for k , v := range msg {
sessionMessages [ i ] [ k ] = v
}
}
return sessionMessages
}
2026-03-27 19:25:18 +08:00
func ( s * ChatSessionService ) initializeReference ( session * entity . ChatSession ) [ ] interface { } {
2026-03-04 19:17:16 +08:00
var reference [ ] interface { }
if len ( session . Reference ) > 0 {
json . Unmarshal ( session . Reference , & reference )
}
// Filter out nil entries and append new reference
var filtered [ ] interface { }
for _ , r := range reference {
if r != nil {
filtered = append ( filtered , r )
}
}
filtered = append ( filtered , map [ string ] interface { } {
"chunks" : [ ] interface { } { } ,
"doc_aggs" : [ ] interface { } { } ,
} )
return filtered
}
func ( s * ChatSessionService ) checkTenantLLMAPIKey ( tenantID , modelName string ) ( bool , error ) {
// Simplified check - in real implementation, check if tenant has API key for this model
return true , nil
}
2026-03-27 19:25:18 +08:00
func ( s * ChatSessionService ) structureAnswer ( session * entity . ChatSession , answer string , messageID , conversationID string , reference [ ] interface { } ) map [ string ] interface { } {
2026-03-04 19:17:16 +08:00
return map [ string ] interface { } {
"answer" : answer ,
"reference" : reference ,
"conversation_id" : conversationID ,
"message_id" : messageID ,
}
}
2026-03-27 19:25:18 +08:00
func ( s * ChatSessionService ) updateSessionMessages ( session * entity . ChatSession , messages [ ] map [ string ] interface { } , reference [ ] interface { } ) {
2026-03-04 19:17:16 +08:00
// Update session with new messages and reference
messagesJSON , _ := json . Marshal ( map [ string ] interface { } {
"messages" : messages ,
} )
referenceJSON , _ := json . Marshal ( reference )
updates := map [ string ] interface { } {
2026-05-25 14:00:08 +08:00
"message" : messagesJSON ,
"reference" : referenceJSON ,
2026-03-04 19:17:16 +08:00
}
s . chatSessionDAO . UpdateByID ( session . ID , updates )
}
// asyncChat performs chat with RAG support (non-streaming)
2026-03-27 19:25:18 +08:00
func ( s * ChatSessionService ) asyncChat ( dialog * entity . Chat , session * entity . ChatSession , messages [ ] map [ string ] interface { } , config map [ string ] interface { } , messageID string , reference [ ] interface { } , stream bool ) ( map [ string ] interface { } , error ) {
2026-03-04 19:17:16 +08:00
// Check if we need RAG (knowledge base or tavily)
hasKB := len ( dialog . KBIDs ) > 0
hasTavily := false
if dialog . PromptConfig != nil {
if tavilyKey , ok := dialog . PromptConfig [ "tavily_api_key" ] . ( string ) ; ok && tavilyKey != "" {
hasTavily = true
}
}
if ! hasKB && ! hasTavily {
// Simple chat without RAG
return s . asyncChatSolo ( dialog , session , messages , config , messageID , reference , stream )
}
// TODO: Full RAG implementation with knowledge base retrieval
// This would include:
// 1. Get embedding model and rerank model
// 2. Extract questions from messages
// 3. Retrieve chunks from knowledge bases
// 4. Rerank chunks
// 5. Build prompt with context
// 6. Call LLM
// For now, fall back to solo chat
return s . asyncChatSolo ( dialog , session , messages , config , messageID , reference , stream )
}
// asyncChatStream performs streaming chat with RAG support
2026-03-27 19:25:18 +08:00
func ( s * ChatSessionService ) asyncChatStream ( dialog * entity . Chat , session * entity . ChatSession , messages [ ] map [ string ] interface { } , config map [ string ] interface { } , messageID string , reference [ ] interface { } ) ( <- chan map [ string ] interface { } , error ) {
2026-03-04 19:17:16 +08:00
resultChan := make ( chan map [ string ] interface { } )
go func ( ) {
defer close ( resultChan )
// Check if we need RAG
hasKB := len ( dialog . KBIDs ) > 0
hasTavily := false
if dialog . PromptConfig != nil {
if tavilyKey , ok := dialog . PromptConfig [ "tavily_api_key" ] . ( string ) ; ok && tavilyKey != "" {
hasTavily = true
}
}
if ! hasKB && ! hasTavily {
// Simple chat without RAG
s . asyncChatSoloStream ( dialog , session , messages , config , messageID , reference , resultChan )
return
}
// TODO: Full RAG streaming implementation
// For now, fall back to solo chat
s . asyncChatSoloStream ( dialog , session , messages , config , messageID , reference , resultChan )
} ( )
return resultChan , nil
}
// asyncChatSolo performs simple chat without RAG (non-streaming)
2026-03-27 19:25:18 +08:00
func ( s * ChatSessionService ) asyncChatSolo ( dialog * entity . Chat , session * entity . ChatSession , messages [ ] map [ string ] interface { } , config map [ string ] interface { } , messageID string , reference [ ] interface { } , stream bool ) ( map [ string ] interface { } , error ) {
2026-05-06 10:41:58 +08:00
common . Info ( "asyncChatSolo started" ,
2026-04-29 14:44:12 +08:00
zap . String ( "tenant_id" , dialog . TenantID ) ,
zap . String ( "llm_id" , dialog . LLMID ) ,
zap . String ( "dialog_id" , dialog . ID ) ,
zap . Int ( "message_count" , len ( messages ) ) )
2026-03-04 19:17:16 +08:00
// Get system prompt
systemPrompt := s . buildSystemPrompt ( dialog )
// Process messages - handle attachments and image files
processedMessages := s . processMessages ( messages , dialog )
2026-06-08 11:49:37 +08:00
var (
driver modelModule . ModelDriver
modelName string
apiConfig * modelModule . APIConfig
err error
)
if dialog . LLMID != "" {
driver , modelName , apiConfig , _ , err = s . modelProviderSvc . GetModelConfigFromProviderInstance (
dialog . TenantID , entity . ModelTypeChat , dialog . LLMID ,
)
} else {
driver , modelName , apiConfig , _ , err = s . modelProviderSvc . GetTenantDefaultModelByType (
dialog . TenantID , entity . ModelTypeChat ,
)
}
2026-03-04 19:17:16 +08:00
if err != nil {
2026-05-06 10:41:58 +08:00
common . Error ( "asyncChatSolo failed to get chat model" , err )
2026-03-04 19:17:16 +08:00
return nil , err
}
2026-06-08 11:49:37 +08:00
chatModel := modelModule . NewChatModel ( driver , & modelName , apiConfig )
2026-03-04 19:17:16 +08:00
2026-04-29 14:44:12 +08:00
// Convert messages to Message format
var msgs [ ] modelModule . Message
if systemPrompt != "" {
msgs = append ( msgs , modelModule . Message { Role : "system" , Content : systemPrompt } )
}
for _ , msg := range processedMessages {
role , _ := msg [ "role" ] . ( string )
2026-04-30 15:25:01 +08:00
if role == "" || role == "system" {
continue
}
if msg [ "content" ] != nil {
msgs = append ( msgs , modelModule . Message { Role : role , Content : msg [ "content" ] } )
2026-04-29 14:44:12 +08:00
}
}
// Get ChatConfig directly from dialog and config
chatConfig := s . buildChatConfig ( dialog , config )
2026-03-04 19:17:16 +08:00
// Perform chat
2026-04-30 19:33:57 +08:00
response , err := chatModel . ModelDriver . ChatWithMessages ( * chatModel . ModelName , msgs , chatModel . APIConfig , chatConfig )
2026-03-04 19:17:16 +08:00
if err != nil {
2026-05-06 10:41:58 +08:00
common . Error ( "asyncChatSolo chat failed" , err )
2026-03-04 19:17:16 +08:00
return nil , err
}
2026-05-06 10:41:58 +08:00
common . Info ( "asyncChatSolo completed" ,
2026-04-29 14:44:12 +08:00
zap . String ( "tenant_id" , dialog . TenantID ) ,
zap . String ( "llm_id" , dialog . LLMID ) ,
2026-04-30 15:25:01 +08:00
zap . Int ( "response_length" , len ( * response . Answer ) ) )
2026-04-29 14:44:12 +08:00
2026-03-04 19:17:16 +08:00
// Structure the answer
ans := map [ string ] interface { } {
2026-04-30 15:25:01 +08:00
"answer" : * response . Answer ,
2026-03-04 19:17:16 +08:00
"reference" : reference [ len ( reference ) - 1 ] ,
"final" : true ,
}
return s . structureAnswerWithConv ( session , ans , messageID , session . ID , reference ) , nil
}
// asyncChatSoloStream performs simple streaming chat without RAG
2026-03-27 19:25:18 +08:00
func ( s * ChatSessionService ) asyncChatSoloStream ( dialog * entity . Chat , session * entity . ChatSession , messages [ ] map [ string ] interface { } , config map [ string ] interface { } , messageID string , reference [ ] interface { } , resultChan chan <- map [ string ] interface { } ) {
2026-05-06 10:41:58 +08:00
common . Info ( "asyncChatSoloStream started" ,
2026-04-29 14:44:12 +08:00
zap . String ( "tenant_id" , dialog . TenantID ) ,
zap . String ( "llm_id" , dialog . LLMID ) ,
zap . String ( "dialog_id" , dialog . ID ) ,
zap . Int ( "message_count" , len ( messages ) ) )
2026-03-04 19:17:16 +08:00
// Get system prompt
systemPrompt := s . buildSystemPrompt ( dialog )
// Process messages
processedMessages := s . processMessages ( messages , dialog )
2026-06-08 11:49:37 +08:00
var (
driver modelModule . ModelDriver
modelName string
apiConfig * modelModule . APIConfig
err error
)
if dialog . LLMID != "" {
driver , modelName , apiConfig , _ , err = s . modelProviderSvc . GetModelConfigFromProviderInstance (
dialog . TenantID , entity . ModelTypeChat , dialog . LLMID ,
)
} else {
driver , modelName , apiConfig , _ , err = s . modelProviderSvc . GetTenantDefaultModelByType (
dialog . TenantID , entity . ModelTypeChat ,
)
}
2026-03-04 19:17:16 +08:00
if err != nil {
2026-05-06 10:41:58 +08:00
common . Error ( "asyncChatSoloStream failed to get chat model" , err )
2026-03-04 19:17:16 +08:00
resultChan <- s . structureAnswer ( session , "**ERROR**: " + err . Error ( ) , messageID , session . ID , reference )
return
}
2026-06-08 11:49:37 +08:00
chatModel := modelModule . NewChatModel ( driver , & modelName , apiConfig )
2026-03-04 19:17:16 +08:00
2026-04-30 19:33:57 +08:00
// Convert messages to []modelModule.Message for ChatStreamlyWithSender
var chatMessages [ ] modelModule . Message
2026-04-29 14:44:12 +08:00
if systemPrompt != "" {
2026-04-30 19:33:57 +08:00
chatMessages = append ( chatMessages , modelModule . Message {
Role : "system" ,
Content : systemPrompt ,
} )
2026-03-04 19:17:16 +08:00
}
2026-04-29 14:44:12 +08:00
for _ , msg := range processedMessages {
role , _ := msg [ "role" ] . ( string )
2026-04-30 19:33:57 +08:00
content := msg [ "content" ]
if role != "" && content != nil && role != "system" {
chatMessages = append ( chatMessages , modelModule . Message {
Role : role ,
Content : content ,
} )
2026-04-29 14:44:12 +08:00
}
}
2026-03-04 19:17:16 +08:00
2026-04-29 14:44:12 +08:00
// Get ChatConfig directly from dialog and config
chatConfig := s . buildChatConfig ( dialog , config )
// Perform streaming chat using ChatStreamlyWithSender
fullAnswer := ""
2026-04-30 19:33:57 +08:00
err = chatModel . ModelDriver . ChatStreamlyWithSender ( * chatModel . ModelName , chatMessages , chatModel . APIConfig , chatConfig , func ( answer * string , reason * string ) error {
2026-04-29 14:44:12 +08:00
if reason != nil && * reason != "" {
fullAnswer += * reason
ans := s . structureAnswer ( session , fullAnswer , messageID , session . ID , reference )
resultChan <- ans
}
if answer != nil && * answer != "" {
fullAnswer += * answer
fullAnswer = s . removeReasoningContent ( fullAnswer )
ans := s . structureAnswer ( session , fullAnswer , messageID , session . ID , reference )
resultChan <- ans
}
return nil
} )
2026-03-04 19:17:16 +08:00
if err != nil {
resultChan <- s . structureAnswer ( session , "**ERROR**: " + err . Error ( ) , messageID , session . ID , reference )
return
}
2026-05-06 10:41:58 +08:00
common . Info ( "asyncChatSoloStream completed" ,
2026-04-29 14:44:12 +08:00
zap . String ( "tenant_id" , dialog . TenantID ) ,
zap . String ( "llm_id" , dialog . LLMID ) ,
zap . Int ( "response_length" , len ( fullAnswer ) ) )
2026-03-04 19:17:16 +08:00
}
// buildSystemPrompt builds the system prompt from dialog configuration
2026-03-27 19:25:18 +08:00
func ( s * ChatSessionService ) buildSystemPrompt ( dialog * entity . Chat ) string {
2026-03-04 19:17:16 +08:00
if dialog . PromptConfig == nil {
return ""
}
system , _ := dialog . PromptConfig [ "system" ] . ( string )
return system
}
// processMessages processes messages and handles attachments
2026-03-27 19:25:18 +08:00
func ( s * ChatSessionService ) processMessages ( messages [ ] map [ string ] interface { } , dialog * entity . Chat ) [ ] map [ string ] interface { } {
2026-03-04 19:17:16 +08:00
// Process each message
processed := make ( [ ] map [ string ] interface { } , len ( messages ) )
for i , msg := range messages {
processed [ i ] = make ( map [ string ] interface { } )
for k , v := range msg {
processed [ i ] [ k ] = v
}
// Clean content - remove file markers
if content , ok := msg [ "content" ] . ( string ) ; ok {
content = s . cleanContent ( content )
processed [ i ] [ "content" ] = content
}
}
return processed
}
// cleanContent removes file markers from content
func ( s * ChatSessionService ) cleanContent ( content string ) string {
// Remove ##N$$ markers
// This is a simplified version - full implementation would use regex
return content
}
// removeReasoningContent removes reasoning/thinking content from answer
func ( s * ChatSessionService ) removeReasoningContent ( answer string ) string {
// Remove </think> tags
if strings . HasSuffix ( answer , "</think>" ) {
answer = answer [ : len ( answer ) - len ( "</think>" ) ]
}
return answer
}
// structureAnswerWithConv structures the answer with conversation update (like Python's structure_answer)
2026-03-27 19:25:18 +08:00
func ( s * ChatSessionService ) structureAnswerWithConv ( session * entity . ChatSession , ans map [ string ] interface { } , messageID , conversationID string , reference [ ] interface { } ) map [ string ] interface { } {
2026-03-04 19:17:16 +08:00
// Extract reference from answer
ref , _ := ans [ "reference" ] . ( map [ string ] interface { } )
if ref == nil {
ref = map [ string ] interface { } {
"chunks" : [ ] interface { } { } ,
"doc_aggs" : [ ] interface { } { } ,
}
ans [ "reference" ] = ref
}
// Format chunks
chunkList := s . chunksFormat ( ref )
ref [ "chunks" ] = chunkList
// Add message ID and session ID
ans [ "id" ] = messageID
ans [ "session_id" ] = conversationID
// Update session message
content , _ := ans [ "answer" ] . ( string )
if ans [ "start_to_think" ] != nil {
content = "<think>"
} else if ans [ "end_to_think" ] != nil {
content = "</think>"
}
// Parse existing messages
var messagesObj map [ string ] interface { }
if len ( session . Message ) > 0 {
json . Unmarshal ( session . Message , & messagesObj )
}
messages , _ := messagesObj [ "messages" ] . ( [ ] interface { } )
// Update or append assistant message
if len ( messages ) == 0 || s . getLastRole ( messages ) != "assistant" {
messages = append ( messages , map [ string ] interface { } {
"role" : "assistant" ,
"content" : content ,
"created_at" : float64 ( time . Now ( ) . Unix ( ) ) ,
"id" : messageID ,
} )
} else {
lastIdx := len ( messages ) - 1
lastMsg , _ := messages [ lastIdx ] . ( map [ string ] interface { } )
if lastMsg != nil {
if ans [ "final" ] == true && ans [ "answer" ] != nil {
lastMsg [ "content" ] = ans [ "answer" ]
} else {
lastMsg [ "content" ] = ( lastMsg [ "content" ] . ( string ) ) + content
}
lastMsg [ "created_at" ] = float64 ( time . Now ( ) . Unix ( ) )
lastMsg [ "id" ] = messageID
messages [ lastIdx ] = lastMsg
}
}
// Update reference
if len ( reference ) > 0 {
reference [ len ( reference ) - 1 ] = ref
}
return ans
}
// getLastRole gets the role of the last message
func ( s * ChatSessionService ) getLastRole ( messages [ ] interface { } ) string {
if len ( messages ) == 0 {
return ""
}
lastMsg , _ := messages [ len ( messages ) - 1 ] . ( map [ string ] interface { } )
if lastMsg != nil {
role , _ := lastMsg [ "role" ] . ( string )
return role
}
return ""
}
// chunksFormat formats chunks for reference (simplified version)
func ( s * ChatSessionService ) chunksFormat ( reference map [ string ] interface { } ) [ ] interface { } {
chunks , _ := reference [ "chunks" ] . ( [ ] interface { } )
if chunks == nil {
return [ ] interface { } { }
}
// Format each chunk
formatted := make ( [ ] interface { } , len ( chunks ) )
for i , chunk := range chunks {
formatted [ i ] = chunk
}
return formatted
}
2026-04-29 14:44:12 +08:00
// buildChatConfig builds ChatConfig directly from dialog.LLMSetting and config
func ( s * ChatSessionService ) buildChatConfig ( dialog * entity . Chat , config map [ string ] interface { } ) * modelModule . ChatConfig {
cfg := & modelModule . ChatConfig { }
// Start with dialog's LLM setting
if dialog . LLMSetting != nil {
if v , ok := dialog . LLMSetting [ "stream" ] . ( bool ) ; ok {
cfg . Stream = & v
}
if v , ok := dialog . LLMSetting [ "thinking" ] . ( bool ) ; ok {
cfg . Thinking = & v
}
2026-04-29 17:04:22 +08:00
if v , ok := dialog . LLMSetting [ "max_tokens" ] . ( float64 ) ; ok {
intVal := int ( v )
cfg . MaxTokens = & intVal
2026-04-29 14:44:12 +08:00
}
if v , ok := dialog . LLMSetting [ "temperature" ] . ( float64 ) ; ok {
cfg . Temperature = & v
}
if v , ok := dialog . LLMSetting [ "top_p" ] . ( float64 ) ; ok {
cfg . TopP = & v
}
if v , ok := dialog . LLMSetting [ "do_sample" ] . ( bool ) ; ok {
cfg . DoSample = & v
}
2026-04-29 17:04:22 +08:00
if v , ok := dialog . LLMSetting [ "stop" ] . ( [ ] interface { } ) ; ok {
stopStrs := make ( [ ] string , 0 , len ( v ) )
for _ , s := range v {
if str , ok := s . ( string ) ; ok {
stopStrs = append ( stopStrs , str )
}
}
cfg . Stop = & stopStrs
2026-04-29 14:44:12 +08:00
}
if v , ok := dialog . LLMSetting [ "model_class" ] . ( string ) ; ok {
cfg . ModelClass = & v
}
if v , ok := dialog . LLMSetting [ "effort" ] . ( string ) ; ok {
cfg . Effort = & v
}
if v , ok := dialog . LLMSetting [ "verbosity" ] . ( string ) ; ok {
cfg . Verbosity = & v
}
}
// Override with request config
if config != nil {
if v , ok := config [ "stream" ] . ( bool ) ; ok {
cfg . Stream = & v
}
if v , ok := config [ "thinking" ] . ( bool ) ; ok {
cfg . Thinking = & v
}
2026-04-29 17:04:22 +08:00
if v , ok := config [ "max_tokens" ] . ( float64 ) ; ok {
intVal := int ( v )
cfg . MaxTokens = & intVal
2026-04-29 14:44:12 +08:00
}
if v , ok := config [ "temperature" ] . ( float64 ) ; ok {
cfg . Temperature = & v
}
if v , ok := config [ "top_p" ] . ( float64 ) ; ok {
cfg . TopP = & v
}
if v , ok := config [ "do_sample" ] . ( bool ) ; ok {
cfg . DoSample = & v
}
2026-04-29 17:04:22 +08:00
if v , ok := config [ "stop" ] . ( [ ] interface { } ) ; ok {
stopStrs := make ( [ ] string , 0 , len ( v ) )
for _ , s := range v {
if str , ok := s . ( string ) ; ok {
stopStrs = append ( stopStrs , str )
}
}
cfg . Stop = & stopStrs
2026-04-29 14:44:12 +08:00
}
if v , ok := config [ "model_class" ] . ( string ) ; ok {
cfg . ModelClass = & v
}
if v , ok := config [ "effort" ] . ( string ) ; ok {
cfg . Effort = & v
}
if v , ok := config [ "verbosity" ] . ( string ) ; ok {
cfg . Verbosity = & v
}
}
return cfg
}