feat: 提交热榜评论分析工具 MVP 基线
This commit is contained in:
@@ -0,0 +1,140 @@
|
||||
import json
|
||||
import time
|
||||
from dataclasses import dataclass
|
||||
from typing import Literal
|
||||
|
||||
import httpx
|
||||
from pydantic import BaseModel, Field, ValidationError
|
||||
|
||||
|
||||
Sentiment = Literal["positive", "negative", "neutral", "unknown"]
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class AIAnalysisResult:
|
||||
comment_id: str
|
||||
sentiment: str
|
||||
labels: list[str]
|
||||
reason: str = ""
|
||||
ai_analysis_status: str = "success"
|
||||
|
||||
|
||||
class AIAnalysisItem(BaseModel):
|
||||
comment_id: str
|
||||
sentiment: Sentiment
|
||||
labels: list[str] = Field(default_factory=list, max_length=3)
|
||||
reason: str = ""
|
||||
|
||||
|
||||
def build_comment_prompt(comments: list[dict[str, str]]) -> str:
|
||||
payload = [
|
||||
{
|
||||
"comment_id": str(comment["comment_id"]),
|
||||
"content": str(comment.get("content", ""))[:150],
|
||||
}
|
||||
for comment in comments
|
||||
]
|
||||
return (
|
||||
"只返回 JSON Array,不输出 Markdown 或解释性自然语言。"
|
||||
"请原样回填输入中的 comment_id,不得修改或生成新 ID。"
|
||||
"sentiment 只能是 positive、negative、neutral、unknown;labels 最多 3 个简短中文短语。\n"
|
||||
f"{json.dumps(payload, ensure_ascii=False)}"
|
||||
)
|
||||
|
||||
|
||||
def parse_ai_comment_response(response_text: str, *, expected_comment_ids: set[str]) -> list[AIAnalysisResult]:
|
||||
try:
|
||||
raw = json.loads(response_text)
|
||||
except json.JSONDecodeError as exc:
|
||||
raise ValueError("AI response is not valid JSON") from exc
|
||||
if not isinstance(raw, list):
|
||||
raise ValueError("AI response must be a JSON Array")
|
||||
|
||||
results = []
|
||||
for item in raw:
|
||||
try:
|
||||
parsed = AIAnalysisItem.model_validate(item)
|
||||
except ValidationError as exc:
|
||||
raise ValueError("AI response item does not match schema") from exc
|
||||
if parsed.comment_id not in expected_comment_ids:
|
||||
raise ValueError("AI response comment_id does not match input")
|
||||
results.append(
|
||||
AIAnalysisResult(
|
||||
comment_id=parsed.comment_id,
|
||||
sentiment=parsed.sentiment,
|
||||
labels=parsed.labels,
|
||||
reason=parsed.reason,
|
||||
)
|
||||
)
|
||||
return results
|
||||
|
||||
|
||||
def calculate_analysis_status(*, success_count: int, total_count: int) -> tuple[float, str]:
|
||||
if total_count <= 0:
|
||||
return 0.0, "insufficient"
|
||||
rate = success_count / total_count
|
||||
return rate, "normal" if rate >= 0.8 else "insufficient"
|
||||
|
||||
|
||||
def analyze_comments_with_retry(
|
||||
comments: list[dict[str, str]],
|
||||
*,
|
||||
requester,
|
||||
max_retries: int = 3,
|
||||
) -> list[AIAnalysisResult]:
|
||||
expected_ids = {str(comment["comment_id"]) for comment in comments}
|
||||
prompt = build_comment_prompt(comments)
|
||||
for attempt in range(max_retries):
|
||||
try:
|
||||
return parse_ai_comment_response(requester(prompt), expected_comment_ids=expected_ids)
|
||||
except Exception:
|
||||
if attempt >= max_retries - 1:
|
||||
break
|
||||
time.sleep(2**attempt)
|
||||
return [
|
||||
AIAnalysisResult(
|
||||
comment_id=comment_id,
|
||||
sentiment="unknown",
|
||||
labels=[],
|
||||
reason="ai_parse_failed",
|
||||
ai_analysis_status="failed",
|
||||
)
|
||||
for comment_id in expected_ids
|
||||
]
|
||||
|
||||
|
||||
class OpenAICompatibleAIClient:
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
base_url: str,
|
||||
api_key: str,
|
||||
model: str,
|
||||
timeout_seconds: int = 30,
|
||||
http_client: httpx.Client | None = None,
|
||||
) -> None:
|
||||
self.base_url = base_url.rstrip("/")
|
||||
self.api_key = api_key
|
||||
self.model = model
|
||||
self._http_client = http_client or httpx.Client(timeout=timeout_seconds)
|
||||
|
||||
def request(self, prompt: str) -> str:
|
||||
endpoint = f"{self.base_url}/chat/completions" if self.base_url.endswith("/v1") else f"{self.base_url}/v1/chat/completions"
|
||||
response = self._http_client.post(
|
||||
endpoint,
|
||||
headers={"Authorization": f"Bearer {self.api_key}"},
|
||||
json={
|
||||
"model": self.model,
|
||||
"messages": [
|
||||
{
|
||||
"role": "system",
|
||||
"content": "只返回 JSON Array,不输出 Markdown 或解释性自然语言。",
|
||||
},
|
||||
{"role": "user", "content": prompt},
|
||||
],
|
||||
"temperature": 0,
|
||||
},
|
||||
)
|
||||
response.raise_for_status()
|
||||
payload = response.json()
|
||||
return str(payload["choices"][0]["message"]["content"])
|
||||
Reference in New Issue
Block a user