diff --git a/LICENSE b/LICENSE deleted file mode 100644 index f63f5a9cf3498818a73068495709cceed67efd6a..0000000000000000000000000000000000000000 --- a/LICENSE +++ /dev/null @@ -1,194 +0,0 @@ -木兰宽松许可证,第2版 - -木兰宽松许可证,第2版 - -2020年1月 http://license.coscl.org.cn/MulanPSL2 - -您对“软件”的复制、使用、修改及分发受木兰宽松许可证,第2版(“本许可证”)的如下条款的约束: - -0. 定义 - -“软件” 是指由“贡献”构成的许可在“本许可证”下的程序和相关文档的集合。 - -“贡献” 是指由任一“贡献者”许可在“本许可证”下的受版权法保护的作品。 - -“贡献者” 是指将受版权法保护的作品许可在“本许可证”下的自然人或“法人实体”。 - -“法人实体” 是指提交贡献的机构及其“关联实体”。 - -“关联实体” 是指,对“本许可证”下的行为方而言,控制、受控制或与其共同受控制的机构,此处的控制是 -指有受控方或共同受控方至少50%直接或间接的投票权、资金或其他有价证券。 - -1. 授予版权许可 - -每个“贡献者”根据“本许可证”授予您永久性的、全球性的、免费的、非独占的、不可撤销的版权许可,您可 -以复制、使用、修改、分发其“贡献”,不论修改与否。 - -2. 授予专利许可 - -每个“贡献者”根据“本许可证”授予您永久性的、全球性的、免费的、非独占的、不可撤销的(根据本条规定 -撤销除外)专利许可,供您制造、委托制造、使用、许诺销售、销售、进口其“贡献”或以其他方式转移其“贡 -献”。前述专利许可仅限于“贡献者”现在或将来拥有或控制的其“贡献”本身或其“贡献”与许可“贡献”时的“软 -件”结合而将必然会侵犯的专利权利要求,不包括对“贡献”的修改或包含“贡献”的其他结合。如果您或您的“ -关联实体”直接或间接地,就“软件”或其中的“贡献”对任何人发起专利侵权诉讼(包括反诉或交叉诉讼)或 -其他专利维权行动,指控其侵犯专利权,则“本许可证”授予您对“软件”的专利许可自您提起诉讼或发起维权 -行动之日终止。 - -3. 无商标许可 - -“本许可证”不提供对“贡献者”的商品名称、商标、服务标志或产品名称的商标许可,但您为满足第4条规定 -的声明义务而必须使用除外。 - -4. 分发限制 - -您可以在任何媒介中将“软件”以源程序形式或可执行形式重新分发,不论修改与否,但您必须向接收者提供“ -本许可证”的副本,并保留“软件”中的版权、商标、专利及免责声明。 - -5. 免责声明与责任限制 - -“软件”及其中的“贡献”在提供时不带任何明示或默示的担保。在任何情况下,“贡献者”或版权所有者不对 -任何人因使用“软件”或其中的“贡献”而引发的任何直接或间接损失承担责任,不论因何种原因导致或者基于 -何种法律理论,即使其曾被建议有此种损失的可能性。 - -6. 语言 - -“本许可证”以中英文双语表述,中英文版本具有同等法律效力。如果中英文版本存在任何冲突不一致,以中文 -版为准。 - -条款结束 - -如何将木兰宽松许可证,第2版,应用到您的软件 - -如果您希望将木兰宽松许可证,第2版,应用到您的新软件,为了方便接收者查阅,建议您完成如下三步: - -1, 请您补充如下声明中的空白,包括软件名、软件的首次发表年份以及您作为版权人的名字; - -2, 请您在软件包的一级目录下创建以“LICENSE”为名的文件,将整个许可证文本放入该文件中; - -3, 请将如下声明文本放入每个源文件的头部注释中。 - -Copyright (c) [Year] [name of copyright holder] -[Software Name] is licensed under Mulan PSL v2. -You can use this software according to the terms and conditions of the Mulan -PSL v2. -You may obtain a copy of Mulan PSL v2 at: - http://license.coscl.org.cn/MulanPSL2 -THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY -KIND, EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO -NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. -See the Mulan PSL v2 for more details. - -Mulan Permissive Software License,Version 2 - -Mulan Permissive Software License,Version 2 (Mulan PSL v2) - -January 2020 http://license.coscl.org.cn/MulanPSL2 - -Your reproduction, use, modification and distribution of the Software shall -be subject to Mulan PSL v2 (this License) with the following terms and -conditions: - -0. Definition - -Software means the program and related documents which are licensed under -this License and comprise all Contribution(s). - -Contribution means the copyrightable work licensed by a particular -Contributor under this License. - -Contributor means the Individual or Legal Entity who licenses its -copyrightable work under this License. - -Legal Entity means the entity making a Contribution and all its -Affiliates. - -Affiliates means entities that control, are controlled by, or are under -common control with the acting entity under this License, ‘control’ means -direct or indirect ownership of at least fifty percent (50%) of the voting -power, capital or other securities of controlled or commonly controlled -entity. - -1. Grant of Copyright License - -Subject to the terms and conditions of this License, each Contributor hereby -grants to you a perpetual, worldwide, royalty-free, non-exclusive, -irrevocable copyright license to reproduce, use, modify, or distribute its -Contribution, with modification or not. - -2. Grant of Patent License - -Subject to the terms and conditions of this License, each Contributor hereby -grants to you a perpetual, worldwide, royalty-free, non-exclusive, -irrevocable (except for revocation under this Section) patent license to -make, have made, use, offer for sale, sell, import or otherwise transfer its -Contribution, where such patent license is only limited to the patent claims -owned or controlled by such Contributor now or in future which will be -necessarily infringed by its Contribution alone, or by combination of the -Contribution with the Software to which the Contribution was contributed. -The patent license shall not apply to any modification of the Contribution, -and any other combination which includes the Contribution. If you or your -Affiliates directly or indirectly institute patent litigation (including a -cross claim or counterclaim in a litigation) or other patent enforcement -activities against any individual or entity by alleging that the Software or -any Contribution in it infringes patents, then any patent license granted to -you under this License for the Software shall terminate as of the date such -litigation or activity is filed or taken. - -3. No Trademark License - -No trademark license is granted to use the trade names, trademarks, service -marks, or product names of Contributor, except as required to fulfill notice -requirements in section 4. - -4. Distribution Restriction - -You may distribute the Software in any medium with or without modification, -whether in source or executable forms, provided that you provide recipients -with a copy of this License and retain copyright, patent, trademark and -disclaimer statements in the Software. - -5. Disclaimer of Warranty and Limitation of Liability - -THE SOFTWARE AND CONTRIBUTION IN IT ARE PROVIDED WITHOUT WARRANTIES OF ANY -KIND, EITHER EXPRESS OR IMPLIED. IN NO EVENT SHALL ANY CONTRIBUTOR OR -COPYRIGHT HOLDER BE LIABLE TO YOU FOR ANY DAMAGES, INCLUDING, BUT NOT -LIMITED TO ANY DIRECT, OR INDIRECT, SPECIAL OR CONSEQUENTIAL DAMAGES ARISING -FROM YOUR USE OR INABILITY TO USE THE SOFTWARE OR THE CONTRIBUTION IN IT, NO -MATTER HOW IT’S CAUSED OR BASED ON WHICH LEGAL THEORY, EVEN IF ADVISED OF -THE POSSIBILITY OF SUCH DAMAGES. - -6. Language - -THIS LICENSE IS WRITTEN IN BOTH CHINESE AND ENGLISH, AND THE CHINESE VERSION -AND ENGLISH VERSION SHALL HAVE THE SAME LEGAL EFFECT. IN THE CASE OF -DIVERGENCE BETWEEN THE CHINESE AND ENGLISH VERSIONS, THE CHINESE VERSION -SHALL PREVAIL. - -END OF THE TERMS AND CONDITIONS - -How to Apply the Mulan Permissive Software License,Version 2 -(Mulan PSL v2) to Your Software - -To apply the Mulan PSL v2 to your work, for easy identification by -recipients, you are suggested to complete following three steps: - -i. Fill in the blanks in following statement, including insert your software -name, the year of the first publication of your software, and your name -identified as the copyright owner; - -ii. Create a file named "LICENSE" which contains the whole context of this -License in the first directory of your software package; - -iii. Attach the statement to the appropriate annotated syntax at the -beginning of each source file. - -Copyright (c) [Year] [name of copyright holder] -[Software Name] is licensed under Mulan PSL v2. -You can use this software according to the terms and conditions of the Mulan -PSL v2. -You may obtain a copy of Mulan PSL v2 at: - http://license.coscl.org.cn/MulanPSL2 -THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY -KIND, EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO -NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. -See the Mulan PSL v2 for more details. diff --git a/README.md b/README.md index 6446d62482e0082ce28a4517f36f82993091b8d3..1cbf6ea676be02e9d3405405ca107e8b3f92405c 100644 --- a/README.md +++ b/README.md @@ -1,55 +1,239 @@ -# 造浪2025AIAgent创新赛 +# 智能合同审查Agent -#### 介绍 -在数字化转型与行业智能化升级浪潮下,企业业务流程复杂度攀升,用户需求多元化。AI Agent 凭借自动化、智能化特征,可优化流程、提升效率、降低出错率,满足更多业务场景的多元化需求,提升服务质量与用户体验,革新我们的工作和生活方式。 -对此,开源中国 携手 独家冠名厂商-商汤大装置 聚焦多业务场景,携手知名技术专家、合作社区推出“造浪 2025 AI Agent 创新赛”,聚焦智慧金融、教育科技、出海辅助、本地生活等多个重点行业领域,面向企业开发者、高校科研团队及个人创客征集具备商业价值与社会效益的 AI Agent 应用。 +## 🌟 项目概述 -![输入图片说明](%E4%BD%9C%E5%93%81%E6%8F%90%E4%BA%A4/ScreenShot_2025-11-24_190342_756.png) +智能合同审查系统是一款基于Web的合同智能审查应用,利用AI技术和关键字匹配算法,为用户提供便捷的合同风险识别和分析服务。系统支持多种文档格式(PDF、Word、TXT、图片等)的上传和解析,并能识别合同中的潜在风险点,如高额违约金、争议解决条款、知识产权归属等问题。 -#### 我们希望看到 -- 通过 Agent 实现的创造性解决方案和产出 -- 能显著提升工作效率的 Agent 工作流 -- 探索 Agent 能力边界的实验性项目 -- 能为公众带来实际价值的 Agent 应用 +项目采用前后端分离架构,前端使用原生HTML/CSS/JavaScript构建,后端采用Python FastAPI框架,结合LazyLLM框架接入大语言模型,提供智能化的合同分析能力。 -#### 大赛亮点 -本次 AI Agent 创新赛官方指定开发框架 LazyLLM ,由商汤 LazyAGI 团队开发,具备一键部署所有模块的能力,简化了多 Agent 应用的部署流程,可依次启动各个子模块(如 LLM 、Embedding 等)服务并配置 URL 的问题,使整个过程更加顺畅高效。 +![image](https://gitee.com/want595/markdown-images/raw/master/images/207/3/2025/12/22/2025122213453488792.png) -- 跨平台兼容:无需修改代码,即可一键切换操作系统和IaaS平台,目前兼容裸金属服务器、开发机、Slurm集群、公有云等。这使得开发中的应用可以无缝迁移到其他IaaS平台,大大减少了代码修改的工作量。 -- 统一的使用体验:统一了不同服务商的线上模型和本地部署模型的使用体验,使得开发者可以随意的切换和升级自己的模型进行尝试。此外,还统一了主流的推理框架、微调框架、关系型数据库、向量数据库、文档数据库的使用体验。 -- 高效的模型微调:支持对应用中的模型进行微调,持续提升应用效果。根据微调场景,自动选择最佳的微调框架和模型切分策略。这不仅简化了模型迭代的维护工作,还让算法研究员能够将更多精力集中在算法和数据迭代上,而无需处理繁琐的工程化任务。 -#### 赛题设计 +## 💼 应用场景与商业价值 -1. 智慧金融 -- 开发一款能分析合同审核的智能Agent -- 开发一款具备风险预警的多模态Agent +在企业日常运营中,合同管理是一项重要而复杂的工作。本系统可以帮助企业和个人: -2. 教育科技 -- 开发一款教案/长篇技术文档生成Agent -- 开发一款多模态作业批改的教培Agent +1. **提高审查效率**:自动化识别合同风险,大幅减少人工审查时间 +2. **降低法律风险**:及时发现合同中的潜在问题,避免经济损失 +3. **标准化审查流程**:建立统一的合同审查标准,提升业务规范性 +4. **节约人力成本**:减少对专业法务人员的依赖,降低企业运营成本 -3. 出海辅助 -- 开发一款跨境电商选品的Agent -- 开发一款收录各大独立站的资讯Agent +## ✨ 核心功能 -#### 参赛规则 +1. **多格式文档支持**:支持PDF、DOC、DOCX、TXT、PNG、JPG、JPEG等多种格式的合同文档上传和解析 +2. **智能风险识别**:基于预定义的风险关键词库,自动识别合同中的潜在风险点 +3. **大语言模型分析**:集成DeepSeek-V3大语言模型,提供自然语言形式的合同分析和建议 +4. **可视化界面**:直观的Web界面展示审查结果,包括风险统计和详细说明 +5. **流式响应**:支持流式输出大模型分析结果,提供更好的用户体验 -一、作品要求: -- 需提交完整的作品方案介绍文档、项目源代码。 -- 参赛者提交的参赛作品必须为原创作品,不得侵犯任何第三方的著作权、商标权及其他知识产权,且不得违反国家相关法律法规,否则将取消其本届大赛的参赛资格; -- 参赛作品应能正常运行并可达到参赛赛项规定的预期结果。参赛作品应与设计文档描述的功能一致。如参赛作品未能实现设计文档中描述的所有功能,应注明未实现功能、占比及其重要程度。 -- 参赛作品的代码注释应清晰、简洁、准确地描述其设计思路、功能和原理等,以提升代码的可读性和可维护性。 +## 🎯 技术亮点 -二、LazyLLM 相关链接 -- 官方文档:https://docs.lazyllm.ai/zh-cn/latest/ -- 下载链接:https://github.com/LazyAGI/LazyLLM +- **多格式文档解析**:支持PDF、Word、TXT、图片等多种文档格式的解析 +- **OCR文字识别**:集成Tesseract OCR引擎,支持图片类合同文档的文字提取 +- **关键字匹配算法**:基于正则表达式的智能风险识别算法 +- **大语言模型集成**:通过LazyLLM框架接入DeepSeek-V3大语言模型 +- **流式响应处理**:支持大模型输出的流式传输,实现实时响应效果 +- **现代化Web技术**:采用原生HTML/CSS/JavaScript构建前端界面 -三、作品提交 -- 作品需要使用商汤 LazyAGI 团队开发到 LazyLLM 开源低代码大模型应用开发工具进行开发,该工具提供从应用搭建、数据准备、模型部署、微调到评测的一站式工具支持,以极低的成本快速构建 AI 应用—— -- 作品提交内容包含:Agent 系统本体、技术文档(项目简介、成员贡献清单、技术栈、架构设计、部署说明)可选:操作录屏视频(≤5min,可以发布在网盘设置公开可见) +## 📁 项目结构 -#### 组委会联系方式 -刘老师 :liuyang3@oschina.cn 王老师 :wanghao@oschina.cn +``` +contract_review/ +├── contracts/ # 上传的合同文件存储目录 +├── contract_llm.py # 大语言模型集成模块 +├── contract_review_mcp.py # 合同审查核心逻辑模块 +├── web_server.py # Web服务主程序 +├── index.html # 前端界面文件 +├── requirements.txt # Python依赖包列表 +├── contract_review_server.log # 运行日志文件 +└── README.md # 项目说明文档 +``` -* 大赛解释权归大会组委会所有 +## 🔧 部署说明 + +### 环境要求 + +- Python >= 3.8 +- pip包管理器 +- LazyLLM框架 +- Tesseract OCR引擎(用于图片文字识别) + +### 快速开始 + +#### 1. 安装依赖 + +```bash +# 安装LazyLLM库 +pip install lazyllm + +# 安装项目依赖 +pip install -r requirements.txt + +``` + +#### 2. 配置环境变量 + +```bash +# 设置LazyLLM服务的API密钥(在商汤大模型平台获取) +export LAZYLLM_SENSENOVA_API_KEY=your_api_key_here +``` + +#### 3. 启动服务 + +```bash +# 启动Web服务 +python web_server.py +``` + +#### 4. 访问应用 + +打开浏览器访问 `http://localhost:8001` + +## 🛠️ 技术栈 + +![image](https://gitee.com/want595/markdown-images/raw/master/images/207/4/2025/12/22/2025122213472418938.png) + + + +### 后端 + +- Python 3.8+ +- FastAPI(Web框架) +- LazyLLM框架 +- DeepSeek-V3大语言模型 +- PyMuPDF(PDF解析) +- python-docx(Word文档解析) +- Pillow + pytesseract(图像OCR) + +### 前端 + +- HTML5 +- CSS3 +- JavaScript ES6+ +- Font Awesome(图标库) + +### 第三方服务 + +- LazyLLM服务(Sensenova) +- Tesseract OCR(光学字符识别) + +## 📱 使用指南 + +1. **上传合同文件**: + - 点击"上传合同文件"区域或拖拽文件到该区域 + - 支持PDF、DOC、DOCX、TXT、PNG、JPG、JPEG格式 + - 选择文件后点击"开始审查"按钮 + +2. **查看审查结果**: + - 左侧面板显示风险统计(高、中、低风险数量) + - 右侧详细列出发现的风险项及其描述和建议 + - 中间面板显示大语言模型的智能分析结果 + +3. **重新审查**: + - 点击"清除文件"按钮可重新上传新合同 + - 系统支持连续审查多个合同文件 + +## 📡 API接口文档 + +### 上传文件 +``` +POST /upload +Content-Type: multipart/form-data + +参数: +- file: 合同文件 + +返回: +{ + "filename": "文件名", + "file_path": "文件存储路径", + "message": "文件上传成功" +} +``` + +### 审查合同 +``` +POST /review +Content-Type: application/json + +参数: +{ + "file_path": "文件存储路径" +} + +返回: +{ + "risks": [ + { + "category": "风险类别", + "severity": "风险等级(高/中/低)", + "location": "风险位置上下文", + "description": "风险描述", + "suggestion": "修改建议" + } + ], + "summary": "总体评估", + "total_risks_found": 风险总数 +} +``` + +### 审查合同并使用大模型分析 +``` +POST /review_with_llm +Content-Type: application/json + +参数: +{ + "file_path": "文件存储路径" +} + +返回: +{ + "review_result": { /* 关键字匹配审查结果 */ }, + "llm_analysis": "大语言模型分析结果" +} +``` + +### 审查合同并流式返回大模型分析 +``` +POST /review_with_llm_stream +Content-Type: application/json + +参数: +{ + "file_path": "文件存储路径" +} + +返回: +文本流(大语言模型分析结果) +``` + +## � 常见问题 + +1. **上传文件后没有反应**: + - 检查文件格式是否受支持 + - 查看浏览器控制台是否有错误信息 + - 确认后端服务是否正常运行 + +2. **大模型分析结果为空**: + - 检查LAZYLLM_SENSENOVA_API_KEY环境变量是否正确设置 + - 确认网络连接是否正常 + - 查看contract_review_server.log日志文件 + +3. **图片文件无法识别文字**: + - 确认已正确安装Tesseract OCR引擎 + - 检查图片质量是否足够清晰 + - 尝试调整图片对比度和亮度 + +4. **响应速度慢**: + - 大语言模型分析需要一定时间,请耐心等待 + - 检查网络连接状况 + - 确认服务器资源配置是否充足 + +## 📄 许可证 + +本项目仅供学习和参考使用,未经许可,禁止任何形式的商业使用。 + +## 💡 总结 + +智能合同审查系统结合了传统的关键字匹配技术和先进的大语言模型能力,为企业和个人提供了一套完整的合同风险识别解决方案。通过自动化和智能化的手段,大幅提升了合同审查的效率和准确性,具有广泛的实用价值和商业前景。 \ No newline at end of file diff --git a/contract_llm.py b/contract_llm.py new file mode 100644 index 0000000000000000000000000000000000000000..f9279ada9ec8c383357625cbced84584bfe831e2 --- /dev/null +++ b/contract_llm.py @@ -0,0 +1,149 @@ +import lazyllm +import asyncio +import json +from typing import Dict, Any, AsyncGenerator +from contract_review_mcp import review_local_contract + +class ContractReviewLLM: + def __init__(self): + # 初始化大模型,启用流式输出 + self.chat = lazyllm.OnlineChatModule("DeepSeek-V3", stream=True) + + async def analyze_contract_with_llm(self, file_path: str, review_result: Dict[str, Any]) -> str: + """ + 使用大模型分析合同审查结果并生成自然语言回复 + """ + try: + # 构建审查报告 + report = self._format_review_result(review_result) + + # 构造提示词,要求口语化回复且不要Markdown格式 + prompt = f""" +你是一个专业的合同法律专家和顾问。请仔细分析以下合同审查结果,并用通俗易懂、口语化的语言向用户解释: + +合同文件: {file_path} + +审查结果: +{report} + +请根据以上信息,提供以下内容: +1. 对合同整体风险的简要评估 +2. 详细解释发现的主要风险点 +3. 针对每个风险点提供具体的修改建议 +4. 总结性建议 + +请注意: +- 回复要口语化,就像在跟朋友聊天一样,不要用太多专业术语 +- 不要使用Markdown格式,只用纯文本回复 +- 可以适当使用一些连接词让语句更流畅 +- 回复要有条理,但不需要严格按照1、2、3、4点来写 +""" + + # 调用大模型 + response = self.chat.forward(prompt) + return response + + except Exception as e: + return f"大模型分析过程中出现错误: {str(e)}" + + async def analyze_contract_with_llm_stream(self, file_path: str, review_result: Dict[str, Any]) -> AsyncGenerator[str, None]: + """ + 使用大模型分析合同审查结果并流式生成自然语言回复 + """ + try: + # 构建审查报告 + report = self._format_review_result(review_result) + + # 构造提示词,要求口语化回复且不要Markdown格式 + prompt = f""" +你是一个专业的合同法律专家和顾问。请仔细分析以下合同审查结果,并用通俗易懂、口语化的语言向用户解释: + +合同文件: {file_path} + +审查结果: +{report} + +请根据以上信息,提供以下内容: +1. 对合同整体风险的简要评估 +2. 详细解释发现的主要风险点 +3. 针对每个风险点提供具体的修改建议 +4. 总结性建议 + +请注意: +- 回复要口语化,就像在跟朋友聊天一样,不要用太多专业术语 +- 不要使用Markdown格式,只用纯文本回复 +- 可以适当使用一些连接词让语句更流畅 +- 回复要有条理,但不需要严格按照1、2、3、4点来写 +""" + + # 先尝试获取完整的响应 + response = self.chat.forward(prompt) + + # 如果响应是字符串,模拟流式传输 + if isinstance(response, str): + # 逐字符yield以模拟流式传输 + for char in response: + yield char + else: + # 尝试处理真正的流式响应 + try: + # 调用大模型(已启用流式输出) + response_generator = self.chat.forward(prompt, stream=True) + + # 流式返回结果 + for chunk in response_generator: + if chunk and hasattr(chunk, 'choices') and len(chunk.choices) > 0: + content = chunk.choices[0].delta.content + if content: + yield content + except Exception: + # 如果流式传输失败,回退到完整响应 + response_str = str(response) + for char in response_str: + yield char + + except Exception as e: + yield f"大模型分析过程中出现错误: {str(e)}" + + def _format_review_result(self, review_result: Dict) -> str: + """ + 格式化审查结果 + """ + if "error" in review_result: + return f"审查过程中出现错误: {review_result['error']}" + + # 构建易于理解的审查报告 + report_lines = [] + report_lines.append(f"发现风险总数: {review_result.get('total_risks_found', 0)}") + report_lines.append(f"总体评价: {review_result.get('summary', '无')}") + report_lines.append("") + + risks = review_result.get("risks", []) + if risks: + report_lines.append("发现的风险详情:") + for i, risk in enumerate(risks, 1): + report_lines.append(f"{i}. 风险类别: {risk.get('category', '未知')}") + report_lines.append(f" 风险等级: {risk.get('severity', '未知')}") + report_lines.append(f" 描述: {risk.get('description', '无')}") + report_lines.append(f" 建议: {risk.get('suggestion', '无')}") + report_lines.append("") + else: + report_lines.append("未发现明显风险。") + + return "\n".join(report_lines) + +# 创建全局实例 +contract_llm = ContractReviewLLM() + +async def get_llm_analysis(file_path: str, review_result: Dict[str, Any]) -> str: + """ + 获取大模型对合同审查结果的分析 + """ + return await contract_llm.analyze_contract_with_llm(file_path, review_result) + +async def get_llm_analysis_stream(file_path: str, review_result: Dict[str, Any]): + """ + 流式获取大模型对合同审查结果的分析 + """ + async for chunk in contract_llm.analyze_contract_with_llm_stream(file_path, review_result): + yield chunk \ No newline at end of file diff --git a/contract_review_mcp.py b/contract_review_mcp.py new file mode 100644 index 0000000000000000000000000000000000000000..721e0f430033bd1f2f361697ec605696843059cf --- /dev/null +++ b/contract_review_mcp.py @@ -0,0 +1,457 @@ +import asyncio +import httpx +import logging +import time +import os +import json +import re +from typing import Annotated, Dict, Optional, List +from fastmcp import FastMCP +from pydantic import Field +import io +import tempfile +from urllib.parse import urlparse, unquote +import fitz # PyMuPDF for PDF parsing +from docx import Document # python-docx for DOCX parsing +from PIL import Image +import pytesseract + +# 配置日志记录 +logging.basicConfig( + level=logging.INFO, + filename="contract_review_server.log", + filemode="a", + format="%(asctime)s - %(name)s - %(levelname)s - %(message)s" +) +logger = logging.getLogger("contract_review_server") + +# 创建MCP服务器 +mcp = FastMCP("contract_review_server") + +# 风险词定义 +RISK_KEYWORDS = { + "高额违约金": { + "patterns": [r"违约金.*?(超过|超出|高于).*?[0-9]+%", r"违约责任.*?支付.*?[0-9]+%"], + "severity": "高", + "description": "合同中约定的违约金比例过高,可能存在法律风险", + "suggestion": "建议调整违约金比例至合理范围(通常不超过实际损失的30%)" + }, + "争议解决地": { + "patterns": [r"争议.*?提交.*?(法院|仲裁|诉讼).*?管辖", r"管辖权.*?归属"], + "severity": "中", + "description": "合同中争议解决地点可能对己方不利", + "suggestion": "建议明确有利于己方的争议解决地点和方式" + }, + "单方解除权": { + "patterns": [r"(\b(?:甲方|乙方)\b).*?可以.*?解除.*?合同", r"任意解除权", r"单方面终止"], + "severity": "高", + "description": "对方拥有单方解除合同的权利,存在较大风险", + "suggestion": "建议取消或限制单方解除权,确保双方权利对等" + }, + "知识产权归属": { + "patterns": [r"知识产权.*?归.*?所有", r"作品.*?权利.*?转移", r"著作权.*?归属"], + "severity": "中", + "description": "知识产权归属条款不明确或不利于己方", + "suggestion": "建议明确知识产权归属,保护己方权益" + }, + "保密条款": { + "patterns": [r"保密.*?义务", r"商业秘密.*?保密", r"保密期限.*?[0-9]"], + "severity": "中", + "description": "保密条款缺失或期限不合理", + "suggestion": "建议完善保密条款,明确保密信息范围及期限" + }, + "不可抗力": { + "patterns": [r"不可抗力.*?免责", r"因.*?原因.*?免除.*?责任"], + "severity": "中", + "description": "不可抗力条款范围过宽,可能导致过多免责情况", + "suggestion": "建议明确不可抗力的具体情形,避免滥用免责条款" + }, + "付款条款": { + "patterns": [r"预付.*?[0-9]+%", r"一次性.*?支付.*?全部", r"款项.*?先付"], + "severity": "中", + "description": "付款方式可能对企业现金流造成压力或风险", + "suggestion": "建议设置分期付款,降低财务风险" + }, + "合同期限": { + "patterns": [r"有效期.*?[0-9]+年", r"长期.*?合作.*?期限"], + "severity": "低", + "description": "合同期限过长可能限制企业经营灵活性", + "suggestion": "建议设置合理的合同期限和续约条件" + } +} + + +def _review_contract_by_keywords(contract_text: str) -> Dict: + """ + 基于风险关键词匹配审查合同内容 + """ + try: + risks = [] + # 对每个风险词进行匹配检查 + for risk_category, risk_info in RISK_KEYWORDS.items(): + patterns = risk_info["patterns"] + matched_positions = [] + + # 在整个合同文本中查找匹配项 + for pattern in patterns: + matches = re.finditer(pattern, contract_text, re.IGNORECASE) + for match in matches: + # 记录匹配位置和内容 + matched_positions.append({ + "position": match.span(), + "content": match.group() + }) + + # 如果找到匹配项,则添加风险 + if matched_positions: + # 获取上下文(匹配词前后各20个字符) + locations = [] + for pos in matched_positions: + start = max(0, pos["position"][0] - 20) + end = min(len(contract_text), pos["position"][1] + 20) + context = contract_text[start:end] + locations.append(context) + + risks.append({ + "category": risk_category, + "severity": risk_info["severity"], + "location": "; ".join(locations[:3]), # 只显示前3个位置 + "description": risk_info["description"], + "suggestion": risk_info["suggestion"] + }) + + # 生成总结 + if risks: + severity_count = {} + for risk in risks: + severity = risk["severity"] + severity_count[severity] = severity_count.get(severity, 0) + 1 + + summary_parts = [] + if severity_count.get("高", 0) > 0: + summary_parts.append(f"发现{severity_count['高']}个高风险问题") + if severity_count.get("中", 0) > 0: + summary_parts.append(f"发现{severity_count['中']}个中风险问题") + if severity_count.get("低", 0) > 0: + summary_parts.append(f"发现{severity_count['低']}个低风险问题") + + summary = ",".join(summary_parts) + ",建议重点关注高风险问题并及时修改。" + else: + summary = "未发现预定义的风险关键词匹配项,合同相对较为规范。" + + return { + "risks": risks, + "summary": summary, + "total_risks_found": len(risks) + } + + except Exception as e: + logger.error(f"关键词匹配审查失败: {str(e)}") + return {"error": f"关键词匹配审查失败: {str(e)}"} + + +@mcp.tool(description="审查合同文档,识别潜在风险") +async def review_contract( + file_url: Annotated[str, Field(description="合同文件URL,支持.pdf、.doc、.docx、.png、.jpg、.jpeg等格式")], + model_version: Annotated[str, Field(description="文档解析模型版本,可选值: pipeline, vlm,默认为pipeline")] = "pipeline", + is_ocr: Annotated[bool, Field(description="是否启用OCR功能,默认true")] = True, + language: Annotated[str, Field(description="文档语言,默认ch")] = "ch" +) -> Annotated[Dict, Field(description="合同审查结果,包含风险列表和整体评估")]: + """ + 审查合同文档,识别潜在风险并返回JSON格式的审查结果 + """ + logger.info(f"开始审查合同: {file_url}") + + try: + # 步骤1: 解析文档 + contract_text = await _parse_document(file_url) + + if not contract_text: + return {"error": "无法从文档中提取文本内容"} + + logger.info(f"成功提取文本内容,长度: {len(contract_text)}") + + # 步骤2: 使用关键词匹配审查 + review_result = _review_contract_by_keywords(contract_text) + + # 添加原始文本到结果中(可选) + review_result["contract_text"] = contract_text[:500] + "..." if len(contract_text) > 500 else contract_text + + return review_result + + except Exception as e: + logger.error(f"审查合同失败: {str(e)}") + return {"error": f"审查合同失败: {str(e)}"} + + +@mcp.tool(description="批量审查多个合同文档") +async def batch_review_contracts( + file_urls: Annotated[List[str], Field(description="合同文件URL列表")], + model_version: Annotated[str, Field(description="文档解析模型版本,可选值: pipeline, vlm,默认为pipeline")] = "pipeline", + is_ocr: Annotated[bool, Field(description="是否启用OCR功能,默认true")] = True, + language: Annotated[str, Field(description="文档语言,默认ch")] = "ch" +) -> Annotated[Dict, Field(description="批量合同审查结果")]: + """ + 批量审查多个合同文档 + """ + results = [] + + for file_url in file_urls: + logger.info(f"开始审查合同: {file_url}") + try: + # 直接调用内部函数而不是被装饰的工具函数 + contract_text = await _parse_document(file_url) + + if not contract_text: + result = {"error": "无法从文档中提取文本内容"} + else: + result = _review_contract_by_keywords(contract_text) + # 添加原始文本到结果中(可选) + result["contract_text"] = contract_text[:500] + "..." if len(contract_text) > 500 else contract_text + except Exception as e: + result = {"error": f"处理文件时发生错误: {str(e)}"} + + results.append({ + "file_url": file_url, + "review_result": result + }) + + return { + "total_files": len(file_urls), + "results": results + } + + +async def _download_file(file_url: str) -> bytes: + """ + 下载文件内容 + """ + try: + async with httpx.AsyncClient() as client: + response = await client.get(file_url) + response.raise_for_status() + return response.content + except Exception as e: + logger.error(f"下载文件失败: {str(e)}") + raise + + +def _get_file_extension(file_url: str) -> str: + """ + 从URL中提取文件扩展名 + """ + # 移除查询参数 + url_path = urlparse(file_url).path + # URL解码 + decoded_path = unquote(url_path) + # 获取扩展名 + extension = os.path.splitext(decoded_path)[1].lower() + return extension + + +def _parse_pdf_content(content: bytes) -> str: + """ + 使用PyMuPDF解析PDF内容 + """ + try: + # 创建内存中的PDF文档 + pdf_document = fitz.open(stream=io.BytesIO(content), filetype="pdf") + text = "" + + # 遍历所有页面 + for page_num in range(len(pdf_document)): + page = pdf_document[page_num] + text += page.get_text() + + pdf_document.close() + return text.strip() + except Exception as e: + logger.error(f"解析PDF文件失败: {str(e)}") + raise + + +def _parse_docx_content(content: bytes) -> str: + """ + 使用python-docx解析DOCX内容 + """ + try: + # 创建内存中的DOCX文档 + doc = Document(io.BytesIO(content)) + text = "" + + # 遍历所有段落 + for paragraph in doc.paragraphs: + text += paragraph.text + "\n" + + return text.strip() + except Exception as e: + logger.error(f"解析DOCX文件失败: {str(e)}") + raise + + +def _parse_image_content(content: bytes) -> str: + """ + 使用OCR解析图像内容 + """ + try: + # 从字节创建图像 + image = Image.open(io.BytesIO(content)) + # 使用tesseract进行OCR识别 + text = pytesseract.image_to_string(image, lang='chi_sim+eng') + return text.strip() + except Exception as e: + logger.error(f"OCR识别图像失败: {str(e)}") + raise + + +def _parse_txt_content(content: bytes) -> str: + """ + 解析TXT文本内容 + """ + try: + # 尝试不同的编码 + encodings = ['utf-8', 'gbk', 'gb2312'] + for encoding in encodings: + try: + return content.decode(encoding).strip() + except UnicodeDecodeError: + continue + # 如果所有编码都失败,使用utf-8并忽略错误 + return content.decode('utf-8', errors='ignore').strip() + except Exception as e: + logger.error(f"解析TXT文件失败: {str(e)}") + raise + + +async def _parse_document(file_url: str) -> str: + """ + 解析文档并提取文本内容 + """ + try: + # 下载文件 + logger.info(f"开始下载文件: {file_url}") + content = await _download_file(file_url) + logger.info(f"文件下载完成,大小: {len(content)} 字节") + + # 获取文件扩展名 + extension = _get_file_extension(file_url) + logger.info(f"文件扩展名: {extension}") + + # 根据扩展名选择解析方法 + if extension == ".pdf": + return _parse_pdf_content(content) + elif extension in [".docx", ".doc"]: + return _parse_docx_content(content) + elif extension in [".jpg", ".jpeg", ".png", ".bmp", ".tiff"]: + return _parse_image_content(content) + elif extension == ".txt": + return _parse_txt_content(content) + else: + # 默认尝试作为文本文件处理 + logger.warning(f"未知文件类型 {extension},尝试作为文本文件处理") + return _parse_txt_content(content) + except Exception as e: + logger.error(f"解析文档失败: {str(e)}") + raise + + +@mcp.tool(description="审查本地合同文档,识别潜在风险") +async def review_local_contract( + file_path: Annotated[str, Field(description="本地合同文件路径,支持.pdf、.doc、.docx、.png、.jpg、.jpeg等格式")], + model_version: Annotated[str, Field(description="文档解析模型版本,可选值: pipeline, vlm,默认为pipeline")] = "pipeline", + is_ocr: Annotated[bool, Field(description="是否启用OCR功能,默认true")] = True, + language: Annotated[str, Field(description="文档语言,默认ch")] = "ch" +) -> Annotated[Dict, Field(description="合同审查结果,包含风险列表和整体评估")]: + """ + 审查本地合同文档,识别潜在风险并返回JSON格式的审查结果 + """ + logger.info(f"开始审查本地合同: {file_path}") + + try: + # 步骤1: 解析文档 + contract_text = await _parse_local_document(file_path) + + if not contract_text: + return {"error": "无法从文档中提取文本内容"} + + logger.info(f"成功提取文本内容,长度: {len(contract_text)}") + + # 步骤2: 使用关键词匹配审查 + review_result = _review_contract_by_keywords(contract_text) + + # 添加原始文本到结果中(可选) + review_result["contract_text"] = contract_text[:500] + "..." if len(contract_text) > 500 else contract_text + + return review_result + + except Exception as e: + logger.error(f"审查合同失败: {str(e)}") + return {"error": f"审查合同失败: {str(e)}"} + + +async def _parse_local_document(file_path: str) -> str: + """ + 解析本地文档并提取文本内容 + """ + try: + # 检查文件是否存在 + if not os.path.exists(file_path): + raise FileNotFoundError(f"文件不存在: {file_path}") + + # 获取文件扩展名 + extension = os.path.splitext(file_path)[1].lower() + logger.info(f"本地文件扩展名: {extension}") + + # 读取文件内容 + with open(file_path, "rb") as f: + content = f.read() + logger.info(f"本地文件读取完成,大小: {len(content)} 字节") + + # 根据扩展名选择解析方法 + if extension == ".pdf": + return _parse_pdf_content(content) + elif extension in [".docx", ".doc"]: + return _parse_docx_content(content) + elif extension in [".jpg", ".jpeg", ".png", ".bmp", ".tiff"]: + return _parse_image_content(content) + elif extension == ".txt": + return _parse_txt_content(content) + else: + # 默认尝试作为文本文件处理 + logger.warning(f"未知文件类型 {extension},尝试作为文本文件处理") + return _parse_txt_content(content) + except Exception as e: + logger.error(f"解析本地文档失败: {str(e)}") + raise + + +async def review_local_contract(file_path: str) -> Dict: + """ + 直接审查本地合同文档的函数,供web_server.py调用 + """ + logger.info(f"开始审查本地合同: {file_path}") + + try: + # 步骤1: 解析文档 + contract_text = await _parse_local_document(file_path) + + if not contract_text: + return {"error": "无法从文档中提取文本内容"} + + logger.info(f"成功提取文本内容,长度: {len(contract_text)}") + + # 步骤2: 使用关键词匹配审查 + review_result = _review_contract_by_keywords(contract_text) + + # 添加原始文本到结果中(可选) + review_result["contract_text"] = contract_text[:500] + "..." if len(contract_text) > 500 else contract_text + + return review_result + + except Exception as e: + logger.error(f"审查合同失败: {str(e)}") + raise + + +if __name__ == "__main__": + mcp.run(transport="http", host="0.0.0.0", port=8010) diff --git a/index.html b/index.html new file mode 100644 index 0000000000000000000000000000000000000000..0f0e06129ce083ce2d5219fde20af2c486b4b483 --- /dev/null +++ b/index.html @@ -0,0 +1,885 @@ + + + + + + 智能合同审查Agent + + + + +
+ +
+ +
+ +
+
+ + 上传合同文件 +
+
+
+ +
+
+ +
+
点击选择文件或拖拽文件到此处
+
支持 PDF, DOC, DOCX, TXT, PNG, JPG, JPEG 格式
+ +
+ +
+
+ + 文件名 +
+
文件大小
+
+ +
+ + + +
+
+
+ + +
+
+ + 智能分析结果 +
+
+
+
+
正在智能分析合同,请稍候...
+
+ +
+
请上传合同文件以开始审查...
+
+
+
+ + +
+
+ + 风险项列表 +
+
+
+
+
0
+
高风险
+
+
+
0
+
中风险
+
+
+
0
+
低风险
+
+
+ +
+

