mirror of
https://github.com/infiniflow/ragflow.git
synced 2026-07-09 21:04:49 +08:00
### What problem does this PR solve? - Tools management - Pregel engine wrapper for better usage - UT race - Coding style ### Type of change - [x] Refactoring
501 lines
13 KiB
Go
501 lines
13 KiB
Go
package graph
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
|
|
"ragflow/internal/harness/graph/channels"
|
|
"ragflow/internal/harness/graph/types"
|
|
)
|
|
|
|
// Message represents a message in the conversation.
|
|
type Message struct {
|
|
ID string // Unique identifier for deduplication
|
|
Role string // e.g., "user", "assistant", "system"
|
|
Content string // The message content
|
|
Extra map[string]interface{} // Additional metadata
|
|
}
|
|
|
|
// NewMessage creates a new message without an ID.
|
|
func NewMessage(role, content string) *Message {
|
|
return &Message{
|
|
ID: "",
|
|
Role: role,
|
|
Content: content,
|
|
Extra: make(map[string]interface{}),
|
|
}
|
|
}
|
|
|
|
// NewMessageWithID creates a new message with an ID.
|
|
func NewMessageWithID(id, role, content string) *Message {
|
|
return &Message{
|
|
ID: id,
|
|
Role: role,
|
|
Content: content,
|
|
Extra: make(map[string]interface{}),
|
|
}
|
|
}
|
|
|
|
// MessagesState represents the state for message-based graphs.
|
|
// It contains a list of messages and optional additional fields.
|
|
type MessagesState struct {
|
|
Messages []*Message
|
|
Extra map[string]interface{}
|
|
}
|
|
|
|
// AddMessages adds messages to the state.
|
|
func (s *MessagesState) AddMessages(msgs ...*Message) {
|
|
s.Messages = append(s.Messages, msgs...)
|
|
}
|
|
|
|
// GetMessages returns all messages.
|
|
func (s *MessagesState) GetMessages() []*Message {
|
|
return s.Messages
|
|
}
|
|
|
|
// GetLastMessage returns the last message.
|
|
func (s *MessagesState) GetLastMessage() *Message {
|
|
if len(s.Messages) == 0 {
|
|
return nil
|
|
}
|
|
return s.Messages[len(s.Messages)-1]
|
|
}
|
|
|
|
// GetMessagesByRole returns messages of a specific role.
|
|
func (s *MessagesState) GetMessagesByRole(role string) []*Message {
|
|
filtered := make([]*Message, 0)
|
|
for _, msg := range s.Messages {
|
|
if msg.Role == role {
|
|
filtered = append(filtered, msg)
|
|
}
|
|
}
|
|
return filtered
|
|
}
|
|
|
|
// AddMessagesReducer is a reducer function that adds messages to the state.
|
|
// It performs deduplication based on message ID.
|
|
func AddMessagesReducer(existing interface{}, updates interface{}) (interface{}, error) {
|
|
msgs, ok := updates.([]*Message)
|
|
if !ok {
|
|
// Try single message
|
|
if msg, ok := updates.(*Message); ok {
|
|
msgs = []*Message{msg}
|
|
} else {
|
|
return nil, &GraphError{Message: fmt.Sprintf("cannot add messages of type %T", updates)}
|
|
}
|
|
}
|
|
|
|
if existing == nil {
|
|
return msgs, nil
|
|
}
|
|
|
|
existingMsgs, ok := existing.([]*Message)
|
|
if !ok {
|
|
return nil, &GraphError{Message: fmt.Sprintf("existing messages is not []*Message, got %T", existing)}
|
|
}
|
|
|
|
// Create a map of existing messages by ID for quick lookup
|
|
existingMap := make(map[string]*Message)
|
|
for _, msg := range existingMsgs {
|
|
if msg.ID != "" {
|
|
existingMap[msg.ID] = msg
|
|
}
|
|
}
|
|
|
|
// Process updates: update existing messages with same ID, append new ones
|
|
result := make([]*Message, 0, len(existingMsgs)+len(msgs))
|
|
// Keep track of which IDs have been processed
|
|
processedIDs := make(map[string]bool)
|
|
|
|
// First, add all existing messages, updating those that have updates
|
|
for _, msg := range existingMsgs {
|
|
if msg.ID == "" {
|
|
// Messages without ID are always kept as-is
|
|
result = append(result, msg)
|
|
continue
|
|
}
|
|
// Check if there's an update for this ID
|
|
var updated *Message
|
|
for _, update := range msgs {
|
|
if update.ID == msg.ID {
|
|
updated = update
|
|
break
|
|
}
|
|
}
|
|
if updated != nil {
|
|
result = append(result, updated)
|
|
processedIDs[msg.ID] = true
|
|
} else {
|
|
result = append(result, msg)
|
|
}
|
|
}
|
|
|
|
// Then, append new messages that don't have matching IDs in existing
|
|
for _, msg := range msgs {
|
|
if msg.ID == "" {
|
|
// Messages without ID are always appended
|
|
result = append(result, msg)
|
|
} else if !processedIDs[msg.ID] && existingMap[msg.ID] == nil {
|
|
// This is a new message with an ID not in existing
|
|
result = append(result, msg)
|
|
}
|
|
}
|
|
|
|
return result, nil
|
|
}
|
|
|
|
// MessageGraph is a graph specialized for message-based workflows.
|
|
// It automatically manages a messages channel with the AddMessages reducer.
|
|
type MessageGraph struct {
|
|
graph *StateGraph
|
|
messagesChannel string
|
|
}
|
|
|
|
// NewMessageGraph creates a new message-based graph.
|
|
func NewMessageGraph() *MessageGraph {
|
|
// Create a simple state schema with messages field
|
|
stateSchema := map[string]interface{}{
|
|
"messages": []any{},
|
|
}
|
|
|
|
g := NewStateGraph(stateSchema)
|
|
|
|
// Register the messages channel so GetMessages works
|
|
messagesChannel := "messages"
|
|
g.AddChannel(messagesChannel, channels.NewLastValue([]*Message{}))
|
|
|
|
return &MessageGraph{
|
|
graph: g,
|
|
messagesChannel: messagesChannel,
|
|
}
|
|
}
|
|
|
|
// AddNode adds a node to the message graph.
|
|
func (g *MessageGraph) AddNode(name string, action types.NodeFunc) *Node {
|
|
return g.graph.AddNode(name, action)
|
|
}
|
|
|
|
// AddEdge adds a directed edge between nodes.
|
|
func (g *MessageGraph) AddEdge(startKey, endKey string) error {
|
|
return g.graph.AddEdge(startKey, endKey)
|
|
}
|
|
|
|
// AddConditionalEdge adds a conditional edge.
|
|
func (g *MessageGraph) AddConditionalEdge(source string, condition types.EdgeFunc, edgeMap map[string]string) error {
|
|
return g.graph.AddConditionalEdges(source, condition, edgeMap)
|
|
}
|
|
|
|
// SetEntryPoint sets the entry point node.
|
|
func (g *MessageGraph) SetEntryPoint(node string) error {
|
|
return g.graph.SetEntryPoint(node)
|
|
}
|
|
|
|
// Build returns a compiled message graph.
|
|
func (g *MessageGraph) Build() (*CompiledGraph, error) {
|
|
return g.graph.Compile()
|
|
}
|
|
|
|
// GetState returns the current state of the graph.
|
|
func (g *MessageGraph) GetState() map[string]interface{} {
|
|
return map[string]interface{}{
|
|
"messages": []any{},
|
|
}
|
|
}
|
|
|
|
// GetMessages returns the messages channel value.
|
|
func (g *MessageGraph) GetMessages(ctx context.Context, channelRegistry *channels.Registry) ([]*Message, error) {
|
|
if ch, ok := channelRegistry.Get(g.messagesChannel); ok {
|
|
data, err := ch.Get()
|
|
if err == nil && data != nil {
|
|
if msgs, ok := data.([]*Message); ok {
|
|
return msgs, nil
|
|
}
|
|
}
|
|
}
|
|
return []*Message{}, nil
|
|
}
|
|
|
|
// GetMessagesFromState extracts messages from state.
|
|
func GetMessagesFromState(state map[string]interface{}) ([]*Message, error) {
|
|
messages, ok := state["messages"]
|
|
if !ok {
|
|
return []*Message{}, nil
|
|
}
|
|
|
|
switch msgs := messages.(type) {
|
|
case []*Message:
|
|
return msgs, nil
|
|
case []interface{}:
|
|
result := make([]*Message, len(msgs))
|
|
for i, m := range msgs {
|
|
if msg, ok := m.(*Message); ok {
|
|
result[i] = msg
|
|
} else {
|
|
return nil, &GraphError{Message: fmt.Sprintf("message at index %d is not *Message, got %T", i, m)}
|
|
}
|
|
}
|
|
return result, nil
|
|
default:
|
|
return nil, &GraphError{Message: fmt.Sprintf("messages is not []*Message, got %T", messages)}
|
|
}
|
|
}
|
|
|
|
// AddMessagesToState adds messages to the state.
|
|
func AddMessagesToState(state map[string]interface{}, msgs ...*Message) error {
|
|
existing, err := GetMessagesFromState(state)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
result := make([]*Message, len(existing)+len(msgs))
|
|
copy(result, existing)
|
|
copy(result[len(existing):], msgs)
|
|
state["messages"] = result
|
|
return nil
|
|
}
|
|
|
|
// MessageRole constants
|
|
const (
|
|
MessageRoleUser = "user"
|
|
MessageRoleAssistant = "assistant"
|
|
MessageRoleSystem = "system"
|
|
MessageRoleTool = "tool"
|
|
MessageRoleFunction = "function"
|
|
)
|
|
|
|
// HumanMessage creates a user message.
|
|
func HumanMessage(content string) *Message {
|
|
return NewMessage(MessageRoleUser, content)
|
|
}
|
|
|
|
// AIMessage creates an assistant message.
|
|
func AIMessage(content string) *Message {
|
|
return NewMessage(MessageRoleAssistant, content)
|
|
}
|
|
|
|
// SystemMessage creates a system message.
|
|
func SystemMessage(content string) *Message {
|
|
return NewMessage(MessageRoleSystem, content)
|
|
}
|
|
|
|
// ToolMessage creates a tool message.
|
|
func ToolMessage(content string, toolCallID string) *Message {
|
|
msg := NewMessage(MessageRoleTool, content)
|
|
if msg.Extra == nil {
|
|
msg.Extra = make(map[string]interface{})
|
|
}
|
|
msg.Extra["tool_call_id"] = toolCallID
|
|
return msg
|
|
}
|
|
|
|
// FunctionMessage creates a function message.
|
|
func FunctionMessage(content string, name string) *Message {
|
|
msg := NewMessage(MessageRoleFunction, content)
|
|
if msg.Extra == nil {
|
|
msg.Extra = make(map[string]interface{})
|
|
}
|
|
msg.Extra["name"] = name
|
|
return msg
|
|
}
|
|
|
|
// MessageHelper provides utility functions for working with messages.
|
|
type MessageHelper struct {
|
|
}
|
|
|
|
// FormatMessages formats messages for display or logging.
|
|
func FormatMessages(msgs []*Message) []string {
|
|
formatted := make([]string, len(msgs))
|
|
for i, msg := range msgs {
|
|
formatted[i] = fmt.Sprintf("%s: %s", msg.Role, msg.Content)
|
|
}
|
|
return formatted
|
|
}
|
|
|
|
// GetLastUserMessage returns the last user message.
|
|
func GetLastUserMessage(msgs []*Message) *Message {
|
|
for i := len(msgs) - 1; i >= 0; i-- {
|
|
if msgs[i].Role == MessageRoleUser {
|
|
return msgs[i]
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// GetLastAIMessage returns the last assistant message.
|
|
func GetLastAIMessage(msgs []*Message) *Message {
|
|
for i := len(msgs) - 1; i >= 0; i-- {
|
|
if msgs[i].Role == MessageRoleAssistant {
|
|
return msgs[i]
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// FilterMessagesByRole returns messages filtered by role.
|
|
func FilterMessagesByRole(msgs []*Message, roles ...string) []*Message {
|
|
roleSet := make(map[string]bool)
|
|
for _, role := range roles {
|
|
roleSet[role] = true
|
|
}
|
|
|
|
filtered := make([]*Message, 0)
|
|
for _, msg := range msgs {
|
|
if roleSet[msg.Role] {
|
|
filtered = append(filtered, msg)
|
|
}
|
|
}
|
|
return filtered
|
|
}
|
|
|
|
// MessagesFilter provides message filtering capabilities.
|
|
type MessagesFilter struct {
|
|
roles []string
|
|
limit int
|
|
offset int
|
|
reverse bool
|
|
predicate func(*Message) bool
|
|
}
|
|
|
|
// NewMessagesFilter creates a new messages filter.
|
|
func NewMessagesFilter() *MessagesFilter {
|
|
return &MessagesFilter{}
|
|
}
|
|
|
|
// WithRole filters by message roles.
|
|
func (f *MessagesFilter) WithRole(roles ...string) *MessagesFilter {
|
|
f.roles = roles
|
|
return f
|
|
}
|
|
|
|
// WithLimit limits the number of messages.
|
|
func (f *MessagesFilter) WithLimit(limit int) *MessagesFilter {
|
|
f.limit = limit
|
|
return f
|
|
}
|
|
|
|
// WithOffset skips the first offset messages.
|
|
func (f *MessagesFilter) WithOffset(offset int) *MessagesFilter {
|
|
f.offset = offset
|
|
return f
|
|
}
|
|
|
|
// WithReverse reverses the message order.
|
|
func (f *MessagesFilter) WithReverse() *MessagesFilter {
|
|
f.reverse = true
|
|
return f
|
|
}
|
|
|
|
// WithPredicate adds a custom predicate function.
|
|
func (f *MessagesFilter) WithPredicate(predicate func(*Message) bool) *MessagesFilter {
|
|
f.predicate = predicate
|
|
return f
|
|
}
|
|
|
|
// Filter applies the filter to the messages.
|
|
func (f *MessagesFilter) Filter(msgs []*Message) []*Message {
|
|
result := make([]*Message, 0)
|
|
|
|
for _, msg := range msgs {
|
|
// Check role filter
|
|
if len(f.roles) > 0 {
|
|
match := false
|
|
for _, role := range f.roles {
|
|
if msg.Role == role {
|
|
match = true
|
|
break
|
|
}
|
|
}
|
|
if !match {
|
|
continue
|
|
}
|
|
}
|
|
|
|
// Check predicate
|
|
if f.predicate != nil && !f.predicate(msg) {
|
|
continue
|
|
}
|
|
|
|
result = append(result, msg)
|
|
}
|
|
|
|
// Apply offset
|
|
if f.offset > 0 && f.offset < len(result) {
|
|
result = result[f.offset:]
|
|
}
|
|
|
|
// Apply limit
|
|
if f.limit > 0 && f.limit < len(result) {
|
|
result = result[:f.limit]
|
|
}
|
|
|
|
// Apply reverse
|
|
if f.reverse {
|
|
for i, j := 0, len(result)-1; i < j; i, j = i+1, j-1 {
|
|
result[i], result[j] = result[j], result[i]
|
|
}
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
// GraphError represents a graph-related error.
|
|
type GraphError struct {
|
|
Message string
|
|
Code string
|
|
}
|
|
|
|
func (e *GraphError) Error() string {
|
|
if e.Code != "" {
|
|
return e.Code + ": " + e.Message
|
|
}
|
|
return e.Message
|
|
}
|
|
|
|
// OpenAI format conversion utilities
|
|
|
|
// OpenAIChatMessage represents a message in OpenAI's chat completion API format.
|
|
type OpenAIChatMessage struct {
|
|
Role string `json:"role"`
|
|
Content string `json:"content"`
|
|
Name string `json:"name,omitempty"`
|
|
// Additional fields like function_call, tool_calls can be added as needed
|
|
}
|
|
|
|
// ToOpenAIChatMessage converts a Message to OpenAI chat message format.
|
|
func (m *Message) ToOpenAIChatMessage() *OpenAIChatMessage {
|
|
return &OpenAIChatMessage{
|
|
Role: m.Role,
|
|
Content: m.Content,
|
|
// Name can be extracted from Extra if needed
|
|
}
|
|
}
|
|
|
|
// MessagesToOpenAIFormat converts a slice of Messages to OpenAI chat completion format.
|
|
func MessagesToOpenAIFormat(messages []*Message) []OpenAIChatMessage {
|
|
result := make([]OpenAIChatMessage, len(messages))
|
|
for i, msg := range messages {
|
|
result[i] = OpenAIChatMessage{
|
|
Role: msg.Role,
|
|
Content: msg.Content,
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
|
|
// OpenAIFormatToMessages converts OpenAI format messages back to Messages.
|
|
func OpenAIFormatToMessages(openaiMessages []OpenAIChatMessage) []*Message {
|
|
result := make([]*Message, len(openaiMessages))
|
|
for i, msg := range openaiMessages {
|
|
result[i] = &Message{
|
|
ID: "", // ID will need to be generated or preserved separately
|
|
Role: msg.Role,
|
|
Content: msg.Content,
|
|
Extra: make(map[string]interface{}),
|
|
}
|
|
if msg.Name != "" {
|
|
result[i].Extra["name"] = msg.Name
|
|
}
|
|
}
|
|
return result
|
|
}
|