feat(captain): align scenario and tool payloads
This commit is contained in:
@@ -7,6 +7,7 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strings"
|
||||
"text/template"
|
||||
"time"
|
||||
@@ -32,7 +33,7 @@ func NewCaptainCustomToolService(toolRepo *repository.CaptainCustomToolRepo) *Ca
|
||||
// CreateCustomToolRequest is the DTO for creating a custom tool.
|
||||
type CreateCustomToolRequest struct {
|
||||
Title string `json:"title" validate:"required"`
|
||||
Slug string `json:"slug" validate:"required"`
|
||||
Slug string `json:"slug"`
|
||||
Description string `json:"description"`
|
||||
EndpointURL string `json:"endpoint_url" validate:"required"`
|
||||
HTTPMethod string `json:"http_method"`
|
||||
@@ -70,11 +71,15 @@ func (s *CaptainCustomToolService) Create(ctx context.Context, accountID uint, r
|
||||
if authType == "" {
|
||||
authType = "none"
|
||||
}
|
||||
slug := req.Slug
|
||||
if slug == "" {
|
||||
slug = customToolSlug(req.Title)
|
||||
}
|
||||
|
||||
tool := &model.CaptainCustomTool{
|
||||
AccountID: accountID,
|
||||
Title: req.Title,
|
||||
Slug: req.Slug,
|
||||
Slug: slug,
|
||||
Description: req.Description,
|
||||
EndpointURL: req.EndpointURL,
|
||||
HTTPMethod: httpMethod,
|
||||
@@ -103,6 +108,15 @@ func (s *CaptainCustomToolService) Get(ctx context.Context, id uint) (*model.Cap
|
||||
return tool, nil
|
||||
}
|
||||
|
||||
func (s *CaptainCustomToolService) GetByAccount(ctx context.Context, accountID, id uint) (*model.CaptainCustomTool, error) {
|
||||
tool, err := s.toolRepo.GetByAccountAndID(ctx, accountID, id)
|
||||
if err != nil {
|
||||
applogger.L().Errorf("Get captain custom tool: %v", err)
|
||||
return nil, fmt.Errorf("get custom tool: %w", err)
|
||||
}
|
||||
return tool, nil
|
||||
}
|
||||
|
||||
// Update updates an existing custom tool.
|
||||
func (s *CaptainCustomToolService) Update(ctx context.Context, id uint, req *UpdateCustomToolRequest) (*model.CaptainCustomTool, error) {
|
||||
tool, err := s.toolRepo.GetByID(ctx, id)
|
||||
@@ -148,6 +162,19 @@ func (s *CaptainCustomToolService) Update(ctx context.Context, id uint, req *Upd
|
||||
return tool, nil
|
||||
}
|
||||
|
||||
func (s *CaptainCustomToolService) UpdateByAccount(ctx context.Context, accountID, id uint, req *UpdateCustomToolRequest) (*model.CaptainCustomTool, error) {
|
||||
tool, err := s.toolRepo.GetByAccountAndID(ctx, accountID, id)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("custom tool not found: %w", err)
|
||||
}
|
||||
applyCustomToolUpdate(tool, req)
|
||||
if err := s.toolRepo.Update(ctx, tool); err != nil {
|
||||
applogger.L().Errorf("Update captain custom tool: %v", err)
|
||||
return nil, fmt.Errorf("update custom tool: %w", err)
|
||||
}
|
||||
return tool, nil
|
||||
}
|
||||
|
||||
// Delete deletes a custom tool by ID.
|
||||
func (s *CaptainCustomToolService) Delete(ctx context.Context, id uint) error {
|
||||
if err := s.toolRepo.Delete(ctx, id); err != nil {
|
||||
@@ -157,6 +184,17 @@ func (s *CaptainCustomToolService) Delete(ctx context.Context, id uint) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *CaptainCustomToolService) DeleteByAccount(ctx context.Context, accountID, id uint) error {
|
||||
if _, err := s.toolRepo.GetByAccountAndID(ctx, accountID, id); err != nil {
|
||||
return fmt.Errorf("custom tool not found: %w", err)
|
||||
}
|
||||
if err := s.toolRepo.DeleteByAccount(ctx, accountID, id); err != nil {
|
||||
applogger.L().Errorf("Delete captain custom tool: %v", err)
|
||||
return fmt.Errorf("delete custom tool: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// List retrieves custom tools for an account with pagination.
|
||||
func (s *CaptainCustomToolService) List(ctx context.Context, accountID uint, offset, limit int) ([]model.CaptainCustomTool, int64, error) {
|
||||
tools, count, err := s.toolRepo.ListByAccount(ctx, accountID, offset, limit)
|
||||
@@ -167,6 +205,49 @@ func (s *CaptainCustomToolService) List(ctx context.Context, accountID uint, off
|
||||
return tools, count, nil
|
||||
}
|
||||
|
||||
func applyCustomToolUpdate(tool *model.CaptainCustomTool, req *UpdateCustomToolRequest) {
|
||||
if req.Title != "" {
|
||||
tool.Title = req.Title
|
||||
}
|
||||
if req.Description != "" {
|
||||
tool.Description = req.Description
|
||||
}
|
||||
if req.EndpointURL != "" {
|
||||
tool.EndpointURL = req.EndpointURL
|
||||
}
|
||||
if req.HTTPMethod != "" {
|
||||
tool.HTTPMethod = req.HTTPMethod
|
||||
}
|
||||
if req.AuthType != "" {
|
||||
tool.AuthType = model.ToolAuthType(req.AuthType)
|
||||
}
|
||||
if len(req.AuthConfig) > 0 && string(req.AuthConfig) != "null" {
|
||||
tool.AuthConfig = req.AuthConfig
|
||||
}
|
||||
if len(req.ParamSchema) > 0 && string(req.ParamSchema) != "null" {
|
||||
tool.ParamSchema = req.ParamSchema
|
||||
}
|
||||
if req.RequestTemplate != "" {
|
||||
tool.RequestTemplate = req.RequestTemplate
|
||||
}
|
||||
if req.ResponseTemplate != "" {
|
||||
tool.ResponseTemplate = req.ResponseTemplate
|
||||
}
|
||||
if req.Enabled != nil {
|
||||
tool.Enabled = *req.Enabled
|
||||
}
|
||||
}
|
||||
|
||||
func customToolSlug(title string) string {
|
||||
slug := strings.ToLower(strings.TrimSpace(title))
|
||||
slug = regexp.MustCompile(`[^a-z0-9]+`).ReplaceAllString(slug, "-")
|
||||
slug = strings.Trim(slug, "-")
|
||||
if slug == "" {
|
||||
return "custom-tool"
|
||||
}
|
||||
return slug
|
||||
}
|
||||
|
||||
// --- Tool Execution ---
|
||||
|
||||
// ExecuteToolResult holds the result of executing a custom tool.
|
||||
@@ -330,8 +411,8 @@ func applyAuth(req *http.Request, tool *model.CaptainCustomTool) error {
|
||||
return nil
|
||||
case model.ToolAuthTypeApiKey:
|
||||
var authCfg struct {
|
||||
Key string `json:"key"`
|
||||
Value string `json:"value"`
|
||||
Key string `json:"key"`
|
||||
Value string `json:"value"`
|
||||
Header string `json:"header"` // default: "X-API-Key"
|
||||
}
|
||||
if err := json.Unmarshal(tool.AuthConfig, &authCfg); err != nil {
|
||||
@@ -381,4 +462,4 @@ func (s *CaptainCustomToolService) TestTool(ctx context.Context, accountID uint,
|
||||
}, nil
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,12 +13,17 @@ import (
|
||||
// CaptainScenarioService implements business logic for CaptainScenario operations.
|
||||
// Reference: Chatwoot enterprise/app/controllers/api/v1/captain/scenarios_controller.rb
|
||||
type CaptainScenarioService struct {
|
||||
scenarioRepo *repository.CaptainScenarioRepo
|
||||
scenarioRepo *repository.CaptainScenarioRepo
|
||||
assistantRepo *repository.CaptainAssistantRepo
|
||||
}
|
||||
|
||||
// NewCaptainScenarioService creates a new CaptainScenarioService.
|
||||
func NewCaptainScenarioService(scenarioRepo *repository.CaptainScenarioRepo) *CaptainScenarioService {
|
||||
return &CaptainScenarioService{scenarioRepo: scenarioRepo}
|
||||
func NewCaptainScenarioService(scenarioRepo *repository.CaptainScenarioRepo, assistantRepo ...*repository.CaptainAssistantRepo) *CaptainScenarioService {
|
||||
s := &CaptainScenarioService{scenarioRepo: scenarioRepo}
|
||||
if len(assistantRepo) > 0 {
|
||||
s.assistantRepo = assistantRepo[0]
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// --- Request DTOs ---
|
||||
@@ -28,7 +33,7 @@ type CreateScenarioRequest struct {
|
||||
Title string `json:"title" validate:"required"`
|
||||
Description string `json:"description"`
|
||||
Instruction string `json:"instruction"`
|
||||
Enabled bool `json:"enabled"`
|
||||
Enabled *bool `json:"enabled"`
|
||||
Tools json.RawMessage `json:"tools"`
|
||||
}
|
||||
|
||||
@@ -45,13 +50,22 @@ type UpdateScenarioRequest struct {
|
||||
|
||||
// Create creates a new scenario for an assistant.
|
||||
func (s *CaptainScenarioService) Create(ctx context.Context, accountID, assistantID uint, req *CreateScenarioRequest) (*model.CaptainScenario, error) {
|
||||
if s.assistantRepo != nil {
|
||||
if _, err := s.assistantRepo.GetByAccountAndID(ctx, accountID, assistantID); err != nil {
|
||||
return nil, fmt.Errorf("assistant not found: %w", err)
|
||||
}
|
||||
}
|
||||
enabled := true
|
||||
if req.Enabled != nil {
|
||||
enabled = *req.Enabled
|
||||
}
|
||||
scenario := &model.CaptainScenario{
|
||||
AccountID: accountID,
|
||||
AssistantID: assistantID,
|
||||
Title: req.Title,
|
||||
Description: req.Description,
|
||||
Instruction: req.Instruction,
|
||||
Enabled: req.Enabled,
|
||||
Enabled: enabled,
|
||||
Tools: req.Tools,
|
||||
}
|
||||
|
||||
@@ -59,6 +73,18 @@ func (s *CaptainScenarioService) Create(ctx context.Context, accountID, assistan
|
||||
applogger.L().Errorf("Create captain scenario: %v", err)
|
||||
return nil, fmt.Errorf("create scenario: %w", err)
|
||||
}
|
||||
if created, err := s.scenarioRepo.GetByAccountAssistantAndID(ctx, accountID, assistantID, scenario.ID); err == nil {
|
||||
return created, nil
|
||||
}
|
||||
return scenario, nil
|
||||
}
|
||||
|
||||
func (s *CaptainScenarioService) Get(ctx context.Context, accountID, assistantID, id uint) (*model.CaptainScenario, error) {
|
||||
scenario, err := s.scenarioRepo.GetByAccountAssistantAndID(ctx, accountID, assistantID, id)
|
||||
if err != nil {
|
||||
applogger.L().Errorf("Get captain scenario: %v", err)
|
||||
return nil, fmt.Errorf("get scenario: %w", err)
|
||||
}
|
||||
return scenario, nil
|
||||
}
|
||||
|
||||
@@ -102,6 +128,35 @@ func (s *CaptainScenarioService) Update(ctx context.Context, id uint, req *Updat
|
||||
return scenario, nil
|
||||
}
|
||||
|
||||
func (s *CaptainScenarioService) UpdateScoped(ctx context.Context, accountID, assistantID, id uint, req *UpdateScenarioRequest) (*model.CaptainScenario, error) {
|
||||
scenario, err := s.scenarioRepo.GetByAccountAssistantAndID(ctx, accountID, assistantID, id)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("scenario not found: %w", err)
|
||||
}
|
||||
|
||||
if req.Title != "" {
|
||||
scenario.Title = req.Title
|
||||
}
|
||||
if req.Description != "" {
|
||||
scenario.Description = req.Description
|
||||
}
|
||||
if req.Instruction != "" {
|
||||
scenario.Instruction = req.Instruction
|
||||
}
|
||||
if req.Enabled != nil {
|
||||
scenario.Enabled = *req.Enabled
|
||||
}
|
||||
if len(req.Tools) > 0 && string(req.Tools) != "null" {
|
||||
scenario.Tools = req.Tools
|
||||
}
|
||||
|
||||
if err := s.scenarioRepo.Update(ctx, scenario); err != nil {
|
||||
applogger.L().Errorf("Update captain scenario: %v", err)
|
||||
return nil, fmt.Errorf("update scenario: %w", err)
|
||||
}
|
||||
return scenario, nil
|
||||
}
|
||||
|
||||
// Delete deletes a scenario by ID.
|
||||
func (s *CaptainScenarioService) Delete(ctx context.Context, id uint) error {
|
||||
if err := s.scenarioRepo.Delete(ctx, id); err != nil {
|
||||
@@ -111,6 +166,17 @@ func (s *CaptainScenarioService) Delete(ctx context.Context, id uint) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *CaptainScenarioService) DeleteScoped(ctx context.Context, accountID, assistantID, id uint) error {
|
||||
if _, err := s.scenarioRepo.GetByAccountAssistantAndID(ctx, accountID, assistantID, id); err != nil {
|
||||
return fmt.Errorf("scenario not found: %w", err)
|
||||
}
|
||||
if err := s.scenarioRepo.DeleteByAccountAssistant(ctx, accountID, assistantID, id); err != nil {
|
||||
applogger.L().Errorf("Delete captain scenario: %v", err)
|
||||
return fmt.Errorf("delete scenario: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ListByAssistant retrieves scenarios for an assistant with pagination.
|
||||
func (s *CaptainScenarioService) ListByAssistant(ctx context.Context, assistantID uint, offset, limit int) ([]model.CaptainScenario, int64, error) {
|
||||
scenarios, count, err := s.scenarioRepo.ListByAssistant(ctx, assistantID, offset, limit)
|
||||
@@ -119,4 +185,18 @@ func (s *CaptainScenarioService) ListByAssistant(ctx context.Context, assistantI
|
||||
return nil, 0, fmt.Errorf("list scenarios: %w", err)
|
||||
}
|
||||
return scenarios, count, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (s *CaptainScenarioService) ListByAccountAssistant(ctx context.Context, accountID, assistantID uint) ([]model.CaptainScenario, int64, error) {
|
||||
if s.assistantRepo != nil {
|
||||
if _, err := s.assistantRepo.GetByAccountAndID(ctx, accountID, assistantID); err != nil {
|
||||
return nil, 0, fmt.Errorf("assistant not found: %w", err)
|
||||
}
|
||||
}
|
||||
scenarios, count, err := s.scenarioRepo.ListEnabledByAccountAssistant(ctx, accountID, assistantID)
|
||||
if err != nil {
|
||||
applogger.L().Errorf("List captain scenarios: %v", err)
|
||||
return nil, 0, fmt.Errorf("list scenarios: %w", err)
|
||||
}
|
||||
return scenarios, count, nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user