| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114 |
- """通用工具函数 —— 文本截断、安全 JSON 解析与环境变量读取."""
- from __future__ import annotations
- import asyncio
- import functools
- import sqlite3
- import logging
- import re
- from typing import Any
- from fastapi import HTTPException
- logger = logging.getLogger(__name__)
- def safe_http_500(op_name: str, exc: Exception) -> HTTPException:
- logger.exception("%s failed", op_name, exc_info=exc)
- return HTTPException(status_code=500, detail="服务暂时不可用,请稍后重试")
- def normalize_arxiv_id(arxiv_id: str | None) -> str | None:
- if not arxiv_id:
- return None
- s = str(arxiv_id).strip()
- if not s:
- return None
- if "v" in s and s.rsplit("v", 1)[-1].isdigit():
- s = s.rsplit("v", 1)[0]
- return s.lower()
- def parse_llm_json(text: str) -> dict[str, Any | None]:
- from app.services.search_intent.parsing import extract_json_object
- return extract_json_object(text)
- def truncate_text(text: str, max_length: int, suffix: str = "…") -> str:
- t = (text or "").strip()
- if len(t) <= max_length:
- return t
- return t[: max_length - len(suffix)] + suffix
- def tokenize_for_keywords(text: str, min_len: int = 3, max_len: int = 26) -> set[str]:
- t = (text or "").lower()
- t = re.sub(r"[^a-z0-9\u4e00-\u9fff]+", " ", t)
- out: set[str] = set()
- for x in t.split():
- s = x.strip()
- if min_len <= len(s) <= max_len:
- out.add(s)
- return out
- def text_has_cjk(s: str) -> bool:
- return any("\u4e00" <= c <= "\u9fff" for c in s)
- def dedupe_strings_preserve_order(items: list[str | None], *, max_n: int) -> list[str]:
- if not items:
- return []
- seen: set[str] = set()
- out: list[str] = []
- for raw in items:
- t = str(raw).strip()
- if not t:
- continue
- k = t.lower()
- if k in seen:
- continue
- seen.add(k)
- out.append(t)
- if len(out) >= max_n:
- break
- return out
- def suppress_exceptions(default_return=None, log_level="debug", log_message=None):
- """Catch sync/async exceptions and return a default value."""
- def decorator(func):
- is_async = asyncio.iscoroutinefunction(func)
- @functools.wraps(func)
- async def async_wrapper(*args, **kwargs):
- try:
- return await func(*args, **kwargs)
- except Exception:
- if log_level == "warning":
- logger.warning(log_message or f"{func.__name__} failed", exc_info=True)
- else:
- logger.debug(log_message or f"{func.__name__} failed", exc_info=True)
- return default_return
- @functools.wraps(func)
- def sync_wrapper(*args, **kwargs):
- try:
- return func(*args, **kwargs)
- except Exception:
- if log_level == "warning":
- logger.warning(log_message or f"{func.__name__} failed", exc_info=True)
- else:
- logger.debug(log_message or f"{func.__name__} failed", exc_info=True)
- return default_return
- return async_wrapper if is_async else sync_wrapper
- return decorator
- suppress_exceptions_async = suppress_exceptions
- def exec_sql(db_path: str, *statements: str) -> None:
- conn = sqlite3.connect(db_path)
- for stmt in statements:
- conn.execute(stmt)
- conn.commit()
- conn.close()
- def build_in_clause(column: str, values: list[Any]) -> tuple[str, tuple[Any, ...]]:
- if not values:
- return f"{column} IN (NULL)", ()
- placeholders = ",".join(["?"] * len(values))
- return f"{column} IN ({placeholders})", tuple(values)
|