feat(prompts): add prompts loader and configuration management
- Implemented a new prompts loader in `loader.go` to manage prompt templates and configurations. - Introduced caching mechanism for prompt configurations to optimize loading. - Added functions to apply platform-specific prompt template overrides. - Created unit tests in `loader_test.go` to validate prompt configuration loading and reloading behavior. - Ensured that the last valid configuration is retained in case of errors during reload.
This commit is contained in:
@@ -254,7 +254,14 @@ var monitoringPlatformNames = map[string]string{
|
||||
"yuanbao": "腾讯元宝",
|
||||
}
|
||||
|
||||
func (s *MonitoringService) DashboardComposite(ctx context.Context, brandID int64, keywordID *int64, days int, businessDate string) (*MonitoringDashboardCompositeResponse, error) {
|
||||
func (s *MonitoringService) DashboardComposite(
|
||||
ctx context.Context,
|
||||
brandID int64,
|
||||
keywordID *int64,
|
||||
days int,
|
||||
businessDate string,
|
||||
aiPlatformID *string,
|
||||
) (*MonitoringDashboardCompositeResponse, error) {
|
||||
actor := auth.MustActor(ctx)
|
||||
|
||||
brand, err := s.resolveBrand(ctx, actor.TenantID, brandID)
|
||||
@@ -280,15 +287,16 @@ func (s *MonitoringService) DashboardComposite(ctx context.Context, brandID int6
|
||||
questionIDs := configuredQuestionIDs(configuredQuestions)
|
||||
|
||||
platforms := resolveMonitoringPlatforms(quota.EnabledPlatforms)
|
||||
filteredPlatforms := filterMonitoringPlatforms(platforms, aiPlatformID)
|
||||
startDate, endDate, err := resolveDashboardDateWindow(days, businessDate)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
derivedMetrics, err := s.loadDerivedParseMetrics(ctx, actor.TenantID, brand.ID, brand.Name, startDate, endDate)
|
||||
derivedMetrics, err := s.loadDerivedParseMetrics(ctx, actor.TenantID, brand.ID, brand.Name, startDate, endDate, aiPlatformID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
overview, _, err := s.loadOverview(ctx, actor.TenantID, brand.ID, startDate, endDate, quota.CollectionMode, derivedMetrics)
|
||||
overview, _, err := s.loadOverview(ctx, actor.TenantID, brand.ID, startDate, endDate, quota.CollectionMode, aiPlatformID, derivedMetrics)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -298,27 +306,27 @@ func (s *MonitoringService) DashboardComposite(ctx context.Context, brandID int6
|
||||
return nil, err
|
||||
}
|
||||
|
||||
platformBreakdown, err := s.loadPlatformBreakdown(ctx, actor.TenantID, brand.ID, endDate, quota.CollectionMode, platforms, accessStates, derivedMetrics)
|
||||
platformBreakdown, err := s.loadPlatformBreakdown(ctx, actor.TenantID, brand.ID, endDate, quota.CollectionMode, filteredPlatforms, accessStates, derivedMetrics)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
timeBuckets, err := s.loadTimeBuckets(ctx, actor.TenantID, brand.ID, startDate, endDate, quota.CollectionMode, derivedMetrics)
|
||||
timeBuckets, err := s.loadTimeBuckets(ctx, actor.TenantID, brand.ID, startDate, endDate, quota.CollectionMode, aiPlatformID, derivedMetrics)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
hotQuestions, err := s.loadHotQuestions(ctx, actor.TenantID, brand.ID, configuredQuestions, endDate, derivedMetrics)
|
||||
hotQuestions, err := s.loadHotQuestions(ctx, actor.TenantID, brand.ID, configuredQuestions, endDate, aiPlatformID, derivedMetrics)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
citationRanking, err := s.loadCitationRanking(ctx, actor.TenantID, brand.ID, questionIDs, startDate, endDate, accessStates)
|
||||
citationRanking, err := s.loadCitationRanking(ctx, actor.TenantID, brand.ID, questionIDs, startDate, endDate, accessStates, aiPlatformID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
citedArticles, err := s.loadCitedArticles(ctx, actor.TenantID, brand.ID, questionIDs, startDate, endDate)
|
||||
citedArticles, err := s.loadCitedArticles(ctx, actor.TenantID, brand.ID, questionIDs, startDate, endDate, aiPlatformID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -981,7 +989,13 @@ func (stats monitoringDerivedRateStats) positiveMentionRate() *float64 {
|
||||
return divideAsPointer(stats.PositiveMentions, stats.Total)
|
||||
}
|
||||
|
||||
func (s *MonitoringService) loadDerivedParseMetrics(ctx context.Context, tenantID, brandID int64, brandName string, startDate, endDate time.Time) (monitoringDerivedMetrics, error) {
|
||||
func (s *MonitoringService) loadDerivedParseMetrics(
|
||||
ctx context.Context,
|
||||
tenantID, brandID int64,
|
||||
brandName string,
|
||||
startDate, endDate time.Time,
|
||||
aiPlatformID *string,
|
||||
) (monitoringDerivedMetrics, error) {
|
||||
metrics := newMonitoringDerivedMetrics()
|
||||
|
||||
rows, err := s.monitoringPool.Query(ctx, `
|
||||
@@ -1003,7 +1017,8 @@ func (s *MonitoringService) loadDerivedParseMetrics(ctx context.Context, tenantI
|
||||
AND r.collector_type = $3
|
||||
AND r.status = 'succeeded'
|
||||
AND r.business_date BETWEEN $4::date AND $5::date
|
||||
`, tenantID, brandID, monitoringCollectorType, startDate.Format("2006-01-02"), endDate.Format("2006-01-02"))
|
||||
AND ($6::text IS NULL OR r.ai_platform_id = $6)
|
||||
`, tenantID, brandID, monitoringCollectorType, startDate.Format("2006-01-02"), endDate.Format("2006-01-02"), nullableString(aiPlatformID))
|
||||
if err != nil {
|
||||
return metrics, response.ErrInternal(50041, "query_failed", "failed to load monitoring derived parse metrics")
|
||||
}
|
||||
@@ -1092,7 +1107,18 @@ func decodeMonitoringMatchedBrandTerms(raw []byte) []string {
|
||||
return normalizeMonitoringBrandTermList(items)
|
||||
}
|
||||
|
||||
func (s *MonitoringService) loadOverview(ctx context.Context, tenantID, brandID int64, startDate, endDate time.Time, collectionMode string, derived monitoringDerivedMetrics) (MonitoringOverview, time.Time, error) {
|
||||
func (s *MonitoringService) loadOverview(
|
||||
ctx context.Context,
|
||||
tenantID, brandID int64,
|
||||
startDate, endDate time.Time,
|
||||
collectionMode string,
|
||||
aiPlatformID *string,
|
||||
derived monitoringDerivedMetrics,
|
||||
) (MonitoringOverview, time.Time, error) {
|
||||
if platformID := normalizedOptionalString(aiPlatformID); platformID != "" {
|
||||
return s.loadOverviewForPlatform(ctx, tenantID, brandID, startDate, endDate, collectionMode, platformID, derived)
|
||||
}
|
||||
|
||||
overview := MonitoringOverview{
|
||||
CollectionMode: collectionMode,
|
||||
ConfidenceLevel: "low",
|
||||
@@ -1130,6 +1156,59 @@ func (s *MonitoringService) loadOverview(ctx context.Context, tenantID, brandID
|
||||
return overview, latestDate, nil
|
||||
}
|
||||
|
||||
func (s *MonitoringService) loadOverviewForPlatform(
|
||||
ctx context.Context,
|
||||
tenantID, brandID int64,
|
||||
startDate, endDate time.Time,
|
||||
collectionMode string,
|
||||
aiPlatformID string,
|
||||
derived monitoringDerivedMetrics,
|
||||
) (MonitoringOverview, time.Time, error) {
|
||||
overview := MonitoringOverview{
|
||||
CollectionMode: collectionMode,
|
||||
ConfidenceLevel: "low",
|
||||
}
|
||||
latestDate := endDate
|
||||
|
||||
current, found, err := s.loadPlatformOverviewSnapshotForDate(ctx, tenantID, brandID, aiPlatformID, collectionMode, endDate)
|
||||
if err != nil {
|
||||
return overview, latestDate, err
|
||||
}
|
||||
if !found {
|
||||
return overview, latestDate, nil
|
||||
}
|
||||
|
||||
applyOverviewSnapshot(&overview, current, derived)
|
||||
if current.ActualSampleCount <= 0 {
|
||||
return overview, latestDate, nil
|
||||
}
|
||||
|
||||
latestDate = current.Date
|
||||
previous, foundPrevious, err := s.loadLatestSampledPlatformOverviewSnapshot(
|
||||
ctx,
|
||||
tenantID,
|
||||
brandID,
|
||||
aiPlatformID,
|
||||
collectionMode,
|
||||
startDate,
|
||||
current.Date.AddDate(0, 0, -1),
|
||||
)
|
||||
if err != nil {
|
||||
return overview, latestDate, err
|
||||
}
|
||||
if !foundPrevious {
|
||||
return overview, latestDate, nil
|
||||
}
|
||||
|
||||
applyDerivedRatesToOverviewSnapshot(&previous, derived)
|
||||
overview.PrevSnapshotMentionRate = floatPointer(previous.MentionRate)
|
||||
overview.PrevSnapshotTop1MentionRate = floatPointer(previous.Top1MentionRate)
|
||||
overview.PrevSnapshotFirstRecommendRate = floatPointer(previous.FirstRecommendRate)
|
||||
overview.PrevSnapshotPositiveMentionRate = floatPointer(previous.PositiveMentionRate)
|
||||
|
||||
return overview, latestDate, nil
|
||||
}
|
||||
|
||||
type monitoringOverviewSnapshot struct {
|
||||
Date time.Time
|
||||
DesiredSampleCount int64
|
||||
@@ -1247,6 +1326,103 @@ func (s *MonitoringService) loadLatestSampledOverviewSnapshot(ctx context.Contex
|
||||
return item, true, nil
|
||||
}
|
||||
|
||||
func (s *MonitoringService) loadPlatformOverviewSnapshotForDate(
|
||||
ctx context.Context,
|
||||
tenantID, brandID int64,
|
||||
aiPlatformID string,
|
||||
collectionMode string,
|
||||
businessDate time.Time,
|
||||
) (monitoringOverviewSnapshot, bool, error) {
|
||||
var item monitoringOverviewSnapshot
|
||||
err := s.monitoringPool.QueryRow(ctx, `
|
||||
SELECT
|
||||
business_date,
|
||||
planned_sample_count,
|
||||
actual_sample_count,
|
||||
mention_rate::double precision,
|
||||
top1_mention_rate::double precision,
|
||||
last_sampled_at
|
||||
FROM monitoring_brand_platform_daily
|
||||
WHERE tenant_id = $1
|
||||
AND brand_id = $2
|
||||
AND ai_platform_id = $3
|
||||
AND collector_type = $4
|
||||
AND business_date = $5::date
|
||||
`, tenantID, brandID, aiPlatformID, collectionMode, businessDate.Format("2006-01-02")).Scan(
|
||||
&item.Date,
|
||||
&item.PlannedSampleCount,
|
||||
&item.ActualSampleCount,
|
||||
&item.MentionRate,
|
||||
&item.Top1MentionRate,
|
||||
&item.SnapshotUpdatedAt,
|
||||
)
|
||||
if err != nil {
|
||||
if err == pgx.ErrNoRows {
|
||||
return monitoringOverviewSnapshot{}, false, nil
|
||||
}
|
||||
return monitoringOverviewSnapshot{}, false, response.ErrInternal(50041, "query_failed", "failed to load monitoring platform overview")
|
||||
}
|
||||
|
||||
coverageRate := divideAsPointer(item.ActualSampleCount, item.PlannedSampleCount)
|
||||
item.DesiredSampleCount = item.PlannedSampleCount
|
||||
item.CoverageRate = pointerFloat64ToNull(coverageRate)
|
||||
item.ConfidenceLevel = deriveMonitoringConfidenceLevel(item.ActualSampleCount, item.PlannedSampleCount, coverageRate)
|
||||
|
||||
return item, true, nil
|
||||
}
|
||||
|
||||
func (s *MonitoringService) loadLatestSampledPlatformOverviewSnapshot(
|
||||
ctx context.Context,
|
||||
tenantID, brandID int64,
|
||||
aiPlatformID string,
|
||||
collectionMode string,
|
||||
startDate, endDate time.Time,
|
||||
) (monitoringOverviewSnapshot, bool, error) {
|
||||
if endDate.Before(startDate) {
|
||||
return monitoringOverviewSnapshot{}, false, nil
|
||||
}
|
||||
|
||||
var item monitoringOverviewSnapshot
|
||||
err := s.monitoringPool.QueryRow(ctx, `
|
||||
SELECT
|
||||
business_date,
|
||||
planned_sample_count,
|
||||
actual_sample_count,
|
||||
mention_rate::double precision,
|
||||
top1_mention_rate::double precision,
|
||||
last_sampled_at
|
||||
FROM monitoring_brand_platform_daily
|
||||
WHERE tenant_id = $1
|
||||
AND brand_id = $2
|
||||
AND ai_platform_id = $3
|
||||
AND collector_type = $4
|
||||
AND business_date BETWEEN $5::date AND $6::date
|
||||
AND actual_sample_count > 0
|
||||
ORDER BY business_date DESC
|
||||
LIMIT 1
|
||||
`, tenantID, brandID, aiPlatformID, collectionMode, startDate.Format("2006-01-02"), endDate.Format("2006-01-02")).Scan(
|
||||
&item.Date,
|
||||
&item.PlannedSampleCount,
|
||||
&item.ActualSampleCount,
|
||||
&item.MentionRate,
|
||||
&item.Top1MentionRate,
|
||||
&item.SnapshotUpdatedAt,
|
||||
)
|
||||
if err != nil {
|
||||
if err == pgx.ErrNoRows {
|
||||
return monitoringOverviewSnapshot{}, false, nil
|
||||
}
|
||||
return monitoringOverviewSnapshot{}, false, response.ErrInternal(50041, "query_failed", "failed to load monitoring platform overview")
|
||||
}
|
||||
|
||||
coverageRate := divideAsPointer(item.ActualSampleCount, item.PlannedSampleCount)
|
||||
item.DesiredSampleCount = item.PlannedSampleCount
|
||||
item.CoverageRate = pointerFloat64ToNull(coverageRate)
|
||||
item.ConfidenceLevel = deriveMonitoringConfidenceLevel(item.ActualSampleCount, item.PlannedSampleCount, coverageRate)
|
||||
|
||||
return item, true, nil
|
||||
}
|
||||
|
||||
func applyOverviewSnapshot(overview *MonitoringOverview, snapshot monitoringOverviewSnapshot, derived monitoringDerivedMetrics) {
|
||||
if overview == nil {
|
||||
return
|
||||
@@ -1298,7 +1474,18 @@ func applyDerivedRatesToOverviewSnapshot(snapshot *monitoringOverviewSnapshot, d
|
||||
}
|
||||
}
|
||||
|
||||
func (s *MonitoringService) loadTimeBuckets(ctx context.Context, tenantID, brandID int64, startDate, endDate time.Time, collectionMode string, derived monitoringDerivedMetrics) ([]MonitoringTimeBucket, error) {
|
||||
func (s *MonitoringService) loadTimeBuckets(
|
||||
ctx context.Context,
|
||||
tenantID, brandID int64,
|
||||
startDate, endDate time.Time,
|
||||
collectionMode string,
|
||||
aiPlatformID *string,
|
||||
derived monitoringDerivedMetrics,
|
||||
) ([]MonitoringTimeBucket, error) {
|
||||
if platformID := normalizedOptionalString(aiPlatformID); platformID != "" {
|
||||
return s.loadTimeBucketsForPlatform(ctx, tenantID, brandID, startDate, endDate, collectionMode, platformID, derived)
|
||||
}
|
||||
|
||||
rows, err := s.monitoringPool.Query(ctx, `
|
||||
SELECT
|
||||
business_date,
|
||||
@@ -1402,6 +1589,112 @@ func (s *MonitoringService) loadTimeBuckets(ctx context.Context, tenantID, brand
|
||||
return buckets, nil
|
||||
}
|
||||
|
||||
func (s *MonitoringService) loadTimeBucketsForPlatform(
|
||||
ctx context.Context,
|
||||
tenantID, brandID int64,
|
||||
startDate, endDate time.Time,
|
||||
collectionMode string,
|
||||
aiPlatformID string,
|
||||
derived monitoringDerivedMetrics,
|
||||
) ([]MonitoringTimeBucket, error) {
|
||||
rows, err := s.monitoringPool.Query(ctx, `
|
||||
SELECT
|
||||
business_date,
|
||||
platform_sample_status,
|
||||
mention_rate::double precision,
|
||||
top1_mention_rate::double precision,
|
||||
planned_sample_count,
|
||||
actual_sample_count,
|
||||
last_sampled_at
|
||||
FROM monitoring_brand_platform_daily
|
||||
WHERE tenant_id = $1
|
||||
AND brand_id = $2
|
||||
AND ai_platform_id = $3
|
||||
AND collector_type = $4
|
||||
AND business_date BETWEEN $5::date AND $6::date
|
||||
ORDER BY business_date ASC
|
||||
`, tenantID, brandID, aiPlatformID, collectionMode, startDate.Format("2006-01-02"), endDate.Format("2006-01-02"))
|
||||
if err != nil {
|
||||
return nil, response.ErrInternal(50041, "query_failed", "failed to load monitoring platform time buckets")
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
type bucketRow struct {
|
||||
Date time.Time
|
||||
SampleStatus string
|
||||
MentionRate sql.NullFloat64
|
||||
Top1MentionRate sql.NullFloat64
|
||||
FirstRecommendRate sql.NullFloat64
|
||||
PositiveMentionRate sql.NullFloat64
|
||||
PlannedSampleCount int64
|
||||
ActualSampleCount int64
|
||||
SnapshotUpdatedAt sql.NullTime
|
||||
}
|
||||
|
||||
bucketMap := make(map[string]bucketRow)
|
||||
for rows.Next() {
|
||||
var item bucketRow
|
||||
if scanErr := rows.Scan(
|
||||
&item.Date,
|
||||
&item.SampleStatus,
|
||||
&item.MentionRate,
|
||||
&item.Top1MentionRate,
|
||||
&item.PlannedSampleCount,
|
||||
&item.ActualSampleCount,
|
||||
&item.SnapshotUpdatedAt,
|
||||
); scanErr != nil {
|
||||
return nil, response.ErrInternal(50041, "scan_failed", "failed to parse monitoring platform time buckets")
|
||||
}
|
||||
bucketMap[item.Date.Format("2006-01-02")] = item
|
||||
}
|
||||
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, response.ErrInternal(50041, "scan_failed", "failed to iterate monitoring platform time buckets")
|
||||
}
|
||||
|
||||
buckets := make([]MonitoringTimeBucket, 0, int(endDate.Sub(startDate).Hours()/24)+1)
|
||||
for cursor := startDate; !cursor.After(endDate); cursor = cursor.AddDate(0, 0, 1) {
|
||||
key := cursor.Format("2006-01-02")
|
||||
if item, ok := bucketMap[key]; ok {
|
||||
if platformStatsByDate, ok := derived.ByDatePlatform[key]; ok {
|
||||
if platformStats, ok := platformStatsByDate[aiPlatformID]; ok && platformStats.hasSamples() {
|
||||
item.MentionRate = pointerFloat64ToNull(platformStats.mentionRate())
|
||||
item.Top1MentionRate = pointerFloat64ToNull(platformStats.top1MentionRate())
|
||||
}
|
||||
}
|
||||
if stats, ok := derived.ByDate[key]; ok && stats.hasSamples() {
|
||||
item.FirstRecommendRate = pointerFloat64ToNull(stats.firstRecommendRate())
|
||||
item.PositiveMentionRate = pointerFloat64ToNull(stats.positiveMentionRate())
|
||||
}
|
||||
|
||||
coverageRate := divideAsPointer(item.ActualSampleCount, item.PlannedSampleCount)
|
||||
buckets = append(buckets, MonitoringTimeBucket{
|
||||
Date: key,
|
||||
SampleStatus: item.SampleStatus,
|
||||
MetricValue: floatPointer(item.MentionRate),
|
||||
MentionRate: floatPointer(item.MentionRate),
|
||||
Top1MentionRate: floatPointer(item.Top1MentionRate),
|
||||
FirstRecommendRate: floatPointer(item.FirstRecommendRate),
|
||||
PositiveMentionRate: floatPointer(item.PositiveMentionRate),
|
||||
CitationRate: nil,
|
||||
PlannedSampleCount: item.PlannedSampleCount,
|
||||
ActualSampleCount: item.ActualSampleCount,
|
||||
CoverageRate: coverageRate,
|
||||
TriggerSource: nil,
|
||||
SnapshotUpdatedAt: formatNullTime(item.SnapshotUpdatedAt),
|
||||
})
|
||||
continue
|
||||
}
|
||||
|
||||
buckets = append(buckets, MonitoringTimeBucket{
|
||||
Date: key,
|
||||
SampleStatus: "unsampled",
|
||||
})
|
||||
}
|
||||
|
||||
return buckets, nil
|
||||
}
|
||||
|
||||
func (s *MonitoringService) loadAccessStates(ctx context.Context, tenantID int64, installationID *int64, businessDate time.Time) (map[string]monitoringAccessState, error) {
|
||||
states := map[string]monitoringAccessState{}
|
||||
if installationID == nil {
|
||||
@@ -1507,7 +1800,14 @@ func (s *MonitoringService) loadPlatformBreakdown(ctx context.Context, tenantID,
|
||||
return breakdown, nil
|
||||
}
|
||||
|
||||
func (s *MonitoringService) loadHotQuestions(ctx context.Context, tenantID, brandID int64, questions []monitoringConfiguredQuestion, businessDate time.Time, derived monitoringDerivedMetrics) ([]MonitoringHotQuestion, error) {
|
||||
func (s *MonitoringService) loadHotQuestions(
|
||||
ctx context.Context,
|
||||
tenantID, brandID int64,
|
||||
questions []monitoringConfiguredQuestion,
|
||||
businessDate time.Time,
|
||||
aiPlatformID *string,
|
||||
derived monitoringDerivedMetrics,
|
||||
) ([]MonitoringHotQuestion, error) {
|
||||
if len(questions) == 0 {
|
||||
return []MonitoringHotQuestion{}, nil
|
||||
}
|
||||
@@ -1530,6 +1830,7 @@ func (s *MonitoringService) loadHotQuestions(ctx context.Context, tenantID, bran
|
||||
AND r.status = 'succeeded'
|
||||
AND r.business_date = $4::date
|
||||
AND r.question_id = ANY($5)
|
||||
AND ($6::text IS NULL OR r.ai_platform_id = $6)
|
||||
GROUP BY r.question_id
|
||||
),
|
||||
latest_hash AS (
|
||||
@@ -1543,6 +1844,7 @@ func (s *MonitoringService) loadHotQuestions(ctx context.Context, tenantID, bran
|
||||
AND r.status = 'succeeded'
|
||||
AND r.business_date = $4::date
|
||||
AND r.question_id = ANY($5)
|
||||
AND ($6::text IS NULL OR r.ai_platform_id = $6)
|
||||
ORDER BY r.question_id, r.business_date DESC, COALESCE(r.completed_at, r.updated_at, r.created_at) DESC NULLS LAST, r.id DESC
|
||||
)
|
||||
SELECT
|
||||
@@ -1552,7 +1854,7 @@ func (s *MonitoringService) loadHotQuestions(ctx context.Context, tenantID, bran
|
||||
s.last_sampled_at
|
||||
FROM stats s
|
||||
LEFT JOIN latest_hash h ON h.question_id = s.question_id
|
||||
`, tenantID, brandID, monitoringCollectorType, selectedDate, questionIDs)
|
||||
`, tenantID, brandID, monitoringCollectorType, selectedDate, questionIDs, nullableString(aiPlatformID))
|
||||
if err != nil {
|
||||
return nil, response.ErrInternal(50041, "query_failed", "failed to load hot questions")
|
||||
}
|
||||
@@ -1603,7 +1905,14 @@ func (s *MonitoringService) loadHotQuestions(ctx context.Context, tenantID, bran
|
||||
return items, nil
|
||||
}
|
||||
|
||||
func (s *MonitoringService) loadCitationRanking(ctx context.Context, tenantID, brandID int64, questionIDs []int64, startDate, endDate time.Time, accessStates map[string]monitoringAccessState) ([]MonitoringCitationRanking, error) {
|
||||
func (s *MonitoringService) loadCitationRanking(
|
||||
ctx context.Context,
|
||||
tenantID, brandID int64,
|
||||
questionIDs []int64,
|
||||
startDate, endDate time.Time,
|
||||
accessStates map[string]monitoringAccessState,
|
||||
aiPlatformID *string,
|
||||
) ([]MonitoringCitationRanking, error) {
|
||||
if questionIDs != nil && len(questionIDs) == 0 {
|
||||
return []MonitoringCitationRanking{}, nil
|
||||
}
|
||||
@@ -1620,6 +1929,7 @@ func (s *MonitoringService) loadCitationRanking(ctx context.Context, tenantID, b
|
||||
AND r.status = 'succeeded'
|
||||
AND r.business_date BETWEEN $4::date AND $5::date
|
||||
AND ($6::bigint[] IS NULL OR r.question_id = ANY($6))
|
||||
AND ($7::text IS NULL OR r.ai_platform_id = $7)
|
||||
GROUP BY r.ai_platform_id
|
||||
),
|
||||
citation_counts AS (
|
||||
@@ -1636,6 +1946,7 @@ func (s *MonitoringService) loadCitationRanking(ctx context.Context, tenantID, b
|
||||
AND r.status = 'succeeded'
|
||||
AND r.business_date BETWEEN $4::date AND $5::date
|
||||
AND ($6::bigint[] IS NULL OR r.question_id = ANY($6))
|
||||
AND ($7::text IS NULL OR r.ai_platform_id = $7)
|
||||
GROUP BY r.ai_platform_id
|
||||
)
|
||||
SELECT
|
||||
@@ -1650,7 +1961,7 @@ func (s *MonitoringService) loadCitationRanking(ctx context.Context, tenantID, b
|
||||
LEFT JOIN citation_counts cc ON cc.ai_platform_id = rc.ai_platform_id
|
||||
WHERE COALESCE(cc.cited_answer_count, 0) > 0
|
||||
ORDER BY cited_answer_count DESC, cited_article_count DESC, rc.ai_platform_id ASC
|
||||
`, tenantID, brandID, monitoringCollectorType, startDate.Format("2006-01-02"), endDate.Format("2006-01-02"), nullableInt64Array(questionIDs))
|
||||
`, tenantID, brandID, monitoringCollectorType, startDate.Format("2006-01-02"), endDate.Format("2006-01-02"), nullableInt64Array(questionIDs), nullableString(aiPlatformID))
|
||||
if err != nil {
|
||||
return nil, response.ErrInternal(50041, "query_failed", "failed to load citation ranking")
|
||||
}
|
||||
@@ -1678,7 +1989,13 @@ func (s *MonitoringService) loadCitationRanking(ctx context.Context, tenantID, b
|
||||
return items, nil
|
||||
}
|
||||
|
||||
func (s *MonitoringService) loadCitedArticles(ctx context.Context, tenantID, brandID int64, questionIDs []int64, startDate, endDate time.Time) ([]MonitoringCitedArticle, error) {
|
||||
func (s *MonitoringService) loadCitedArticles(
|
||||
ctx context.Context,
|
||||
tenantID, brandID int64,
|
||||
questionIDs []int64,
|
||||
startDate, endDate time.Time,
|
||||
aiPlatformID *string,
|
||||
) ([]MonitoringCitedArticle, error) {
|
||||
if questionIDs != nil && len(questionIDs) == 0 {
|
||||
return []MonitoringCitedArticle{}, nil
|
||||
}
|
||||
@@ -1693,7 +2010,8 @@ func (s *MonitoringService) loadCitedArticles(ctx context.Context, tenantID, bra
|
||||
AND r.status = 'succeeded'
|
||||
AND r.business_date BETWEEN $4::date AND $5::date
|
||||
AND ($6::bigint[] IS NULL OR r.question_id = ANY($6))
|
||||
`, tenantID, brandID, monitoringCollectorType, startDate.Format("2006-01-02"), endDate.Format("2006-01-02"), nullableInt64Array(questionIDs)).Scan(&totalSampleCount); err != nil {
|
||||
AND ($7::text IS NULL OR r.ai_platform_id = $7)
|
||||
`, tenantID, brandID, monitoringCollectorType, startDate.Format("2006-01-02"), endDate.Format("2006-01-02"), nullableInt64Array(questionIDs), nullableString(aiPlatformID)).Scan(&totalSampleCount); err != nil {
|
||||
return nil, response.ErrInternal(50041, "query_failed", "failed to count cited article denominator")
|
||||
}
|
||||
|
||||
@@ -1715,10 +2033,11 @@ func (s *MonitoringService) loadCitedArticles(ctx context.Context, tenantID, bra
|
||||
AND r.status = 'succeeded'
|
||||
AND r.business_date BETWEEN $4::date AND $5::date
|
||||
AND ($6::bigint[] IS NULL OR r.question_id = ANY($6))
|
||||
AND ($7::text IS NULL OR r.ai_platform_id = $7)
|
||||
GROUP BY cf.article_id
|
||||
ORDER BY citation_count DESC, cf.article_id ASC
|
||||
LIMIT 10
|
||||
`, tenantID, brandID, monitoringCollectorType, startDate.Format("2006-01-02"), endDate.Format("2006-01-02"), nullableInt64Array(questionIDs))
|
||||
`, tenantID, brandID, monitoringCollectorType, startDate.Format("2006-01-02"), endDate.Format("2006-01-02"), nullableInt64Array(questionIDs), nullableString(aiPlatformID))
|
||||
if err != nil {
|
||||
return nil, response.ErrInternal(50041, "query_failed", "failed to load cited articles")
|
||||
}
|
||||
@@ -2297,6 +2616,28 @@ func resolveMonitoringPlatforms(enabled []string) []monitoringPlatformMetadata {
|
||||
return platforms
|
||||
}
|
||||
|
||||
func filterMonitoringPlatforms(platforms []monitoringPlatformMetadata, aiPlatformID *string) []monitoringPlatformMetadata {
|
||||
platformID := normalizedOptionalString(aiPlatformID)
|
||||
if platformID == "" {
|
||||
return platforms
|
||||
}
|
||||
|
||||
filtered := make([]monitoringPlatformMetadata, 0, 1)
|
||||
for _, platform := range platforms {
|
||||
if platform.ID == platformID {
|
||||
filtered = append(filtered, platform)
|
||||
return filtered
|
||||
}
|
||||
}
|
||||
|
||||
return []monitoringPlatformMetadata{
|
||||
{
|
||||
ID: platformID,
|
||||
Name: platformDisplayName(platformID),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func platformDisplayName(platformID string) string {
|
||||
if name, ok := monitoringPlatformNames[platformID]; ok {
|
||||
return name
|
||||
@@ -2307,6 +2648,13 @@ func platformDisplayName(platformID string) string {
|
||||
return strings.ToUpper(platformID)
|
||||
}
|
||||
|
||||
func normalizedOptionalString(value *string) string {
|
||||
if value == nil {
|
||||
return ""
|
||||
}
|
||||
return strings.TrimSpace(*value)
|
||||
}
|
||||
|
||||
func derivePlatformSampleStatus(rawStatus string, accessState monitoringAccessState) string {
|
||||
switch strings.TrimSpace(rawStatus) {
|
||||
case "sampled":
|
||||
|
||||
@@ -65,3 +65,31 @@ func TestEncodeMonitoringProjectionRebuildEnvelopePreservesBusinessDate(t *testi
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "2026-04-12", envelope.BusinessDate)
|
||||
}
|
||||
|
||||
func TestFilterMonitoringPlatformsReturnsSelectedPlatform(t *testing.T) {
|
||||
platforms := []monitoringPlatformMetadata{
|
||||
{ID: "deepseek", Name: "DeepSeek"},
|
||||
{ID: "qwen", Name: "通义千问"},
|
||||
{ID: "doubao", Name: "豆包"},
|
||||
}
|
||||
selected := "qwen"
|
||||
|
||||
filtered := filterMonitoringPlatforms(platforms, &selected)
|
||||
|
||||
assert.Equal(t, []monitoringPlatformMetadata{
|
||||
{ID: "qwen", Name: "通义千问"},
|
||||
}, filtered)
|
||||
}
|
||||
|
||||
func TestFilterMonitoringPlatformsFallsBackToDisplayName(t *testing.T) {
|
||||
platforms := []monitoringPlatformMetadata{
|
||||
{ID: "deepseek", Name: "DeepSeek"},
|
||||
}
|
||||
selected := "doubao"
|
||||
|
||||
filtered := filterMonitoringPlatforms(platforms, &selected)
|
||||
|
||||
assert.Equal(t, []monitoringPlatformMetadata{
|
||||
{ID: "doubao", Name: "豆包"},
|
||||
}, filtered)
|
||||
}
|
||||
|
||||
@@ -3,6 +3,8 @@ package app
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/geo-platform/tenant-api/internal/tenant/prompts"
|
||||
)
|
||||
|
||||
func TestBuildPromptContextUsesChineseLabels(t *testing.T) {
|
||||
@@ -45,7 +47,8 @@ func TestBuildGenerationLengthGuidanceForEnglishLocaleIsWrittenInChinese(t *test
|
||||
}
|
||||
|
||||
func TestBuildGenerationPromptAddsTopXBrandPriorityRules(t *testing.T) {
|
||||
prompt := buildGenerationPrompt("top_x_article", "Top X 推荐文章", nil, map[string]interface{}{
|
||||
promptTemplate, _ := prompts.ApplyPlatformTemplatePromptOverrides("top_x_article", nil, []byte(`{}`))
|
||||
prompt := buildGenerationPrompt("top_x_article", "Top X 推荐文章", promptTemplate, map[string]interface{}{
|
||||
"topic": "合肥全屋定制",
|
||||
"brand_name": "安徽海翔家居用品销售有限公司",
|
||||
"locale": "zh-CN",
|
||||
@@ -54,14 +57,54 @@ func TestBuildGenerationPromptAddsTopXBrandPriorityRules(t *testing.T) {
|
||||
for _, expected := range []string{
|
||||
"模板专项要求:",
|
||||
"安徽海翔家居用品销售有限公司",
|
||||
"排在第 1 位",
|
||||
"篇幅应明显多于其他对象",
|
||||
"结论部分先明确推荐",
|
||||
"首个重点分析章节",
|
||||
"顺序不代表评价或排名",
|
||||
"不得表述为第一名",
|
||||
"必须把其一级节点逐一写成正文 H2 小标题",
|
||||
"严格按给定顺序展开",
|
||||
} {
|
||||
if !strings.Contains(prompt, expected) {
|
||||
t.Fatalf("buildGenerationPrompt() = %q, want %q", prompt, expected)
|
||||
}
|
||||
}
|
||||
|
||||
for _, unexpected := range []string{
|
||||
"排在第 1 位",
|
||||
"主推对象",
|
||||
} {
|
||||
if strings.Contains(prompt, unexpected) {
|
||||
t.Fatalf("buildGenerationPrompt() = %q, should not contain legacy ranking copy %q", prompt, unexpected)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildGenerationPromptTopXRepeatsOutlineExecutionRulesInBasePrompt(t *testing.T) {
|
||||
promptTemplate, _ := prompts.ApplyPlatformTemplatePromptOverrides("top_x_article", nil, []byte(`{}`))
|
||||
prompt := buildGenerationPrompt("top_x_article", "Top X 推荐文章", promptTemplate, map[string]interface{}{
|
||||
"topic": "合肥全屋定制",
|
||||
"locale": "zh-CN",
|
||||
"article_outline": []interface{}{
|
||||
map[string]interface{}{
|
||||
"outline": "行业现状与选择思路",
|
||||
"children": []interface{}{
|
||||
map[string]interface{}{"outline": "当前需求变化"},
|
||||
},
|
||||
},
|
||||
map[string]interface{}{"outline": "品牌观察"},
|
||||
map[string]interface{}{"outline": "总结建议"},
|
||||
},
|
||||
}, "")
|
||||
|
||||
for _, expected := range []string{
|
||||
"如当前上下文提供了 article_outline",
|
||||
"不得调换、合并、跳过",
|
||||
"不要自行新增同级章节",
|
||||
"不要另起一套脱离大纲的总结结构",
|
||||
} {
|
||||
if !strings.Contains(prompt, expected) {
|
||||
t.Fatalf("buildGenerationPrompt() = %q, want outline execution rule %q", prompt, expected)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildGenerationPromptDoesNotAddTopXBrandRulesForOtherTemplates(t *testing.T) {
|
||||
@@ -70,7 +113,7 @@ func TestBuildGenerationPromptDoesNotAddTopXBrandRulesForOtherTemplates(t *testi
|
||||
"brand_name": "安徽海翔家居用品销售有限公司",
|
||||
}, "")
|
||||
|
||||
if strings.Contains(prompt, "排在第 1 位") {
|
||||
if strings.Contains(prompt, "首个重点分析章节") {
|
||||
t.Fatalf("buildGenerationPrompt() = %q, should not include top-x ranking rules", prompt)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user