+ + 发现的风险项 +

+
+ 请上传合同文件以开始审查... +
+
+
+
+
+ + + + \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..d901cbd067dcfb7d1206f2247da44ca94f2ee367 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,6 @@ +fastmcp +httpx +pymupdf +python-docx +Pillow +pytesseract \ No newline at end of file diff --git a/web_server.py b/web_server.py new file mode 100644 index 0000000000000000000000000000000000000000..4fdc325b6ea0b81c5c4e6aadbbdd66369ae4f0df --- /dev/null +++ b/web_server.py @@ -0,0 +1,141 @@ +import os +import uuid +from fastapi import FastAPI, UploadFile, File, HTTPException +from fastapi.responses import HTMLResponse, JSONResponse, StreamingResponse +from fastapi.staticfiles import StaticFiles +import shutil +from pathlib import Path +import asyncio +import httpx +import json + +# 导入合同审查功能 +from contract_review_mcp import review_local_contract +# 导入大模型分析功能 +from contract_llm import get_llm_analysis, get_llm_analysis_stream + +app = FastAPI(title="智能合同审查系统") + +# 创建数据目录 - 确保目录存在 +DATA_DIR = Path("contracts") +try: + DATA_DIR.mkdir(exist_ok=True) + print(f"数据目录已创建或已存在: {DATA_DIR.absolute()}") +except Exception as e: + print(f"创建数据目录失败: {e}") + # 如果无法创建目录,使用当前目录 + DATA_DIR = Path(".") + +# 挂载静态文件 +app.mount("/static", StaticFiles(directory="."), name="static") + +@app.get("/", response_class=HTMLResponse) +async def read_root(): + """返回主页面""" + with open("index.html", "r", encoding="utf-8") as f: + return f.read() + +# 添加一个处理Vite客户端请求的路由,返回404或空响应以避免错误 +@app.get("/@vite/client") +async def vite_client(): + """处理对Vite客户端的请求,避免404错误""" + return "" + +@app.post("/upload") +async def upload_file(file: UploadFile = File(...)): + """上传文件并保存到data目录""" + try: + # 生成唯一文件名 + file_extension = Path(file.filename).suffix + unique_filename = f"{uuid.uuid4()}{file_extension}" + file_path = DATA_DIR / unique_filename + + # 确保目录存在 + file_path.parent.mkdir(parents=True, exist_ok=True) + + # 保存文件 + with file_path.open("wb") as buffer: + shutil.copyfileobj(file.file, buffer) + + return JSONResponse( + content={ + "filename": file.filename, + "file_path": str(file_path.absolute()), # 返回绝对路径 + "message": "文件上传成功" + } + ) + except Exception as e: + raise HTTPException(status_code=500, detail=f"文件上传失败: {str(e)}") + +@app.post("/review") +async def review_contract(request: dict): + """审查合同文件""" + try: + file_path = request.get("file_path") + if not file_path: + raise HTTPException(status_code=400, detail="缺少文件路径参数") + + # 检查文件是否存在 + if not os.path.exists(file_path): + raise HTTPException(status_code=404, detail="文件不存在") + + # 调用本地合同审查功能 + result = await review_local_contract(file_path=file_path) + return JSONResponse(content=result) + except Exception as e: + raise HTTPException(status_code=500, detail=f"合同审查失败: {str(e)}") + +@app.post("/review_with_llm") +async def review_contract_with_llm(request: dict): + """审查合同文件并使用大模型分析结果""" + try: + file_path = request.get("file_path") + if not file_path: + raise HTTPException(status_code=400, detail="缺少文件路径参数") + + # 检查文件是否存在 + if not os.path.exists(file_path): + raise HTTPException(status_code=404, detail="文件不存在") + + # 调用本地合同审查功能 + review_result = await review_local_contract(file_path=file_path) + + # 使用大模型分析审查结果 + llm_analysis = await get_llm_analysis(file_path, review_result) + + # 返回审查结果和大模型分析 + response_data = { + "review_result": review_result, + "llm_analysis": llm_analysis + } + + return JSONResponse(content=response_data) + except Exception as e: + raise HTTPException(status_code=500, detail=f"合同审查失败: {str(e)}") + +@app.post("/review_with_llm_stream") +async def review_contract_with_llm_stream(request: dict): + """审查合同文件并使用大模型流式分析结果""" + try: + file_path = request.get("file_path") + if not file_path: + raise HTTPException(status_code=400, detail="缺少文件路径参数") + + # 检查文件是否存在 + if not os.path.exists(file_path): + raise HTTPException(status_code=404, detail="文件不存在") + + # 调用本地合同审查功能 + review_result = await review_local_contract(file_path=file_path) + + # 返回流式响应 + return StreamingResponse( + get_llm_analysis_stream(file_path, review_result), + media_type="text/plain" + ) + except Exception as e: + raise HTTPException(status_code=500, detail=f"合同审查失败: {str(e)}") + +if __name__ == "__main__": + import uvicorn + uvicorn.run(app, host="0.0.0.0", port=8001) \ No newline at end of file diff --git "a/\344\275\234\345\223\201\346\217\220\344\272\244/ScreenShot_2025-11-24_190342_756.png" "b/\344\275\234\345\223\201\346\217\220\344\272\244/ScreenShot_2025-11-24_190342_756.png" deleted file mode 100644 index a5597718d183d9ab923fcecbd83c88808a0585c6..0000000000000000000000000000000000000000 Binary files "a/\344\275\234\345\223\201\346\217\220\344\272\244/ScreenShot_2025-11-24_190342_756.png" and /dev/null differ diff --git "a/\344\275\234\345\223\201\346\217\220\344\272\244/\346\217\220\344\272\244\350\246\201\346\261\202" "b/\344\275\234\345\223\201\346\217\220\344\272\244/\346\217\220\344\272\244\350\246\201\346\261\202" deleted file mode 100644 index b54113293fc6dcff5132bbe72c95a2590c855b1d..0000000000000000000000000000000000000000 --- "a/\344\275\234\345\223\201\346\217\220\344\272\244/\346\217\220\344\272\244\350\246\201\346\261\202" +++ /dev/null @@ -1,5 +0,0 @@ -需提交完整的作品方案介绍文档、项目源代码; - -作品需要使用商汤 LazyAGI 团队开发到 LazyLLM 开源低代码大模型应用开发工具进行开发; - -作品提交内容包含:Agent 系统本体、技术文档(项目简介、成员贡献清单、技术栈、架构设计、部署说明)可选:操作录屏视频(≤5min,可以发布在网盘设置公开可见) \ No newline at end of file