Commit 70149414 by ccran

feat: add party role

parent 447b8d67
...@@ -7,12 +7,13 @@ ...@@ -7,12 +7,13 @@
# Keep Python source files # Keep Python source files
!**/*.py !**/*.py
!**/*.doc # !**/*.doc
!**/*.docx # !**/*.docx
!**/*.xlsx # !**/*.xlsx
!**/*.pdf # !**/*.pdf
!**/*.xls # !**/*.xls
!workflow/** !workflow/**
!data/**.xlsx
!README.md !README.md
......
...@@ -12,16 +12,27 @@ use_docker = False ...@@ -12,16 +12,27 @@ use_docker = False
# model: str = "deepseek-v4-pro" # 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 @dataclass
class LLMConfig: class LLMConfig:
base_url: str = "http://192.168.252.71:9002/v1" base_url: str = "http://192.168.252.71:9002/v1"
api_key: str = "none" api_key: str = "none"
model: str = "Qwen2-72B-Instruct" model: str = "Qwen2-72B-Instruct"
# 最大分片数量
min_single_chunk_size = 2000
max_single_chunk_size = 100000
max_chunk_page = 10
# MAX_SINGLE_CHUNK_SIZE=100000 # MAX_SINGLE_CHUNK_SIZE = 100000
MERGE_RULE_PROMPT = False
MAX_SINGLE_CHUNK_SIZE = 5000 MAX_SINGLE_CHUNK_SIZE = 5000
# MAX_SINGLE_CHUNK_SIZE = 2000
MERGE_RULE_PROMPT = False
META_KEY = "META" META_KEY = "META"
DEFAULT_RULESET_ID = "通用" DEFAULT_RULESET_ID = "通用"
## 规则集ID列表,需与rules.xlsx中的sheet名称保持一致!!! ## 规则集ID列表,需与rules.xlsx中的sheet名称保持一致!!!
...@@ -33,7 +44,7 @@ ALL_RULESET_IDS = [ ...@@ -33,7 +44,7 @@ ALL_RULESET_IDS = [
"金盘", "金盘",
"金盘简化", "金盘简化",
"麓发测试", "麓发测试",
"麓发标准", "通用(原始)",
"金盘B类", "金盘B类",
"合同信息提取(合同组)", "合同信息提取(合同组)",
"合同信息提取(技术部)", "合同信息提取(技术部)",
...@@ -45,11 +56,13 @@ MAX_WORKERS = 10 ...@@ -45,11 +56,13 @@ MAX_WORKERS = 10
FILE_SUFFIX = "-审核批注" FILE_SUFFIX = "-审核批注"
## 关键参数** ## 关键参数**
use_non_fastgpt_llm = False use_non_fastgpt_llm = True
use_lufa = False use_lufa = False
use_jp_machine = True use_jp_machine = True
debug_mode = False
## 关键参数** ## 关键参数**
max_model_len = 131072
ocr_url = 'http://192.168.252.71:8202/openapi/ocrUploadFile' ocr_url = 'http://192.168.252.71:8202/openapi/ocrUploadFile'
if use_lufa: if use_lufa:
outer_backend_url = "http://znkf.lgfzgroup.com:48081" outer_backend_url = "http://znkf.lgfzgroup.com:48081"
...@@ -67,9 +80,9 @@ else: ...@@ -67,9 +80,9 @@ else:
base_fastgpt_url = "http://192.168.252.71:18088" base_fastgpt_url = "http://192.168.252.71:18088"
base_backend_url = "http://192.168.252.71:48080" base_backend_url = "http://192.168.252.71:48080"
else: 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_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" ocr_url = "http://172.21.107.45:8202/openapi/ocrUploadFile"
segment_review_api_key = ( segment_review_api_key = (
"fastgpt-vLu2JHAfqwEq5FUQhvATFDK0yDS6fs804v7KwWBMyU4sRrHzh4UGl89Zpa" "fastgpt-vLu2JHAfqwEq5FUQhvATFDK0yDS6fs804v7KwWBMyU4sRrHzh4UGl89Zpa"
...@@ -108,7 +121,3 @@ LLM = { ...@@ -108,7 +121,3 @@ LLM = {
} }
doc_support_formats = [".docx", ".doc", ".wps"] doc_support_formats = [".docx", ".doc", ".wps"]
pdf_support_formats = [".txt", ".md", ".pdf"] 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 ( ...@@ -19,6 +19,7 @@ from core.config import (
pdf_support_formats, pdf_support_formats,
MERGE_RULE_PROMPT, MERGE_RULE_PROMPT,
use_lufa, use_lufa,
max_model_len
) )
from core.tools.segment_summary import SegmentSummaryTool from core.tools.segment_summary import SegmentSummaryTool
from core.tools.segment_review import SegmentReviewTool from core.tools.segment_review import SegmentReviewTool
...@@ -29,6 +30,7 @@ from core.tools.reflect_retry import ReflectRetryTool ...@@ -29,6 +30,7 @@ from core.tools.reflect_retry import ReflectRetryTool
from core.tools.segment_merger import SegmentMergerTool from core.tools.segment_merger import SegmentMergerTool
from core.tools.fact_merger import FactMergerTool from core.tools.fact_merger import FactMergerTool
from core.tools.ruleset_router import RulesetRouterTool from core.tools.ruleset_router import RulesetRouterTool
from core.tools.party_role import PartyRoleTool
from core.memory import Finding from core.memory import Finding
from core.memory import FINDING_KEY_MERGE, FINDING_KEY_REFLECT, FINDING_KEY_REVIEW from core.memory import FINDING_KEY_MERGE, FINDING_KEY_REFLECT, FINDING_KEY_REVIEW
...@@ -44,6 +46,7 @@ reflect_tool = ReflectRetryTool() ...@@ -44,6 +46,7 @@ reflect_tool = ReflectRetryTool()
merger_tool = SegmentMergerTool() merger_tool = SegmentMergerTool()
fact_merger_tool = FactMergerTool() fact_merger_tool = FactMergerTool()
ruleset_router_tool = RulesetRouterTool() ruleset_router_tool = RulesetRouterTool()
party_role_tool = PartyRoleTool()
@app.post("/sleep") @app.post("/sleep")
...@@ -84,6 +87,17 @@ class RulesetRouteResponse(BaseModel): ...@@ -84,6 +87,17 @@ class RulesetRouteResponse(BaseModel):
reason: str = "" 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) @app.post("/rulesets/route", response_model=RulesetRouteResponse)
def route_ruleset(payload: RulesetRouteRequest) -> RulesetRouteResponse: def route_ruleset(payload: RulesetRouteRequest) -> RulesetRouteResponse:
question = (payload.question or "").strip() question = (payload.question or "").strip()
...@@ -98,6 +112,26 @@ def route_ruleset(payload: RulesetRouteRequest) -> RulesetRouteResponse: ...@@ -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) @app.post("/documents/parse", response_model=DocumentParseResponse)
async def parse_document(payload: DocumentParseRequest) -> DocumentParseResponse: async def parse_document(payload: DocumentParseRequest) -> DocumentParseResponse:
if not payload.urls: 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