Skip to content
Toggle navigation
P
Projects
G
Groups
S
Snippets
Help
ccran
/
lufa-contract
This project
Loading...
Sign in
Toggle navigation
Go to a project
Project
Repository
Issues
0
Merge Requests
0
Pipelines
Wiki
Snippets
Members
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Commit
a79604f2
authored
Apr 16, 2026
by
ccran
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
feat: 维修时间f1>80
parent
2347d107
Show whitespace changes
Inline
Side-by-side
Showing
12 changed files
with
306 additions
and
150 deletions
+306
-150
__pycache__/main.cpython-312.pyc
+0
-0
core/__pycache__/config.cpython-312.pyc
+0
-0
core/tools/__pycache__/segment_llm.cpython-312.pyc
+0
-0
core/tools/__pycache__/segment_review.cpython-312.pyc
+0
-0
core/tools/segment_llm.py
+6
-2
core/tools/segment_merger.py
+100
-86
core/tools/segment_review.py
+151
-33
data/batch/batch.py
+35
-24
data/benchmark/compare_annotation.py
+12
-1
data/benchmark/eval.py
+1
-1
data/rules.xlsx
+0
-0
main.py
+1
-3
No files found.
__pycache__/main.cpython-312.pyc
View file @
a79604f2
No preview for this file type
core/__pycache__/config.cpython-312.pyc
View file @
a79604f2
No preview for this file type
core/tools/__pycache__/segment_llm.cpython-312.pyc
View file @
a79604f2
No preview for this file type
core/tools/__pycache__/segment_review.cpython-312.pyc
View file @
a79604f2
No preview for this file type
core/tools/segment_llm.py
View file @
a79604f2
...
@@ -12,7 +12,9 @@ from core.config import LLM, MAX_WORKERS
...
@@ -12,7 +12,9 @@ from core.config import LLM, MAX_WORKERS
class
LLMTool
(
ToolBase
):
class
LLMTool
(
ToolBase
):
"""LLM-backed processor: builds prompts, calls LLM, parses JSON."""
"""LLM-backed processor: builds prompts, calls LLM, parses JSON."""
def
__init__
(
self
,
system_prompt
:
str
,
llm_key
:
str
=
"fastgpt_segment_review"
)
->
None
:
def
__init__
(
self
,
system_prompt
:
str
,
llm_key
:
str
=
"fastgpt_segment_review"
)
->
None
:
super
()
.
__init__
()
super
()
.
__init__
()
self
.
system_prompt
=
system_prompt
self
.
system_prompt
=
system_prompt
self
.
llm
=
OpenAITool
(
LLM
[
llm_key
],
max_workers
=
MAX_WORKERS
)
self
.
llm
=
OpenAITool
(
LLM
[
llm_key
],
max_workers
=
MAX_WORKERS
)
...
@@ -33,7 +35,9 @@ class LLMTool(ToolBase):
...
@@ -33,7 +35,9 @@ class LLMTool(ToolBase):
try
:
try
:
return
asyncio
.
run
(
coro
)
return
asyncio
.
run
(
coro
)
except
RuntimeError
as
e
:
except
RuntimeError
as
e
:
print
(
f
'RuntimeError in run_with_loop: {e}, trying to get event loop and run until complete.'
)
print
(
f
"RuntimeError in run_with_loop: {e}, trying to get event loop and run until complete."
)
loop
=
asyncio
.
get_event_loop
()
loop
=
asyncio
.
get_event_loop
()
return
loop
.
run_until_complete
(
coro
)
return
loop
.
run_until_complete
(
coro
)
...
...
core/tools/segment_merger.py
View file @
a79604f2
...
@@ -4,78 +4,36 @@ import difflib
...
@@ -4,78 +4,36 @@ import difflib
import
json
import
json
import
re
import
re
import
unicodedata
import
unicodedata
from
typing
import
Any
,
Dict
,
List
from
typing
import
Any
,
Callable
,
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
loguru
import
logger
from
loguru
import
logger
import
traceback
MERGER_SYSTEM_PROMPT
=
"""
MERGER_SYSTEM_PROMPT
=
"""
你是合同审查结果合并智能体(SegmentMerger)。
你将收到同一组 findings 的 issue 与 suggestion 列表,请做信息融合而非机械拼接。
你的任务是:接收一组 findings,按 original_text 分组后合并。
要求:
1. 输入中已经包含同组条款原文`original_text`,请仅将其作为分析依据。
【规则】
2. `issue`:提炼并合并组内风险点,去重、保留关键信息,语言精炼。
1. 以“文本重叠关系”分组:
3. `suggestion`:合并为一条可执行建议,必须基于输入原文的具体表述来给出,避免空泛、泛化或与原文脱节,必要时按“先补充条款、再明确标准”这类逻辑组织。
- 完全相同:可合并。
4. 禁止输出与输入无关的信息。
- 存在公共子句(如一方是另一方子串,或两句共享连续核心片段):可合并。
- 无明显公共子句、语义独立:不可合并。
2. 每个分组最终只保留 1 条 finding。
3. 对于“公共子句合并”的分组,合并后的 original_text 取“并集文本”:
- 若 A 是 B 的子串,取 B。
- 若 A、B 各有新增片段且围绕同一事实,拼接为不重复、语义通顺的一条完整原文。
4. 完全不相关的句子必须保留为不同分组。
【分组示例】
- 句子1:"A:提交数据后15日内开票并付款。"
- 句子2:"B:提交数据后15日内开票并付款。另由子公司承担服务费。"
- 句子3:"甲方:xx公司"
应分为两组:
- 分组1(句子1+句子2):"提交数据后15日内开票并付款。另由子公司承担服务费。"
- 分组2(句子3):"甲方:xx公司"
【合并要求】
- 同步合并 issue 和 suggestion,兼顾组内要点
- 保留关键信息,不要机械拼接
【字段约束】
- 输出字段固定为:rule_title, segment_id, original_text, issue, risk_level, suggestion, result
- risk_level 仅允许 H/M/L/空字符串
- result 仅允许 合格/不合格/空字符串
- original_text 必须与该组合的原文一致
【输出要求】
- 严格输出 JSON
- 顶层结构必须是:{"findings": [...]}
"""
"""
MERGER_USER_PROMPT
=
"""
MERGER_USER_PROMPT
=
"""
【输入 findings】
输入:
{findings_json}
{payload}
【任务】
请按“文本重叠关系(完全相同或存在公共子句)”分组后合并,并返回合并结果。
若同组文本可互补,请将 original_text 扩展为覆盖组内信息的并集文本;无关文本必须分到不同组。
输出 JSON
。
输出 JSON
,格式如下:
"""
"""
OUTPUT_EXAMPLE
=
"""
OUTPUT_EXAMPLE
=
"""
```json
```json
{
{
"findings": [
"issue": "提炼合并后的风险点",
{
"suggestion": "提炼合并后的建议"
"rule_title": "付款条款完整性",
"segment_id": 3,
"original_text": "甲方应于验收后30日内支付合同款项",
"issue": "付款期限明确,但未约定逾期违约责任,风险控制不足。",
"risk_level": "M",
"suggestion": "补充约定逾期付款违约金标准及计算方式。",
"result": "不合格"
}
]
}
}
```
```
"""
"""
...
@@ -173,7 +131,10 @@ def _risk_rank(level: str) -> int:
...
@@ -173,7 +131,10 @@ def _risk_rank(level: str) -> int:
return
{
""
:
0
,
"L"
:
1
,
"M"
:
2
,
"H"
:
3
}
.
get
(
level
,
0
)
return
{
""
:
0
,
"L"
:
1
,
"M"
:
2
,
"H"
:
3
}
.
get
(
level
,
0
)
def
_merge_group
(
group
:
List
[
Dict
[
str
,
Any
]])
->
Dict
[
str
,
Any
]:
def
_merge_group
(
group
:
List
[
Dict
[
str
,
Any
]],
field_merger
:
Optional
[
Callable
[[
List
[
Dict
[
str
,
Any
]]],
Dict
[
str
,
str
]]]
=
None
,
)
->
Dict
[
str
,
Any
]:
if
not
group
:
if
not
group
:
return
_normalize_finding
({})
return
_normalize_finding
({})
...
@@ -200,20 +161,41 @@ def _merge_group(group: List[Dict[str, Any]]) -> Dict[str, Any]:
...
@@ -200,20 +161,41 @@ def _merge_group(group: List[Dict[str, Any]]) -> Dict[str, Any]:
else
:
else
:
merged_result
=
""
merged_result
=
""
merged_issue
=
_merge_unique_text
(
issues
)
merged_suggestion
=
_merge_unique_text
(
suggestions
)
if
field_merger
:
try
:
merged_fields
=
field_merger
(
group
)
merged_issue
=
str
(
merged_fields
.
get
(
"issue"
,
""
)
or
merged_issue
)
merged_suggestion
=
str
(
merged_fields
.
get
(
"suggestion"
,
""
)
or
merged_suggestion
)
except
Exception
:
error_msg
=
traceback
.
format_exc
()
logger
.
error
(
f
"Error in field_merger, fallback to unique merge:{error_msg}"
,
exc_info
=
True
,
)
# Fall back to deterministic merge when model merge is unavailable.
pass
return
_normalize_finding
(
return
_normalize_finding
(
{
{
"rule_title"
:
" / "
.
join
([
t
for
t
in
dict
.
fromkeys
(
rule_titles
)
if
t
]),
"rule_title"
:
" / "
.
join
([
t
for
t
in
dict
.
fromkeys
(
rule_titles
)
if
t
]),
"segment_id"
:
min
(
segment_ids
)
if
segment_ids
else
0
,
"segment_id"
:
min
(
segment_ids
)
if
segment_ids
else
0
,
"original_text"
:
merged_text
,
"original_text"
:
merged_text
,
"issue"
:
_merge_unique_text
(
issues
)
,
"issue"
:
merged_issue
,
"risk_level"
:
best_risk
,
"risk_level"
:
best_risk
,
"suggestion"
:
_merge_unique_text
(
suggestions
)
,
"suggestion"
:
merged_suggestion
,
"result"
:
merged_result
,
"result"
:
merged_result
,
}
}
)
)
def
_rule_based_merge
(
findings
:
List
[
Dict
[
str
,
Any
]])
->
List
[
Dict
[
str
,
Any
]]:
def
_rule_based_merge
(
findings
:
List
[
Dict
[
str
,
Any
]],
field_merger
:
Optional
[
Callable
[[
List
[
Dict
[
str
,
Any
]]],
Dict
[
str
,
str
]]]
=
None
,
)
->
List
[
Dict
[
str
,
Any
]]:
n
=
len
(
findings
)
n
=
len
(
findings
)
if
n
<=
1
:
if
n
<=
1
:
return
findings
return
findings
...
@@ -243,7 +225,16 @@ def _rule_based_merge(findings: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
...
@@ -243,7 +225,16 @@ def _rule_based_merge(findings: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
groups
.
append
([
findings
[
idx
]
for
idx
in
group_idx
])
groups
.
append
([
findings
[
idx
]
for
idx
in
group_idx
])
return
[
_merge_group
(
group
)
for
group
in
groups
]
return
[
_merge_group
(
group
,
field_merger
=
field_merger
)
for
group
in
groups
]
def
_deterministic_field_merge
(
group
:
List
[
Dict
[
str
,
Any
]])
->
Dict
[
str
,
str
]:
return
{
"issue"
:
_merge_unique_text
([
item
.
get
(
"issue"
,
""
)
for
item
in
group
]),
"suggestion"
:
_merge_unique_text
(
[
item
.
get
(
"suggestion"
,
""
)
for
item
in
group
]
),
}
@tool
(
"segment_merger"
,
"同证据 findings 合并"
)
@tool
(
"segment_merger"
,
"同证据 findings 合并"
)
...
@@ -268,39 +259,62 @@ class SegmentMergerTool(LLMTool):
...
@@ -268,39 +259,62 @@ class SegmentMergerTool(LLMTool):
def
run
(
def
run
(
self
,
findings
:
List
[
Dict
[
str
,
Any
]],
merge_mode
:
str
=
"llm"
self
,
findings
:
List
[
Dict
[
str
,
Any
]],
merge_mode
:
str
=
"llm"
)
->
Dict
[
str
,
List
[
Dict
[
str
,
Any
]]]:
)
->
Dict
[
str
,
List
[
Dict
[
str
,
Any
]]]:
normalized_input
=
[
normalized_findings
=
self
.
_normalize_findings
(
findings
)
_normalize_finding
(
_as_dict
(
item
))
for
item
in
(
findings
or
[])
if
not
normalized_findings
:
]
if
not
normalized_input
:
return
{
"findings"
:
[]}
return
{
"findings"
:
[]}
mode
=
str
(
merge_mode
or
"llm"
)
.
lower
()
mode
=
str
(
merge_mode
or
"llm"
)
.
lower
()
if
mode
==
"rule"
:
field_merger
=
self
.
_resolve_field_merger
(
mode
)
return
{
"findings"
:
_rule_based_merge
(
normalized_input
)}
merged
=
_rule_based_merge
(
normalized_findings
,
field_merger
=
field_merger
)
return
{
"findings"
:
merged
}
msgs
=
self
.
_build_prompt
(
normalized_input
)
def
_normalize_findings
(
self
,
findings
:
List
[
Dict
[
str
,
Any
]]
)
->
List
[
Dict
[
str
,
Any
]]:
return
[
_normalize_finding
(
_as_dict
(
item
))
for
item
in
(
findings
or
[])]
def
_resolve_field_merger
(
self
,
mode
:
str
)
->
Callable
[[
List
[
Dict
[
str
,
Any
]]],
Dict
[
str
,
str
]]:
if
mode
==
"llm"
:
return
self
.
_safe_llm_field_merge
return
_deterministic_field_merge
def
_safe_llm_field_merge
(
self
,
group
:
List
[
Dict
[
str
,
Any
]])
->
Dict
[
str
,
str
]:
try
:
try
:
resp
=
self
.
run_with_loop
(
self
.
chat_async
(
msgs
))
return
self
.
_merge_issue_suggestion_with_llm
(
group
)
data
=
self
.
parse_first_json
(
resp
)
except
Exception
:
raw_findings
=
data
.
get
(
"findings"
)
or
data
.
get
(
"merged_findings"
)
or
[]
error_msg
=
traceback
.
format_exc
()
if
not
isinstance
(
raw_findings
,
list
):
logger
.
error
(
raw_findings
=
[]
f
"LLM field merge failed, fallback to deterministic merge:{error_msg}"
,
normalized_output
=
[
exc_info
=
True
,
_normalize_finding
(
_as_dict
(
item
))
for
item
in
raw_findings
)
]
return
_deterministic_field_merge
(
group
)
return
{
"findings"
:
normalized_output
}
except
Exception
as
e
:
def
_merge_issue_suggestion_with_llm
(
logger
.
error
(
f
"SegmentMergerTool run error: {e}"
)
self
,
group
:
List
[
Dict
[
str
,
Any
]]
return
{
"findings"
:
_rule_based_merge
(
normalized_input
)}
)
->
Dict
[
str
,
str
]:
payload
=
{
def
_build_prompt
(
self
,
findings
:
List
[
Dict
[
str
,
Any
]])
->
List
[
Dict
[
str
,
str
]]:
"original_texts"
:
[
str
(
item
.
get
(
"original_text"
,
""
)
or
""
)
for
item
in
group
],
"issues"
:
[
str
(
item
.
get
(
"issue"
,
""
)
or
""
)
for
item
in
group
],
"suggestions"
:
[
str
(
item
.
get
(
"suggestion"
,
""
)
or
""
)
for
item
in
group
],
}
user_content
=
(
user_content
=
(
MERGER_USER_PROMPT
.
format
(
MERGER_USER_PROMPT
.
format
(
findings_json
=
json
.
dumps
(
findings
,
ensure_ascii
=
False
,
indent
=
2
)
payload
=
json
.
dumps
(
payload
,
ensure_ascii
=
False
,
indent
=
2
)
)
)
+
OUTPUT_EXAMPLE
+
OUTPUT_EXAMPLE
)
)
return
self
.
build_messages
(
user_content
)
msgs
=
self
.
build_messages
(
user_content
)
resp
=
self
.
run_with_loop
(
self
.
chat_async
(
msgs
))
data
=
self
.
parse_first_json
(
resp
)
issue
=
str
(
data
.
get
(
"issue"
,
""
)
or
""
)
.
strip
()
suggestion
=
str
(
data
.
get
(
"suggestion"
,
""
)
or
""
)
.
strip
()
if
not
issue
and
not
suggestion
:
raise
ValueError
(
"empty issue/suggestion returned by model"
)
return
{
"issue"
:
issue
,
"suggestion"
:
suggestion
}
if
__name__
==
"__main__"
:
if
__name__
==
"__main__"
:
...
@@ -334,4 +348,4 @@ if __name__ == "__main__":
...
@@ -334,4 +348,4 @@ if __name__ == "__main__":
"result"
:
"不合格"
,
"result"
:
"不合格"
,
},
},
]
]
print
(
json
.
dumps
(
tool
.
run
(
sample
,
merge_mode
=
"rule"
),
ensure_ascii
=
False
,
indent
=
2
))
print
(
json
.
dumps
(
tool
.
run
(
sample
),
ensure_ascii
=
False
,
indent
=
2
))
core/tools/segment_review.py
View file @
a79604f2
...
@@ -5,11 +5,132 @@ import json
...
@@ -5,11 +5,132 @@ import json
from
typing
import
Dict
,
List
,
Optional
from
typing
import
Dict
,
List
,
Optional
from
core.tool
import
tool
,
tool_func
from
core.tool
import
tool
,
tool_func
from
core.config
import
use_lufa
from
core.tools.segment_llm
import
LLMTool
from
core.tools.segment_llm
import
LLMTool
import
re
import
re
from
loguru
import
logger
from
loguru
import
logger
REVIEW_SYSTEM_PROMPT
=
"""
REVIEW_SYSTEM_PROMPT_LF
=
"""
你是一个专业的合同分段审查智能体(SegmentReview)。
你的任务是:基于给定审查规则,对“当前分段”进行审查,识别其中与规则相关且证据充分的条款,并判断其结果为“合格”或“不合格”,输出审查结论及必要的修改建议。
【审查范围】
你只能审查当前分段自身已经明确体现的内容。
你只能识别以下两类结果:
1. 合格条款:当前分段中存在与审查规则相关的明确表述,且该表述符合规则要求;
2. 不合格条款:当前分段中存在与审查规则相关的明确表述,且该表述不符合规则要求,例如:对我方不利、表述不清、逻辑冲突、责任失衡、触发条件不明确、关键限制缺失等。
【审查原则】
- 严格基于给定的审查规则进行审查,不得脱离规则自行扩展审查标准。
- 只审查当前分段原文,不得使用上下文信息补充、修正或推断当前分段含义。
- 优先识别“确定成立”的合格或不合格结论,不输出模糊怀疑类表述。
【立场约束规则(强制)】
你必须严格站在“我方利益最大化”的立场进行审查与建议生成,遵循以下规则:
一、禁止削弱我方权益(硬性约束)
在任何情况下,suggestion 不得包含或导致以下结果:
- 将“我方完成后付款”修改为分期付款或预付款
- 新增或加重我方违约金、赔偿责任、利息等负担
- 新增我方履约义务、配合义务或缩短我方时限
- 削弱我方验收权(如“默示验收”“逾期视为验收”)
- 新增对方权利或免除对方责任
- 以“平衡双方权利义务”为理由削弱我方优势
- 给予对方奖励、补偿或额外利益
- 将原本由对方承担的风险转移至我方
若原条款已存在上述问题,应判定为“不合格”,并提出纠偏建议。
二、建议方向约束(必须遵守)
当 result="不合格" 时,suggestion 必须优先朝以下方向优化:
- 强化我方权利(如单方决定权、解释权、验收权、解除权)
- 降低我方责任(限制责任范围、金额、触发条件)
- 增加对方义务与违约责任
- 延长我方期限、缩短对方期限
- 增加对方违约成本(违约金/赔偿/利息)
- 明确触发条件,避免我方被动承担风险
三、禁止中立化建议(非常重要)
- 不得提出“双方协商一致”“友好协商解决”等中性建议
- 不得提出“建议双方平衡”“建议公平调整”等弱化我方立场的建议
- 所有 suggestion 必须体现明确的偏向我方的修改方向
【完整性要求(非常重要)】
你必须对当前分段进行“穷举式审查”,不得只输出部分结果。
执行方式:
- 应逐句扫描当前分段
- 对每一句或关键子句,判断其是否与审查规则相关
- 只要存在证据充分的问题或合格表述,必须全部列出,不得遗漏
特别要求:
- 不得因为已找到1条或少量finding而提前停止
- 若一个段落中存在多处问题,必须分别输出多个 findings
- findings 数量应与段落中实际存在的问题数量大致一致,不得明显偏少
错误示例(禁止):
- 一个段落有多个风险点,但只输出1条
正确行为:
- 覆盖所有可以独立成立的审查点
【结果判定规则】
- result 只能取以下两个值之一:
- "合格":当前分段存在与规则相关的明确内容,且符合该规则要求;
- "不合格":当前分段存在与规则相关的明确内容,且不符合该规则要求。
- 如果当前分段与某条审查规则无关,或虽疑似相关但证据不足,则不得生成 finding。
【证据要求】
每个 findings 都必须包含 original_text,且必须是合同原文的直接引用。
【单一证据约束(非常重要)】
每一个 finding 必须只对应一个“独立判断点”和一个“最小证据句”。
具体要求:
- 一个 finding 只能基于一个关键句或一个最小语义单元;
- 若多个句子分别支持不同问题,必须拆分为多个 findings;
- 严禁将多个不同问题合并为一个 finding;
- 严禁在 original_text 中拼接多个不连续句子作为证据;
- 若 original_text 涉及跨句或跨段内容,必须拆分为多个 findings。
判断标准:
- 如果去掉 original_text 中的一部分,仍能形成一个独立判断 → 说明应该拆分
【issue 要求】
- issue 必须说明:该条款为什么合格或为什么不合格。
- 当 result="合格" 时,issue 应说明该表述满足了什么规则要求、为什么可认定为合格。
- 当 result="不合格" 时,issue 应说明该表述违反了什么规则要求、为什么构成风险或缺陷。
- issue 必须紧扣规则和原文,不得空泛评价。
【建议要求】
- suggestion 必须具体、可执行。
- 当 result="不合格" 时:
- 若能在当前分段内直接修正,请给出可直接替换或新增的条款措辞;
- 若无法直接改写,请给出明确修改方向和应补充的关键要素;
- 严禁提出削弱我方权益或中立化的建议;
- 当 result="合格" 时:
- suggestion 应简洁填写,可写“无需修改”;
- 不得为了凑内容而提出与审查结论无关的修改建议。
【输出约束】
- 严格按照指定 JSON Schema 输出。
- 不得输出任何 JSON 之外的解释性文字。
- 若未发现证据充分的合格或不合格条款,返回 {"findings": []}。
在生成最终 JSON 之前,你必须执行以下内部步骤(不输出):
Step A:将当前分段拆分为若干句子或语义单元
Step B:逐句判断该句是否涉及任一审查规则
Step C:若涉及规则,判断其为合格或不合格
Step D:为每一个成立的判断生成一个 finding
只有完成上述穷举后,才允许输出最终结果
提示:在合同审查中,一个分段通常可能包含多个独立风险点或合规点,findings 数量通常大于1,除非该段确实只涉及单一事项。
"""
REVIEW_SYSTEM_PROMPT_JP
=
"""
你是一个专业的合同分段审查智能体(SegmentReview)。
你是一个专业的合同分段审查智能体(SegmentReview)。
你的任务是:基于给定审查规则,对“当前分段”进行审查,识别其中与规则相关且证据充分的条款,并判断其结果为“合格”或“不合格”,输出审查结论及必要的修改建议。
你的任务是:基于给定审查规则,对“当前分段”进行审查,识别其中与规则相关且证据充分的条款,并判断其结果为“合格”或“不合格”,输出审查结论及必要的修改建议。
...
@@ -113,8 +234,8 @@ REVIEW_USER_PROMPT = """
...
@@ -113,8 +234,8 @@ REVIEW_USER_PROMPT = """
【特别要求】
【特别要求】
- 仅基于当前分段原文进行判断,不得参考任何上下文、摘要或记忆信息。
- 仅基于当前分段原文进行判断,不得参考任何上下文、摘要或记忆信息。
- 若一个段落中存在多处问题,必须分别输出多个 findings。
- findings 中每一项都必须包含 result 字段,且 result 只能为 "合格" 或 "不合格"。
- findings 中每一项都必须包含 result 字段,且 result 只能为 "合格" 或 "不合格"。
- 只有当前分段中存在与规则相关且证据充分的内容时,才输出 finding。
- findings 中的 original_text 必须为合同原文直接引用,且应为最小充分证据片段。
- findings 中的 original_text 必须为合同原文直接引用,且应为最小充分证据片段。
- 当 result="合格" 时,suggestion 填写“无需修改”。
- 当 result="合格" 时,suggestion 填写“无需修改”。
- 当 result="不合格" 时,suggestion 应尽量提供可直接落地的修改文本;若无法安全地直接改写,请给出明确的修改方向和应补充的关键要素。
- 当 result="不合格" 时,suggestion 应尽量提供可直接落地的修改文本;若无法安全地直接改写,请给出明确的修改方向和应补充的关键要素。
...
@@ -157,7 +278,10 @@ def _has_evidence(f: Dict) -> bool:
...
@@ -157,7 +278,10 @@ def _has_evidence(f: Dict) -> bool:
@tool
(
"segment_review"
,
"合同分段审查"
)
@tool
(
"segment_review"
,
"合同分段审查"
)
class
SegmentReviewTool
(
LLMTool
):
class
SegmentReviewTool
(
LLMTool
):
def
__init__
(
self
):
def
__init__
(
self
):
super
()
.
__init__
(
REVIEW_SYSTEM_PROMPT
)
if
use_lufa
:
super
()
.
__init__
(
REVIEW_SYSTEM_PROMPT_LF
)
else
:
super
()
.
__init__
(
REVIEW_SYSTEM_PROMPT_JP
)
@tool_func
(
@tool_func
(
{
{
...
@@ -369,48 +493,42 @@ class SegmentReviewTool(LLMTool):
...
@@ -369,48 +493,42 @@ class SegmentReviewTool(LLMTool):
if
__name__
==
"__main__"
:
if
__name__
==
"__main__"
:
tool
=
SegmentReviewTool
()
tool
=
SegmentReviewTool
()
segment_text
=
"""
segment_text
=
"""
变压器本体漏(渗)油,每起扣除质保金2000元,发现5台及以上变压器本体出现漏油情况,除应赔偿的质保金外,需延长该批次变压器的保质期5年。
val: 买方需在卖方产品验收通过后24个月内,买方在收到卖方提交的下列全部单据并经审核无误后60日内,向卖方支付合同价格的10
%
。
箱变出现漏水或渗水现象每起扣4000元。
val: (1)银行开具的质保金保函,保函期限不少于质量保证期。
箱变散热片出现生锈现象每起扣3000元。发现5台及以上变压器散热片出现生锈情况,除应赔偿的质保金外,招标方可拒收该批次变压器,由招标方重新生产该批次变压器散热片。
val: 3.2.1.5质保金:采购合同内设备验收投运满24个月后,买方验收无质量问题及索赔事项,经买方同意后,卖方可向买方提交质保金保函等额替代质保金,买方向卖方支付合同价格的10
%
作为质保金。
交接试验期间,出现设备不能满足或因设备自身问题没有通过交接试验项目复测仍无法通过的情况,招标方可无条件拒收该设备,投标人应赔偿由设备损坏或性能不达标对招标方造成的工期延误、劳务费用、发电量和信誉等所有损失并重新生产该设备。
val: 8.2(补充为)如在质量保证期内发现合同设备部件出现缺陷但不影响合同设备的正常运行,经维修或更换后的部件的质量保证期重新计算。在质量保证期内,由于卖方责任导致合同设备停运时,该台合同设备的质量保证期自卖方消除该缺陷后重新计算。
因设备自身故障停电超过7天的,每起扣除质保金5000元。
高低压室、变压器等温升超出技术协议要求值5k以上并持续时间超过3小时的,每起扣除质保金10000元。
箱变满载运行时,高低压室、变压器等温升超出厂家承诺值3k以上并持续时间超过1小时的,每起扣除质保金10000元,单批次出现两台以上的,业主单位可拒绝签收该批次设备。
UPS在散热设计上着重考虑,出现UPS因过热死机现象,每起扣除质保金2000元。
按照项目所用箱式变压器年度统计可用率达到99
%
为基准,每降低5‰扣除质保金10000元。
"""
"""
result
=
tool
.
run
(
result
=
tool
.
run
(
segment_id
=
1
,
segment_id
=
1
,
segment_text
=
segment_text
,
segment_text
=
segment_text
,
rules
=
[
rules
=
[
{
{
"title"
:
"
技术性能
审查"
,
"title"
:
"
质保期
审查"
,
"rule"
:
"""
"rule"
:
"""
## 背景知识
1)质保期(而非寿命期)必须提及“到货/交付/运行XX天/月”条件作为起算时间,如果有多个条件必须提及“先到为准”
技术性能(性能指标)
2)质保期(而非寿命期)期限/时长不超过(小于等于)“到货24个月/2年”或“交付后18个月/1年半”或"运行后12个月/1年",最长不超过2年
包括:一些泛指:"技术性能","性能指标","参数标准",或者具体的技术性能参数比如散热、工艺、负载、用料、漏水、渗水等
3)没有明确提及质保期的起算点和时长,审查合格
不包括:一些泛指,比如设备质量问题,设备验收问题,设备故障,设备考核等
"""
,
## 审查规则
"suggestion_template"
:
"""
1)前提条件:明确说明“设备(货物)的技术性能(性能指标)不能满足/达到保证值(合同要求)”
质保期为交付之日起18个月或运行之日起12个月或到货后24个月,先到为准
2)后果条件:明确链接到“惩罚性经济责任”(违约,赔偿等责任)或“合同解约权”(合同解约)。
同时满足前提与后果条件,则审查不合格,否则,审查合格。
"""
,
"""
,
"level"
:
"M"
,
"suggestion_template"
:
"1、提醒不合规的技术性能要求"
,
"case"
:
"""
"case"
:
"""
## 案例1:
## 案例1:
原文:承揽人根据附件2《技术协议》进行性能考核
原文:质保期期限为产品交付之日起48个月,或产品运行之日起40个月,两者以先到时间为准。
结论:前提条件不完整(只提及了性能考核)并且没有后果条件,因此审查合格。
结论:质保期的起算时间提及了交付XX月,以及先到为准,满足条件;原文的交付后48个月超过了24个月,运行后40个月超过了12个月,不满足时长要求,因此审查不合格。
## 案例2
## 案例2:
原文:产品散热设计故障,每起扣款2000元。
原文:质保期期限为产品交付之日起两年。
结论:同时满足前提与后果条件,散热设计故障属于技术性能不能满足保证值,扣款表明需要损失赔偿,因此审查不合格。
结论:质保期时长最长时间交付后1年半,原文要求2年,超过质保期时长要求,因此审查不合格。
## 案例3
## 案例3:
原文:出现设备故障或者设备质量问题导致验收不合格,需要扣除违约金。
原文:质保期期限为通电验收起两年。
结论:前提条件不完整,没有明确提及设备故障、质量问题、验收不合格为技术性能(性能指标)不满足保证值(合同要求),因此审查合格。
结论:质保期起算时间必须提及“到货/交付/运行XX天/月”,而非通电验收,因此审查不合格。
"""
,
## 案例4:
原文:质保期出现问题需要赔偿违约金。
结论:没有明确提及质保期的起算时间和时长,因此审查合格。
"""
,
}
}
],
],
party_role
=
"金盘"
,
party_role
=
"金盘
(卖方、供方、乙方)
"
,
)
)
print
(
json
.
dumps
(
result
,
ensure_ascii
=
False
,
indent
=
2
))
print
(
json
.
dumps
(
result
,
ensure_ascii
=
False
,
indent
=
2
))
...
...
data/batch/batch.py
View file @
a79604f2
...
@@ -13,30 +13,35 @@ from loguru import logger
...
@@ -13,30 +13,35 @@ 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
=
"_麓发迁移"
use_lufa
=
False
batch_input_dir_path
=
"jp-input"
batch_output_dir_path
=
f
"/home/ccran/lufa-contract/data/benchmark/results/jp-output-lufa-{time.strftime('
%
Y
%
m
%
d-
%
H
%
M
%
S', time.localtime())}"
if
not
use_lufa
:
SUFFIX
=
"_麓发迁移"
batch_input_dir_path
=
"jp-input"
batch_output_dir_path
=
f
"/home/ccran/lufa-contract/data/benchmark/results/jp-output-lufa-{time.strftime('
%
Y
%
m
%
d-
%
H
%
M
%
S', time.localtime())}"
# 金盘fastgpt接口
url
=
"http://192.168.252.71:18088/api/v1/chat/completions"
# 金盘迁移麓发合同审查测试token
token
=
"fastgpt-vykT6qs07g7hR4tL2MNJE6DdNCIxaQjEu3Cxw9nuTBFg8MAG3CkByvnXKxSNEyMK7"
# 人机交互测试(测试环境)
# token = 'fastgpt-p189K5zoTX5wjp0dBybFCwsbWm3juIwlJxt2wTGyiaOWOANI5Y10pKEZzyt'
# 人机交互测试(生产环境)
# token = 'fastgpt-ry4jIjgNwmNgufMr5jR0ncvJVmSS4GZl4bx2ItsNPoncdQzW9Na3IP1Xrankr'
# 提取后审查测试
# token = 'fastgpt-n74gGX5ZqLT6o1ysMBSGUTjIciswYOWDRfQ75krMkE5gDVDkpzsbz8u'
else
:
SUFFIX
=
"_麓发"
batch_input_dir_path
=
"lufa-input"
batch_output_dir_path
=
"lufa-output-standard"
# 麓发fastgpt接口
url
=
"http://192.168.252.71:18089/api/v1/chat/completions"
# 麓发合同审查生产token
# token = "fastgpt-ek3Z6PxI6sXgYc0jxzZ5bVGqrxwM6aVyfSmA6JVErJYBMr2KmYxrHwEUOIMSYz"
# 麓发合同审查生产token-标准化
token
=
"fastgpt-mg5tQUgreJeF7peoOr5zqP0NR4EIrfS2bEVXge6FUL94Suu1TvEMR1sGNRSiV"
# SUFFIX = "_麓发"
# batch_input_dir_path = "lufa-input"
# batch_output_dir_path = "lufa-output-standard"
batch_size
=
5
batch_size
=
5
# 麓发fastgpt接口
# url = "http://192.168.252.71:18089/api/v1/chat/completions"
# 金盘fastgpt接口
url
=
"http://192.168.252.71:18088/api/v1/chat/completions"
# 麓发合同审查生产token
# token = "fastgpt-ek3Z6PxI6sXgYc0jxzZ5bVGqrxwM6aVyfSmA6JVErJYBMr2KmYxrHwEUOIMSYz"
# 麓发合同审查生产token-标准化
# token = "fastgpt-mg5tQUgreJeF7peoOr5zqP0NR4EIrfS2bEVXge6FUL94Suu1TvEMR1sGNRSiV"
# 金盘迁移麓发合同审查测试token
token
=
"fastgpt-vykT6qs07g7hR4tL2MNJE6DdNCIxaQjEu3Cxw9nuTBFg8MAG3CkByvnXKxSNEyMK7"
# 人机交互测试(测试环境)
# token = 'fastgpt-p189K5zoTX5wjp0dBybFCwsbWm3juIwlJxt2wTGyiaOWOANI5Y10pKEZzyt'
# 人机交互测试(生产环境)
# token = 'fastgpt-ry4jIjgNwmNgufMr5jR0ncvJVmSS4GZl4bx2ItsNPoncdQzW9Na3IP1Xrankr'
# 提取后审查测试
# token = 'fastgpt-n74gGX5ZqLT6o1ysMBSGUTjIciswYOWDRfQ75krMkE5gDVDkpzsbz8u'
def
extract_url
(
text
):
def
extract_url
(
text
):
...
@@ -94,10 +99,16 @@ def process_single_file(
...
@@ -94,10 +99,16 @@ def process_single_file(
excel_url
,
doc_url
=
extract_url
(
result
)
excel_url
,
doc_url
=
extract_url
(
result
)
if
excel_url
and
doc_url
:
if
excel_url
and
doc_url
:
download_file
(
download_file
(
excel_url
.
replace
(
"218.77.58.8"
,
"192.168.252.71"
),
des_excel_file
excel_url
.
replace
(
"218.77.58.8"
,
"192.168.252.71"
)
.
replace
(
"znkf.lgfzgroup.com"
,
"192.168.252.71"
),
des_excel_file
,
)
)
download_file
(
download_file
(
doc_url
.
replace
(
"218.77.58.8"
,
"192.168.252.71"
),
des_doc_file
doc_url
.
replace
(
"218.77.58.8"
,
"192.168.252.71"
)
.
replace
(
"znkf.lgfzgroup.com"
,
"192.168.252.71"
),
des_doc_file
,
)
)
logger
.
info
(
logger
.
info
(
f
"第{counter}个文件下载:{excel_url}到{des_excel_file} {des_doc_file}"
f
"第{counter}个文件下载:{excel_url}到{des_excel_file} {des_doc_file}"
...
...
data/benchmark/compare_annotation.py
View file @
a79604f2
...
@@ -24,8 +24,19 @@ def _load_rows(path: Path) -> list[tuple[str, str]]:
...
@@ -24,8 +24,19 @@ def _load_rows(path: Path) -> list[tuple[str, str]]:
for
col
in
first_two
.
columns
:
for
col
in
first_two
.
columns
:
first_two
[
col
]
=
first_two
[
col
]
.
map
(
_normalize_cell
)
first_two
[
col
]
=
first_two
[
col
]
.
map
(
_normalize_cell
)
first_two
=
first_two
.
replace
(
""
,
pd
.
NA
)
.
dropna
(
how
=
"all"
)
first_two
=
first_two
.
replace
(
""
,
pd
.
NA
)
.
dropna
(
how
=
"all"
)
rows
:
list
[
tuple
[
str
,
str
]]
=
[]
seen
:
set
[
tuple
[
str
,
str
]]
=
set
()
for
item
,
text
in
first_two
.
itertuples
(
index
=
False
,
name
=
None
):
row
=
(
""
if
pd
.
isna
(
item
)
else
str
(
item
)
.
strip
(),
""
if
pd
.
isna
(
text
)
else
str
(
text
)
.
strip
(),
)
if
row
in
seen
:
continue
seen
.
add
(
row
)
rows
.
append
(
row
)
return
list
(
map
(
tuple
,
first_two
.
itertuples
(
index
=
False
,
name
=
None
)))
return
rows
def
_compare_impl
(
val_dir
:
Path
,
answer_dir
:
Path
)
->
None
:
def
_compare_impl
(
val_dir
:
Path
,
answer_dir
:
Path
)
->
None
:
...
...
data/benchmark/eval.py
View file @
a79604f2
...
@@ -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-202604
08-182708
"
,
default
=
base
/
"results"
/
"jp-output-lufa-202604
16-000112
"
,
help
=
"Directory containing Word files with annotations."
,
help
=
"Directory containing Word files with annotations."
,
)
)
parser
.
add_argument
(
parser
.
add_argument
(
...
...
data/rules.xlsx
View file @
a79604f2
No preview for this file type
main.py
View file @
a79604f2
...
@@ -394,9 +394,7 @@ def merge_segment_findings(payload: MergerRequest) -> MergerResponse:
...
@@ -394,9 +394,7 @@ def merge_segment_findings(payload: MergerRequest) -> MergerResponse:
unqualified_findings
=
[
unqualified_findings
=
[
f
for
f
in
segment_findings
if
(
f
.
result
or
""
)
.
strip
()
==
"不合格"
f
for
f
in
segment_findings
if
(
f
.
result
or
""
)
.
strip
()
==
"不合格"
]
]
merged_result
=
merger_tool
.
run
(
merged_result
=
merger_tool
.
run
([
f
.
__dict__
for
f
in
unqualified_findings
])
[
f
.
__dict__
for
f
in
unqualified_findings
],
merge_mode
=
"rule"
)
merged_findings
=
merged_result
.
get
(
"findings"
,
[])
or
[]
merged_findings
=
merged_result
.
get
(
"findings"
,
[])
or
[]
for
f
in
merged_findings
:
for
f
in
merged_findings
:
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment