fc / apps /document_recognition /services /info_extractor.py
jun2024's picture
Upload 2241 files
b4f2238 verified
Raw History Blame Contribute Delete
10.9 kB
"""
关键信息提取器
调用 Ollama 大模型从法院文书中提取关键信息(案号、开庭时间等)。
支持正则表达式提取和 Ollama 交叉校验机制。
Requirements: 4.3, 4.4, 4.7
"""
import logging
from datetime import datetime
from typing import Any
from django.utils.translation import gettext_lazy as _
from apps.core.exceptions import RecognitionTimeoutError, ServiceUnavailableError
from apps.core.interfaces import ServiceLocator
from apps.core.llm.config import LLMConfig
from apps.core.llm.exceptions import LLMNetworkError, LLMTimeoutError
from ._case_number_mixin import CaseNumberMixin
from ._datetime_extraction_mixin import DatetimeExtractionMixin
from ._response_parser_mixin import ResponseParserMixin
logger = logging.getLogger("apps.document_recognition")
def get_ollama_model() -> str:
"""兼容旧测试与调用方:保留模块级配置读取入口。"""
return LLMConfig.get_ollama_model()
def get_ollama_base_url() -> str:
"""兼容旧测试与调用方:保留模块级配置读取入口。"""
return LLMConfig.get_ollama_base_url()
def chat(
*,
messages: list[dict[str, str]],
model: str | None = None,
llm_service: Any | None = None,
**kwargs: Any,
) -> dict[str, Any]:
"""
兼容旧测试与调用方:保留模块级 chat 入口,内部转发到统一 LLM 服务。
"""
service = llm_service or ServiceLocator.get_llm_service()
llm_response = service.chat(
messages=messages,
backend="ollama",
model=model,
fallback=False,
**kwargs,
)
return {"message": {"content": llm_response.content}}
class InfoExtractor(CaseNumberMixin, DatetimeExtractionMixin, ResponseParserMixin):
"""
关键信息提取器
使用 Ollama 大模型从法院文书中提取关键信息:
- 传票:案号、开庭时间
- 执行裁定书:案号、财产保全到期时间(预留扩展)
Requirements: 4.3, 4.4
"""
SUMMONS_PROMPT = """请从以下传票内容中提取案号和开庭时间。
传票内容:
{text}
提取要求:
1. 案号格式:(年份)法院代码+案件类型字号+序号+号
- 年份:4位数字,如2024、2025
- 法院代码:省份简称+区县代码,如"粤0604"、"京0105"、"沪0115"
- 案件类型字号(必须包含):民初、民终、刑初、刑终、执、执保、执异、执恢、破、行初、行终等
- 序号:数字
- 必须以"号"字结尾
- 示例:(2024)粤0604民初41257号、(2025)京0105刑初12345号
2. 重要:案号中必须包含案件类型字号(如民初、民终、刑初等),不能省略!
- 错误示例:(2025)粤060441257(缺少案件类型)
- 正确示例:(2025)粤0604民初41257号
3. 开庭时间需要包含完整的日期和时间,格式为:YYYY-MM-DD HH:MM
4. 如果无法确定某个字段,请返回 null
请严格按照以下 JSON 格式返回结果,不要包含其他内容:
{{"case_number": "案号或null", "court_time": "YYYY-MM-DD HH:MM或null"}}
"""
EXECUTION_PROMPT = """请从以下执行裁定书中提取案号和财产保全到期时间。
裁定书内容:
{text}
提取要求:
1. 案号格式:(年份)法院代码+案件类型字号+序号+号
- 年份:4位数字,如2024、2025
- 法院代码:省份简称+区县代码,如"粤0604"、"京0105"
- 案件类型字号(必须包含):执、执保、执异、执恢、民初、民终等
- 序号:数字
- 必须以"号"字结尾
- 示例:(2024)粤0604执保12345号、(2025)京0105执12345号
2. 重要:案号中必须包含案件类型字号,不能省略!
3. 财产保全到期时间格式为:YYYY-MM-DD
4. 如果无法确定某个字段,请返回 null
请严格按照以下 JSON 格式返回结果,不要包含其他内容:
{{"case_number": "案号或null", "preservation_deadline": "YYYY-MM-DD或null"}}
"""
def __init__(
self,
ollama_model: str | None = None,
ollama_base_url: str | None = None,
llm_service: Any | None = None,
):
self.ollama_model = ollama_model or get_ollama_model()
self.ollama_base_url = ollama_base_url or get_ollama_base_url()
self._llm_service = llm_service
@property
def llm_service(self) -> Any:
if self._llm_service is None:
self._llm_service = ServiceLocator.get_llm_service()
return self._llm_service
def extract_summons_info(self, text: str) -> dict[str, Any]:
"""
提取传票信息
Requirements: 4.3
"""
if not text or not text.strip():
logger.warning(
"传票内容为空,无法提取信息",
extra={"action": "extract_summons_info", "result": "empty_text"},
)
return {"case_number": None, "court_time": None, "extraction_method": None}
truncated_text = text[:4000] if len(text) > 4000 else text
logger.info(
"开始提取传票信息",
extra={"action": "extract_summons_info", "text_length": len(text), "truncated_length": len(truncated_text)},
)
regex_case_number = self._extract_case_number_by_regex(truncated_text)
if regex_case_number:
logger.info(f"正则成功提取案号: {regex_case_number}")
regex_datetimes = self._extract_datetime_by_regex(truncated_text)
logger.info(f"正则提取到 {len(regex_datetimes)} 个时间候选")
for dt, matched_text, score in regex_datetimes:
logger.info(f" - {dt} (原文: {matched_text}, 得分: {score})")
ollama_result: dict[str, Any] = {"case_number": None, "court_time": None}
ollama_datetime: datetime | None = None
try:
prompt = self.SUMMONS_PROMPT.format(text=truncated_text)
response = chat(
messages=[{"role": "user", "content": prompt}],
model=self.ollama_model,
llm_service=self.llm_service,
)
ollama_result = self._parse_summons_response(response)
ollama_datetime = ollama_result.get("court_time")
if ollama_datetime:
logger.info(f"Ollama 提取到时间: {ollama_datetime}")
if regex_case_number is None and ollama_result.get("case_number"):
logger.info(f"Ollama 提取到案号: {ollama_result.get('case_number')}")
except (LLMNetworkError, ConnectionError) as e:
logger.warning(f"Ollama 服务不可用,将仅使用正则结果: {e}")
except LLMTimeoutError as e:
logger.warning(f"Ollama 提取超时,将仅使用正则结果: {e}")
except Exception as e:
logger.warning(f"Ollama 提取失败,将仅使用正则结果: {e}")
best_datetime, extraction_method = self._select_best_datetime(regex_datetimes, ollama_datetime)
logger.info(f"最终选择时间: {best_datetime}, 方法: {extraction_method}")
final_case_number = regex_case_number if regex_case_number else ollama_result.get("case_number")
case_number_source = "regex" if regex_case_number else ("ollama" if ollama_result.get("case_number") else None)
result = {
"case_number": final_case_number,
"court_time": best_datetime,
"extraction_method": extraction_method,
}
logger.info(
"传票信息提取完成",
extra={
"action": "extract_summons_info",
"case_number": result.get("case_number"),
"case_number_source": case_number_source,
"court_time": str(result.get("court_time")) if result.get("court_time") else None,
"extraction_method": extraction_method,
},
)
return result
def extract_execution_info(self, text: str) -> dict[str, Any]:
"""
提取执行裁定书信息(预留扩展)
Requirements: 4.4
"""
if not text or not text.strip():
logger.warning(
"执行裁定书内容为空,无法提取信息",
extra={"action": "extract_execution_info", "result": "empty_text"},
)
return {"case_number": None, "preservation_deadline": None}
truncated_text = text[:4000] if len(text) > 4000 else text
logger.info(
"开始提取执行裁定书信息",
extra={
"action": "extract_execution_info",
"text_length": len(text),
"truncated_length": len(truncated_text),
},
)
try:
prompt = self.EXECUTION_PROMPT.format(text=truncated_text)
response = chat(
messages=[{"role": "user", "content": prompt}],
model=self.ollama_model,
llm_service=self.llm_service,
)
result = self._parse_execution_response(response)
logger.info(
"执行裁定书信息提取完成",
extra={
"action": "extract_execution_info",
"case_number": result.get("case_number"),
"preservation_deadline": (
str(result.get("preservation_deadline")) if result.get("preservation_deadline") else None
),
},
)
return result
except (LLMNetworkError, ConnectionError) as e:
logger.error(
f"Ollama 服务不可用: {e}",
extra={"action": "extract_execution_info", "error_type": "connection_error", "error": str(e)},
)
raise ServiceUnavailableError(
message=_("AI 服务暂时不可用,请稍后重试"),
code="OLLAMA_SERVICE_UNAVAILABLE",
errors={"service": "Ollama 服务连接失败"},
service_name="Ollama",
) from e
except LLMTimeoutError as e:
logger.error(
f"执行裁定书信息提取超时: {e}",
extra={"action": "extract_execution_info", "error_type": "timeout_error", "error": str(e)},
)
raise RecognitionTimeoutError(
message=_("信息提取超时,请重试"),
code="EXTRACTION_TIMEOUT",
errors={"timeout": "AI 提取超时"},
) from e
except Exception as e:
logger.error(
f"执行裁定书信息提取失败: {e!s}",
extra={"action": "extract_execution_info", "error_type": type(e).__name__, "error": str(e)},
exc_info=True,
)
raise RuntimeError(f"执行裁定书信息提取失败: {e!s}") from e