Commit 5f18aa67 by ccran

feat:付款时间审查答案更新;付款时间审查F1达标;

parent 3bb9ff31
...@@ -4,53 +4,61 @@ from dataclasses import dataclass ...@@ -4,53 +4,61 @@ from dataclasses import dataclass
# 可配置运行参数 # 可配置运行参数
use_docker = False use_docker = False
@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"
# MAX_SINGLE_CHUNK_SIZE=100000 # MAX_SINGLE_CHUNK_SIZE=100000
MERGE_RULE_PROMPT = False MERGE_RULE_PROMPT = False
MAX_SINGLE_CHUNK_SIZE=5000 MAX_SINGLE_CHUNK_SIZE = 5000
META_KEY="META" META_KEY = "META"
DEFAULT_RULESET_ID = "通用" DEFAULT_RULESET_ID = "通用"
ALL_RULESET_IDS = ["通用","借款","担保","财务口","金盘","金盘简化"] ALL_RULESET_IDS = ["通用", "借款", "担保", "财务口", "金盘", "金盘简化"]
use_lufa = False use_lufa = False
if use_lufa: if use_lufa:
outer_backend_url = "http://znkf.lgfzgroup.com:48081" outer_backend_url = "http://znkf.lgfzgroup.com:48081"
base_fastgpt_url = "http://192.168.252.71:18089" base_fastgpt_url = "http://192.168.252.71:18089"
base_backend_url = "http://192.168.252.71:48081" base_backend_url = "http://192.168.252.71:48081"
segment_review_api_key = "fastgpt-zMavJKKgqA9jRNHLXxzXCVZx1JXxfuNkH1p2qfLhtPfMp41UvdSQvt8" segment_review_api_key = (
reflect_retry_api_key = "fastgpt-ao3al2vgfnArt9qi2bTpPeRHouCO7qngUZiQsIM1E2x91u22z65J" "fastgpt-zMavJKKgqA9jRNHLXxzXCVZx1JXxfuNkH1p2qfLhtPfMp41UvdSQvt8"
)
reflect_retry_api_key = (
"fastgpt-ao3al2vgfnArt9qi2bTpPeRHouCO7qngUZiQsIM1E2x91u22z65J"
)
else: else:
outer_backend_url = "http://218.77.58.8:48080" outer_backend_url = "http://218.77.58.8:48080"
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"
segment_review_api_key = "fastgpt-vLu2JHAfqwEq5FUQhvATFDK0yDS6fs804v7KwWBMyU4sRrHzh4UGl89Zpa" segment_review_api_key = (
reflect_retry_api_key = "fastgpt-abxzi4CC7SGuVdxDVFmhAFFQHqi6owK5YsIfXdvOMEAcpIhZWDPObTz2Xn" "fastgpt-vLu2JHAfqwEq5FUQhvATFDK0yDS6fs804v7KwWBMyU4sRrHzh4UGl89Zpa"
)
reflect_retry_api_key = (
"fastgpt-abxzi4CC7SGuVdxDVFmhAFFQHqi6owK5YsIfXdvOMEAcpIhZWDPObTz2Xn"
)
# 项目根目录 # 项目根目录
root_path = r"E:\PycharmProject\contract_review_agent" root_path = r"E:\PycharmProject\contract_review_agent"
system = platform.system() system = platform.system()
if system == "Linux": if system == "Linux":
# root_path = "/data/home/ccran/contract_review_agent" # root_path = "/data/home/ccran/contract_review_agent"
root_path = '/home/ccran/contract_review_agent' root_path = "/home/ccran/contract_review_agent"
elif system == "Darwin": elif system == "Darwin":
root_path = "/Users/chenran/PycharmProjects/contract_review_agent" root_path = "/Users/chenran/PycharmProjects/contract_review_agent"
# docker设置 # docker设置
if use_docker: if use_docker:
root_path = '/app' root_path = "/app"
MAX_WORKERS = 20 MAX_WORKERS = 20
LLM = { LLM = {
"base_tool_llm": LLMConfig(), "base_tool_llm": LLMConfig(),
"fastgpt_segment_review": LLMConfig( "fastgpt_segment_review": LLMConfig(
base_url=f"{base_fastgpt_url}/api/v1", base_url=f"{base_fastgpt_url}/api/v1", api_key=segment_review_api_key
api_key=segment_review_api_key
), ),
"fastgpt_reflect_retry": LLMConfig( "fastgpt_reflect_retry": LLMConfig(
base_url=f"{base_fastgpt_url}/api/v1", base_url=f"{base_fastgpt_url}/api/v1", api_key=reflect_retry_api_key
api_key=reflect_retry_api_key
), ),
} }
doc_support_formats = [".docx", ".doc", ".wps"] doc_support_formats = [".docx", ".doc", ".wps"]
......
...@@ -9,30 +9,108 @@ from core.tools.segment_llm import LLMTool ...@@ -9,30 +9,108 @@ from core.tools.segment_llm import LLMTool
REFLECT_SYSTEM_PROMPT = ''' REFLECT_SYSTEM_PROMPT = '''
你是一个合同审查反思智能体(ReviewReflection)。 你是一个合同审查反思智能体(ReviewReflection)。
你的任务不是重新审查合同,而是基于已有 findings、当前审查规则和合同全文,对 findings 进行复核,输出最终 findings。 你的任务不是从零重新审查合同,也不是简单删减 findings,
而是基于“已有 findings、当前审查规则、合同全文、合同摘要事实记忆”,
对 findings 进行规则内复核、去重、校正、拆分、合并与定稿,输出最终 final_findings。
【通用反思任务】 【你的角色定位】
你只能做以下事情: 你是“终审校准器”,不是“初审生成器”。
你的目标是让 final_findings 同时满足以下要求:
1. 与当前审查规则严格相关;
2. 能被合同全文直接支持;
3. 不重复、不冲突;
4. 表述准确、建议可执行;
5. 对已有 findings 中已经涉及的规则问题做到完整定稿,而不是机械保留或机械删除。
【允许执行的操作】
你只能在“已有 findings 已涉及的规则范围内”做以下处理:
1. 删除重复 findings; 1. 删除重复 findings;
2. 删除证据不足不能由合同原文直接支持的 findings; 2. 删除证据不足、引用不当、不能由合同原文直接支持的 findings;
3. 删除超出当前审查规则范围的 findings; 3. 删除超出当前审查规则范围的 findings;
4. 修订 issue、result 或 suggestion 不准确的 findings; 4. 修订 issue、result、original_text 或 suggestion 不准确的 findings;
5. 合并指向同一原文实质问题的 findings; 5. 合并多个指向同一原文实质问题的 findings;
6. 基于合同全文对已有 findings 做必要校正。 6. 拆分一个同时包含多个独立问题的 finding,将其改写为多个 final findings;
当出现以下情况时,必须拆分:
- original_text 包含多个句子或多个不连续片段;
- 一个 finding 的 issue 实际对应多个独立风险点;
- 不同句子分别支撑不同判断;
拆分要求:
- 每个拆分后的 finding 只保留一个独立问题;
- 每个 finding 的 original_text 只引用一个最小充分证据句;
- 不得在一个 finding 中保留多个证据来源;
7. 基于合同全文对已有 findings 做必要校正;
8. 在不扩展新审查维度的前提下,对已有 findings 中已经涉及但表达混杂、粒度过粗、遗漏独立结论的内容进行重组和细化。
【结构违规检测(必须执行)】
你必须检查每一个已有 finding 是否违反以下结构规则:
1. original_text 是否超过一个句子?
2. 是否包含多个不连续文本片段?
3. issue 是否描述了多个问题?
4. suggestion 是否同时针对多个问题?
如果任一为“是”,则该 finding 必须被拆分为多个 final findings。
【禁止事项】
你不得:
- 脱离当前审查规则新增全新的审查维度;
- 凭空创造合同中不存在的事实;
- 仅因措辞保守就删除一个本来成立的 finding;
- 仅因已有 findings 数量较多就刻意压缩结果数量;
- 输出无法由合同原文直接支持的结论;
- 输出模糊、空泛、不可执行的 suggestion。
通用判定原则】 核心判定原则】
- findings 只是候选结论,不当然等于最终结论; - findings 只是候选结论,不当然等于最终结论;
- final result 必须以合同全文和当前审查规则为准; - final result 必须以“当前审查规则 + 合同全文 + 合同立场”为准;
- 每条 final finding 必须有合同原文直接引用 - 每条 final finding 必须能被合同原文直接支持
- original_text 必须能够直接支撑该 finding - original_text 必须是能够直接支撑该 finding 的最小充分证据片段
- result 只能为“合格”或“不合格”; - result 只能为“合格”或“不合格”;
- 若 result 为“合格”,suggestion 填写“无需修改”; - 若 result 为“合格”,suggestion 必须填写“无需修改”;
- 若 result 为“不合格”,suggestion 必须具体、可执行; - 若 result 为“不合格”,suggestion 必须具体、可执行,优先给出可直接替换或新增的条款表述;若无法安全直接改写,则明确指出应补充的关键要素。
- 若反思后无成立 findings,返回 {"final_findings": []}。
【全文校正规则(非常重要)】
你必须结合合同全文检查每条已有 finding 是否存在以下情况:
1. 该问题在合同其他部分已有明确补充、限制、例外或纠正;
2. 该 finding 对原文存在断章取义;
3. 该 finding 忽略了适用条件、前提、例外或定义;
4. 该 finding 的 original_text 不能直接支撑其 issue 或 result;
5. 该 finding 与合同立场下的风险判断不一致;
6. 两条 findings 看似不同,但实质上指向同一风险;
7. 一条 finding 看似一条,实际上包含多个独立成立的判断,应拆分。
若合同全文已经对某一已有风险作出充分补正或限制,导致该 finding 不再成立,则应删除或修订,而不是机械保留。
【完整性要求】
反思的目标不是尽量减少 findings,而是输出“准确、去重、完整”的 final_findings。
如果已有 findings 中实际上包含多个独立成立的问题,必须在 final_findings 中完整呈现,不得因为反思阶段而无故收缩为1条。
【内部执行步骤(不得输出)】
在输出最终 JSON 前,你必须完成以下内部步骤:
Step 1:逐条审阅已有 findings,判断其是否仍成立;
Step 2:检查每条 finding 是否与当前规则相关,是否有合同原文直接支持;
Step 3:结合合同全文核验该 finding 是否被其他条款补充、限制、修正或否定;
Step 4:识别重复项、交叉项、包含多个问题的混合项;
Step 5:对 findings 进行删除、修订、合并或拆分;
Step 6:确保 final_findings 中每一条都可独立成立,且合并后不遗漏已有 findings 所涉及的有效问题;
Step 7:再输出最终 JSON。
【输出约束】 【输出约束】
- 严格输出 JSON; - 严格输出 JSON;
- 不得输出任何解释性文字。 - 不得输出任何解释性文字;
- 若反思后无成立 findings,返回 {"final_findings": []}。
在输出 final_findings 前,你必须逐条自检(不输出):
1. 这条 finding 是否仍在当前审查规则范围内?
2. original_text 是否真的能直接支持 issue 和 result?
3. issue 是否准确说明了为什么合格/不合格?
4. 是否被合同全文其他条款补正、限制或推翻?
5. 是否与其他 finding 重复?
6. 是否其实包含多个独立问题,需要拆分?
7. suggestion 是否具体、可执行、与 result 一致?
8. 若 result=合格,suggestion 是否为“无需修改”?
9. 删除、合并、拆分后,是否遗漏了已有 findings 中本来成立的有效问题?
''' '''
REFLECT_USER_PROMPT = ''' REFLECT_USER_PROMPT = '''
...@@ -52,17 +130,25 @@ REFLECT_USER_PROMPT = ''' ...@@ -52,17 +130,25 @@ REFLECT_USER_PROMPT = '''
站在 {party_role} 的立场进行反思审查。 站在 {party_role} 的立场进行反思审查。
【任务】 【任务】
请基于通用反思要求、当前审查规则、规则专属反思思路、已有 findings 和合同全文, 请基于当前审查规则、规则专属反思思路、已有 findings、合同摘要事实记忆和合同全文,
输出最终 final_findings。 对已有 findings 进行规则内复核、校正、去重、合并、拆分与定稿,输出最终 final_findings。
【特别要求】 【特别要求】
- 仅在已有 findings 的基础上做复核、删除、修订、合并; - 你不是从零重新审查,而是对已有 findings 做终审校准;
- 不得扩展新的审查维度; - 不得扩展新的审查维度,但可以在已有 findings 已涉及的规则范围内进行修订、合并、拆分和重组;
- 必须结合合同全文进行校正; - 必须结合合同全文进行校正,防止断章取义;
- 若合同其他条款已对某一风险作出明确补充、限制、例外或修正,应据此删除或修订对应 finding;
- 若一个 finding 实际包含多个独立问题,应拆分为多个 final findings;
- 若多个 findings 实际指向同一问题,应合并;
- 每条 final finding 必须包含 result,且 result 只能为“合格”或“不合格”; - 每条 final finding 必须包含 result,且 result 只能为“合格”或“不合格”;
- 每条 final finding 的 original_text 必须是能够直接支撑该判断的合同原文最小充分证据片段;
- 当 result="合格" 时,suggestion 必须填写“无需修改”;
- 当 result="不合格" 时,suggestion 应尽量提供可直接落地的修改文本;若无法安全直接改写,请给出明确修改方向和应补充的关键要素;
- final_findings 应准确、去重、完整,不得无故少于已有 findings 中实际成立的独立问题数量;
- 若无成立 findings,返回 {{"final_findings": []}}; - 若无成立 findings,返回 {{"final_findings": []}};
- 仅输出 JSON。 - 仅输出 JSON。
''' '''
OUTPUT_FORMAT_SCHEMA = ''' OUTPUT_FORMAT_SCHEMA = '''
```json ```json
{ {
...@@ -98,12 +184,23 @@ class ReflectRetryTool(LLMTool): ...@@ -98,12 +184,23 @@ class ReflectRetryTool(LLMTool):
"required": ["party_role", "rule", "facts", "findings"], "required": ["party_role", "rule", "facts", "findings"],
} }
) )
def _stringify_rule(self, rule:Dict) -> str:
res = ''
res += f"## 审查项标题\n{rule.get('title','')}\n"
res += f"## 审查规则\n{rule.get('rule','')}\n"
res += f"## 风险等级\n{rule.get('level','')}\n"
res += f"## 建议模板\n{rule.get('suggestion_template','')}\n"
res += f"## 参考案例\n{rule.get('case','')}\n"
return res
def run(self, party_role: str, rule: Dict, facts: Optional[List[Dict]] = None, findings: Optional[List[Dict]] = None) -> List[Dict]: def run(self, party_role: str, rule: Dict, facts: Optional[List[Dict]] = None, findings: Optional[List[Dict]] = None) -> List[Dict]:
base_findings = self._build_findings_with_ids(findings or []) base_findings = self._build_findings_with_ids(findings or [])
if len(base_findings) == 0: if len(base_findings) == 0:
return [] return []
user_content = REFLECT_USER_PROMPT.format( user_content = REFLECT_USER_PROMPT.format(
rule=rule.get("rule",""), rule=self._stringify_rule(rule),
findings_json=json.dumps(base_findings, ensure_ascii=False), findings_json=json.dumps(base_findings, ensure_ascii=False),
facts_json=json.dumps(facts or [], ensure_ascii=False), facts_json=json.dumps(facts or [], ensure_ascii=False),
party_role=party_role, party_role=party_role,
......
...@@ -7,6 +7,7 @@ from typing import Dict, List, Optional ...@@ -7,6 +7,7 @@ from typing import Dict, List, Optional
from core.tool import tool, tool_func from core.tool import tool, tool_func
from core.tools.segment_llm import LLMTool from core.tools.segment_llm import LLMTool
import re import re
from loguru import logger
REVIEW_SYSTEM_PROMPT = ''' REVIEW_SYSTEM_PROMPT = '''
你是一个专业的合同分段审查智能体(SegmentReview)。 你是一个专业的合同分段审查智能体(SegmentReview)。
...@@ -21,10 +22,28 @@ REVIEW_SYSTEM_PROMPT = ''' ...@@ -21,10 +22,28 @@ REVIEW_SYSTEM_PROMPT = '''
【审查原则】 【审查原则】
- 严格基于给定的审查规则进行审查,不得脱离规则自行扩展审查标准。 - 严格基于给定的审查规则进行审查,不得脱离规则自行扩展审查标准。
- 只有在证据充分时才生成 finding。弱猜测、无充分文本支持的问题或合格判断不得输出。
- 只审查当前分段原文,不得使用上下文信息补充、修正或推断当前分段含义。 - 只审查当前分段原文,不得使用上下文信息补充、修正或推断当前分段含义。
- 优先识别“确定成立”的合格或不合格结论,不输出模糊怀疑类表述。 - 优先识别“确定成立”的合格或不合格结论,不输出模糊怀疑类表述。
【完整性要求(非常重要)】
你必须对当前分段进行“穷举式审查”,不得只输出部分结果。
执行方式:
- 应逐句扫描当前分段
- 对每一句或关键子句,判断其是否与审查规则相关
- 只要存在证据充分的问题或合格表述,必须全部列出,不得遗漏
特别要求:
- 不得因为已找到1条或少量finding而提前停止
- 若一个段落中存在多处问题,必须分别输出多个 findings
- findings 数量应与段落中实际存在的问题数量大致一致,不得明显偏少
错误示例(禁止):
- 一个段落有多个风险点,但只输出1条
正确行为:
- 覆盖所有可以独立成立的审查点
【结果判定规则】 【结果判定规则】
- result 只能取以下两个值之一: - result 只能取以下两个值之一:
- "合格":当前分段存在与规则相关的明确内容,且符合该规则要求; - "合格":当前分段存在与规则相关的明确内容,且符合该规则要求;
...@@ -32,29 +51,20 @@ REVIEW_SYSTEM_PROMPT = ''' ...@@ -32,29 +51,20 @@ REVIEW_SYSTEM_PROMPT = '''
- 如果当前分段与某条审查规则无关,或虽疑似相关但证据不足,则不得生成 finding。 - 如果当前分段与某条审查规则无关,或虽疑似相关但证据不足,则不得生成 finding。
【证据要求】 【证据要求】
每个 finding 都必须包含 original_text,且必须是合同原文的直接引用。 每个 findings 都必须包含 original_text,且必须是合同原文的直接引用。
【证据粒度(关键句原则)】 【单一证据约束(非常重要)】
original_text 必须满足“最小充分证据原则”: 每一个 finding 必须只对应一个“独立判断点”和一个“最小证据句”。
- 只引用能够证明该判断成立的最小文本片段;
- 优先引用单句或关键子句; 具体要求:
- 不得复制整段条款。 - 一个 finding 只能基于一个关键句或一个最小语义单元;
- 若多个句子分别支持不同问题,必须拆分为多个 findings;
引用长度限制: - 严禁将多个不同问题合并为一个 finding;
- 推荐:20–80 字 - 严禁在 original_text 中拼接多个不连续句子作为证据;
- 最大:120 字 - 若 original_text 涉及跨句或跨段内容,必须拆分为多个 findings。
- 若一句话即可证明该判断,则只允许引用该句
判断标准:
生成 finding 时必须执行: - 如果去掉 original_text 中的一部分,仍能形成一个独立判断 → 说明应该拆分
Step 1:定位能够证明结果成立的关键句
Step 2:仅提取该句作为 original_text
Step 3:再判断该句对应 result="合格" 还是 result="不合格"
Step 4:输出 issue 与 suggestion
禁止:
- 复制整段条款
- 引用超过 3 句文本
- 引用与该判断无关的上下文
【issue 要求】 【issue 要求】
- issue 必须说明:该条款为什么合格或为什么不合格。 - issue 必须说明:该条款为什么合格或为什么不合格。
...@@ -76,7 +86,7 @@ Step 4:输出 issue 与 suggestion ...@@ -76,7 +86,7 @@ Step 4:输出 issue 与 suggestion
在执行任何审查规则之前,你必须先判断: 在执行任何审查规则之前,你必须先判断:
当前分段是否包含与该审查规则相关的信息维度。 当前分段是否包含与该审查规则相关的信息维度。
如果当前分段与该审查规则无关,则: 如果当前分段与该审查规则无关,则:
- 不得生成 finding - 不得生成 findings
- 不得引用原文 - 不得引用原文
- 继续检查下一条规则 - 继续检查下一条规则
如果所有规则均无关或均无证据充分的结论,则返回 {"findings": []}。 如果所有规则均无关或均无证据充分的结论,则返回 {"findings": []}。
...@@ -85,6 +95,17 @@ Step 4:输出 issue 与 suggestion ...@@ -85,6 +95,17 @@ Step 4:输出 issue 与 suggestion
- 严格按照指定 JSON Schema 输出。 - 严格按照指定 JSON Schema 输出。
- 不得输出任何 JSON 之外的解释性文字。 - 不得输出任何 JSON 之外的解释性文字。
- 若未发现证据充分的合格或不合格条款,返回 {"findings": []}。 - 若未发现证据充分的合格或不合格条款,返回 {"findings": []}。
在生成最终 JSON 之前,你必须执行以下内部步骤(不输出):
Step A:将当前分段拆分为若干句子或语义单元
Step B:逐句判断该句是否涉及任一审查规则
Step C:若涉及规则,判断其为合格或不合格
Step D:为每一个成立的判断生成一个 finding
只有完成上述穷举后,才允许输出最终结果
提示:在合同审查中,一个分段通常可能包含多个独立风险点或合规点,findings 数量通常大于1(如2–5条或更多),除非该段确实只涉及单一事项。
''' '''
REVIEW_USER_PROMPT = ''' REVIEW_USER_PROMPT = '''
【当前分段文本】 【当前分段文本】
...@@ -97,7 +118,7 @@ REVIEW_USER_PROMPT = ''' ...@@ -97,7 +118,7 @@ REVIEW_USER_PROMPT = '''
{ruleset_text} {ruleset_text}
【任务】 【任务】
请基于审查规则,仅针对当前分段文本进行审查,提取证据充分的合格条款和不合格条款,并输出审查结果。 请基于审查规则,参考审查案例,仅针对当前分段文本进行审查,提取证据充分的合格条款和不合格条款,并输出审查结果。
【特别要求】 【特别要求】
- 仅基于当前分段原文进行判断,不得参考任何上下文、摘要或记忆信息。 - 仅基于当前分段原文进行判断,不得参考任何上下文、摘要或记忆信息。
...@@ -230,7 +251,8 @@ class SegmentReviewTool(LLMTool): ...@@ -230,7 +251,8 @@ class SegmentReviewTool(LLMTool):
try: try:
responses = await self.chat_batch_async(msgs) responses = await self.chat_batch_async(msgs)
except Exception: except Exception as e:
logger.error(f'Error in segment review for segment {segment_id}: {e}')
return {"findings": []} return {"findings": []}
all_findings: List[Dict] = [] all_findings: List[Dict] = []
...@@ -287,33 +309,48 @@ class SegmentReviewTool(LLMTool): ...@@ -287,33 +309,48 @@ class SegmentReviewTool(LLMTool):
if __name__=="__main__": if __name__=="__main__":
tool = SegmentReviewTool() tool = SegmentReviewTool()
segment_text = '''
3.1 合同价格
3.1.1 合同协议书中载明的签约合同价包括卖方为完成合同全部义务应承担的一切成本、费用和支出以及卖方的合理利润。
3.1.2 除专用合同条款另有约定外,签约合同价为固定价格。
3.2 合同价款的支付
除专用合同条款另有约定外,买方应通过以下方式和比例向卖方支付合同价款:
3.2.1 预付款
合同生效后,买方在收到卖方开具的注明应付预付款金额的财务收据正本一份并经审核无误后 30 日内,向卖方支付签约合同价的10%作为预付款。买方支付预付款后,如卖方未履行合同义务,则买方有权收回预付款;如卖方依约履行了 合同义务,则预付款抵作合同价款。
3.2.2 交货款
卖方按合同约定交付全部合同设备后,买方在收到卖方提交的下列全部单据并经审核无误后30日内,向卖方支付合同价格的 60%
(1) 卖方出具的交货清单正本一份;
(2) 买方签署的收货清单正本一份;
(3) 制造商出具的出厂质量合格证正本一份;
(4) 合同价格100%金额的增值税发票正本一份。
3.2.3 验收款
买方在收到卖方提交的买卖双方签署的合同设备验收证书或已生效的验收款支付函正本一份并经审核无误后30 日内,向卖方支付合同价格的 25%
3.2.4 结清款
买方在收到卖方提交的买方签署的质量保证期届满证书或已生效的结清款支付函正本一份并经审核无误后 30 日内,向卖方支付合同价格的 5%。如果依照合同第 9.1 项,卖方应向买方支付费用的,买方有权从结清款中直接扣除该笔费用。
除专用合同条款另有约定外,在买方向卖方支付验收款的同时或其后的任何时间内,卖方可在向买方提交买方可接受的金额为合同价格 5%的合同结清款保函的前提下,要求买方支付合同结清款,买方不得拒绝。'''
result = tool.run( result = tool.run(
segment_id=1, segment_id=1,
segment_text="本合同自双方签字盖章之日起生效,有效期为两年,期满后自动续展一年,除非一方提前30天书面通知对方终止。", segment_text=segment_text,
rules=[ rules=[
{ {
"title": "期限与续展条款清晰性", "title": "付款时间审查",
"rule": "合同期限、续展触发与终止通知期限应明确,避免自动续展引发不确定性。", "rule": "1)我司规定如果发货前全额付款,审查合格2)如果发货前没有全额付款,发货后每个付款项的支付条件(除了预付款,发货款以外,例如到货款,交货款,验收款,结清款等),要以“到货XX天/月”作为充分条件之一,如果有多个充分条件,需要提及“先到为准”。(要有到货XX天/月作为闭口时间)",
"level": "M", "level": "M",
"suggestion_template": "明确续展条件、通知方式与生效时间。", "suggestion_template": "1、修改付款条款,使得“到货XX天/月”作为付款的充分条件之一",
"case": "自动续展条款未约定通知送达标准导致争议。", "case": """
## 案例1:
原文:收到乙方正式发货通知及进度款发票后15个工作日内支付 30% 发货款
结论:非全额付款,但是发货款无需“到货XX天/月”的条件作为闭口条件,因此审查合格
## 案例2
原文:交付合同设备后,现场经清点无误并验收合格,到货款付款20%
结论:非全额付款,非发货款,并且没有“到货XX天/月”的条件作为闭口时间,因此审查不合格
## 案例3
原文:交货款付款20%;验收款付款20%;结清款付款10%,质保款在质量保证期满后一次性支付
结论:所有非发货款(包括交货款,验收款,结清款,质保款),没有“到货XX天/月”的条件作为闭口时间,因此审查不合格
"""
} }
], ],
party_role="甲方", party_role="金盘",
context_summaries=[
{
"付款": {"方式": "银行转账", "期限": "验收后30日内"},
"META": {"segment_id": 0}
}
],
context_memories=[
{
"segment_id": 0,
"overall_conclusion": "pass",
"findings": [],
"missing_info": []
}
]
) )
print(json.dumps(result, ensure_ascii=False, indent=2)) print(json.dumps(result, ensure_ascii=False, indent=2))
......
...@@ -7,6 +7,7 @@ from typing import Dict, List, Optional ...@@ -7,6 +7,7 @@ from typing import Dict, List, Optional
from core.tool import tool, tool_func from core.tool import tool, tool_func
from core.tools.segment_llm import LLMTool from core.tools.segment_llm import LLMTool
from core.config import META_KEY from core.config import META_KEY
from loguru import logger
SUMMARY_SYSTEM_PROMPT = f''' SUMMARY_SYSTEM_PROMPT = f'''
你是合同事实提取智能体(SegmentSummary)。 你是合同事实提取智能体(SegmentSummary)。
...@@ -183,7 +184,7 @@ class SegmentSummaryTool(LLMTool): ...@@ -183,7 +184,7 @@ class SegmentSummaryTool(LLMTool):
data = self.parse_first_json(resp) data = self.parse_first_json(resp)
facts = data.get("facts") or {} facts = data.get("facts") or {}
except Exception as e: except Exception as e:
print(f'Error in segment summary for segment {segment_id}: {e}') logger.info(f'Error in segment summary for segment {segment_id}: {e}')
facts = {} facts = {}
facts[META_KEY] = { facts[META_KEY] = {
"segment_id": segment_id, "segment_id": segment_id,
......
...@@ -12,18 +12,21 @@ from loguru import logger ...@@ -12,18 +12,21 @@ from loguru import logger
from utils.common_util import random_str from utils.common_util import random_str
from utils.http_util import upload_file, fastgpt_openai_chat, download_file from utils.http_util import upload_file, fastgpt_openai_chat, download_file
SUFFIX='_麓发迁移' # SUFFIX='_麓发迁移'
batch_input_dir_path = 'jp-input' # batch_input_dir_path = 'jp-input'
batch_output_dir_path = 'jp-output-lufa-simple-new' # batch_output_dir_path = 'jp-output-lufa-new'
SUFFIX='_麓发'
batch_input_dir_path = 'lufa-input'
batch_output_dir_path = 'lufa-output'
batch_size = 5 batch_size = 5
# 麓发fastgpt接口 # 麓发fastgpt接口
# url = 'http://192.168.252.71:18089/api/v1/chat/completions' url = 'http://192.168.252.71:18089/api/v1/chat/completions'
# 金盘fastgpt接口 # 金盘fastgpt接口
url = 'http://192.168.252.71:18088/api/v1/chat/completions' # url = 'http://192.168.252.71:18088/api/v1/chat/completions'
# 麓发合同审查生产token # 麓发合同审查生产token
# token = 'fastgpt-ek3Z6PxI6sXgYc0jxzZ5bVGqrxwM6aVyfSmA6JVErJYBMr2KmYxrHwEUOIMSYz' token = 'fastgpt-ek3Z6PxI6sXgYc0jxzZ5bVGqrxwM6aVyfSmA6JVErJYBMr2KmYxrHwEUOIMSYz'
# 金盘迁移麓发合同审查测试token # 金盘迁移麓发合同审查测试token
token = 'fastgpt-vykT6qs07g7hR4tL2MNJE6DdNCIxaQjEu3Cxw9nuTBFg8MAG3CkByvnXKxSNEyMK7' # token = 'fastgpt-vykT6qs07g7hR4tL2MNJE6DdNCIxaQjEu3Cxw9nuTBFg8MAG3CkByvnXKxSNEyMK7'
# 人机交互测试(测试环境) # 人机交互测试(测试环境)
# token = 'fastgpt-p189K5zoTX5wjp0dBybFCwsbWm3juIwlJxt2wTGyiaOWOANI5Y10pKEZzyt' # token = 'fastgpt-p189K5zoTX5wjp0dBybFCwsbWm3juIwlJxt2wTGyiaOWOANI5Y10pKEZzyt'
# 人机交互测试(生产环境) # 人机交互测试(生产环境)
......
...@@ -2,9 +2,9 @@ from spire.doc import * ...@@ -2,9 +2,9 @@ from spire.doc import *
from spire.doc.common import * from spire.doc.common import *
# 创建一个 Document 类对象并加载一个 Word 文档 # 创建一个 Document 类对象并加载一个 Word 文档
benchmark_path = '/home/ccran/contract_review_agent/benchmark' benchmark_path = "/home/ccran/contract_review_agent/benchmark"
datasets_path = f'{benchmark_path}/datasets' datasets_path = f"{benchmark_path}/datasets"
clean_path = f'{benchmark_path}/clean' clean_path = f"{benchmark_path}/clean"
items = os.listdir(datasets_path) items = os.listdir(datasets_path)
for item in items: for item in items:
# 创建一个 Document 类的对象 # 创建一个 Document 类的对象
......
...@@ -101,6 +101,9 @@ def _compare_impl(val_dir: Path, answer_dir: Path) -> None: ...@@ -101,6 +101,9 @@ def _compare_impl(val_dir: Path, answer_dir: Path) -> None:
unmatched_val_count = sum(len(v) for v in unmatched_val_by_item.values()) unmatched_val_count = sum(len(v) for v in unmatched_val_by_item.values())
unmatched_answer_count = sum(len(v) for v in unmatched_answer_by_item.values()) unmatched_answer_count = sum(len(v) for v in unmatched_answer_by_item.values())
file_precision = (matched_total / val_total) if val_total != 0 else 0
file_recall = (matched_total / answer_total) if answer_total != 0 else 0
file_f1 = (2 * file_precision * file_recall / (file_precision + file_recall)) if (file_precision + file_recall) else 0
file_false_positive_rate = (unmatched_val_count / val_total) if val_total != 0 else 0 file_false_positive_rate = (unmatched_val_count / val_total) if val_total != 0 else 0
# 累加到各“审查项”的全局统计 # 累加到各“审查项”的全局统计
...@@ -115,8 +118,10 @@ def _compare_impl(val_dir: Path, answer_dir: Path) -> None: ...@@ -115,8 +118,10 @@ def _compare_impl(val_dir: Path, answer_dir: Path) -> None:
print('#' * 40) print('#' * 40)
print( print(
f"{val_file.name}: matched {matched_total} | val {val_total} | answer {answer_total} " f"{val_file.name}: matched {matched_total} | val {val_total} | answer {answer_total} "
f"| unmatched val {unmatched_val_count} | unmatched answer {unmatched_answer_count} | recall {matched_total / answer_total:.2%} | false_positive_rate {file_false_positive_rate:.2%}" f"| unmatched val {unmatched_val_count} | unmatched answer {unmatched_answer_count} | precision {file_precision:.2%} | recall {file_recall:.2%} | f1 {file_f1:.2%} | false_positive_rate {file_false_positive_rate:.2%}"
) )
import json
print(f'unmatched_val_by_item: {json.dumps(unmatched_val_by_item, ensure_ascii=False, indent=2)}')
for item in sorted(answer_counts): for item in sorted(answer_counts):
item_matches = matched_by_item.get(item, []) item_matches = matched_by_item.get(item, [])
print(f" 审查项 {item}: matched {len(item_matches)} / {answer_counts[item]}") print(f" 审查项 {item}: matched {len(item_matches)} / {answer_counts[item]}")
...@@ -136,10 +141,12 @@ def _compare_impl(val_dir: Path, answer_dir: Path) -> None: ...@@ -136,10 +141,12 @@ def _compare_impl(val_dir: Path, answer_dir: Path) -> None:
for t in uv: for t in uv:
print(f" val: {t}") print(f" val: {t}")
# break # only first file for demo # break # only first file for demo
precision = overall_matched / overall_val if overall_val else 0
recall = overall_matched / overall_answer if overall_answer else 0 recall = overall_matched / overall_answer if overall_answer else 0
f1 = (2 * precision * recall / (precision + recall)) if (precision + recall) else 0
overall_false_positive_rate = (overall_val - overall_matched) / overall_val if overall_val else 0 overall_false_positive_rate = (overall_val - overall_matched) / overall_val if overall_val else 0
print( print(
f"Overall: matched {overall_matched} | val {overall_val} | answer {overall_answer} | recall {recall:.2%} | false_positive_rate {overall_false_positive_rate:.2%}" f"Overall: matched {overall_matched} | val {overall_val} | answer {overall_answer} | precision {precision:.2%} | recall {recall:.2%} | f1 {f1:.2%}"
) )
# 按“审查项”的 overall 结果 # 按“审查项”的 overall 结果
...@@ -153,7 +160,9 @@ def _compare_impl(val_dir: Path, answer_dir: Path) -> None: ...@@ -153,7 +160,9 @@ def _compare_impl(val_dir: Path, answer_dir: Path) -> None:
mat = overall_item_matched.get(it, 0) mat = overall_item_matched.get(it, 0)
u_ans = overall_item_unmatched_answer.get(it, 0) u_ans = overall_item_unmatched_answer.get(it, 0)
u_val = overall_item_unmatched_val.get(it, 0) u_val = overall_item_unmatched_val.get(it, 0)
item_precision = (mat / (mat + u_val)) if (mat + u_val) else 0
acc = (mat / ans) if ans else 0 acc = (mat / ans) if ans else 0
item_f1 = (2 * item_precision * acc / (item_precision + acc)) if (item_precision + acc) else 0
item_false_positive_rate = u_val / (mat + u_val) if (mat + u_val) else 0 item_false_positive_rate = u_val / (mat + u_val) if (mat + u_val) else 0
rows_by_item.append({ rows_by_item.append({
"审查项": it, "审查项": it,
...@@ -161,16 +170,20 @@ def _compare_impl(val_dir: Path, answer_dir: Path) -> None: ...@@ -161,16 +170,20 @@ def _compare_impl(val_dir: Path, answer_dir: Path) -> None:
"合同所有不合格项": ans, "合同所有不合格项": ans,
"大模型其他不合格项": u_val, "大模型其他不合格项": u_val,
"大模型未匹配上的不合格项(C-B)": u_ans, "大模型未匹配上的不合格项(C-B)": u_ans,
"查准率(B/B+D)": item_precision,
"查全率(B/C)": acc, "查全率(B/C)": acc,
"F1": item_f1,
"误报率(D/B+D)": item_false_positive_rate, "误报率(D/B+D)": item_false_positive_rate,
}) })
print( print(
f" 审查项 {it}: matched {mat} / answer {ans} | unmatched val {u_val} | unmatched answer {u_ans} | recall {acc:.2%} | false_positive_rate {item_false_positive_rate:.2%}" f" 审查项 {it}: matched {mat} / answer {ans} | unmatched val {u_val} | unmatched answer {u_ans} | precision {item_precision:.2%} | recall {acc:.2%} | f1 {item_f1:.2%}"
) )
overall_by_item_df = pd.DataFrame(rows_by_item, columns=["审查项", "大模型匹配上的不合格项", "合同所有不合格项", "大模型其他不合格项", "大模型未匹配上的不合格项(C-B)", "查全率(B/C)", "误报率(D/B+D)"]) overall_by_item_df = pd.DataFrame(rows_by_item, columns=["审查项", "大模型匹配上的不合格项", "合同所有不合格项", "大模型其他不合格项", "大模型未匹配上的不合格项(C-B)", "查准率(B/B+D)", "查全率(B/C)", "F1", "误报率(D/B+D)"])
unmatched_val_total = sum(overall_item_unmatched_val.values()) unmatched_val_total = sum(overall_item_unmatched_val.values())
unmatched_answer_total = sum(overall_item_unmatched_answer.values()) unmatched_answer_total = sum(overall_item_unmatched_answer.values())
overall_precision = overall_matched / (overall_matched + unmatched_val_total) if (overall_matched + unmatched_val_total) else 0
overall_f1 = (2 * overall_precision * recall / (overall_precision + recall)) if (overall_precision + recall) else 0
overall_invalid_rate = unmatched_val_total / (overall_matched + unmatched_val_total) if (overall_matched + unmatched_val_total) else 0 overall_invalid_rate = unmatched_val_total / (overall_matched + unmatched_val_total) if (overall_matched + unmatched_val_total) else 0
overall_total_df = pd.DataFrame([ overall_total_df = pd.DataFrame([
{ {
...@@ -179,10 +192,12 @@ def _compare_impl(val_dir: Path, answer_dir: Path) -> None: ...@@ -179,10 +192,12 @@ def _compare_impl(val_dir: Path, answer_dir: Path) -> None:
"合同所有不合格项": overall_answer, "合同所有不合格项": overall_answer,
"大模型其他不合格项": unmatched_val_total, "大模型其他不合格项": unmatched_val_total,
"大模型未匹配上的不合格项(C-B)": unmatched_answer_total, "大模型未匹配上的不合格项(C-B)": unmatched_answer_total,
"查准率(B/B+D)": overall_precision,
"查全率(B/C)": recall, "查全率(B/C)": recall,
"F1": overall_f1,
"误报率(D/B+D)": overall_invalid_rate, "误报率(D/B+D)": overall_invalid_rate,
} }
], columns=["审查项", "大模型匹配上的不合格项", "合同所有不合格项", "大模型其他不合格项", "大模型未匹配上的不合格项(C-B)", "查全率(B/C)", "误报率(D/B+D)"]) ], columns=["审查项", "大模型匹配上的不合格项", "合同所有不合格项", "大模型其他不合格项", "大模型未匹配上的不合格项(C-B)", "查准率(B/B+D)", "查全率(B/C)", "F1", "误报率(D/B+D)"])
combined_df = pd.concat([overall_by_item_df, overall_total_df], ignore_index=True) combined_df = pd.concat([overall_by_item_df, overall_total_df], ignore_index=True)
compare_dir_name = val_dir.name compare_dir_name = val_dir.name
......
...@@ -13,10 +13,10 @@ from compare_annotation import compare_with_log ...@@ -13,10 +13,10 @@ from compare_annotation import compare_with_log
# Map raw comment authors to unified review item names. # Map raw comment authors to unified review item names.
COMMENT_AUTHOR_MAPPING: dict[str, str] = { COMMENT_AUTHOR_MAPPING: dict[str, str] = {
"三方货款审查":"第三方审查", "三方货款审查": "第三方审查",
"履行义务审查":"第三方审查", "履行义务审查": "第三方审查",
"违约条款审查":"违约与延期审查", "违约条款审查": "违约与延期审查",
"延期审查":"违约与延期审查" "延期审查": "违约与延期审查",
} }
...@@ -121,7 +121,7 @@ def _parse_args() -> argparse.Namespace: ...@@ -121,7 +121,7 @@ def _parse_args() -> argparse.Namespace:
parser.add_argument( parser.add_argument(
"--datasets-dir", "--datasets-dir",
type=Path, type=Path,
default=base / "results" / "jp-output-lufa-simple-new", default=base / "results" / "jp-output-renji",
help="Directory containing Word files with annotations.", help="Directory containing Word files with annotations.",
) )
parser.add_argument( parser.add_argument(
...@@ -133,13 +133,13 @@ def _parse_args() -> argparse.Namespace: ...@@ -133,13 +133,13 @@ def _parse_args() -> argparse.Namespace:
parser.add_argument( parser.add_argument(
"--val-dir", "--val-dir",
type=Path, type=Path,
default=base / "results" / "jp-output-lufa-simple-new-extracted", default=base / "results" / "jp-output-renji-extracted",
help="Directory to store extracted xlsx files for comparison.", help="Directory to store extracted xlsx files for comparison.",
) )
parser.add_argument( parser.add_argument(
"--strip-suffixes", "--strip-suffixes",
nargs="*", nargs="*",
default=['_麓发改进','_人机交互','_麓发迁移'], default=["_麓发改进", "_人机交互", "_麓发迁移"],
help=( help=(
"Optional filename suffixes to strip from generated val xlsx stems before " "Optional filename suffixes to strip from generated val xlsx stems before "
"comparison, e.g. --strip-suffixes _v1 _审阅版" "comparison, e.g. --strip-suffixes _v1 _审阅版"
......
No preview for this file type
import numpy as np for _ in range(input()):
try:
def calculate_grpo_advantages(rewards, epsilon=1e-8): eval(raw_input())
""" print("YES")
计算 GRPO 的组优势值 except TypeError:
:param rewards: 列表或数组,包含同一组样本的奖励值 print("NO")
:param epsilon: 稳定性系数,防止除以 0 except:
:return: 归一化后的优势值数组 print("NO")
"""
rewards = np.array(rewards)
# 1. 计算当前组的平均值
mean = np.mean(rewards)
# 2. 计算当前组的标准差
std = np.std(rewards)
# 3. 归一化计算优势
# 减去均值除以标准差,使得该组优势值满足均值为 0,标准差为 1
advantages = (rewards - mean) / (std + epsilon)
return advantages
# 示例数据
your_rewards = [1.1,1.1,1.1,1.1]
advantages = calculate_grpo_advantages(your_rewards)
print(f"原始奖励值: {your_rewards}")
print(f"GRPO 优势值: {advantages.round(4)}")
\ No newline at end of file
...@@ -14,7 +14,7 @@ from loguru import logger ...@@ -14,7 +14,7 @@ from loguru import logger
from utils.common_util import extract_url_file, format_now from utils.common_util import extract_url_file, format_now
from utils.http_util import download_file from utils.http_util import download_file
from core.cache import get_cached_doc_tool, get_cached_memory from core.cache import get_cached_doc_tool, get_cached_memory
from core.config import doc_support_formats, pdf_support_formats,MERGE_RULE_PROMPT from core.config import doc_support_formats, pdf_support_formats, MERGE_RULE_PROMPT
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
from core.tools.segment_rule_router import SegmentRuleRouterTool from core.tools.segment_rule_router import SegmentRuleRouterTool
...@@ -36,21 +36,23 @@ merger_tool = SegmentMergerTool() ...@@ -36,21 +36,23 @@ merger_tool = SegmentMergerTool()
@app.post("/sleep") @app.post("/sleep")
def sleep(t:int): def sleep(t: int):
import time import time
time.sleep(t) time.sleep(t)
return { return {"res": f"sleep over for {t} seconds."}
'res':f'sleep over for {t} seconds.'
}
######################################################################################################################## ########################################################################################################################
class DocumentParseRequest(BaseModel): class DocumentParseRequest(BaseModel):
conversation_id: str conversation_id: str
urls: List[str] = Field(..., description="File download url") urls: List[str] = Field(..., description="File download url")
file_ext: Optional[str] = None file_ext: Optional[str] = None
ruleset_id: Optional[str] = "通用" ruleset_id: Optional[str] = "通用"
class DocumentParseResponse(BaseModel): class DocumentParseResponse(BaseModel):
conversation_id: str conversation_id: str
segment_ids: List[int] segment_ids: List[int]
...@@ -79,7 +81,9 @@ async def parse_document(payload: DocumentParseRequest) -> DocumentParseResponse ...@@ -79,7 +81,9 @@ async def parse_document(payload: DocumentParseRequest) -> DocumentParseResponse
try: try:
doc_obj, _ = get_cached_doc_tool(payload.conversation_id, file_ext) doc_obj, _ = get_cached_doc_tool(payload.conversation_id, file_ext)
except Exception as exc: except Exception as exc:
raise HTTPException(status_code=400, detail=f"Document tool not available: {exc}") raise HTTPException(
status_code=400, detail=f"Document tool not available: {exc}"
)
doc_obj.load(file_path) doc_obj.load(file_path)
# ocr # ocr
await doc_obj.get_from_ocr() await doc_obj.get_from_ocr()
...@@ -91,7 +95,8 @@ async def parse_document(payload: DocumentParseRequest) -> DocumentParseResponse ...@@ -91,7 +95,8 @@ async def parse_document(payload: DocumentParseRequest) -> DocumentParseResponse
ruleset_id = payload.ruleset_id or reference_tool.default_ruleset_id ruleset_id = payload.ruleset_id or reference_tool.default_ruleset_id
ruleset_items = reference_tool.run(ruleset_id=ruleset_id).get("rules", []) ruleset_items = reference_tool.run(ruleset_id=ruleset_id).get("rules", [])
ruleset_review_items = [ ruleset_review_items = [
t for t in (r.get("title") for r in ruleset_items) t
for t in (r.get("title") for r in ruleset_items)
if isinstance(t, str) and t.strip() if isinstance(t, str) and t.strip()
] ]
return DocumentParseResponse( return DocumentParseResponse(
...@@ -99,11 +104,13 @@ async def parse_document(payload: DocumentParseRequest) -> DocumentParseResponse ...@@ -99,11 +104,13 @@ async def parse_document(payload: DocumentParseRequest) -> DocumentParseResponse
text=text, text=text,
segment_ids=segment_ids, segment_ids=segment_ids,
ruleset_items=ruleset_review_items, ruleset_items=ruleset_review_items,
file_ext = file_ext file_ext=file_ext,
) )
######################################################################################################################## ########################################################################################################################
class SegmentSummaryRequest(BaseModel): class SegmentSummaryRequest(BaseModel):
conversation_id: str conversation_id: str
segment_id: int segment_id: int
...@@ -126,13 +133,18 @@ def summarize_facts(payload: SegmentSummaryRequest) -> SegmentSummaryResponse: ...@@ -126,13 +133,18 @@ def summarize_facts(payload: SegmentSummaryRequest) -> SegmentSummaryResponse:
try: try:
doc_obj, _ = get_cached_doc_tool(payload.conversation_id, payload.file_ext) doc_obj, _ = get_cached_doc_tool(payload.conversation_id, payload.file_ext)
except Exception as exc: except Exception as exc:
raise HTTPException(status_code=400, detail=f"Document tool not available: {exc}") raise HTTPException(
status_code=400, detail=f"Document tool not available: {exc}"
)
segment_idx = payload.segment_id - 1 segment_idx = payload.segment_id - 1
try: try:
segment_text = doc_obj.get_chunk_item(segment_idx) segment_text = doc_obj.get_chunk_item(segment_idx)
except Exception as exc: except Exception as exc:
raise HTTPException(status_code=404, detail=f"Segment text not found for id {payload.segment_id}: {exc}. Please parse document first.") raise HTTPException(
status_code=404,
detail=f"Segment text not found for id {payload.segment_id}: {exc}. Please parse document first.",
)
ruleset_id = payload.ruleset_id or reference_tool.default_ruleset_id ruleset_id = payload.ruleset_id or reference_tool.default_ruleset_id
rules = reference_tool.run( rules = reference_tool.run(
...@@ -154,8 +166,10 @@ def summarize_facts(payload: SegmentSummaryRequest) -> SegmentSummaryResponse: ...@@ -154,8 +166,10 @@ def summarize_facts(payload: SegmentSummaryRequest) -> SegmentSummaryResponse:
facts=result, facts=result,
) )
######################################################################################################################## ########################################################################################################################
class SegmentReviewRequest(BaseModel): class SegmentReviewRequest(BaseModel):
conversation_id: str conversation_id: str
segment_id: int segment_id: int
...@@ -180,19 +194,25 @@ class SegmentRuleRouterResponse(BaseModel): ...@@ -180,19 +194,25 @@ class SegmentRuleRouterResponse(BaseModel):
routed_rule_titles: List[str] routed_rule_titles: List[str]
routed_rules: List[Dict] routed_rules: List[Dict]
@app.post("/segments/review/findings", response_model=SegmentReviewResponse) @app.post("/segments/review/findings", response_model=SegmentReviewResponse)
def review_segment(payload: SegmentReviewRequest) -> SegmentReviewResponse: def review_segment(payload: SegmentReviewRequest) -> SegmentReviewResponse:
store = get_cached_memory(payload.conversation_id) store = get_cached_memory(payload.conversation_id)
try: try:
doc_obj, _ = get_cached_doc_tool(payload.conversation_id, payload.file_ext) doc_obj, _ = get_cached_doc_tool(payload.conversation_id, payload.file_ext)
except Exception as exc: except Exception as exc:
raise HTTPException(status_code=400, detail=f"Document tool not available: {exc}") raise HTTPException(
status_code=400, detail=f"Document tool not available: {exc}"
)
segment_idx = payload.segment_id - 1 segment_idx = payload.segment_id - 1
try: try:
segment_text = doc_obj.get_chunk_item(segment_idx) segment_text = doc_obj.get_chunk_item(segment_idx)
except Exception as exc: except Exception as exc:
raise HTTPException(status_code=404, detail=f"Segment text not found for id {payload.segment_id}: {exc}. Please parse document first.") raise HTTPException(
status_code=404,
detail=f"Segment text not found for id {payload.segment_id}: {exc}. Please parse document first.",
)
ruleset_id = payload.ruleset_id or reference_tool.default_ruleset_id ruleset_id = payload.ruleset_id or reference_tool.default_ruleset_id
rules = reference_tool.run( rules = reference_tool.run(
...@@ -220,15 +240,19 @@ def review_segment(payload: SegmentReviewRequest) -> SegmentReviewResponse: ...@@ -220,15 +240,19 @@ def review_segment(payload: SegmentReviewRequest) -> SegmentReviewResponse:
try: try:
store.add_finding( store.add_finding(
FINDING_KEY_REVIEW, FINDING_KEY_REVIEW,
Finding.from_dict({ Finding.from_dict(
{
"rule_title": f.get("rule_title", ""), "rule_title": f.get("rule_title", ""),
"segment_id": segment_idx, "segment_id": segment_idx,
"original_text": f.get("original_text",''), "original_text": f.get("original_text", ""),
"issue": f.get("issue", ""), "issue": f.get("issue", ""),
"risk_level": (f.get("risk_level") or f.get("level") or "").upper(), "risk_level": (
f.get("risk_level") or f.get("level") or ""
).upper(),
"result": f.get("result", ""), "result": f.get("result", ""),
"suggestion": f.get("suggestion", ""), "suggestion": f.get("suggestion", ""),
}) }
),
) )
except Exception as e: except Exception as e:
logger.error(e) logger.error(e)
...@@ -246,13 +270,18 @@ def route_segment_rules(payload: SegmentReviewRequest) -> SegmentRuleRouterRespo ...@@ -246,13 +270,18 @@ def route_segment_rules(payload: SegmentReviewRequest) -> SegmentRuleRouterRespo
try: try:
doc_obj, _ = get_cached_doc_tool(payload.conversation_id, payload.file_ext) doc_obj, _ = get_cached_doc_tool(payload.conversation_id, payload.file_ext)
except Exception as exc: except Exception as exc:
raise HTTPException(status_code=400, detail=f"Document tool not available: {exc}") raise HTTPException(
status_code=400, detail=f"Document tool not available: {exc}"
)
segment_idx = payload.segment_id - 1 segment_idx = payload.segment_id - 1
try: try:
segment_text = doc_obj.get_chunk_item(segment_idx) segment_text = doc_obj.get_chunk_item(segment_idx)
except Exception as exc: except Exception as exc:
raise HTTPException(status_code=404, detail=f"Segment text not found for id {payload.segment_id}: {exc}. Please parse document first.") raise HTTPException(
status_code=404,
detail=f"Segment text not found for id {payload.segment_id}: {exc}. Please parse document first.",
)
ruleset_id = payload.ruleset_id or reference_tool.default_ruleset_id ruleset_id = payload.ruleset_id or reference_tool.default_ruleset_id
rules = reference_tool.run(ruleset_id=ruleset_id).get("rules", []) rules = reference_tool.run(ruleset_id=ruleset_id).get("rules", [])
...@@ -272,6 +301,7 @@ def route_segment_rules(payload: SegmentReviewRequest) -> SegmentRuleRouterRespo ...@@ -272,6 +301,7 @@ def route_segment_rules(payload: SegmentReviewRequest) -> SegmentRuleRouterRespo
routed_rules=result.get("routed_rules", []), routed_rules=result.get("routed_rules", []),
) )
######################################################################################################################## ########################################################################################################################
class ReflectReviewRequest(BaseModel): class ReflectReviewRequest(BaseModel):
conversation_id: str conversation_id: str
...@@ -279,6 +309,7 @@ class ReflectReviewRequest(BaseModel): ...@@ -279,6 +309,7 @@ class ReflectReviewRequest(BaseModel):
ruleset_id: Optional[str] = "通用" ruleset_id: Optional[str] = "通用"
rule_title: str rule_title: str
class ReflectReviewResponse(BaseModel): class ReflectReviewResponse(BaseModel):
conversation_id: str conversation_id: str
rule_title: str rule_title: str
...@@ -303,13 +334,22 @@ def reflect_review(payload: ReflectReviewRequest) -> ReflectReviewResponse: ...@@ -303,13 +334,22 @@ def reflect_review(payload: ReflectReviewRequest) -> ReflectReviewResponse:
store = get_cached_memory(payload.conversation_id) store = get_cached_memory(payload.conversation_id)
ruleset_id = payload.ruleset_id or reference_tool.default_ruleset_id ruleset_id = payload.ruleset_id or reference_tool.default_ruleset_id
ruleset_items = reference_tool.run(ruleset_id=ruleset_id).get("rules", []) ruleset_items = reference_tool.run(ruleset_id=ruleset_id).get("rules", [])
rule = next((r for r in ruleset_items if r.get("title") == payload.rule_title), None) rule = next(
(r for r in ruleset_items if r.get("title") == payload.rule_title), None
)
if not rule: if not rule:
raise HTTPException(status_code=404, detail=f"Rule not found: {payload.rule_title}") raise HTTPException(
status_code=404, detail=f"Rule not found: {payload.rule_title}"
)
summary_keywords = reference_tool.summary_keywords([rule]) summary_keywords = reference_tool.summary_keywords([rule])
context_summaries_facts = store.search_facts(summary_keywords) context_summaries_facts = store.search_facts(summary_keywords)
# 查找审查规则对应的 findings # 查找审查规则对应的 findings
findings = [f.__dict__ for f in store.search_findings(FINDING_KEY_REVIEW, "", rule_title=payload.rule_title)] findings = [
f.__dict__
for f in store.search_findings(
FINDING_KEY_REVIEW, "", rule_title=payload.rule_title
)
]
final_findings = reflect_tool.run( final_findings = reflect_tool.run(
party_role=payload.party_role, party_role=payload.party_role,
rule=rule, rule=rule,
...@@ -321,15 +361,19 @@ def reflect_review(payload: ReflectReviewRequest) -> ReflectReviewResponse: ...@@ -321,15 +361,19 @@ def reflect_review(payload: ReflectReviewRequest) -> ReflectReviewResponse:
try: try:
store.add_finding( store.add_finding(
FINDING_KEY_REFLECT, FINDING_KEY_REFLECT,
Finding.from_dict({ Finding.from_dict(
{
"rule_title": f.get("rule_title", ""), "rule_title": f.get("rule_title", ""),
"segment_id": f.get("segment_id", 0), "segment_id": f.get("segment_id", 0),
"original_text": f.get("original_text", ""), "original_text": f.get("original_text", ""),
"issue": f.get("issue", ""), "issue": f.get("issue", ""),
"risk_level": (f.get("risk_level") or f.get("level") or "H").upper(), "risk_level": (
f.get("risk_level") or f.get("level") or "H"
).upper(),
"suggestion": f.get("suggestion", ""), "suggestion": f.get("suggestion", ""),
"result": f.get("result", "") "result": f.get("result", ""),
}) }
),
) )
except Exception: except Exception:
continue continue
...@@ -347,7 +391,9 @@ def merge_segment_findings(payload: MergerRequest) -> MergerResponse: ...@@ -347,7 +391,9 @@ def merge_segment_findings(payload: MergerRequest) -> MergerResponse:
target_segment_id = payload.segment_id - 1 target_segment_id = payload.segment_id - 1
segment_findings = store.get_findings_by_segment(source_key, target_segment_id) segment_findings = store.get_findings_by_segment(source_key, target_segment_id)
unqualified_findings = [f for f in segment_findings if (f.result or "").strip() == "不合格"] unqualified_findings = [
f for f in segment_findings if (f.result or "").strip() == "不合格"
]
merged_result = merger_tool.run([f.__dict__ for f in unqualified_findings]) merged_result = merger_tool.run([f.__dict__ for f in unqualified_findings])
merged_findings = merged_result.get("findings", []) or [] merged_findings = merged_result.get("findings", []) or []
...@@ -362,11 +408,13 @@ def merge_segment_findings(payload: MergerRequest) -> MergerResponse: ...@@ -362,11 +408,13 @@ def merge_segment_findings(payload: MergerRequest) -> MergerResponse:
"segment_id": target_segment_id, "segment_id": target_segment_id,
"original_text": f.get("original_text", ""), "original_text": f.get("original_text", ""),
"issue": f.get("issue", ""), "issue": f.get("issue", ""),
"risk_level": (f.get("risk_level") or f.get("level") or "").upper(), "risk_level": (
f.get("risk_level") or f.get("level") or ""
).upper(),
"suggestion": f.get("suggestion", ""), "suggestion": f.get("suggestion", ""),
"result": f.get("result", ""), "result": f.get("result", ""),
} }
) ),
) )
except Exception as e: except Exception as e:
logger.error(e) logger.error(e)
...@@ -379,19 +427,23 @@ def merge_segment_findings(payload: MergerRequest) -> MergerResponse: ...@@ -379,19 +427,23 @@ def merge_segment_findings(payload: MergerRequest) -> MergerResponse:
merged_findings=merged_findings, merged_findings=merged_findings,
) )
######################################################################################################################## ########################################################################################################################
class ConversationResponse(BaseModel): class ConversationResponse(BaseModel):
conversation_id: str conversation_id: str
created_at: str created_at: str
@app.post("/conversations/new", response_model=ConversationResponse) @app.post("/conversations/new", response_model=ConversationResponse)
def new_conversation() -> ConversationResponse: def new_conversation() -> ConversationResponse:
conversation_id = uuid4().hex conversation_id = uuid4().hex
created_at = format_now() created_at = format_now()
return ConversationResponse(conversation_id=conversation_id, created_at=created_at) return ConversationResponse(conversation_id=conversation_id, created_at=created_at)
######################################################################################################################## ########################################################################################################################
class FactsRetrieveRequest(BaseModel): class FactsRetrieveRequest(BaseModel):
conversation_id: str conversation_id: str
keywords: List[str] = Field(..., description="facts 检索关键字列表") keywords: List[str] = Field(..., description="facts 检索关键字列表")
...@@ -406,7 +458,9 @@ class FactsRetrieveResponse(BaseModel): ...@@ -406,7 +458,9 @@ class FactsRetrieveResponse(BaseModel):
@app.post("/memory/facts/retrieve", response_model=FactsRetrieveResponse) @app.post("/memory/facts/retrieve", response_model=FactsRetrieveResponse)
def retrieve_facts(payload: FactsRetrieveRequest) -> FactsRetrieveResponse: def retrieve_facts(payload: FactsRetrieveRequest) -> FactsRetrieveResponse:
keywords = [k.strip() for k in (payload.keywords or []) if isinstance(k, str) and k.strip()] keywords = [
k.strip() for k in (payload.keywords or []) if isinstance(k, str) and k.strip()
]
if not keywords: if not keywords:
raise HTTPException(status_code=400, detail="keywords cannot be empty") raise HTTPException(status_code=400, detail="keywords cannot be empty")
...@@ -419,8 +473,10 @@ def retrieve_facts(payload: FactsRetrieveRequest) -> FactsRetrieveResponse: ...@@ -419,8 +473,10 @@ def retrieve_facts(payload: FactsRetrieveRequest) -> FactsRetrieveResponse:
total=len(matched_facts), total=len(matched_facts),
) )
######################################################################################################################## ########################################################################################################################
class MemoryExportRequest(BaseModel): class MemoryExportRequest(BaseModel):
conversation_id: str conversation_id: str
file_ext: str file_ext: str
...@@ -440,7 +496,9 @@ def export_memory(payload: MemoryExportRequest) -> MemoryExportResponse: ...@@ -440,7 +496,9 @@ def export_memory(payload: MemoryExportRequest) -> MemoryExportResponse:
try: try:
doc_obj, _ = get_cached_doc_tool(payload.conversation_id, payload.file_ext) doc_obj, _ = get_cached_doc_tool(payload.conversation_id, payload.file_ext)
except Exception as exc: except Exception as exc:
raise HTTPException(status_code=400, detail=f"Document tool not available: {exc}") raise HTTPException(
status_code=400, detail=f"Document tool not available: {exc}"
)
try: try:
excel_res = store.export_to_excel(file_name=payload.file_name) excel_res = store.export_to_excel(file_name=payload.file_name)
except ImportError as exc: except ImportError as exc:
...@@ -449,10 +507,14 @@ def export_memory(payload: MemoryExportRequest) -> MemoryExportResponse: ...@@ -449,10 +507,14 @@ def export_memory(payload: MemoryExportRequest) -> MemoryExportResponse:
raise HTTPException(status_code=500, detail=f"Export failed: {exc}") raise HTTPException(status_code=500, detail=f"Export failed: {exc}")
try: try:
doc_res = store.export_findings_to_doc_comments(doc_obj, finding_key=payload.finding_key or FINDING_KEY_REVIEW) doc_res = store.export_findings_to_doc_comments(
doc_obj, finding_key=payload.finding_key or FINDING_KEY_REVIEW
)
except Exception as exc: except Exception as exc:
traceback.print_exc() traceback.print_exc()
raise HTTPException(status_code=500, detail=f"Export doc comments failed: {exc}") raise HTTPException(
status_code=500, detail=f"Export doc comments failed: {exc}"
)
return MemoryExportResponse( return MemoryExportResponse(
conversation_id=payload.conversation_id, conversation_id=payload.conversation_id,
...@@ -460,12 +522,12 @@ def export_memory(payload: MemoryExportRequest) -> MemoryExportResponse: ...@@ -460,12 +522,12 @@ def export_memory(payload: MemoryExportRequest) -> MemoryExportResponse:
doc_url=doc_res, doc_url=doc_res,
) )
if __name__ == "__main__": if __name__ == "__main__":
from core.config import use_lufa from core.config import use_lufa
if use_lufa: if use_lufa:
port = 18168 port = 18168
else: else:
port = 18169 port = 18169
uvicorn.run( uvicorn.run("main:app", host="0.0.0.0", port=port, log_level="info", reload=False)
"main:app", host="0.0.0.0", port=port, log_level="info", reload=False
)
\ No newline at end of file
...@@ -20,7 +20,7 @@ class OpenAITool: ...@@ -20,7 +20,7 @@ class OpenAITool:
base_url=llm_config.base_url, api_key=llm_config.api_key base_url=llm_config.base_url, api_key=llm_config.api_key
) )
@retry(stop=stop_after_delay(600) | stop_after_attempt(1), wait=wait_fixed(1)) @retry(stop=stop_after_delay(600) | stop_after_attempt(3), wait=wait_fixed(1))
async def chat(self, msg, tools=None): async def chat(self, msg, tools=None):
if tools is None: if tools is None:
extra_body = None extra_body = None
......
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