142 lines
3.8 KiB
Go
142 lines
3.8 KiB
Go
package service
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"regexp"
|
|
"strings"
|
|
"time"
|
|
|
|
"git.threesix.ai/jordan/persona-community-5/pkg/logging"
|
|
"git.threesix.ai/jordan/persona-community-5/pkg/queue"
|
|
"git.threesix.ai/jordan/persona-community-5/pkg/realtime"
|
|
"git.threesix.ai/jordan/persona-community-5/services/persona-api/internal/domain"
|
|
"git.threesix.ai/jordan/persona-community-5/services/persona-api/internal/port"
|
|
)
|
|
|
|
// PersonaService handles persona-related business logic.
|
|
type PersonaService struct {
|
|
repo port.PersonaRepository
|
|
queue queue.Producer
|
|
sseHub *realtime.SSEHub
|
|
logger *logging.Logger
|
|
}
|
|
|
|
// NewPersonaService creates a new persona service.
|
|
func NewPersonaService(repo port.PersonaRepository, q queue.Producer, hub *realtime.SSEHub, logger *logging.Logger) *PersonaService {
|
|
return &PersonaService{
|
|
repo: repo,
|
|
queue: q,
|
|
sseHub: hub,
|
|
logger: logger.WithService("PersonaService"),
|
|
}
|
|
}
|
|
|
|
// Create creates a new persona and enqueues a generate_spec job.
|
|
func (s *PersonaService) Create(ctx context.Context, description, gender, customName string) (*domain.Persona, error) {
|
|
name := customName
|
|
if name == "" {
|
|
name = generatePersonaName(description)
|
|
}
|
|
|
|
handle := generateHandle(name)
|
|
now := time.Now().UTC()
|
|
|
|
persona := &domain.Persona{
|
|
Name: name,
|
|
Handle: handle,
|
|
Gender: gender,
|
|
Description: description,
|
|
Tags: []string{},
|
|
ImageURLs: []string{},
|
|
VideoURLs: []string{},
|
|
Status: domain.PersonaStatusPending,
|
|
CreatedAt: now,
|
|
}
|
|
|
|
if err := s.repo.Create(ctx, persona); err != nil {
|
|
return nil, fmt.Errorf("create persona: %w", err)
|
|
}
|
|
|
|
// Enqueue generate_spec job to kick off the pipeline.
|
|
_, err := s.queue.Enqueue(ctx, "generate_spec", map[string]any{
|
|
"persona_id": persona.ID.String(),
|
|
"stage": string(domain.StageSpec),
|
|
})
|
|
if err != nil {
|
|
s.logger.Error("failed to enqueue generate_spec job", "error", err, "persona_id", persona.ID)
|
|
// Persona is created but job failed to enqueue — not fatal.
|
|
// Caller can retry or a reconciler can pick it up.
|
|
}
|
|
|
|
s.publishUpdate(persona)
|
|
s.logger.Info("persona created", "id", persona.ID, "name", name, "handle", handle)
|
|
return persona, nil
|
|
}
|
|
|
|
// GetByID returns a persona by ID.
|
|
func (s *PersonaService) GetByID(ctx context.Context, id domain.PersonaID) (*domain.Persona, error) {
|
|
return s.repo.GetByID(ctx, id)
|
|
}
|
|
|
|
// List returns personas with pagination.
|
|
func (s *PersonaService) List(ctx context.Context, limit, offset int) ([]*domain.Persona, error) {
|
|
if limit <= 0 {
|
|
limit = 20
|
|
}
|
|
if limit > 100 {
|
|
limit = 100
|
|
}
|
|
if offset < 0 {
|
|
offset = 0
|
|
}
|
|
return s.repo.List(ctx, limit, offset)
|
|
}
|
|
|
|
// publishUpdate sends a persona_updated SSE event to channel:personas.
|
|
func (s *PersonaService) publishUpdate(persona *domain.Persona) {
|
|
if s.sseHub == nil {
|
|
return
|
|
}
|
|
s.sseHub.SendToChannel("channel:personas", &realtime.SSEEvent{
|
|
Type: "persona_updated",
|
|
Result: persona,
|
|
})
|
|
}
|
|
|
|
// handleRegex strips non-alphanumeric characters for handle generation.
|
|
var handleRegex = regexp.MustCompile(`[^a-z0-9]+`)
|
|
|
|
// generateHandle creates a URL-safe handle from a name.
|
|
func generateHandle(name string) string {
|
|
h := strings.ToLower(strings.TrimSpace(name))
|
|
h = handleRegex.ReplaceAllString(h, "_")
|
|
h = strings.Trim(h, "_")
|
|
if len(h) > 40 {
|
|
h = h[:40]
|
|
h = strings.TrimRight(h, "_")
|
|
}
|
|
// Append timestamp suffix to reduce collisions
|
|
suffix := fmt.Sprintf("_%d", time.Now().UnixMilli()%100000)
|
|
return h + suffix
|
|
}
|
|
|
|
// generatePersonaName derives a placeholder name from the description.
|
|
func generatePersonaName(description string) string {
|
|
words := strings.Fields(description)
|
|
if len(words) > 3 {
|
|
words = words[:3]
|
|
}
|
|
// Capitalize first letter of each word
|
|
for i, w := range words {
|
|
if len(w) > 0 {
|
|
words[i] = strings.ToUpper(w[:1]) + w[1:]
|
|
}
|
|
}
|
|
name := strings.Join(words, " ")
|
|
if len(name) > 50 {
|
|
name = name[:50]
|
|
}
|
|
return name
|
|
}
|