package assistant import ( "context" "fmt" "path" "time" "github.com/fatih/color" jsoniter "github.com/json-iterator/go" "github.com/yaoapp/gou/fs" "github.com/yaoapp/gou/rag/driver" "github.com/yaoapp/kun/log" sui "github.com/yaoapp/yao/sui/core" ) // Save save the assistant func (ast *Assistant) Save() error { if storage == nil { return fmt.Errorf("storage is not set") } _, err := storage.SaveAssistant(ast.Map()) if err != nil { return err } // Update Index in background go func() { err := ast.UpdateIndex() if err != nil { log.Error("failed to update index for assistant %s: %s", ast.ID, err) color.Red("failed to update index for assistant %s: %s", ast.ID, err) } }() return nil } // UpdateIndex update the index for RAG func (ast *Assistant) UpdateIndex() error { // RAG is not enabled if rag == nil { return nil } if rag.Engine == nil { return fmt.Errorf("engine is not set") } // Update Index index := fmt.Sprintf("%sassistants", rag.Setting.IndexPrefix) id := fmt.Sprintf("assistant_%s", ast.ID) ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) defer cancel() // Check if the index exists exists, err := rag.Engine.HasIndex(ctx, index) if err != nil { return err } // Create the index if it does not exist if !exists { ctxCreate, cancelCreate := context.WithTimeout(context.Background(), 2*time.Second) defer cancelCreate() err = rag.Engine.CreateIndex(ctxCreate, driver.IndexConfig{Name: index}) if err != nil { return err } } // Check if the document exists exists, err = rag.Engine.HasDocument(ctx, index, id) if err != nil { return err } // Check if the document is updated if exists { metadata, err := rag.Engine.GetMetadata(ctx, index, id) if err != nil { return err } if v, ok := metadata["updated_at"].(string); ok { updatedAt, err := stringToTimestamp(v) if err != nil { return err } if updatedAt >= ast.UpdatedAt { return nil } } } // Update the index content, err := jsoniter.MarshalToString(ast.Map()) if err != nil { return err } metadata := map[string]interface{}{ "assistant_id": ast.ID, "type": ast.Type, "name": ast.Name, "updated_at": fmt.Sprintf("%d", ast.UpdatedAt), } return rag.Engine.IndexDoc(ctx, index, &driver.Document{ DocID: id, Content: content, Metadata: metadata, }) } // Map convert the assistant to a map func (ast *Assistant) Map() map[string]interface{} { if ast == nil { return nil } return map[string]interface{}{ "assistant_id": ast.ID, "type": ast.Type, "name": ast.Name, "readonly": ast.Readonly, "avatar": ast.Avatar, "connector": ast.Connector, "path": ast.Path, "built_in": ast.BuiltIn, "sort": ast.Sort, "description": ast.Description, "options": ast.Options, "prompts": ast.Prompts, "tools": ast.Tools, "tags": ast.Tags, "mentionable": ast.Mentionable, "automated": ast.Automated, "placeholder": ast.Placeholder, "locales": ast.Locales, "created_at": timeToMySQLFormat(ast.CreatedAt), "updated_at": timeToMySQLFormat(ast.UpdatedAt), } } // Validate validates the assistant configuration func (ast *Assistant) Validate() error { if ast.ID == "" { return fmt.Errorf("assistant_id is required") } if ast.Name == "" { return fmt.Errorf("name is required") } if ast.Connector == "" { return fmt.Errorf("connector is required") } return nil } // Assets get the assets content func (ast *Assistant) Assets(name string, data sui.Data) (string, error) { app, err := fs.Get("app") if err != nil { return "", err } root := path.Join(ast.Path, "assets", name) raw, err := app.ReadFile(root) if err != nil { return "", err } if data != nil { content, _ := data.Replace(string(raw)) return content, nil } return string(raw), nil } // Clone creates a deep copy of the assistant func (ast *Assistant) Clone() *Assistant { if ast == nil { return nil } clone := &Assistant{ ID: ast.ID, Type: ast.Type, Name: ast.Name, Avatar: ast.Avatar, Connector: ast.Connector, Path: ast.Path, BuiltIn: ast.BuiltIn, Sort: ast.Sort, Description: ast.Description, Readonly: ast.Readonly, Mentionable: ast.Mentionable, Automated: ast.Automated, Script: ast.Script, openai: ast.openai, } // Deep copy tags if ast.Tags != nil { clone.Tags = make([]string, len(ast.Tags)) copy(clone.Tags, ast.Tags) } // Deep copy options if ast.Options != nil { clone.Options = make(map[string]interface{}) for k, v := range ast.Options { clone.Options[k] = v } } // Deep copy prompts if ast.Prompts != nil { clone.Prompts = make([]Prompt, len(ast.Prompts)) copy(clone.Prompts, ast.Prompts) } // Deep copy tools if ast.Tools != nil { clone.Tools = &ToolCalls{} if ast.Tools.Tools != nil { clone.Tools.Tools = make([]Tool, len(ast.Tools.Tools)) copy(clone.Tools.Tools, ast.Tools.Tools) } if ast.Tools.Prompts != nil { clone.Tools.Prompts = make([]Prompt, len(ast.Tools.Prompts)) copy(clone.Tools.Prompts, ast.Tools.Prompts) } } // Deep copy workflow if ast.Workflow != nil { clone.Workflow = make(map[string]interface{}) for k, v := range ast.Workflow { clone.Workflow[k] = v } } return clone } // Update updates the assistant properties func (ast *Assistant) Update(data map[string]interface{}) error { if ast == nil { return fmt.Errorf("assistant is nil") } if v, ok := data["name"].(string); ok { ast.Name = v } if v, ok := data["avatar"].(string); ok { ast.Avatar = v } if v, ok := data["description"].(string); ok { ast.Description = v } if v, ok := data["connector"].(string); ok { ast.Connector = v } if v, has := data["tools"]; has { switch tools := v.(type) { case []Tool: ast.Tools = &ToolCalls{ Tools: tools, Prompts: ast.Prompts, } case *ToolCalls: ast.Tools = tools default: raw, err := jsoniter.Marshal(tools) if err != nil { return err } ast.Tools = &ToolCalls{} err = jsoniter.Unmarshal(raw, &ast.Tools) if err != nil { return err } } } if v, ok := data["type"].(string); ok { ast.Type = v } if v, ok := data["sort"].(int); ok { ast.Sort = v } if v, ok := data["mentionable"].(bool); ok { ast.Mentionable = v } if v, ok := data["automated"].(bool); ok { ast.Automated = v } if v, ok := data["tags"].([]string); ok { ast.Tags = v } if v, ok := data["options"].(map[string]interface{}); ok { ast.Options = v } return ast.Validate() }