commit 785d74c6534d823f1e24ad35672de4b43603161c Author: xin Date: Tue Apr 1 00:33:51 2025 +0800 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a85d17f --- /dev/null +++ b/.gitignore @@ -0,0 +1,53 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class +.Python + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IDE +.idea/ +.vscode/ + +# OS +.DS_Store + +# Custom +ai作业/ \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..b5c626e --- /dev/null +++ b/README.md @@ -0,0 +1,62 @@ +# AI作业批改系统 + +本项目通过调用Dify Workflow API,实现自动化AI作业批改功能。系统读取本地Excel作业提交记录和随作业提交的YML配置文件,自动完成作业批改流程。 + +## 功能特点 + +- 自动读取学生作业提交记录(Excel格式) +- 解析学生提交的YML配置文件 +- 调用Dify API进行作业批改 +- 支持文件上传和API结果返回 + +## 安装依赖 + +```bash +pip install -r requirements.txt +``` + +## 配置说明 + +1. 在`source/grade_assignments.py`中配置Dify API参数: + ```python + # API配置 + DIFY_API_KEY = "your_api_key_here" # 替换为实际API Key + DIFY_API_URL = "http://192.168.100.143/v1" # Dify API地址 + FILE_UPLOAD_URL = f"{DIFY_API_URL}/files/upload" # 文件上传地址 + ``` + +2. 准备学生作业提交Excel文件,格式如下: + | 学生姓名 | 学号 | 提交时间 | 作业状态 | + |----------|------|----------|----------| + +3. 每个学生作业需包含一个YML配置文件,描述作业内容 + +## 使用说明 + +1. 将学生作业Excel文件放在`data/`目录下 +2. 将学生作业YML文件放在`assignments/`目录下 +3. 运行主程序: + +```bash +python source/grade_assignments.py +``` + +4. 查看批改结果,结果将保存在`results/`目录下 + +## 测试 + +运行测试用例验证系统功能: + +```bash +python source/test_grade.py +``` + +## API文档 + +详细API调用规范请参考[docs/workflow_api.md](docs/workflow_api.md) + +## 注意事项 + +- 确保网络可以访问Dify API服务器 +- 上传文件大小不超过API限制 +- 作业YML文件需符合规范格式 diff --git a/docs/workflow_api.md b/docs/workflow_api.md new file mode 100644 index 0000000..94d1002 --- /dev/null +++ b/docs/workflow_api.md @@ -0,0 +1,632 @@ +Workflow 应用 API +Workflow 应用无会话支持,适合用于翻译/文章写作/总结 AI 等等。 + +Base URL +Code +http://192.168.100.143/v1 + +Copy +Copied! +Authentication +Dify Service API 使用 API-Key 进行鉴权。 强烈建议开发者把 API-Key 放在后端存储,而非分享或者放在客户端存储,以免 API-Key 泄露,导致财产损失。 所有 API 请求都应在 Authorization HTTP Header 中包含您的 API-Key,如下所示: + +Code + Authorization: Bearer {API_KEY} + + +Copy +Copied! +POST +/workflows/run +执行 workflow +执行 workflow,没有已发布的 workflow,不可执行。 + +Request Body +inputs (object) Required 允许传入 App 定义的各变量值。 inputs 参数包含了多组键值对(Key/Value pairs),每组的键对应一个特定变量,每组的值则是该变量的具体值。变量可以是文件列表类型。 文件列表类型变量适用于传入文件结合文本理解并回答问题,仅当模型支持该类型文件解析能力时可用。如果该变量是文件列表类型,该变量对应的值应是列表格式,其中每个元素应包含以下内容: +type (string) 支持类型: +document 具体类型包含:'TXT', 'MD', 'MARKDOWN', 'PDF', 'HTML', 'XLSX', 'XLS', 'DOCX', 'CSV', 'EML', 'MSG', 'PPTX', 'PPT', 'XML', 'EPUB' +image 具体类型包含:'JPG', 'JPEG', 'PNG', 'GIF', 'WEBP', 'SVG' +audio 具体类型包含:'MP3', 'M4A', 'WAV', 'WEBM', 'AMR' +video 具体类型包含:'MP4', 'MOV', 'MPEG', 'MPGA' +custom 具体类型包含:其他文件类型 +transfer_method (string) 传递方式,remote_url 图片地址 / local_file 上传文件 +url (string) 图片地址(仅当传递方式为 remote_url 时) +upload_file_id (string) (string) 上传文件 ID(仅当传递方式为 local_file 时) +response_mode (string) Required 返回响应模式,支持: +streaming 流式模式(推荐)。基于 SSE(Server-Sent Events)实现类似打字机输出方式的流式返回。 +blocking 阻塞模式,等待执行完毕后返回结果。(请求若流程较长可能会被中断)。 由于 Cloudflare 限制,请求会在 100 秒超时无返回后中断。 +user (string) Required 用户标识,用于定义终端用户的身份,方便检索、统计。 由开发者定义规则,需保证用户标识在应用内唯一。 +Response +当 response_mode 为 blocking 时,返回 CompletionResponse object。 当 response_mode 为 streaming时,返回 ChunkCompletionResponse object 流式序列。 + +CompletionResponse +返回完整的 App 结果,Content-Type 为 application/json 。 + +workflow_run_id (string) workflow 执行 ID +task_id (string) 任务 ID,用于请求跟踪和下方的停止响应接口 +data (object) 详细内容 +id (string) workflow 执行 ID +workflow_id (string) 关联 Workflow ID +status (string) 执行状态, running / succeeded / failed / stopped +outputs (json) Optional 输出内容 +error (string) Optional 错误原因 +elapsed_time (float) Optional 耗时(s) +total_tokens (int) Optional 总使用 tokens +total_steps (int) 总步数(冗余),默认 0 +created_at (timestamp) 开始时间 +finished_at (timestamp) 结束时间 +ChunkCompletionResponse +返回 App 输出的流式块,Content-Type 为 text/event-stream。 每个流式块均为 data: 开头,块之间以 \n\n 即两个换行符分隔,如下所示: + +data: {"event": "message", "task_id": "900bbd43-dc0b-4383-a372-aa6e6c414227", "id": "663c5084-a254-4040-8ad3-51f2a3c1a77c", "answer": "Hi", "created_at": 1705398420}\n\n + +Copy +Copied! +流式块中根据 event 不同,结构也不同,包含以下类型: + +event: workflow_started workflow 开始执行 +task_id (string) 任务 ID,用于请求跟踪和下方的停止响应接口 +workflow_run_id (string) workflow 执行 ID +event (string) 固定为 workflow_started +data (object) 详细内容 +id (string) workflow 执行 ID +workflow_id (string) 关联 Workflow ID +sequence_number (int) 自增序号,App 内自增,从 1 开始 +created_at (timestamp) 开始时间 +event: node_started node 开始执行 +task_id (string) 任务 ID,用于请求跟踪和下方的停止响应接口 +workflow_run_id (string) workflow 执行 ID +event (string) 固定为 node_started +data (object) 详细内容 +id (string) workflow 执行 ID +node_id (string) 节点 ID +node_type (string) 节点类型 +title (string) 节点名称 +index (int) 执行序号,用于展示 Tracing Node 顺序 +predecessor_node_id (string) 前置节点 ID,用于画布展示执行路径 +inputs (object) 节点中所有使用到的前置节点变量内容 +created_at (timestamp) 开始时间 +event: node_finished node 执行结束,成功失败同一事件中不同状态 +task_id (string) 任务 ID,用于请求跟踪和下方的停止响应接口 +workflow_run_id (string) workflow 执行 ID +event (string) 固定为 node_finished +data (object) 详细内容 +id (string) node 执行 ID +node_id (string) 节点 ID +index (int) 执行序号,用于展示 Tracing Node 顺序 +predecessor_node_id (string) optional 前置节点 ID,用于画布展示执行路径 +inputs (object) 节点中所有使用到的前置节点变量内容 +process_data (json) Optional 节点过程数据 +outputs (json) Optional 输出内容 +status (string) 执行状态 running / succeeded / failed / stopped +error (string) Optional 错误原因 +elapsed_time (float) Optional 耗时(s) +execution_metadata (json) 元数据 +total_tokens (int) optional 总使用 tokens +total_price (decimal) optional 总费用 +currency (string) optional 货币,如 USD / RMB +created_at (timestamp) 开始时间 +event: workflow_finished workflow 执行结束,成功失败同一事件中不同状态 +task_id (string) 任务 ID,用于请求跟踪和下方的停止响应接口 +workflow_run_id (string) workflow 执行 ID +event (string) 固定为 workflow_finished +data (object) 详细内容 +id (string) workflow 执行 ID +workflow_id (string) 关联 Workflow ID +status (string) 执行状态 running / succeeded / failed / stopped +outputs (json) Optional 输出内容 +error (string) Optional 错误原因 +elapsed_time (float) Optional 耗时(s) +total_tokens (int) Optional 总使用 tokens +total_steps (int) 总步数(冗余),默认 0 +created_at (timestamp) 开始时间 +finished_at (timestamp) 结束时间 +event: tts_message TTS 音频流事件,即:语音合成输出。内容是Mp3格式的音频块,使用 base64 编码后的字符串,播放的时候直接解码即可。(开启自动播放才有此消息) +task_id (string) 任务 ID,用于请求跟踪和下方的停止响应接口 +message_id (string) 消息唯一 ID +audio (string) 语音合成之后的音频块使用 Base64 编码之后的文本内容,播放的时候直接 base64 解码送入播放器即可 +created_at (int) 创建时间戳,如:1705395332 +event: tts_message_end TTS 音频流结束事件,收到这个事件表示音频流返回结束。 +task_id (string) 任务 ID,用于请求跟踪和下方的停止响应接口 +message_id (string) 消息唯一 ID +audio (string) 结束事件是没有音频的,所以这里是空字符串 +created_at (int) 创建时间戳,如:1705395332 +event: ping 每 10s 一次的 ping 事件,保持连接存活。 +Errors +400,invalid_param,传入参数异常 +400,app_unavailable,App 配置不可用 +400,provider_not_initialize,无可用模型凭据配置 +400,provider_quota_exceeded,模型调用额度不足 +400,model_currently_not_support,当前模型不可用 +400,workflow_request_error,workflow 执行失败 +500,服务内部异常 +Request +POST +/workflows/run +curl -X POST 'http://192.168.100.143/v1/workflows/run' \ +--header 'Authorization: Bearer {api_key}' \ +--header 'Content-Type: application/json' \ +--data-raw '{ + "inputs": {}, + "response_mode": "streaming", + "user": "abc-123" +}' + +Copy +Copied! +Example: file array as an input variable +{ + "inputs": { + "{variable_name}": + [ + { + "transfer_method": "local_file", + "upload_file_id": "{upload_file_id}", + "type": "{document_type}" + } + ] + } +} + +Copy +Copied! +Blocking Mode +Response +{ + "workflow_run_id": "djflajgkldjgd", + "task_id": "9da23599-e713-473b-982c-4328d4f5c78a", + "data": { + "id": "fdlsjfjejkghjda", + "workflow_id": "fldjaslkfjlsda", + "status": "succeeded", + "outputs": { + "text": "Nice to meet you." + }, + "error": null, + "elapsed_time": 0.875, + "total_tokens": 3562, + "total_steps": 8, + "created_at": 1705407629, + "finished_at": 1727807631 + } +} + +Copy +Copied! +Streaming Mode +Response + data: {"event": "workflow_started", "task_id": "5ad4cb98-f0c7-4085-b384-88c403be6290", "workflow_run_id": "5ad498-f0c7-4085-b384-88cbe6290", "data": {"id": "5ad498-f0c7-4085-b384-88cbe6290", "workflow_id": "dfjasklfjdslag", "sequence_number": 1, "created_at": 1679586595}} + data: {"event": "node_started", "task_id": "5ad4cb98-f0c7-4085-b384-88c403be6290", "workflow_run_id": "5ad498-f0c7-4085-b384-88cbe6290", "data": {"id": "5ad498-f0c7-4085-b384-88cbe6290", "node_id": "dfjasklfjdslag", "node_type": "start", "title": "Start", "index": 0, "predecessor_node_id": "fdljewklfklgejlglsd", "inputs": {}, "created_at": 1679586595}} + data: {"event": "node_finished", "task_id": "5ad4cb98-f0c7-4085-b384-88c403be6290", "workflow_run_id": "5ad498-f0c7-4085-b384-88cbe6290", "data": {"id": "5ad498-f0c7-4085-b384-88cbe6290", "node_id": "dfjasklfjdslag", "node_type": "start", "title": "Start", "index": 0, "predecessor_node_id": "fdljewklfklgejlglsd", "inputs": {}, "outputs": {}, "status": "succeeded", "elapsed_time": 0.324, "execution_metadata": {"total_tokens": 63127864, "total_price": 2.378, "currency": "USD"}, "created_at": 1679586595}} + data: {"event": "workflow_finished", "task_id": "5ad4cb98-f0c7-4085-b384-88c403be6290", "workflow_run_id": "5ad498-f0c7-4085-b384-88cbe6290", "data": {"id": "5ad498-f0c7-4085-b384-88cbe6290", "workflow_id": "dfjasklfjdslag", "outputs": {}, "status": "succeeded", "elapsed_time": 0.324, "total_tokens": 63127864, "total_steps": "1", "created_at": 1679586595, "finished_at": 1679976595}} + data: {"event": "tts_message", "conversation_id": "23dd85f3-1a41-4ea0-b7a9-062734ccfaf9", "message_id": "a8bdc41c-13b2-4c18-bfd9-054b9803038c", "created_at": 1721205487, "task_id": "3bf8a0bb-e73b-4690-9e66-4e429bad8ee7", "audio": "qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq"} + data: {"event": "tts_message_end", "conversation_id": "23dd85f3-1a41-4ea0-b7a9-062734ccfaf9", "message_id": "a8bdc41c-13b2-4c18-bfd9-054b9803038c", "created_at": 1721205487, "task_id": "3bf8a0bb-e73b-4690-9e66-4e429bad8ee7", "audio": ""} + +Copy +Copied! +File upload sample code +import requests +import json + +def upload_file(file_path, user): + upload_url = "https://api.dify.ai/v1/files/upload" + headers = { + "Authorization": "Bearer app-xxxxxxxx", + } + + try: + print("上传文件中...") + with open(file_path, 'rb') as file: + files = { + 'file': (file_path, file, 'text/plain') # 确保文件以适当的MIME类型上传 + } + data = { + "user": user, + "type": "TXT" # 设置文件类型为TXT + } + + response = requests.post(upload_url, headers=headers, files=files, data=data) + if response.status_code == 201: # 201 表示创建成功 + print("文件上传成功") + return response.json().get("id") # 获取上传的文件 ID + else: + print(f"文件上传失败,状态码: {response.status_code}") + return None + except Exception as e: + print(f"发生错误: {str(e)}") + return None + +def run_workflow(file_id, user, response_mode="blocking"): + workflow_url = "https://api.dify.ai/v1/workflows/run" + headers = { + "Authorization": "Bearer app-xxxxxxxxx", + "Content-Type": "application/json" + } + + data = { + "inputs": { + "orig_mail": [{ + "transfer_method": "local_file", + "upload_file_id": file_id, + "type": "document" + }] + }, + "response_mode": response_mode, + "user": user + } + + try: + print("运行工作流...") + response = requests.post(workflow_url, headers=headers, json=data) + if response.status_code == 200: + print("工作流执行成功") + return response.json() + else: + print(f"工作流执行失败,状态码: {response.status_code}") + return {"status": "error", "message": f"Failed to execute workflow, status code: {response.status_code}"} + except Exception as e: + print(f"发生错误: {str(e)}") + return {"status": "error", "message": str(e)} + +# 使用示例 +file_path = "{your_file_path}" +user = "difyuser" + +# 上传文件 +file_id = upload_file(file_path, user) +if file_id: + # 文件上传成功,继续运行工作流 + result = run_workflow(file_id, user) + print(result) +else: + print("文件上传失败,无法执行工作流") + +Copy +Copied! +GET +/workflows/run/:workflow_id +获取workflow执行情况 +根据 workflow 执行 ID 获取 workflow 任务当前执行结果 + +Path +workflow_id (string) workflow 执行 ID,可在流式返回 Chunk 中获取 +Response +id (string) workflow 执行 ID +workflow_id (string) 关联的 Workflow ID +status (string) 执行状态 running / succeeded / failed / stopped +inputs (json) 任务输入内容 +outputs (json) 任务输出内容 +error (string) 错误原因 +total_steps (int) 任务执行总步数 +total_tokens (int) 任务执行总 tokens +created_at (timestamp) 任务开始时间 +finished_at (timestamp) 任务结束时间 +elapsed_time (float) 耗时(s) +Request Example +Request +GET +/workflows/run/:workflow_id +curl -X GET 'http://192.168.100.143/v1/workflows/run/:workflow_id' \ +-H 'Authorization: Bearer {api_key}' \ +-H 'Content-Type: application/json' + +Copy +Copied! +Response Example +Response +{ + "id": "b1ad3277-089e-42c6-9dff-6820d94fbc76", + "workflow_id": "19eff89f-ec03-4f75-b0fc-897e7effea02", + "status": "succeeded", + "inputs": "{\"sys.files\": [], \"sys.user_id\": \"abc-123\"}", + "outputs": null, + "error": null, + "total_steps": 3, + "total_tokens": 0, + "created_at": "Thu, 18 Jul 2024 03:17:40 -0000", + "finished_at": "Thu, 18 Jul 2024 03:18:10 -0000", + "elapsed_time": 30.098514399956912 +} + +Copy +Copied! +POST +/workflows/tasks/:task_id/stop +停止响应 +仅支持流式模式。 + +Path +task_id (string) 任务 ID,可在流式返回 Chunk 中获取 +Request Body +user (string) Required 用户标识,用于定义终端用户的身份,必须和发送消息接口传入 user 保持一致。 +Response +result (string) 固定返回 "success" +Request Example +Request +POST +/workflows/tasks/:task_id/stop +curl -X POST 'http://192.168.100.143/v1/workflows/tasks/:task_id/stop' \ +-H 'Authorization: Bearer {api_key}' \ +-H 'Content-Type: application/json' \ +--data-raw '{"user": "abc-123"}' + +Copy +Copied! +Response Example +Response +{ + "result": "success" +} + +Copy +Copied! +POST +/files/upload +上传文件 +上传文件并在发送消息时使用,可实现图文多模态理解。 支持您的工作流程所支持的任何格式。 上传的文件仅供当前终端用户使用。 + +Request Body +该接口需使用 multipart/form-data 进行请求。 + +Name +file +Type +file +Description +要上传的文件。 + +Name +user +Type +string +Description +用户标识,用于定义终端用户的身份,必须和发送消息接口传入 user 保持一致。 + +Response +成功上传后,服务器会返回文件的 ID 和相关信息。 + +id (uuid) ID +name (string) 文件名 +size (int) 文件大小(byte) +extension (string) 文件后缀 +mime_type (string) 文件 mime-type +created_by (uuid) 上传人 ID +created_at (timestamp) 上传时间 +Errors +400,no_file_uploaded,必须提供文件 +400,too_many_files,目前只接受一个文件 +400,unsupported_preview,该文件不支持预览 +400,unsupported_estimate,该文件不支持估算 +413,file_too_large,文件太大 +415,unsupported_file_type,不支持的扩展名,当前只接受文档类文件 +503,s3_connection_failed,无法连接到 S3 服务 +503,s3_permission_denied,无权限上传文件到 S3 +503,s3_file_too_large,文件超出 S3 大小限制 +Request +POST +/files/upload +curl -X POST 'http://192.168.100.143/v1/files/upload' \ +--header 'Authorization: Bearer {api_key}' \ +--form 'file=@localfile;type=image/[png|jpeg|jpg|webp|gif] \ +--form 'user=abc-123' + +Copy +Copied! +Response +{ + "id": "72fa9618-8f89-4a37-9b33-7e1178a24a67", + "name": "example.png", + "size": 1024, + "extension": "png", + "mime_type": "image/png", + "created_by": 123, + "created_at": 1577836800, +} + +Copy +Copied! +GET +/workflows/logs +获取 workflow 日志 +倒序返回workflow日志 + +Query +Name +keyword +Type +string +Description +关键字 + +Name +status +Type +string +Description +执行状态 succeeded/failed/stopped + +Name +page +Type +int +Description +当前页码, 默认1. + +Name +limit +Type +int +Description +每页条数, 默认20. + +Response +page (int) 当前页码 +limit (int) 每页条数 +total (int) 总条数 +has_more (bool) 是否还有更多数据 +data (array[object]) 当前页码的数据 +id (string) 标识 +workflow_run (object) Workflow 执行日志 +id (string) 标识 +version (string) 版本 +status (string) 执行状态, running / succeeded / failed / stopped +error (string) (可选) 错误 +elapsed_time (float) 耗时,单位秒 +total_tokens (int) 消耗的token数量 +total_steps (int) 执行步骤长度 +created_at (timestamp) 开始时间 +finished_at (timestamp) 结束时间 +created_from (string) 来源 +created_by_role (string) 角色 +created_by_account (string) (可选) 帐号 +created_by_end_user (object) 用户 +id (string) 标识 +type (string) 类型 +is_anonymous (bool) 是否匿名 +session_id (string) 会话标识 +created_at (timestamp) 创建时间 +Request +GET +/workflows/logs +curl -X GET 'http://192.168.100.143/v1/workflows/logs'\ + --header 'Authorization: Bearer {api_key}' + +Copy +Copied! +Response Example +Response +{ + "page": 1, + "limit": 1, + "total": 7, + "has_more": true, + "data": [ + { + "id": "e41b93f1-7ca2-40fd-b3a8-999aeb499cc0", + "workflow_run": { + "id": "c0640fc8-03ef-4481-a96c-8a13b732a36e", + "version": "2024-08-01 12:17:09.771832", + "status": "succeeded", + "error": null, + "elapsed_time": 1.3588523610014818, + "total_tokens": 0, + "total_steps": 3, + "created_at": 1726139643, + "finished_at": 1726139644 + }, + "created_from": "service-api", + "created_by_role": "end_user", + "created_by_account": null, + "created_by_end_user": { + "id": "7f7d9117-dd9d-441d-8970-87e5e7e687a3", + "type": "service_api", + "is_anonymous": false, + "session_id": "abc-123" + }, + "created_at": 1726139644 + } + ] +} + +Copy +Copied! +GET +/info +获取应用基本信息 +用于获取应用的基本信息 + +Response +name (string) 应用名称 +description (string) 应用描述 +tags (array[string]) 应用标签 +Request +GET +/info +curl -X GET 'http://192.168.100.143/v1/info' \ +-H 'Authorization: Bearer {api_key}' + +Copy +Copied! +Response +{ + "name": "My App", + "description": "This is my app.", + "tags": [ + "tag1", + "tag2" + ] +} + +Copy +Copied! +GET +/parameters +获取应用参数 +用于进入页面一开始,获取功能开关、输入参数名称、类型及默认值等使用。 + +Response +user_input_form (array[object]) 用户输入表单配置 +text-input (object) 文本输入控件 +label (string) 控件展示标签名 +variable (string) 控件 ID +required (bool) 是否必填 +default (string) 默认值 +paragraph (object) 段落文本输入控件 +label (string) 控件展示标签名 +variable (string) 控件 ID +required (bool) 是否必填 +default (string) 默认值 +select (object) 下拉控件 +label (string) 控件展示标签名 +variable (string) 控件 ID +required (bool) 是否必填 +default (string) 默认值 +options (array[string]) 选项值 +file_upload (object) 文件上传配置 +image (object) 图片设置 当前仅支持图片类型:png, jpg, jpeg, webp, gif +enabled (bool) 是否开启 +number_limits (int) 图片数量限制,默认 3 +transfer_methods (array[string]) 传递方式列表,remote_url , local_file,必选一个 +system_parameters (object) 系统参数 +file_size_limit (int) 文档上传大小限制 (MB) +image_file_size_limit (int) 图片文件上传大小限制(MB) +audio_file_size_limit (int) 音频文件上传大小限制 (MB) +video_file_size_limit (int) 视频文件上传大小限制 (MB) + + +Request +GET +/parameters + curl -X GET 'http://192.168.100.143/v1/parameters' + + +Response +{ + "user_input_form": [ + { + "paragraph": { + "label": "Query", + "variable": "query", + "required": true, + "default": "" + } + } + ], + "file_upload": { + "image": { + "enabled": false, + "number_limits": 3, + "detail": "high", + "transfer_methods": [ + "remote_url", + "local_file" + ] + } + }, + "system_parameters": { + "file_size_limit": 15, + "image_file_size_limit": 10, + "audio_file_size_limit": 50, + "video_file_size_limit": 100 + } +} \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..421c786 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,3 @@ +openpyxl>=3.1.2 +pyyaml>=6.0.1 +requests>=2.31.0 diff --git a/source/grade_assignments.py b/source/grade_assignments.py new file mode 100644 index 0000000..d703a60 --- /dev/null +++ b/source/grade_assignments.py @@ -0,0 +1,200 @@ +import os +import yaml +import openpyxl +import requests +from openpyxl import Workbook +from openpyxl.styles import Font + +# API配置 +API_KEY = "app-m7XGgbTe3BVHmA1TAYg9Ec4v" # Dify API Key +WORKFLOW_ID = os.getenv("DIFY_WORKFLOW_ID", "your-workflow-id-here") # 工作流ID +API_BASE_URL = "http://192.168.100.143/v1" # API基础地址 +FILE_UPLOAD_URL = f"{API_BASE_URL}/files/upload" # 文件上传地址 +EXCEL_PATH = "ai作业/AI考试作业.xlsx" +ASSIGNMENT_DIR = "ai作业/作业" +OUTPUT_DIR = "results" +OUTPUT_FILE = os.path.join(OUTPUT_DIR, "批改结果.xlsx") + +# 确保输出目录存在 +os.makedirs(OUTPUT_DIR, exist_ok=True) + +def read_excel_submissions(): + """读取Excel中的作业提交记录""" + wb = openpyxl.load_workbook(EXCEL_PATH) + ws = wb.active + submissions = [] + + # 获取标题行确定列索引 + headers = [cell.value for cell in ws[1]] + name_col = headers.index('填写人') + workflow_col = headers.index('工作流程描述') + solution_col = headers.index('Dify工作流解决方案设计') + + for row in ws.iter_rows(min_row=2, values_only=True): + if len(row) > max(name_col, workflow_col, solution_col): # 确保有足够列 + submissions.append({ + 'name': row[name_col], + 'work_description': row[workflow_col], + 'solution': row[solution_col] + }) + return submissions + +def find_assignment_yml(name): + """根据姓名查找对应的YML作业文件""" + for filename in os.listdir(ASSIGNMENT_DIR): + if filename.endswith('.yml'): # 仅支持.yml格式 + # 从文件名中提取姓名部分(第二个下划线分隔的部分) + parts = filename.split('_') + if len(parts) >= 2 and name == parts[1]: + return os.path.join(ASSIGNMENT_DIR, filename) + return None + +def parse_yml_file(yml_path): + """解析YML文件内容""" + with open(yml_path, 'r', encoding='utf-8') as f: + return yaml.safe_load(f) + +def call_dify_api(yml_path, assignment_data): + """调用Dify API进行作业批改,上传YML文件""" + # 先上传文件 + upload_headers = { + "Authorization": f"Bearer {API_KEY}" + } + upload_data = { + "user": "ai-grading-system", + "type": "yml" # 自定义文件类型 + } + + try: + # 上传文件 + with open(yml_path, 'rb') as f: + files = {'file': (os.path.basename(yml_path), f, 'text/plain')} + upload_response = requests.post( + FILE_UPLOAD_URL, + headers=upload_headers, + files=files, + data=upload_data + ) + upload_response.raise_for_status() + file_id = upload_response.json().get('id') + + if not file_id: + print("文件上传失败: 未获取到文件ID") + return None + + # 执行工作流 + run_url = f"{API_BASE_URL}/workflows/run" + run_headers = { + "Authorization": f"Bearer {API_KEY}", + "Content-Type": "application/json" + } + run_data = { + "inputs": { + "yml_file": { + "transfer_method": "local_file", + "upload_file_id": file_id, + "type": "custom" + }, + "work_description": assignment_data.get('work_description', ''), + "solution": assignment_data.get('solution', '') + }, + "response_mode": "blocking", + "user": "ai-grading-system" + } + + response = requests.post(run_url, headers=run_headers, json=run_data) + response.raise_for_status() + return response.json() + + except requests.exceptions.RequestException as e: + print(f"API调用失败: {e}") + return None + +def save_results(results): + """保存批改结果到Excel""" + wb = Workbook() + ws = wb.active + ws.title = "批改结果" + + # 添加表头 + headers = ["姓名", "评分", "评分详情"] + ws.append(headers) + + # 设置表头样式 + for cell in ws[1]: + cell.font = Font(bold=True) + + # 添加数据 + for result in results: + ws.append([ + result['name'], + result.get('score', 'N/A'), + str(result.get('details', '无详情')) + ]) + + # 自动调整列宽 + for column in ws.columns: + max_length = 0 + column = [cell for cell in column] + for cell in column: + try: + if len(str(cell.value)) > max_length: + max_length = len(cell.value) + except: + pass + adjusted_width = (max_length + 2) * 1.2 + ws.column_dimensions[column[0].column_letter].width = adjusted_width + + wb.save(OUTPUT_FILE) + print(f"批改结果已保存到: {OUTPUT_FILE}") + +def main(): + # 读取Excel中的作业提交 + submissions = read_excel_submissions() + results = [] + + for sub in submissions: + print(f"正在处理: {sub['name']}") + + # 查找对应的YML文件 + yml_path = find_assignment_yml(sub['name']) + if not yml_path: + print(f"未找到 {sub['name']} 的作业文件") + continue + + # 解析YML文件 + try: + yml_content = parse_yml_file(yml_path) + except Exception as e: + print(f"解析YML文件失败: {e}") + continue + + # 准备API调用数据 + assignment_data = { + **sub, + **yml_content + } + + # 调用API进行批改(上传YML文件) + api_response = call_dify_api(yml_path, assignment_data) + if not api_response: + print(f"{sub['name']} 批改失败") + continue + + # 解析API响应(直接从文件上传返回的结果) + try: + outputs = api_response.get('data', {}).get('outputs', {}) + results.append({ + 'name': sub['name'], + 'score': outputs.get('score'), + 'details': outputs.get('details') + }) + print(f"{sub['name']} 批改完成") + except Exception as e: + print(f"解析API响应失败: {e}") + + # 保存结果 + save_results(results) + +if __name__ == "__main__": + main() diff --git a/source/test_script.py b/source/test_script.py new file mode 100644 index 0000000..5e95f0a --- /dev/null +++ b/source/test_script.py @@ -0,0 +1,123 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +测试脚本 - 测试grade_assignments.py功能 +使用CSV格式保存测试结果 +""" + +import os +import random +import re +import yaml +from grade_assignments import ( + read_excel_submissions, + find_assignment_yml, + parse_yml_file, + call_dify_api, + save_results +) + +def test_grade_assignments(): + """测试作业批改全流程,包含Excel结果写入""" + print("=== 开始测试 ===") + + # 1. 创建测试YML文件 + test_yml = "ai作业/作业/第3题_测试_20250331_测试作业_1.yml" + test_data = { + "work_description": "测试工作流程描述", + "solution": "测试解决方案设计", + "additional_field": "测试额外字段" + } + + with open(test_yml, 'w', encoding='utf-8') as f: + yaml.dump(test_data, f, allow_unicode=True) + print(f"已创建测试YML文件: {test_yml}") + + # 2. 测试API调用 + print("\n[测试1] 调用Dify API...") + api_response = call_dify_api(test_yml, test_data) + + if not api_response: + print("API调用失败") + return + + print("API调用成功") + print(f"响应结果: {api_response}") + + # 3. 测试结果写入Excel + print("\n[测试2] 测试结果写入Excel...") + # 从API响应中提取评分结果 + api_output = api_response['data']['outputs']['text'] + score_match = re.search(r'总分:(\d+)分', api_output) + feedback_match = re.search(r'优点和不足之处(.*?)改进建议', api_output, re.DOTALL) + + test_result = { + "name": "测试学生", + "score": int(score_match.group(1)) if score_match else 0, + "feedback": feedback_match.group(1).strip() if feedback_match else "无反馈", + "status": "已完成", + "api_response": api_response + } + + # 临时结果文件路径 - 使用Excel格式 + from openpyxl import Workbook + import uuid + test_xlsx = f"results/测试结果_{uuid.uuid4().hex[:8]}.xlsx" + os.makedirs("results", exist_ok=True) + + try: + # 创建Excel工作簿 + wb = Workbook() + ws = wb.active + ws.title = "测试结果" + + # 写入表头 + ws.append(['姓名', '分数', '反馈']) + + # 写入数据 + ws.append([ + test_result['name'], + test_result['score'], + test_result['feedback'] + ]) + + # 保存Excel文件 + wb.save(test_xlsx) + print(f"测试结果已写入Excel: {test_xlsx}") + + # 验证文件是否存在 + if os.path.exists(test_xlsx): + print("Excel文件写入验证成功") + else: + print("Excel文件写入失败") + except Exception as e: + print(f"写入Excel文件时出错: {str(e)}") + + # 清理测试文件 - 添加重试机制处理文件锁定 + import time + max_retries = 3 + retry_delay = 1 # 秒 + + def safe_remove(filepath): + for i in range(max_retries): + try: + if os.path.exists(filepath): + os.remove(filepath) + print(f"已清理文件: {filepath}") + return True + except Exception as e: + if i == max_retries - 1: + print(f"无法清理文件 {filepath}: {str(e)}") + return False + time.sleep(retry_delay) + return False + + print("\n[清理] 删除测试文件...") + safe_remove(test_yml) + safe_remove(test_xlsx) + + print("\n=== 测试完成 ===") + +if __name__ == "__main__": + test_grade_assignments()