common.py 3.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114
  1. """通用工具函数 —— 文本截断、安全 JSON 解析与环境变量读取."""
  2. from __future__ import annotations
  3. import asyncio
  4. import functools
  5. import sqlite3
  6. import logging
  7. import re
  8. from typing import Any
  9. from fastapi import HTTPException
  10. logger = logging.getLogger(__name__)
  11. def safe_http_500(op_name: str, exc: Exception) -> HTTPException:
  12. logger.exception("%s failed", op_name, exc_info=exc)
  13. return HTTPException(status_code=500, detail="服务暂时不可用,请稍后重试")
  14. def normalize_arxiv_id(arxiv_id: str | None) -> str | None:
  15. if not arxiv_id:
  16. return None
  17. s = str(arxiv_id).strip()
  18. if not s:
  19. return None
  20. if "v" in s and s.rsplit("v", 1)[-1].isdigit():
  21. s = s.rsplit("v", 1)[0]
  22. return s.lower()
  23. def parse_llm_json(text: str) -> dict[str, Any | None]:
  24. from app.services.search_intent.parsing import extract_json_object
  25. return extract_json_object(text)
  26. def truncate_text(text: str, max_length: int, suffix: str = "…") -> str:
  27. t = (text or "").strip()
  28. if len(t) <= max_length:
  29. return t
  30. return t[: max_length - len(suffix)] + suffix
  31. def tokenize_for_keywords(text: str, min_len: int = 3, max_len: int = 26) -> set[str]:
  32. t = (text or "").lower()
  33. t = re.sub(r"[^a-z0-9\u4e00-\u9fff]+", " ", t)
  34. out: set[str] = set()
  35. for x in t.split():
  36. s = x.strip()
  37. if min_len <= len(s) <= max_len:
  38. out.add(s)
  39. return out
  40. def text_has_cjk(s: str) -> bool:
  41. return any("\u4e00" <= c <= "\u9fff" for c in s)
  42. def dedupe_strings_preserve_order(items: list[str | None], *, max_n: int) -> list[str]:
  43. if not items:
  44. return []
  45. seen: set[str] = set()
  46. out: list[str] = []
  47. for raw in items:
  48. t = str(raw).strip()
  49. if not t:
  50. continue
  51. k = t.lower()
  52. if k in seen:
  53. continue
  54. seen.add(k)
  55. out.append(t)
  56. if len(out) >= max_n:
  57. break
  58. return out
  59. def suppress_exceptions(default_return=None, log_level="debug", log_message=None):
  60. """Catch sync/async exceptions and return a default value."""
  61. def decorator(func):
  62. is_async = asyncio.iscoroutinefunction(func)
  63. @functools.wraps(func)
  64. async def async_wrapper(*args, **kwargs):
  65. try:
  66. return await func(*args, **kwargs)
  67. except Exception:
  68. if log_level == "warning":
  69. logger.warning(log_message or f"{func.__name__} failed", exc_info=True)
  70. else:
  71. logger.debug(log_message or f"{func.__name__} failed", exc_info=True)
  72. return default_return
  73. @functools.wraps(func)
  74. def sync_wrapper(*args, **kwargs):
  75. try:
  76. return func(*args, **kwargs)
  77. except Exception:
  78. if log_level == "warning":
  79. logger.warning(log_message or f"{func.__name__} failed", exc_info=True)
  80. else:
  81. logger.debug(log_message or f"{func.__name__} failed", exc_info=True)
  82. return default_return
  83. return async_wrapper if is_async else sync_wrapper
  84. return decorator
  85. suppress_exceptions_async = suppress_exceptions
  86. def exec_sql(db_path: str, *statements: str) -> None:
  87. conn = sqlite3.connect(db_path)
  88. for stmt in statements:
  89. conn.execute(stmt)
  90. conn.commit()
  91. conn.close()
  92. def build_in_clause(column: str, values: list[Any]) -> tuple[str, tuple[Any, ...]]:
  93. if not values:
  94. return f"{column} IN (NULL)", ()
  95. placeholders = ",".join(["?"] * len(values))
  96. return f"{column} IN ({placeholders})", tuple(values)