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
4ecb616b
authored
Jul 30, 2026
by
ccran
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
feat: add diff;
parent
d710d227
Hide whitespace changes
Inline
Side-by-side
Showing
2 changed files
with
257 additions
and
0 deletions
+257
-0
core/tools/document_diff_comment.py
+225
-0
main.py
+32
-0
No files found.
core/tools/document_diff_comment.py
0 → 100644
View file @
4ecb616b
from
__future__
import
annotations
import
difflib
from
itertools
import
zip_longest
from
pathlib
import
Path
from
typing
import
Any
,
Dict
,
List
from
uuid
import
uuid4
from
core.config
import
FULL_TEXT_SEGMENT_ID
,
doc_support_formats
from
core.tool
import
ToolBase
,
tool
,
tool_func
from
utils.http_util
import
download_file
,
upload_file
from
utils.spire_word_util
import
SpireWordDoc
@tool
(
"document_diff_comment"
,
"对比两份 Word 文档、添加双向差异批注并上传"
)
class
DocumentDiffCommentTool
(
ToolBase
):
def
__init__
(
self
,
tmp_dir
:
Path
)
->
None
:
self
.
_tmp_dir
=
Path
(
tmp_dir
)
self
.
_tmp_dir
.
mkdir
(
parents
=
True
,
exist_ok
=
True
)
@tool_func
(
{
"type"
:
"object"
,
"properties"
:
{
"urls"
:
{
"type"
:
"array"
,
"items"
:
{
"type"
:
"string"
},
"minItems"
:
2
,
"maxItems"
:
2
,
}
},
"required"
:
[
"urls"
],
}
)
def
run
(
self
,
urls
:
List
[
str
])
->
Dict
[
str
,
List
[
str
]]:
normalized_urls
=
[
str
(
url
or
""
)
.
strip
()
for
url
in
urls
or
[]]
if
len
(
normalized_urls
)
!=
2
:
raise
ValueError
(
"Exactly two URLs are required"
)
if
any
(
not
url
for
url
in
normalized_urls
):
raise
ValueError
(
"URL cannot be empty"
)
request_dir
=
self
.
_tmp_dir
/
f
"document-diff-{uuid4().hex}"
downloaded_paths
=
self
.
_download_documents
(
normalized_urls
,
request_dir
)
texts
=
self
.
_read_document_texts
(
downloaded_paths
)
comments_by_document
=
[
self
.
_build_diff_comments
(
texts
[
0
],
texts
[
1
],
"本文件"
,
"文件2"
),
self
.
_build_diff_comments
(
texts
[
1
],
texts
[
0
],
"本文件"
,
"文件1"
),
]
output_paths
=
self
.
_write_commented_documents
(
downloaded_paths
,
comments_by_document
,
)
return
{
"urls"
:
self
.
_upload_documents
(
output_paths
)}
def
_download_documents
(
self
,
urls
:
List
[
str
],
request_dir
:
Path
)
->
List
[
Path
]:
downloaded_paths
:
List
[
Path
]
=
[]
for
index
,
url
in
enumerate
(
urls
,
start
=
1
):
download_dir
=
request_dir
/
f
"source-{index}"
download_dir
.
mkdir
(
parents
=
True
,
exist_ok
=
True
)
try
:
downloaded_path
=
download_file
(
url
,
download_dir
)
except
Exception
as
exc
:
raise
RuntimeError
(
f
"File {index} download failed: {exc}"
)
from
exc
if
not
downloaded_path
:
raise
RuntimeError
(
f
"File {index} download failed"
)
downloaded_path
=
Path
(
downloaded_path
)
if
downloaded_path
.
suffix
.
lower
()
not
in
doc_support_formats
:
raise
ValueError
(
f
"File {index} is not a supported Word document; "
f
"supported formats: {', '.join(doc_support_formats)}"
)
downloaded_paths
.
append
(
downloaded_path
)
return
downloaded_paths
def
_read_document_texts
(
self
,
paths
:
List
[
Path
])
->
List
[
str
]:
documents
:
List
[
SpireWordDoc
]
=
[]
try
:
for
path
in
paths
:
document
=
SpireWordDoc
()
documents
.
append
(
document
)
document
.
load
(
str
(
path
))
return
[
document
.
get_all_text
()
for
document
in
documents
]
except
Exception
as
exc
:
raise
ValueError
(
f
"Word document loading failed: {exc}"
)
from
exc
finally
:
for
document
in
documents
:
if
getattr
(
document
,
"_doc"
,
None
):
document
.
_doc
.
Close
()
def
_write_commented_documents
(
self
,
source_paths
:
List
[
Path
],
comments_by_document
:
List
[
List
[
Dict
[
str
,
Any
]]],
)
->
List
[
Path
]:
output_paths
:
List
[
Path
]
=
[]
for
index
,
(
source_path
,
comments
)
in
enumerate
(
zip
(
source_paths
,
comments_by_document
),
start
=
1
,
):
output_path
=
source_path
.
with_name
(
f
"{source_path.stem}_差异批注_{index}{source_path.suffix}"
)
try
:
self
.
_save_commented_document
(
source_path
,
output_path
,
comments
)
except
Exception
as
exc
:
raise
RuntimeError
(
f
"Adding comments to file {index} failed: {exc}"
)
from
exc
output_paths
.
append
(
output_path
)
return
output_paths
def
_upload_documents
(
self
,
paths
:
List
[
Path
])
->
List
[
str
]:
uploaded_urls
:
List
[
str
]
=
[]
for
index
,
path
in
enumerate
(
paths
,
start
=
1
):
try
:
uploaded_urls
.
append
(
upload_file
(
str
(
path
)))
except
Exception
as
exc
:
raise
RuntimeError
(
f
"File {index} upload failed: {exc}"
)
from
exc
return
uploaded_urls
@staticmethod
def
_nearest_diff_anchor
(
lines
:
List
[
str
],
position
:
int
)
->
str
:
for
offset
in
range
(
len
(
lines
)
+
1
):
before_idx
=
position
-
1
-
offset
if
before_idx
>=
0
and
lines
[
before_idx
]:
return
lines
[
before_idx
]
after_idx
=
position
+
offset
if
after_idx
<
len
(
lines
)
and
lines
[
after_idx
]:
return
lines
[
after_idx
]
return
""
def
_build_diff_comments
(
self
,
current_text
:
str
,
other_text
:
str
,
current_label
:
str
,
other_label
:
str
,
)
->
List
[
Dict
[
str
,
Any
]]:
current_lines
=
[
line
.
strip
()
for
line
in
current_text
.
splitlines
()]
other_lines
=
[
line
.
strip
()
for
line
in
other_text
.
splitlines
()]
comment_batch_id
=
uuid4
()
.
hex
[:
12
]
matcher
=
difflib
.
SequenceMatcher
(
None
,
current_lines
,
other_lines
,
autojunk
=
False
,
)
comments
:
List
[
Dict
[
str
,
Any
]]
=
[]
for
opcode_idx
,
(
tag
,
current_start
,
current_end
,
other_start
,
other_end
,
)
in
enumerate
(
matcher
.
get_opcodes
()):
if
tag
==
"equal"
:
continue
current_part
=
current_lines
[
current_start
:
current_end
]
other_part
=
other_lines
[
other_start
:
other_end
]
for
row_idx
,
(
current_line
,
other_line
)
in
enumerate
(
zip_longest
(
current_part
,
other_part
,
fillvalue
=
""
)
):
if
not
current_line
and
not
other_line
:
continue
if
current_line
:
anchor
=
current_line
if
other_line
:
suggest
=
(
f
"与{other_label}内容不一致。
\n
"
f
"{current_label}:{current_line}
\n
"
f
"{other_label}:{other_line}"
)
else
:
suggest
=
(
f
"与{other_label}内容不一致:"
f
"本文件包含“{current_line}”,{other_label}中缺失该内容。"
)
else
:
anchor
=
self
.
_nearest_diff_anchor
(
current_lines
,
current_start
)
suggest
=
(
f
"与{other_label}内容不一致:"
f
"{other_label}包含“{other_line}”,本文件中缺失该内容。"
)
comments
.
append
(
{
"id"
:
f
"diff-{comment_batch_id}-{opcode_idx}-{row_idx}"
,
"key_points"
:
"文件差异"
,
"result"
:
"不合格"
,
"suggest"
:
suggest
,
"original_text"
:
anchor
,
"chunk_id"
:
FULL_TEXT_SEGMENT_ID
,
}
)
return
comments
@staticmethod
def
_save_commented_document
(
source_path
:
Path
,
output_path
:
Path
,
comments
:
List
[
Dict
[
str
,
Any
]],
)
->
None
:
document
=
SpireWordDoc
()
try
:
document
.
load
(
str
(
source_path
))
if
comments
:
original_comment_count
=
document
.
_doc
.
Comments
.
Count
document
.
add_chunk_comment
(
FULL_TEXT_SEGMENT_ID
,
comments
)
added_comment_count
=
(
document
.
_doc
.
Comments
.
Count
-
original_comment_count
)
if
added_comment_count
!=
len
(
comments
):
raise
RuntimeError
(
f
"{len(comments)} comments expected, "
f
"but only {added_comment_count} were added"
)
document
.
to_file
(
str
(
output_path
))
finally
:
if
getattr
(
document
,
"_doc"
,
None
):
document
.
_doc
.
Close
()
main.py
View file @
4ecb616b
...
...
@@ -35,6 +35,7 @@ from core.tools.fact_merger import FactMergerTool
from
core.tools.ruleset_router
import
RulesetRouterTool
from
core.tools.party_role
import
PartyRoleTool
from
core.tools.segment_llm
import
LLMTool
from
core.tools.document_diff_comment
import
DocumentDiffCommentTool
from
core.memory
import
Finding
from
core.memory
import
FINDING_KEY_MERGE
,
FINDING_KEY_REFLECT
,
FINDING_KEY_REVIEW
...
...
@@ -58,6 +59,7 @@ fact_merger_tool = FactMergerTool()
ruleset_router_tool
=
RulesetRouterTool
()
party_role_tool
=
PartyRoleTool
()
nanobot_llm_tool
=
LLMTool
(
llm_key
=
"nanobot_llm"
)
document_diff_comment_tool
=
DocumentDiffCommentTool
(
TMP_DIR
)
class
TestModel
(
BaseModel
):
...
...
@@ -118,6 +120,36 @@ class DocumentParseResponse(BaseModel):
file_name
:
Optional
[
str
]
=
None
class
DocumentDiffCommentRequest
(
BaseModel
):
urls
:
List
[
str
]
=
Field
(
...
,
min_length
=
2
,
max_length
=
2
,
description
=
"需要对比并批注的两个 Word 文件下载 URL"
,
)
class
DocumentDiffCommentResponse
(
BaseModel
):
urls
:
List
[
str
]
=
Field
(
description
=
"两份已添加差异批注的 Word 文件上传 URL"
)
@app.post
(
"/documents/diff-comments"
,
response_model
=
DocumentDiffCommentResponse
,
)
def
diff_and_comment_documents
(
payload
:
DocumentDiffCommentRequest
,
)
->
DocumentDiffCommentResponse
:
try
:
result
=
document_diff_comment_tool
.
run
(
payload
.
urls
)
except
ValueError
as
exc
:
raise
HTTPException
(
status_code
=
400
,
detail
=
str
(
exc
))
from
exc
except
Exception
as
exc
:
logger
.
exception
(
"Document diff comment processing failed"
)
raise
HTTPException
(
status_code
=
500
,
detail
=
str
(
exc
))
from
exc
return
DocumentDiffCommentResponse
(
urls
=
result
[
"urls"
])
class
RulesetRouteRequest
(
BaseModel
):
question
:
str
=
Field
(
...
,
description
=
"User question used to select ruleset_id"
)
...
...
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