Files
geo/server/internal/tenant/app/monitoring_answer_llm_parse.go
T
root 6066f43a7d 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.
2026-04-12 09:56:18 +08:00

174 lines
5.6 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 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"
}
}