Restructure the monorepo into clear top-level directories: - backend/: Go module root (cmd, internal, pkg, configs, migrations, docs/swagger, scripts, tests, go.mod, Makefile, .air.toml) - deploy/: Docker (Dockerfile, docker-compose*), quickstart, fluentd - docs/: project documentation + reports/ (moved from repo root) - AGENTS.md: new AI coding-agent guide at repo root Update all references to the new layout: - Dockerfile: COPY backend/go.mod, COPY backend/ (context = repo root) - docker-compose files: context ../.., dockerfile deploy/docker/Dockerfile, env_file ../../.env, volume mounts ../../backend:/app - deploy/quickstart/compose.yaml: dockerfile deploy/docker/Dockerfile - CI: working-directory: backend for go commands, file deploy/docker/Dockerfile, coverage path backend/coverage.out, health_check backend/scripts/ - backend/Makefile: docker target uses -f ../deploy/docker/Dockerfile ../ - README: architecture tree, quickstart, config paths updated Move root stray scripts (rename_models.*, run_m11_tests.sh, verify_build.sh, gorm_bool_main.go) to backend/scripts/legacy/. All moves via git mv to preserve history. Build, vet, SQLite tests, and docker compose config verified.
203 lines
6.8 KiB
Go
203 lines
6.8 KiB
Go
package service
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
|
|
"github.com/gochat/gochat/internal/model"
|
|
"github.com/gochat/gochat/internal/repository"
|
|
applogger "github.com/gochat/gochat/pkg/logger"
|
|
)
|
|
|
|
// CaptainScenarioService implements business logic for CaptainScenario operations.
|
|
// Reference: Chatwoot enterprise/app/controllers/api/v1/captain/scenarios_controller.rb
|
|
type CaptainScenarioService struct {
|
|
scenarioRepo *repository.CaptainScenarioRepo
|
|
assistantRepo *repository.CaptainAssistantRepo
|
|
}
|
|
|
|
// NewCaptainScenarioService creates a new CaptainScenarioService.
|
|
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 ---
|
|
|
|
// CreateScenarioRequest is the DTO for creating a scenario.
|
|
type CreateScenarioRequest struct {
|
|
Title string `json:"title" validate:"required"`
|
|
Description string `json:"description"`
|
|
Instruction string `json:"instruction"`
|
|
Enabled *bool `json:"enabled"`
|
|
Tools json.RawMessage `json:"tools"`
|
|
}
|
|
|
|
// UpdateScenarioRequest is the DTO for updating a scenario.
|
|
type UpdateScenarioRequest struct {
|
|
Title string `json:"title"`
|
|
Description string `json:"description"`
|
|
Instruction string `json:"instruction"`
|
|
Enabled *bool `json:"enabled"`
|
|
Tools json.RawMessage `json:"tools"`
|
|
}
|
|
|
|
// --- Service methods ---
|
|
|
|
// 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: enabled,
|
|
Tools: req.Tools,
|
|
}
|
|
|
|
if err := s.scenarioRepo.Create(ctx, scenario); err != nil {
|
|
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
|
|
}
|
|
|
|
// GetByID retrieves a scenario by ID.
|
|
func (s *CaptainScenarioService) GetByID(ctx context.Context, id uint) (*model.CaptainScenario, error) {
|
|
scenario, err := s.scenarioRepo.GetByID(ctx, id)
|
|
if err != nil {
|
|
applogger.L().Errorf("Get captain scenario: %v", err)
|
|
return nil, fmt.Errorf("get scenario: %w", err)
|
|
}
|
|
return scenario, nil
|
|
}
|
|
|
|
// Update updates an existing scenario.
|
|
func (s *CaptainScenarioService) Update(ctx context.Context, id uint, req *UpdateScenarioRequest) (*model.CaptainScenario, error) {
|
|
scenario, err := s.scenarioRepo.GetByID(ctx, 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
|
|
}
|
|
|
|
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 {
|
|
applogger.L().Errorf("Delete captain scenario: %v", err)
|
|
return fmt.Errorf("delete scenario: %w", err)
|
|
}
|
|
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)
|
|
if err != nil {
|
|
applogger.L().Errorf("List captain scenarios: %v", err)
|
|
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
|
|
}
|