Add monitoring service and database schema
- 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.
This commit is contained in:
@@ -0,0 +1,173 @@
|
||||
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"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user