Files
hr_data_analyzer/controllers/ai.go
T
2026-05-09 12:00:34 +08:00

271 lines
9.4 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// controllers/ai.go
package controllers
import (
"context" // 在此处添加 context 导入
"fmt"
"github.com/gin-gonic/gin"
"github.com/sashabaranov/go-openai"
"hr_receiver/util"
"io"
"io/ioutil"
"log"
"mime/multipart"
"net/http"
"os"
"strings"
)
// 配置文件 (与 main.go 保持一致)
const (
BaseURL = "https://tokenhub.tencentmaas.com/v1/"
APIKey = "sk-KJNOFMltNzhSKh2IxW3G3MKmZF3q2RrOlvSk497CfTHp1Z4u" // 请替换为实际的 API Key
Model = "deepseek-v4-flash"
)
// readDocxContent 读取 .docx 文件并将其转换为结构化文本
// 修改为先保存临时文件再读取
func readDocxContent(fileHeader *multipart.FileHeader) (string, error) {
// 1. 创建临时文件
tempFile, err := os.CreateTemp("", "upload_*.docx")
if err != nil {
return "", fmt.Errorf("failed to create temporary file: %w", err)
}
defer os.Remove(tempFile.Name()) // 确保函数结束时删除临时文件
defer tempFile.Close()
// 2. 打开上传的文件流
src, err := fileHeader.Open()
if err != nil {
return "", fmt.Errorf("failed to open uploaded file: %w", err)
}
defer src.Close()
// 3. 将上传的文件内容复制到临时文件
_, err = io.Copy(tempFile, src)
if err != nil {
return "", fmt.Errorf("failed to copy file to temporary location: %w", err)
}
// 4. 获取临时文件的完整路径
tempFilePath := tempFile.Name()
str, err := util.DocxToStructuredPrompt(tempFilePath)
if err != nil {
return "", fmt.Errorf("failed to parse docx with go-docx: %w", err)
}
// 注意:表格、图片等复杂元素的处理可能需要更复杂的逻辑,这里仅处理简单文本
return str, nil
}
// readCSVContent 读取 .csv 文件内容
// 修改为先保存临时文件再读取
// readCSVContent 读取 .csv 文件内容
// 修改为先保存临时文件再读取,并增加了数据压缩逻辑以解决 token 超长问题
func readCSVContent(fileHeader *multipart.FileHeader) (string, error) {
// 1. 创建临时文件
tempFile, err := os.CreateTemp("", "upload_*.csv")
if err != nil {
return "", fmt.Errorf("failed to create temporary file: %w", err)
}
defer os.Remove(tempFile.Name()) // 确保函数结束时删除临时文件
defer tempFile.Close()
// 2. 打开上传的文件流
src, err := fileHeader.Open()
if err != nil {
return "", fmt.Errorf("failed to open uploaded file: %w", err)
}
defer src.Close()
// 3. 将上传的文件内容复制到临时文件
_, err = io.Copy(tempFile, src)
if err != nil {
return "", fmt.Errorf("failed to copy file to temporary location: %w", err)
}
// 4. 读取临时文件内容
content, err := ioutil.ReadFile(tempFile.Name())
if err != nil {
return "", fmt.Errorf("failed to read CSV content from temporary file: %w", err)
}
lines := strings.Split(string(content), "\n")
var compressedLines []string
for i, line := range lines {
// 1. 必须保留第一行(表头),让 AI 知道每一列是什么
if i == 0 {
compressedLines = append(compressedLines, line)
continue
}
// 2. 跳过空行
if strings.TrimSpace(line) == "" {
continue
}
// 3. 抽样逻辑:每 4 行保留 1 行
// i=1 是数据第1行i=2 是数据第2行...
// (i-1)%4 == 0 意味着数据第1, 5, 9, 13... 行会被保留
if (i-1)%4 == 0 {
compressedLines = append(compressedLines, line)
}
}
// --- 修改逻辑结束 ---
// 将处理后的行重新组合成字符串
resultContent := strings.Join(compressedLines, "\n")
// --- 新增逻辑结束 ---
return resultContent, nil
}
// buildAnalysisPrompt 构建发送给 AI 的提示词
func buildAnalysisPrompt(teachingPlanContent, heartRateContent string) string {
return fmt.Sprintf(`请根据以下体育课堂的教案和心率监测数据,生成一份详细的课堂分析报告:
## 教案内容:
%s
## 心率监测数据:
%s
这是一份幼儿园体育课的教案和课程心率监测数据,请帮对照分析课程教学效果,运动量和运动负荷情况是否科学,并提出课程设计的优化方案。
优化方案参考如下格式,教学过程需要详细一些:
# 幼儿体育教案(华侨大学版本)
| 项目 | 内容 |
| ------------ | -------------------------------- |
| **课程名** | |
| **年段** | 小 中 大 |
| **教师姓名** | |
| **时间** | 年 月 日 |
| **地点** | |
| **人数** | 男: 女: |
| **时长** | 分钟 |
| **天气预报** | 晴 雨 阴;温度 ℃ |
| **器材准备** | |
## 教学目标
| 类型 | 目标 |
| -------- | ------------ |
| **体能目标** | |
| **技能目标** | |
| **情感目标** | |
## 教学过程
| 阶段 | 阶段 | 项目名称 | 引导语及教学方法 | 队形/站位/留意点 | 目标心率区间 | 时间(分) |
| ---------- | -------- | ----------------------------- | ------------------------ | --------------------- | ------------ | ---------- |
| **准备部分** | 热身 | | | | | 3 |
| | 注意力游戏 | | | | | 3 |
| **正课部分** | 基本素质练习及常规意识培养环节 | | | | | 5 |
| | 复习环节 | | | | | 5 |
| | 新授环节 | | | | | 8 |
| **结束部分** | 社会性及情感目标游戏 | | | | | 4 |
| | 整理放松 | | | | | 2 |
请以专业体育教师的视角,提供详细的数据分析和教学建议。请直接输出报告内容,不要包含“好的”、“收到”、“作为一名...”等任何开场白或客套话。`, teachingPlanContent, heartRateContent)
}
// callAIForAnalysis 调用大模型进行分析
func callAIForAnalysis(prompt string) (string, error) {
sizeInBytes := len(prompt)
sizeInKB := float64(sizeInBytes) / 1024.0
// 在日志中打印大小,保留两位小数
log.Printf("=== 发送给 AI 的内容大小: %.2f KB (%d 字节) ===", sizeInKB, sizeInBytes)
config := openai.DefaultConfig(APIKey)
config.BaseURL = BaseURL
client := openai.NewClientWithConfig(config)
resp, err := client.CreateChatCompletion(
context.Background(),
openai.ChatCompletionRequest{
Model: Model,
Messages: []openai.ChatCompletionMessage{
{
Role: openai.ChatMessageRoleUser,
Content: prompt,
},
},
Temperature: 0.6, // 可调整
TopP: 0.6, // 可调整
MaxTokens: 4000, // 根据需要调整
},
)
if err != nil {
return "", fmt.Errorf("API call failed: %w", err)
}
if len(resp.Choices) == 0 {
return "", fmt.Errorf("no choices returned from API")
}
return resp.Choices[0].Message.Content, nil
}
// AnalyzeByAI Gin 控制器方法
func (tc *TrainingController) AnalyzeByAI(c *gin.Context) {
// 1. 解析多部分表单请求
form, err := c.MultipartForm()
if err != nil {
log.Printf("Error parsing multipart form: %v", err)
c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("Failed to parse form: %v", err)})
return
}
// 2. 获取文件列表
docxFiles := form.File["teaching_plan"] // 假设前端字段名为 'teaching_plan'
csvFiles := form.File["heart_rate_data"] // 假设前端字段名为 'heart_rate_data'
if len(docxFiles) == 0 || len(csvFiles) == 0 {
c.JSON(http.StatusBadRequest, gin.H{"error": "Missing required files: teaching_plan (.docx) or heart_rate_data (.csv)"})
return
}
// 3. 读取文件内容
// 注意:这里我们只取第一个上传的文件
teachingPlanFileHeader := docxFiles[0]
heartRateFileHeader := csvFiles[0]
teachingPlanContent, err := readDocxContent(teachingPlanFileHeader)
if err != nil {
log.Printf("Error reading teaching plan file (%s): %v", teachingPlanFileHeader.Filename, err)
c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("Failed to process teaching plan file: %v", err)})
return
}
heartRateContent, err := readCSVContent(heartRateFileHeader)
if err != nil {
log.Printf("Error reading heart rate file (%s): %v", heartRateFileHeader.Filename, err)
c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("Failed to process heart rate file: %v", err)})
return
}
// 4. 构建 Prompt
prompt := buildAnalysisPrompt(teachingPlanContent, heartRateContent)
// 5. 调用 AI 分析
analysisResult, err := callAIForAnalysis(prompt)
if err != nil {
log.Printf("Error calling AI for analysis: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("AI analysis failed: %v", err)})
return
}
//outputFile := ".md"
//ioutil.WriteFile(outputFile, []byte(analysisResult), 0644)
// 6. 返回结果
// 方式一:返回 JSON 结构
c.JSON(http.StatusOK, gin.H{
"status": "success",
"data": analysisResult,
})
}