similar_plan_recommend.py 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145
  1. # -*- coding: utf-8 -*-
  2. """相似片段检索接口。"""
  3. from typing import List, Optional
  4. from pydantic import BaseModel, Field
  5. from fastapi import APIRouter, HTTPException
  6. from starlette.concurrency import run_in_threadpool
  7. from foundation.observability.logger.loggering import write_logger as logger
  8. from foundation.infrastructure.tracing import TraceContext, auto_trace
  9. from core.construction_write.component.similar_fragment_service import (
  10. CHILD_COLLECTION,
  11. PARENT_COLLECTION,
  12. search_similar_fragments,
  13. )
  14. # ==================== 路由 ====================
  15. similar_fragment_router = APIRouter(prefix="/sgbx", tags=["施工方案编写"])
  16. # ==================== 数据模型 ====================
  17. class SimilarFragmentSearchRequest(BaseModel):
  18. title_level_1: Optional[str] = Field(None, description="一级标题展示文本", example="施工工艺技术")
  19. title_level_2: Optional[str] = Field(None, description="二级标题展示文本", example="主要施工方法概述")
  20. chapter_level_1: str = Field(..., description="一级章节类型,需与向量库字段匹配", example="technology")
  21. chapter_level_2: str = Field(..., description="二级章节类型,需与向量库字段匹配", example="MethodsOverview")
  22. chapter_id: str = Field(..., description="当前章节ID(原样回传)")
  23. project_id: str = Field(..., description="方案ID(原样回传)")
  24. sgbx_code: Optional[str] = Field(None, description="施工编写章节编码")
  25. search_text: str = Field(..., description="用户输入的检索信息")
  26. class Config:
  27. extra = "ignore"
  28. class SimilarFragmentItem(BaseModel):
  29. chapter_level_1: str = Field(..., description="一级标题")
  30. chapter_level_2: str = Field(..., description="二级标题")
  31. chapter_id: str = Field(..., description="请求传入的章节ID,原样回传")
  32. project_id: str = Field(..., description="请求传入的方案ID,原样回传")
  33. text: str = Field(..., description="相似片段内容")
  34. file_name: str = Field(..., description="文件名称")
  35. similarity_percent: float = Field(..., description="相似度百分比", ge=0.0, le=100.0)
  36. class SimilarFragmentSearchResponse(BaseModel):
  37. code: int
  38. message: str
  39. data: List[SimilarFragmentItem] = Field(default_factory=list)
  40. # ==================== API 路由 ====================
  41. @similar_fragment_router.post("/similar_fragment_search", response_model=SimilarFragmentSearchResponse)
  42. @auto_trace(generate_if_missing=True)
  43. async def similar_fragment_search(request: SimilarFragmentSearchRequest):
  44. """
  45. 相似片段检索接口
  46. 根据用户输入的一级/二级标题和检索信息,从知识库向量表中检索出最相关的相似片段。
  47. Args:
  48. request: 检索请求,包含一级标题、二级标题、章节ID、方案ID、检索文本
  49. Returns:
  50. 相似片段列表,包含内容、文件名、相似度百分比
  51. """
  52. trace_id = TraceContext.get_trace_id()
  53. logger.info(
  54. f"[{trace_id}] 相似片段检索: title_level_1={request.title_level_1}, "
  55. f"title_level_2={request.title_level_2}, chapter_level_1={request.chapter_level_1}, "
  56. f"chapter_level_2={request.chapter_level_2}, 检索文本={request.search_text[:50]}..."
  57. )
  58. # 参数校验
  59. if not request.chapter_level_1.strip() or not request.chapter_level_2.strip():
  60. return SimilarFragmentSearchResponse(
  61. code=400,
  62. message="章节类型 chapter_level_1 和 chapter_level_2 不能为空",
  63. data=[]
  64. )
  65. if not request.search_text.strip():
  66. return SimilarFragmentSearchResponse(
  67. code=400,
  68. message="检索信息不能为空",
  69. data=[]
  70. )
  71. if len(request.search_text.strip()) < 3:
  72. return SimilarFragmentSearchResponse(
  73. code=400,
  74. message="检索信息过短,请输入至少3个字符",
  75. data=[]
  76. )
  77. try:
  78. raw_results = await run_in_threadpool(
  79. search_similar_fragments,
  80. level1=request.chapter_level_1,
  81. level2=request.chapter_level_2,
  82. search_text=request.search_text,
  83. top_k=5,
  84. )
  85. # 组装返回
  86. items = []
  87. for r in raw_results:
  88. similarity_percent = round(r["similarity"] * 100, 2)
  89. items.append(SimilarFragmentItem(
  90. chapter_level_1=r["chapter_level_1"],
  91. chapter_level_2=r["chapter_level_2"],
  92. chapter_id=request.chapter_id,
  93. project_id=request.project_id,
  94. text=r["text"],
  95. file_name=r["file_name"],
  96. similarity_percent=similarity_percent,
  97. ))
  98. return SimilarFragmentSearchResponse(
  99. code=200,
  100. message="success",
  101. data=items
  102. )
  103. except Exception as e:
  104. logger.error(f"[{trace_id}] 相似片段检索异常: {e}", exc_info=True)
  105. raise HTTPException(
  106. status_code=500,
  107. detail=f"相似片段检索失败: {str(e)}"
  108. )
  109. @similar_fragment_router.get("/similar_fragment_search_health")
  110. async def health_check():
  111. """相似片段检索健康检查"""
  112. return {
  113. "status": "healthy",
  114. "vector_db": "Milvus (lq_db)",
  115. "child_collection": CHILD_COLLECTION,
  116. "parent_collection": PARENT_COLLECTION,
  117. }