155 lines
4.8 KiB
Go
155 lines
4.8 KiB
Go
|
|
package copy_mission
|
|||
|
|
|
|||
|
|
import (
|
|||
|
|
"context"
|
|||
|
|
"strings"
|
|||
|
|
|
|||
|
|
app "haixun-backend/internal/library/errors"
|
|||
|
|
"haixun-backend/internal/library/errors/code"
|
|||
|
|
"haixun-backend/internal/library/placement"
|
|||
|
|
"haixun-backend/internal/library/style8d"
|
|||
|
|
libviral "haixun-backend/internal/library/viral"
|
|||
|
|
"haixun-backend/internal/library/websearch"
|
|||
|
|
domai "haixun-backend/internal/model/ai/domain/usecase"
|
|||
|
|
aiusecase "haixun-backend/internal/model/ai/usecase"
|
|||
|
|
"haixun-backend/internal/svc"
|
|||
|
|
"haixun-backend/internal/types"
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
type InspireCopyMissionLogic struct {
|
|||
|
|
ctx context.Context
|
|||
|
|
svcCtx *svc.ServiceContext
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func NewInspireCopyMissionLogic(ctx context.Context, svcCtx *svc.ServiceContext) *InspireCopyMissionLogic {
|
|||
|
|
return &InspireCopyMissionLogic{ctx: ctx, svcCtx: svcCtx}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func (l *InspireCopyMissionLogic) InspireCopyMission(req *types.PersonaCopyMissionsPath) (*types.CopyMissionInspirationData, error) {
|
|||
|
|
tenantID, uid, err := actorFrom(l.ctx)
|
|||
|
|
if err != nil {
|
|||
|
|
return nil, err
|
|||
|
|
}
|
|||
|
|
personaID := strings.TrimSpace(req.PersonaID)
|
|||
|
|
persona, err := l.svcCtx.Persona.Get(l.ctx, tenantID, uid, personaID)
|
|||
|
|
if err != nil {
|
|||
|
|
return nil, err
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
missionList, err := l.svcCtx.CopyMission.List(l.ctx, tenantID, uid, personaID)
|
|||
|
|
if err != nil {
|
|||
|
|
return nil, err
|
|||
|
|
}
|
|||
|
|
recentLabels := make([]string, 0, 8)
|
|||
|
|
recentSeeds := make([]string, 0, 8)
|
|||
|
|
for _, mission := range missionList.List {
|
|||
|
|
if mission.Status == "archived" {
|
|||
|
|
continue
|
|||
|
|
}
|
|||
|
|
if label := strings.TrimSpace(mission.Label); label != "" {
|
|||
|
|
recentLabels = append(recentLabels, label)
|
|||
|
|
}
|
|||
|
|
if seed := strings.TrimSpace(mission.SeedQuery); seed != "" {
|
|||
|
|
recentSeeds = append(recentSeeds, seed)
|
|||
|
|
}
|
|||
|
|
if len(recentLabels) >= 8 && len(recentSeeds) >= 8 {
|
|||
|
|
break
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
trendSnippets := []libviral.MissionInspireTrendSnippet{}
|
|||
|
|
webSearchProvider := ""
|
|||
|
|
llmOnly := true
|
|||
|
|
|
|||
|
|
research, researchErr := l.svcCtx.Placement.ResearchSettings(l.ctx, tenantID, uid)
|
|||
|
|
if researchErr == nil && placement.WebSearchAvailable(research) {
|
|||
|
|
memberCtx, memberErr := l.svcCtx.ThreadsAccount.ResolveMemberPlacementContext(l.ctx, tenantID, uid, research)
|
|||
|
|
if memberErr == nil {
|
|||
|
|
webClient := websearch.New(memberCtx.WebSearchConfig())
|
|||
|
|
trendSnippets = libviral.CollectMissionInspireTrends(
|
|||
|
|
l.ctx,
|
|||
|
|
webClient,
|
|||
|
|
memberCtx,
|
|||
|
|
persona.Brief,
|
|||
|
|
persona.StyleBenchmark,
|
|||
|
|
)
|
|||
|
|
webSearchProvider = memberCtx.WebSearchProviderLabel()
|
|||
|
|
llmOnly = len(trendSnippets) == 0
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
webSearchUsed := len(trendSnippets) > 0
|
|||
|
|
|
|||
|
|
credential, err := l.svcCtx.ThreadsAccount.ResolveMemberAiCredential(l.ctx, tenantID, uid)
|
|||
|
|
if err != nil {
|
|||
|
|
return nil, err
|
|||
|
|
}
|
|||
|
|
providerID, err := aiusecase.MapWorkerProvider(credential.Provider)
|
|||
|
|
if err != nil {
|
|||
|
|
return nil, err
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
personaBlock := style8d.ResolvePersonaBlock(persona.Persona, persona.StyleProfile, persona.Brief)
|
|||
|
|
result, err := l.svcCtx.AI.GenerateText(l.ctx, domai.GenerateRequest{
|
|||
|
|
Provider: providerID,
|
|||
|
|
Model: credential.Model,
|
|||
|
|
Credential: domai.Credential{
|
|||
|
|
APIKey: credential.APIKey,
|
|||
|
|
},
|
|||
|
|
System: libviral.BuildMissionInspireSystemPrompt(),
|
|||
|
|
Messages: []domai.Message{
|
|||
|
|
{
|
|||
|
|
Role: "user",
|
|||
|
|
Content: libviral.BuildMissionInspireUserPrompt(libviral.MissionInspireInput{
|
|||
|
|
PersonaDisplayName: persona.DisplayName,
|
|||
|
|
PersonaBrief: persona.Brief,
|
|||
|
|
PersonaBlock: personaBlock,
|
|||
|
|
StyleBenchmark: persona.StyleBenchmark,
|
|||
|
|
PersonaAudience: persona.CopyResearchMap.AudienceSummary,
|
|||
|
|
PersonaContentGoal: persona.CopyResearchMap.ContentGoal,
|
|||
|
|
PersonaQuestions: append([]string(nil), persona.CopyResearchMap.Questions...),
|
|||
|
|
PersonaPillars: append([]string(nil), persona.CopyResearchMap.Pillars...),
|
|||
|
|
RecentMissionLabels: recentLabels,
|
|||
|
|
RecentSeedQueries: recentSeeds,
|
|||
|
|
TrendSnippets: trendSnippets,
|
|||
|
|
WebSearchProvider: webSearchProvider,
|
|||
|
|
LLMOnly: llmOnly,
|
|||
|
|
}),
|
|||
|
|
},
|
|||
|
|
},
|
|||
|
|
})
|
|||
|
|
if err != nil {
|
|||
|
|
return nil, err
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
parsed, err := libviral.ParseMissionInspireOutput(result.Text)
|
|||
|
|
if err != nil {
|
|||
|
|
return nil, app.For(code.AI).SvcThirdParty("靈感骰子 LLM 回傳無法解析:" + err.Error())
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
sources := make([]types.CopyMissionInspirationSourceData, 0, len(trendSnippets))
|
|||
|
|
for _, item := range trendSnippets {
|
|||
|
|
sources = append(sources, types.CopyMissionInspirationSourceData{
|
|||
|
|
Query: item.Query,
|
|||
|
|
Title: item.Title,
|
|||
|
|
Snippet: item.Snippet,
|
|||
|
|
URL: item.URL,
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
message := "已依近期趨勢產出任務靈感,可微調後建立任務"
|
|||
|
|
if !webSearchUsed {
|
|||
|
|
message = "未設定搜尋 API key,已改由 LLM 依人設推測靈感;補上 Brave/Exa key 可取得更貼近熱搜的結果"
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return &types.CopyMissionInspirationData{
|
|||
|
|
Label: parsed.Label,
|
|||
|
|
SeedQuery: parsed.SeedQuery,
|
|||
|
|
Brief: parsed.Brief,
|
|||
|
|
TrendReason: parsed.TrendReason,
|
|||
|
|
TrendKeywords: parsed.TrendKeywords,
|
|||
|
|
Sources: sources,
|
|||
|
|
WebSearchUsed: webSearchUsed,
|
|||
|
|
Message: message,
|
|||
|
|
}, nil
|
|||
|
|
}
|