6066f43a7d
- Implement monitoring service with heartbeat, lease tasks, resume tasks, and task result handling. - Create monitoring time utilities for business date calculations. - Add unit tests for date window resolution and business day handling. - Define database schema for monitoring-related tables including quotas, daily reports, and task management. - Establish migration scripts for creating and dropping monitoring tables.
174 lines
5.6 KiB
Go
174 lines
5.6 KiB
Go
package app
|
||
|
||
import (
|
||
"context"
|
||
"encoding/json"
|
||
"fmt"
|
||
"strings"
|
||
"time"
|
||
|
||
sharedllm "github.com/geo-platform/tenant-api/internal/shared/llm"
|
||
)
|
||
|
||
const (
|
||
monitoringAnswerParseLLMModel = "doubao-seed-2-0-lite-260215"
|
||
monitoringAnswerParseLLMTimeout = 30 * time.Second
|
||
monitoringAnswerParseLLMMaxOutputTokens = 600
|
||
)
|
||
|
||
var monitoringAnswerParseLLMSchema = []byte(`{
|
||
"type": "object",
|
||
"additionalProperties": false,
|
||
"required": [
|
||
"brand_mentioned",
|
||
"brand_mention_position",
|
||
"first_recommended",
|
||
"sentiment_label",
|
||
"matched_brand_terms"
|
||
],
|
||
"properties": {
|
||
"brand_mentioned": {
|
||
"type": "boolean"
|
||
},
|
||
"brand_mention_position": {
|
||
"type": "string",
|
||
"enum": ["top1", "mentioned", "not_mentioned"]
|
||
},
|
||
"first_recommended": {
|
||
"type": "boolean"
|
||
},
|
||
"sentiment_label": {
|
||
"type": "string",
|
||
"enum": ["positive", "neutral", "negative", "unknown"]
|
||
},
|
||
"matched_brand_terms": {
|
||
"type": "array",
|
||
"items": {
|
||
"type": "string"
|
||
}
|
||
}
|
||
}
|
||
}`)
|
||
|
||
type monitoringAnswerLLMParsePayload struct {
|
||
BrandMentioned bool `json:"brand_mentioned"`
|
||
BrandMentionPosition string `json:"brand_mention_position"`
|
||
FirstRecommended bool `json:"first_recommended"`
|
||
SentimentLabel string `json:"sentiment_label"`
|
||
MatchedBrandTerms []string `json:"matched_brand_terms"`
|
||
}
|
||
|
||
func parseMonitoringAnswerWithLLM(
|
||
ctx context.Context,
|
||
client sharedllm.Client,
|
||
answer string,
|
||
brandName string,
|
||
) (monitoringAnswerParseSummary, error) {
|
||
answer = strings.TrimSpace(answer)
|
||
brandName = strings.TrimSpace(brandName)
|
||
if answer == "" {
|
||
return monitoringAnswerParseSummary{}, fmt.Errorf("answer is empty")
|
||
}
|
||
if brandName == "" {
|
||
return monitoringAnswerParseSummary{}, fmt.Errorf("brand name is empty")
|
||
}
|
||
|
||
result, err := client.Generate(ctx, sharedllm.GenerateRequest{
|
||
Model: monitoringAnswerParseLLMModel,
|
||
Prompt: buildMonitoringAnswerParsePrompt(answer, brandName),
|
||
Timeout: monitoringAnswerParseLLMTimeout,
|
||
MaxOutputTokens: monitoringAnswerParseLLMMaxOutputTokens,
|
||
ResponseFormat: &sharedllm.ResponseFormat{
|
||
Type: sharedllm.ResponseFormatTypeJSONSchema,
|
||
Name: "monitoring_answer_parse",
|
||
Description: "Parse brand mention signals from an AI answer for brand monitoring.",
|
||
SchemaJSON: monitoringAnswerParseLLMSchema,
|
||
Strict: true,
|
||
},
|
||
}, nil)
|
||
if err != nil {
|
||
return monitoringAnswerParseSummary{}, err
|
||
}
|
||
|
||
return decodeMonitoringAnswerLLMParseResult(result.Content)
|
||
}
|
||
|
||
func buildMonitoringAnswerParsePrompt(answer string, brandName string) string {
|
||
var builder strings.Builder
|
||
builder.WriteString("你是品牌监测解析助手。\n")
|
||
builder.WriteString("请只基于给定回答内容,判断目标品牌在回答中的提及、排序、推荐与情感,不要猜测回答外的信息。\n")
|
||
builder.WriteString("\n判断规则:\n")
|
||
builder.WriteString("1. brand_mentioned:回答中是否明确提到目标品牌,或可明确判断是在指代该品牌。\n")
|
||
builder.WriteString("2. brand_mention_position:\n")
|
||
builder.WriteString(" - top1:目标品牌被排在第一位,或被明确表述为首选/最推荐。\n")
|
||
builder.WriteString(" - mentioned:目标品牌被提到,但不是第一位。\n")
|
||
builder.WriteString(" - not_mentioned:未提到目标品牌。\n")
|
||
builder.WriteString("3. first_recommended:是否明确把目标品牌作为首选推荐。\n")
|
||
builder.WriteString("4. sentiment_label:结合品牌相关表述判断 positive / neutral / negative / unknown。\n")
|
||
builder.WriteString("5. matched_brand_terms:把回答里实际出现、并用于指代该品牌的词语原样列出;没有就返回空数组。\n")
|
||
builder.WriteString("\n只返回 JSON。\n")
|
||
builder.WriteString("\n目标品牌:")
|
||
builder.WriteString(brandName)
|
||
builder.WriteString("\n回答内容:\n")
|
||
builder.WriteString(answer)
|
||
return builder.String()
|
||
}
|
||
|
||
func decodeMonitoringAnswerLLMParseResult(raw string) (monitoringAnswerParseSummary, error) {
|
||
var lastErr error
|
||
for _, candidate := range extractJSONCandidates(raw) {
|
||
var payload monitoringAnswerLLMParsePayload
|
||
if err := json.Unmarshal([]byte(candidate), &payload); err != nil {
|
||
lastErr = err
|
||
continue
|
||
}
|
||
return monitoringAnswerParseSummary{
|
||
BrandMentioned: payload.BrandMentioned,
|
||
BrandMentionPosition: normalizeMonitoringBrandMentionPosition(payload.BrandMentionPosition, payload.BrandMentioned),
|
||
FirstRecommended: payload.FirstRecommended,
|
||
SentimentLabel: normalizeMonitoringSentiment(payload.SentimentLabel, payload.BrandMentioned),
|
||
MatchedBrandTerms: normalizeMonitoringBrandTermList(payload.MatchedBrandTerms),
|
||
}, nil
|
||
}
|
||
if lastErr == nil {
|
||
lastErr = fmt.Errorf("empty content")
|
||
}
|
||
return monitoringAnswerParseSummary{}, fmt.Errorf("decode monitoring answer parse result: %w", lastErr)
|
||
}
|
||
|
||
func normalizeMonitoringBrandMentionPosition(value string, brandMentioned bool) string {
|
||
switch strings.ToLower(strings.TrimSpace(value)) {
|
||
case "top1":
|
||
if !brandMentioned {
|
||
return "not_mentioned"
|
||
}
|
||
return "top1"
|
||
case "mentioned":
|
||
if !brandMentioned {
|
||
return "not_mentioned"
|
||
}
|
||
return "mentioned"
|
||
case "not_mentioned":
|
||
return "not_mentioned"
|
||
default:
|
||
if brandMentioned {
|
||
return "mentioned"
|
||
}
|
||
return "not_mentioned"
|
||
}
|
||
}
|
||
|
||
func normalizeMonitoringSentiment(value string, brandMentioned bool) string {
|
||
switch strings.ToLower(strings.TrimSpace(value)) {
|
||
case "positive", "neutral", "negative":
|
||
return strings.ToLower(strings.TrimSpace(value))
|
||
case "unknown":
|
||
return "unknown"
|
||
default:
|
||
if brandMentioned {
|
||
return "neutral"
|
||
}
|
||
return "unknown"
|
||
}
|
||
}
|