thread-master/apps/backend/internal/module/radar/usecase/demand_map_enrich.go

94 lines
4.1 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package usecase
import (
"context"
"encoding/json"
"fmt"
"strings"
"apps/backend/internal/module/radar/domain"
usageDomain "apps/backend/internal/module/usage/domain"
)
type demandMapEnrichment struct {
PainPhrases []domain.DemandMapPhrase `json:"pain_phrases"`
ScenarioPhrases []domain.DemandMapPhrase `json:"scenario_phrases"`
DesiredOutcomes []domain.DemandMapPhrase `json:"desired_outcomes"`
SolutionSignals []domain.DemandMapPhrase `json:"solution_signals"`
ExclusionSignals []domain.DemandMapPhrase `json:"exclusion_signals"`
CustomPhrases []domain.DemandMapPhrase `json:"custom_phrases"`
}
func (s *Service) EnrichDemandMap(ctx context.Context, ownerUID int64, productID, previewID string, expectedVersion int64, creditCeiling int) (out *domain.DemandMap, err error) {
if ownerUID <= 0 || strings.TrimSpace(productID) == "" {
return nil, fmt.Errorf("%w: owner and product are required", domain.ErrValidation)
}
if expectedVersion < 1 {
return nil, fmt.Errorf("%w: expected map version required", domain.ErrValidation)
}
if creditCeiling < 1 {
return nil, fmt.Errorf("%w: credit ceiling must be positive", domain.ErrValidation)
}
current, err := s.GetDemandMap(ctx, ownerUID, productID)
if err != nil {
return nil, err
}
charge, err := s.bill(ctx, ownerUID, usageDomain.MeterAICopy, "需求地圖 AI 補全", "radar.demand_map_enrich")
if err != nil {
return nil, err
}
defer charge.Settle(ctx, &err)
if s.AI == nil && s.AIRegistry == nil && s.ResolveAI == nil {
return nil, fmt.Errorf("%w: AI provider unavailable", domain.ErrNotReady)
}
prompt := fmt.Sprintf("請只輸出 JSON 物件,根據需求地圖補充使用者會說的短詞,不要使用品牌或產品名稱。痛點=%q情境=%q結果=%q能力=%q。欄位為 pain_phrases、scenario_phrases、desired_outcomes、solution_signals、exclusion_signals、custom_phrases每則含 text、kind、basis_kind、basis_text、origin=ai、enabled=true。", phraseTexts(current.PainPhrases), phraseTexts(current.ScenarioPhrases), phraseTexts(current.DesiredOutcomes), phraseTexts(current.SolutionSignals))
raw, err := s.completeAI(ctx, ownerUID, prompt)
if err != nil {
return nil, err
}
var additions demandMapEnrichment
start, end := strings.Index(raw, "{"), strings.LastIndex(raw, "}")
if start < 0 || end <= start || json.Unmarshal([]byte(raw[start:end+1]), &additions) != nil {
return nil, fmt.Errorf("%w: AI returned invalid demand map JSON", domain.ErrValidation)
}
mergePhrases := func(existing []domain.DemandMapPhrase, incoming []domain.DemandMapPhrase) []domain.DemandMapPhrase {
out := append([]domain.DemandMapPhrase(nil), existing...)
seen := map[string]bool{}
for _, p := range out {
seen[strings.ToLower(strings.TrimSpace(p.Text))] = true
}
for _, p := range incoming {
p.Text = strings.TrimSpace(p.Text)
p.Origin = "ai"
p.Enabled = p.Enabled && p.Text != ""
if p.Enabled && !seen[strings.ToLower(p.Text)] {
out = append(out, p)
seen[strings.ToLower(p.Text)] = true
}
}
return out
}
current.PainPhrases = mergePhrases(current.PainPhrases, additions.PainPhrases)
current.ScenarioPhrases = mergePhrases(current.ScenarioPhrases, additions.ScenarioPhrases)
current.DesiredOutcomes = mergePhrases(current.DesiredOutcomes, additions.DesiredOutcomes)
current.SolutionSignals = mergePhrases(current.SolutionSignals, additions.SolutionSignals)
current.ExclusionSignals = mergePhrases(current.ExclusionSignals, additions.ExclusionSignals)
current.CustomPhrases = mergePhrases(current.CustomPhrases, additions.CustomPhrases)
current.AIEnrichedAt = domain.NowNano()
if current.State != "ready" && len(current.PainPhrases) > 0 && len(current.ScenarioPhrases) > 0 && len(current.SolutionSignals) > 0 {
current.State = "ready"
}
_ = previewID // preview is a client confirmation token; no credit is reserved by preview.
return s.UpdateDemandMap(ctx, ownerUID, current, expectedVersion)
}
func phraseTexts(in []domain.DemandMapPhrase) []string {
out := make([]string, 0, len(in))
for _, p := range in {
if p.Enabled && strings.TrimSpace(p.Text) != "" {
out = append(out, strings.TrimSpace(p.Text))
}
}
return out
}