Commit 70149414 by ccran

feat: add party role

parent 447b8d67
......@@ -7,12 +7,13 @@
# Keep Python source files
!**/*.py
!**/*.doc
!**/*.docx
!**/*.xlsx
!**/*.pdf
!**/*.xls
# !**/*.doc
# !**/*.docx
# !**/*.xlsx
# !**/*.pdf
# !**/*.xls
!workflow/**
!data/**.xlsx
!README.md
......
......@@ -12,16 +12,27 @@ use_docker = False
# model: str = "deepseek-v4-pro"
# @dataclass
# class LLMConfig:
# base_url: str = "http://172.21.107.80:9002/v1"
# api_key: str = "none"
# model: str = "Qwen2-72B-Instruct"
@dataclass
class LLMConfig:
base_url: str = "http://192.168.252.71:9002/v1"
api_key: str = "none"
model: str = "Qwen2-72B-Instruct"
# 最大分片数量
min_single_chunk_size = 2000
max_single_chunk_size = 100000
max_chunk_page = 10
# MAX_SINGLE_CHUNK_SIZE=100000
MERGE_RULE_PROMPT = False
# MAX_SINGLE_CHUNK_SIZE = 100000
MAX_SINGLE_CHUNK_SIZE = 5000
# MAX_SINGLE_CHUNK_SIZE = 2000
MERGE_RULE_PROMPT = False
META_KEY = "META"
DEFAULT_RULESET_ID = "通用"
## 规则集ID列表,需与rules.xlsx中的sheet名称保持一致!!!
......@@ -33,7 +44,7 @@ ALL_RULESET_IDS = [
"金盘",
"金盘简化",
"麓发测试",
"麓发标准",
"通用(原始)",
"金盘B类",
"合同信息提取(合同组)",
"合同信息提取(技术部)",
......@@ -45,11 +56,13 @@ MAX_WORKERS = 10
FILE_SUFFIX = "-审核批注"
## 关键参数**
use_non_fastgpt_llm = False
use_non_fastgpt_llm = True
use_lufa = False
use_jp_machine = True
debug_mode = False
## 关键参数**
max_model_len = 131072
ocr_url = 'http://192.168.252.71:8202/openapi/ocrUploadFile'
if use_lufa:
outer_backend_url = "http://znkf.lgfzgroup.com:48081"
......@@ -67,9 +80,9 @@ else:
base_fastgpt_url = "http://192.168.252.71:18088"
base_backend_url = "http://192.168.252.71:48080"
else:
outer_backend_url = "http://172.21.107.45:48080"
outer_backend_url = "https://172.21.107.45:48080"
base_fastgpt_url = "http://172.21.107.45:3030"
base_backend_url = "http://172.21.107.45:48080"
base_backend_url = "http://172.21.107.45:1122"
ocr_url = "http://172.21.107.45:8202/openapi/ocrUploadFile"
segment_review_api_key = (
"fastgpt-vLu2JHAfqwEq5FUQhvATFDK0yDS6fs804v7KwWBMyU4sRrHzh4UGl89Zpa"
......@@ -108,7 +121,3 @@ LLM = {
}
doc_support_formats = [".docx", ".doc", ".wps"]
pdf_support_formats = [".txt", ".md", ".pdf"]
# 最大分片数量
min_single_chunk_size = 2000
max_single_chunk_size = 20000
max_chunk_page = 10
from __future__ import annotations
from typing import Dict
from loguru import logger
from core.tool import tool, tool_func
from core.tools.segment_llm import LLMTool
PARTY_ROLE_SYSTEM_PROMPT = """
你是一名专业合同审查律师。
"""
PARTY_ROLE_USER_PROMPT = """
请分析指定公司在合同中的实际商业角色。
注意:
- 不要仅依据“甲方/乙方”字样判断
- 必须结合实际权利义务关系判断
重点考虑:
- 谁提出需求
- 谁支付费用
- 谁接受服务或交付成果
- 谁拥有验收、管理、监督权
- 谁负责提供产品或服务
输出要求:
只输出一个 JSON:
{{
"party_role": "demand_side | supplier_side | unclear",
"reason": ""
}}
其中:
- demand_side:需求方、委托人、采购方、客户、甲方等
- supplier_side:供应商、服务方、承包方、实施方、乙方等
- unclear:无法明确判断
reason 要用一句话简要说明核心依据。
禁止输出任何Markdown、代码块或额外文本。
待分析公司:
{company_name}
合同内容:
{contract_text}
"""
VALID_PARTY_ROLES = {"demand_side", "supplier_side", "mixed", "unclear"}
PARTY_ROLES_MAP={
"demand_side": "需求方、委托人、采购方、客户、甲方等",
"supplier_side": "供应商、服务方、承包方、实施方、乙方等",
"unclear": "",
}
@tool("party_role", "识别指定公司在合同中的实际商业角色")
class PartyRoleTool(LLMTool):
def __init__(self) -> None:
super().__init__(PARTY_ROLE_SYSTEM_PROMPT)
@tool_func(
{
"type": "object",
"properties": {
"company_name": {"type": "string"},
"contract_text": {"type": "string"},
},
"required": ["company_name", "contract_text"],
}
)
def run(self, company_name: str, contract_text: str) -> Dict[str, str]:
company_name = (company_name or "").strip()
contract_text = (contract_text or "").strip()
if not company_name or not contract_text:
return {"party_role": "", "reason": ""}
user_content = PARTY_ROLE_USER_PROMPT.format(
company_name=company_name,
contract_text=contract_text,
)
try:
resp = self.run_with_loop(self.chat_async(self.build_messages(user_content)))
data = self.parse_first_json(resp)
party_role = str(data.get("party_role", "")).strip()
reason = str(data.get("reason", "")).strip()
if party_role in VALID_PARTY_ROLES:
return {"party_role": PARTY_ROLES_MAP.get(party_role, ""), "reason": reason}
except Exception as exc:
logger.error("Party role detection LLM failed: %s", exc)
return {"party_role": "", "reason": ""}
if __name__ == "__main__":
tool = PartyRoleTool()
print(
tool.run(
company_name="麓发公司",
contract_text="麓发委托乙公司开发系统,甲公司支付费用并负责验收。",
)
)
No preview for this file type
for _ in range(input()):
try:
eval(raw_input())
print("YES")
except TypeError:
print("NO")
except:
print("NO")
......@@ -19,6 +19,7 @@ from core.config import (
pdf_support_formats,
MERGE_RULE_PROMPT,
use_lufa,
max_model_len
)
from core.tools.segment_summary import SegmentSummaryTool
from core.tools.segment_review import SegmentReviewTool
......@@ -29,6 +30,7 @@ from core.tools.reflect_retry import ReflectRetryTool
from core.tools.segment_merger import SegmentMergerTool
from core.tools.fact_merger import FactMergerTool
from core.tools.ruleset_router import RulesetRouterTool
from core.tools.party_role import PartyRoleTool
from core.memory import Finding
from core.memory import FINDING_KEY_MERGE, FINDING_KEY_REFLECT, FINDING_KEY_REVIEW
......@@ -44,6 +46,7 @@ reflect_tool = ReflectRetryTool()
merger_tool = SegmentMergerTool()
fact_merger_tool = FactMergerTool()
ruleset_router_tool = RulesetRouterTool()
party_role_tool = PartyRoleTool()
@app.post("/sleep")
......@@ -84,6 +87,17 @@ class RulesetRouteResponse(BaseModel):
reason: str = ""
class PartyRoleRequest(BaseModel):
company_name: str = Field(..., description="待分析公司名称")
contract_text: str = Field(..., description="合同全文或合同片段内容")
class PartyRoleResponse(BaseModel):
company_name: str
party_role: str
reason: Optional[str] = ""
@app.post("/rulesets/route", response_model=RulesetRouteResponse)
def route_ruleset(payload: RulesetRouteRequest) -> RulesetRouteResponse:
question = (payload.question or "").strip()
......@@ -98,6 +112,26 @@ def route_ruleset(payload: RulesetRouteRequest) -> RulesetRouteResponse:
)
@app.post("/contracts/party-role", response_model=PartyRoleResponse)
def detect_party_role(payload: PartyRoleRequest) -> PartyRoleResponse:
company_name = (payload.company_name or "").strip()
contract_text = (payload.contract_text or "").strip()
if not company_name:
raise HTTPException(status_code=400, detail="company_name cannot be empty")
if not contract_text:
raise HTTPException(status_code=400, detail="contract_text cannot be empty")
result = party_role_tool.run(
company_name=company_name,
contract_text=contract_text[:max_model_len], # TODO 合同文本过长时,优先保留包含公司名称的片段内容,后续优化
)
return PartyRoleResponse(
company_name=company_name,
party_role=result.get("party_role", ""),
reason=result.get("reason", ""),
)
@app.post("/documents/parse", response_model=DocumentParseResponse)
async def parse_document(payload: DocumentParseRequest) -> DocumentParseResponse:
if not payload.urls:
......
import asyncio
import os
import re
from urllib import parse
from urllib.parse import urlparse
import fitz
from loguru import logger
from utils.common_util import random_str
from utils.http_util import download_file, url_replace_fastgpt
class TesseractOCRUtil:
def __init__(self, lang='chi_sim+eng'):
self.lang = lang
def ocr_image(self, file_path):
import pytesseract
from PIL import Image
with Image.open(file_path) as img:
return pytesseract.image_to_string(img, lang=self.lang)
async def ocr_image_async(self, path_list):
tasks = [asyncio.to_thread(self.ocr_image, file_path) for file_path in path_list]
responses = await asyncio.gather(*tasks)
res_dict = {}
for file_path, content in zip(path_list, responses):
page_num = int(self.get_pdf_2_img_page_num(file_path))
res_dict[page_num] = content
logger.info(f'tesseract ocr_image_async finish. all pages:{len(res_dict)}')
return [res_dict[key] for key in sorted(res_dict)]
def set_pdf_2_img_page(self, path, page_idx):
return f'{path}_{page_idx + 1}.png'
def get_pdf_2_img_page_num(self, path):
match = re.search(r'_(\d+)\.png$', path)
if not match:
raise ValueError(f'Invalid pdf page image path: {path}')
return match.group(1)
def pdf_2_img(self, path, zoom_x=2, zoom_y=2):
pdf = fitz.open(path)
pdf_list = []
for pg in range(0, pdf.page_count):
page = pdf[pg]
trans = fitz.Matrix(zoom_x, zoom_y)
pm = page.get_pixmap(matrix=trans, alpha=False)
dest_png = self.set_pdf_2_img_page(path, pg)
pm.save(dest_png)
pdf_list.append(dest_png)
pdf.close()
return pdf_list
def ocr_download_path(self, url):
logger.info(f'tesseract ocr url:{url}')
url = url_replace_fastgpt(url)
url_parsed = urlparse(url)
query_dict = parse.parse_qs(url_parsed.query)
if 'filename' in query_dict:
filename = query_dict.get('filename')[0]
else:
filename = f'{random_str()}.pdf'
dest_path = f'ocr/{filename}'
download_file(url, dest_path)
return dest_path
async def ocr_result_pdf(self, dest_path):
pdf_list = self.pdf_2_img(dest_path)
try:
return await self.ocr_image_async(pdf_list)
finally:
for pdf in pdf_list:
if os.path.exists(pdf):
os.remove(pdf)
if __name__ == '__main__':
ocr_util = TesseractOCRUtil()
result = asyncio.run(ocr_util.ocr_result_pdf('../demo/2020100593中建大成建筑(B类).pdf'))
print(f'len(result):{len(result)}')
print(result)
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment