feat: init

This commit is contained in:
2026-09-21 23:56:27 +08:00
commit 9bddbc3aa5
18 changed files with 1730 additions and 0 deletions
View File
View File
View File
+16
View File
@@ -0,0 +1,16 @@
from functools import cache
from langchain_openai import ChatOpenAI
from app.config import settings
@cache
def get_llm() -> ChatOpenAI:
return ChatOpenAI(
model=settings.llm_model,
api_key=settings.llm_api_key,
base_url=settings.llm_base_url,
timeout=settings.llm_timeout,
max_retries=settings.llm_max_retries,
)
+25
View File
@@ -0,0 +1,25 @@
import os
from dataclasses import dataclass, field
from pathlib import Path
from dotenv import load_dotenv
load_dotenv(Path(__file__).resolve().parent.parent / ".env")
@dataclass(frozen=True)
class Settings:
# LLM 配置
llm_api_key: str = field(
default_factory=lambda: os.environ["LLM_API_KEY"],
repr=False,
)
llm_model: str = field(default_factory=lambda: os.environ["LLM_MODEL"])
llm_base_url: str | None = field(
default_factory=lambda: os.getenv("LLM_BASE_URL") or None
)
llm_timeout: float = 60.0
llm_max_retries: int = 2
settings = Settings()
View File
+22
View File
@@ -0,0 +1,22 @@
class AppError(Exception):
"""应用业务异常。"""
class ImportProcessError(AppError):
"""导入流程异常,保留节点名称和原始异常信息"""
def __init__(
self, message: str, node_name: str = "", cause: Exception | None = None
):
self.node_name = node_name
self.cause = cause
super().__init__(message)
def __str__(self) -> str:
parts = []
if self.node_name:
parts.append(f"[{self.node_name}]")
parts.append(super().__str__())
if self.cause is not None:
parts.append(f"(原因: {self.cause})")
return " ".join(parts)
View File
+49
View File
@@ -0,0 +1,49 @@
import logging
import time
from abc import ABC, abstractmethod
from typing import TypeVar
from app.config import Settings, settings
from app.exceptions import AppError, ImportProcessError
T = TypeVar("T")
class BaseNode(ABC):
name: str = "base_node"
def __init__(self, config: Settings | None = None):
"""传入配置时使用传入值,否则使用全局配置"""
self.config = config if config is not None else settings
self.logger = logging.getLogger(f"import.{self.name}")
def __call__(self, state: T) -> T:
"""执行节点,统一记录日志、耗时并保留业务异常类型"""
start_time = time.perf_counter()
self.logger.info("--- %s 开始 ---", self.name)
try:
result = self.process(state)
except AppError:
self.logger.exception("%s 执行失败", self.name)
raise
except Exception as e:
self.logger.exception("%s 执行失败", self.name)
raise ImportProcessError(str(e), node_name=self.name) from e
else:
self.logger.info("--- %s 完成 ---", self.name)
return result
finally:
self.logger.info(
"%s 耗时 %.3f 秒", self.name, time.perf_counter() - start_time
)
@abstractmethod
def process(self, state: T) -> T:
"""子类实现节点核心逻辑,返回状态更新"""
def log_step(self, step_name: str, message: str = ""):
"""记录步骤日志。"""
log_msg = f"[{step_name}]"
if message:
log_msg += f" {message}"
self.logger.info(log_msg)
View File
View File
View File
View File