初始化
This commit is contained in:
+144
@@ -0,0 +1,144 @@
|
||||
"""LLM 翻译 + 投资事件抽取数据模型 (M4)。
|
||||
|
||||
EnTranslatedArticle 是 M4 最终落盘格式,包含中英文双语内容和抽取的事件。
|
||||
EventExtraction 是 LLM JSON 输出直接映射,经 Pydantic 强校验。
|
||||
"""
|
||||
|
||||
import re
|
||||
from datetime import datetime
|
||||
from enum import StrEnum
|
||||
|
||||
from pydantic import BaseModel, Field, field_validator
|
||||
|
||||
|
||||
class Sentiment(StrEnum):
|
||||
"""事件情绪倾向。"""
|
||||
|
||||
POSITIVE = "positive" # 利好
|
||||
NEUTRAL = "neutral" # 中性
|
||||
NEGATIVE = "negative" # 利空
|
||||
|
||||
|
||||
# 国际财经事件类型(LLM Prompt 中展示)
|
||||
INTERNATIONAL_EVENT_TYPES: tuple[str, ...] = (
|
||||
"财报披露",
|
||||
"并购收购",
|
||||
"产品发布",
|
||||
"监管政策",
|
||||
"宏观经济",
|
||||
"央行决议",
|
||||
"行业动态",
|
||||
"技术突破",
|
||||
"高管变动",
|
||||
"诉讼法律",
|
||||
"市场异动",
|
||||
"地缘政治",
|
||||
"大宗商品",
|
||||
"外汇波动",
|
||||
"其他",
|
||||
)
|
||||
|
||||
# 美股代码正则:1-5 个大写字母
|
||||
_US_STOCK_RE = re.compile(r"^[A-Z]{1,5}$")
|
||||
|
||||
MIN_IMPORTANCE = 1
|
||||
MAX_IMPORTANCE = 5
|
||||
|
||||
|
||||
class EventExtraction(BaseModel):
|
||||
"""LLM 输出的单个事件,直接映射 JSON。"""
|
||||
|
||||
event_type: str = Field(..., description="事件类型,见 INTERNATIONAL_EVENT_TYPES")
|
||||
stock_codes: list[str] = Field(
|
||||
default_factory=list,
|
||||
description="涉及美股代码,如 AAPL、TSLA;无相关股票时为空",
|
||||
)
|
||||
sentiment: Sentiment = Field(..., description="positive/neutral/negative")
|
||||
importance: int = Field(
|
||||
..., ge=MIN_IMPORTANCE, le=MAX_IMPORTANCE, description="1-5 重要程度"
|
||||
)
|
||||
summary_zh: str = Field(
|
||||
default="",
|
||||
max_length=200,
|
||||
description="一句话中文事件摘要",
|
||||
)
|
||||
|
||||
@field_validator("stock_codes")
|
||||
@classmethod
|
||||
def _validate_stock_codes(cls, v: list[str]) -> list[str]:
|
||||
"""剔除非美股代码格式、去重、统一大写。"""
|
||||
cleaned: list[str] = []
|
||||
seen: set[str] = set()
|
||||
for code in v:
|
||||
s = (code or "").strip().upper()
|
||||
if not s or not _US_STOCK_RE.match(s):
|
||||
continue
|
||||
if s not in seen:
|
||||
seen.add(s)
|
||||
cleaned.append(s)
|
||||
return cleaned
|
||||
|
||||
@field_validator("event_type")
|
||||
@classmethod
|
||||
def _normalize_event_type(cls, v: str) -> str:
|
||||
s = (v or "").strip()
|
||||
return s if s else "其他"
|
||||
|
||||
|
||||
class LLMTranslationOutput(BaseModel):
|
||||
"""LLM 单次调用的完整输出 JSON 映射。"""
|
||||
|
||||
title_zh: str = Field(..., description="中文翻译标题")
|
||||
content_zh: str = Field(..., description="中文翻译正文")
|
||||
events: list[EventExtraction] = Field(
|
||||
default_factory=list, description="提取的投资事件列表"
|
||||
)
|
||||
|
||||
|
||||
class EnTranslatedArticle(BaseModel):
|
||||
"""M4 最终落盘格式:双语文章 + 抽取事件 + 调用元信息。"""
|
||||
|
||||
# ── 来源标识 ──
|
||||
source_id: str
|
||||
source_name: str
|
||||
url: str
|
||||
url_hash: str
|
||||
|
||||
# ── 双语内容 ──
|
||||
title: str = "" # 英文原标题
|
||||
title_zh: str = "" # 中文翻译标题
|
||||
content_en: str = "" # 英文原文
|
||||
content_zh: str = "" # 中文翻译
|
||||
publish_time: str = "" # ISO 8601
|
||||
word_count: int = 0 # 英文词数
|
||||
word_count_zh: int = 0 # 中文译文字数
|
||||
|
||||
# ── 抽取事件 ──
|
||||
events: list[EventExtraction] = Field(default_factory=list)
|
||||
|
||||
# ── 调用元信息 ──
|
||||
provider: str = "" # deepseek / qwen
|
||||
model: str = ""
|
||||
translated_at: datetime = Field(default_factory=datetime.now)
|
||||
attempts: int = 1 # 实际调用次数(含重试)
|
||||
prompt_tokens: int | None = None
|
||||
completion_tokens: int | None = None
|
||||
|
||||
def short_summary(self) -> str:
|
||||
codes = set()
|
||||
for ev in self.events:
|
||||
codes.update(ev.stock_codes)
|
||||
codes_str = ",".join(sorted(codes)[:5]) or "-"
|
||||
return (
|
||||
f"[{self.source_id}] {self.title[:30]}... "
|
||||
f"→ {len(self.events)}events, stocks: {codes_str}"
|
||||
)
|
||||
|
||||
|
||||
class LLMCallError(Exception):
|
||||
"""LLM 调用失败(网络 / 解析 / 校验)。"""
|
||||
|
||||
def __init__(self, reason: str, *, attempts: int = 0) -> None:
|
||||
super().__init__(reason)
|
||||
self.reason = reason
|
||||
self.attempts = attempts
|
||||
Reference in New Issue
Block a user