Initial commit

This commit is contained in:
xin 2025-04-01 00:33:51 +08:00
commit 785d74c653
6 changed files with 1073 additions and 0 deletions

53
.gitignore vendored Normal file
View File

@ -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作业/

62
README.md Normal file
View File

@ -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文件需符合规范格式

632
docs/workflow_api.md Normal file
View File

@ -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 流式模式(推荐)。基于 SSEServer-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
400invalid_param传入参数异常
400app_unavailableApp 配置不可用
400provider_not_initialize无可用模型凭据配置
400provider_quota_exceeded模型调用额度不足
400model_currently_not_support当前模型不可用
400workflow_request_errorworkflow 执行失败
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
400no_file_uploaded必须提供文件
400too_many_files目前只接受一个文件
400unsupported_preview该文件不支持预览
400unsupported_estimate该文件不支持估算
413file_too_large文件太大
415unsupported_file_type不支持的扩展名当前只接受文档类文件
503s3_connection_failed无法连接到 S3 服务
503s3_permission_denied无权限上传文件到 S3
503s3_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
}
}

3
requirements.txt Normal file
View File

@ -0,0 +1,3 @@
openpyxl>=3.1.2
pyyaml>=6.0.1
requests>=2.31.0

200
source/grade_assignments.py Normal file
View File

@ -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()

123
source/test_script.py Normal file
View File

@ -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()