chore: 集成 LLM 增强 — config/main/articles schema/workers + .env.example 加 Agnes 配置
This commit is contained in:
10
.env.example
10
.env.example
@@ -56,3 +56,13 @@ FETCH_MAX_RETRIES=2
|
|||||||
DOMAIN=
|
DOMAIN=
|
||||||
# 邮箱(Let's Encrypt 用)
|
# 邮箱(Let's Encrypt 用)
|
||||||
ACME_EMAIL=you@example.com
|
ACME_EMAIL=you@example.com
|
||||||
|
|
||||||
|
# ===== Agnes LLM(翻译后智能增强)=====
|
||||||
|
# 留空 = 不启用 LLM 增强(只走翻译)
|
||||||
|
# Agnes 控制台申请:https://platform.agnes-ai.com/
|
||||||
|
AGNES_API_KEY=your_agnes_api_key
|
||||||
|
AGNES_BASE_URL=https://apihub.agnes-ai.com/v1
|
||||||
|
AGNES_CHAT_MODEL=agnes-2.0-flash
|
||||||
|
AGNES_IMAGE_MODEL=agnes-image-2.1-flash
|
||||||
|
# LLM 调用间隔(秒,避免被限流;chat + image 各 1 个串行)
|
||||||
|
LLM_INTERVAL_SEC=2.0
|
||||||
|
|||||||
@@ -169,14 +169,20 @@ async def get_article(
|
|||||||
title_zh=article.title_zh,
|
title_zh=article.title_zh,
|
||||||
body_zh_html=article.body_zh_html,
|
body_zh_html=article.body_zh_html,
|
||||||
body_zh_text=article.body_zh_text,
|
body_zh_text=article.body_zh_text,
|
||||||
|
body_zh_formatted=article.body_zh_formatted,
|
||||||
summary_zh=article.summary_zh,
|
summary_zh=article.summary_zh,
|
||||||
lang_src=article.lang_src,
|
lang_src=article.lang_src,
|
||||||
author=article.author,
|
author=article.author,
|
||||||
image_url=article.image_url,
|
image_url=article.image_url,
|
||||||
|
image_ai_url=article.image_ai_url,
|
||||||
translation_status=article.translation_status,
|
translation_status=article.translation_status,
|
||||||
translation_engine=article.translation_engine,
|
translation_engine=article.translation_engine,
|
||||||
translated_at=article.translated_at,
|
translated_at=article.translated_at,
|
||||||
category=article.category,
|
category=article.category,
|
||||||
|
format_status=article.format_status,
|
||||||
|
classify_status=article.classify_status,
|
||||||
|
image_ai_status=article.image_ai_status,
|
||||||
|
commentary_status=article.commentary_status,
|
||||||
commentary=article.commentary,
|
commentary=article.commentary,
|
||||||
entities=article.entities,
|
entities=article.entities,
|
||||||
sentiment=article.sentiment,
|
sentiment=article.sentiment,
|
||||||
|
|||||||
@@ -92,6 +92,15 @@ class Settings(BaseSettings):
|
|||||||
domain: str = ""
|
domain: str = ""
|
||||||
acme_email: str = ""
|
acme_email: str = ""
|
||||||
|
|
||||||
|
# ===== Agnes LLM(智能增强)=====
|
||||||
|
# 留空 = 不启用 LLM 增强(翻译后只走默认排版,提示词也不读)
|
||||||
|
agnes_api_key: str = ""
|
||||||
|
agnes_base_url: str = "https://apihub.agnes-ai.com/v1"
|
||||||
|
agnes_chat_model: str = "agnes-2.0-flash"
|
||||||
|
agnes_image_model: str = "agnes-image-2.1-flash"
|
||||||
|
# 全局 LLM 调用间隔(秒),避免被限流
|
||||||
|
llm_interval_sec: float = 2.0
|
||||||
|
|
||||||
# ===== 内部路径(部署后可调) =====
|
# ===== 内部路径(部署后可调) =====
|
||||||
project_root: Path = Path(__file__).resolve().parents[2]
|
project_root: Path = Path(__file__).resolve().parents[2]
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ from fastapi.middleware.cors import CORSMiddleware
|
|||||||
from fastapi.responses import JSONResponse
|
from fastapi.responses import JSONResponse
|
||||||
from starlette.exceptions import HTTPException as StarletteHTTPException
|
from starlette.exceptions import HTTPException as StarletteHTTPException
|
||||||
|
|
||||||
from app.api import admin, articles, auth, bookmarks, me, sources, subscriptions
|
from app.api import admin, admin_llm, articles, auth, bookmarks, me, sources, subscriptions
|
||||||
from app.config import settings
|
from app.config import settings
|
||||||
from app.database import engine
|
from app.database import engine
|
||||||
from app.redis_client import close_redis, get_redis
|
from app.redis_client import close_redis, get_redis
|
||||||
@@ -100,6 +100,7 @@ app.include_router(sources.router, prefix=API_PREFIX)
|
|||||||
app.include_router(bookmarks.router, prefix=API_PREFIX)
|
app.include_router(bookmarks.router, prefix=API_PREFIX)
|
||||||
app.include_router(subscriptions.router, prefix=API_PREFIX)
|
app.include_router(subscriptions.router, prefix=API_PREFIX)
|
||||||
app.include_router(admin.router, prefix=API_PREFIX)
|
app.include_router(admin.router, prefix=API_PREFIX)
|
||||||
|
app.include_router(admin_llm.router, prefix=API_PREFIX)
|
||||||
|
|
||||||
|
|
||||||
# === 健康检查 ===
|
# === 健康检查 ===
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
from app.models.api_token import ApiToken # noqa: F401
|
from app.models.api_token import ApiToken # noqa: F401
|
||||||
from app.models.article import Article # noqa: F401
|
from app.models.article import Article # noqa: F401
|
||||||
from app.models.bookmark import Bookmark # noqa: F401
|
from app.models.bookmark import Bookmark # noqa: F401
|
||||||
|
from app.models.llm_setting import LlmSetting # noqa: F401
|
||||||
from app.models.source import Source, SourceKind # noqa: F401
|
from app.models.source import Source, SourceKind # noqa: F401
|
||||||
from app.models.subscription import Subscription # noqa: F401
|
from app.models.subscription import Subscription # noqa: F401
|
||||||
from app.models.user import User, UserRole # noqa: F401
|
from app.models.user import User, UserRole # noqa: F401
|
||||||
@@ -13,6 +14,7 @@ __all__ = [
|
|||||||
"ApiToken",
|
"ApiToken",
|
||||||
"Article",
|
"Article",
|
||||||
"Bookmark",
|
"Bookmark",
|
||||||
|
"LlmSetting",
|
||||||
"Source",
|
"Source",
|
||||||
"SourceKind",
|
"SourceKind",
|
||||||
"Subscription",
|
"Subscription",
|
||||||
|
|||||||
@@ -46,14 +46,21 @@ class ArticleDetail(BaseModel):
|
|||||||
title_zh: str | None = None
|
title_zh: str | None = None
|
||||||
body_zh_html: str | None = None
|
body_zh_html: str | None = None
|
||||||
body_zh_text: str | None = None
|
body_zh_text: str | None = None
|
||||||
|
body_zh_formatted: str | None = None # LLM 排版后
|
||||||
summary_zh: str | None = None
|
summary_zh: str | None = None
|
||||||
lang_src: str | None = None
|
lang_src: str | None = None
|
||||||
author: str | None = None
|
author: str | None = None
|
||||||
image_url: str | None = None
|
image_url: str | None = None
|
||||||
|
image_ai_url: str | None = None # LLM 生成的插图
|
||||||
translation_status: str
|
translation_status: str
|
||||||
translation_engine: str | None = None
|
translation_engine: str | None = None
|
||||||
translated_at: datetime | None = None
|
translated_at: datetime | None = None
|
||||||
|
# === LLM 增强状态 + 内容 ===
|
||||||
category: str | None = None
|
category: str | None = None
|
||||||
|
format_status: str | None = None # pending/ok/failed/n/a
|
||||||
|
classify_status: str | None = None
|
||||||
|
image_ai_status: str | None = None
|
||||||
|
commentary_status: str | None = None
|
||||||
commentary: str | None = None
|
commentary: str | None = None
|
||||||
entities: dict | None = None
|
entities: dict | None = None
|
||||||
sentiment: float | None = None
|
sentiment: float | None = None
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ from sqlalchemy import select
|
|||||||
from app.config import settings
|
from app.config import settings
|
||||||
from app.database import AsyncSessionLocal
|
from app.database import AsyncSessionLocal
|
||||||
from app.models.source import Source
|
from app.models.source import Source
|
||||||
|
from app.services.llm.enrichment import enrichment_loop
|
||||||
from app.workers.pipeline import fetch_one_source, run_once, translation_loop
|
from app.workers.pipeline import fetch_one_source, run_once, translation_loop
|
||||||
|
|
||||||
logger = logging.getLogger("news.worker")
|
logger = logging.getLogger("news.worker")
|
||||||
@@ -93,6 +94,10 @@ async def main() -> None:
|
|||||||
translation_task = asyncio.create_task(translation_loop(), name="translation_loop")
|
translation_task = asyncio.create_task(translation_loop(), name="translation_loop")
|
||||||
logger.info("translation_loop task scheduled (1 article/sec)")
|
logger.info("translation_loop task scheduled (1 article/sec)")
|
||||||
|
|
||||||
|
# 独立的 LLM 增强后台循环(翻译完成后,跑 4 项 LLM 任务)
|
||||||
|
enrichment_task = asyncio.create_task(enrichment_loop(), name="enrichment_loop")
|
||||||
|
logger.info("enrichment_loop task scheduled (scans translated articles)")
|
||||||
|
|
||||||
stop = asyncio.Event()
|
stop = asyncio.Event()
|
||||||
|
|
||||||
def _signal_handler():
|
def _signal_handler():
|
||||||
@@ -108,12 +113,13 @@ async def main() -> None:
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
await stop.wait()
|
await stop.wait()
|
||||||
logger.info("stopping scheduler and translation loop")
|
logger.info("stopping scheduler and background loops")
|
||||||
translation_task.cancel()
|
for t in (translation_task, enrichment_task):
|
||||||
try:
|
t.cancel()
|
||||||
await translation_task
|
try:
|
||||||
except asyncio.CancelledError:
|
await t
|
||||||
pass
|
except asyncio.CancelledError:
|
||||||
|
pass
|
||||||
scheduler.shutdown(wait=False)
|
scheduler.shutdown(wait=False)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user