309 lines
7.6 KiB
Go
309 lines
7.6 KiB
Go
|
|
package service
|
|||
|
|
|
|||
|
|
import (
|
|||
|
|
"bytes"
|
|||
|
|
"context"
|
|||
|
|
"encoding/json"
|
|||
|
|
"fmt"
|
|||
|
|
"io"
|
|||
|
|
"net/http"
|
|||
|
|
"os"
|
|||
|
|
"strings"
|
|||
|
|
"time"
|
|||
|
|
|
|||
|
|
"github.com/Nanqipro/YunQue-Tech-Projects/ai_english_learning/serve/internal/model"
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
// AIService AI服务接口
|
|||
|
|
type AIService interface {
|
|||
|
|
// 写作批改
|
|||
|
|
CorrectWriting(ctx context.Context, content string, taskType string) (*model.WritingCorrection, error)
|
|||
|
|
// 口语评估
|
|||
|
|
EvaluateSpeaking(ctx context.Context, audioText string, prompt string) (*model.SpeakingEvaluation, error)
|
|||
|
|
// 智能推荐
|
|||
|
|
GetRecommendations(ctx context.Context, userLevel string, learningHistory []string) (*model.AIRecommendation, error)
|
|||
|
|
// 生成练习题
|
|||
|
|
GenerateExercise(ctx context.Context, content string, exerciseType string) (*model.Exercise, error)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
type aiService struct {
|
|||
|
|
apiKey string
|
|||
|
|
baseURL string
|
|||
|
|
client *http.Client
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// NewAIService 创建AI服务实例
|
|||
|
|
func NewAIService() AIService {
|
|||
|
|
apiKey := os.Getenv("OPENAI_API_KEY")
|
|||
|
|
baseURL := os.Getenv("OPENAI_BASE_URL")
|
|||
|
|
if baseURL == "" {
|
|||
|
|
baseURL = "https://api.openai.com/v1"
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return &aiService{
|
|||
|
|
apiKey: apiKey,
|
|||
|
|
baseURL: baseURL,
|
|||
|
|
client: &http.Client{
|
|||
|
|
Timeout: 30 * time.Second,
|
|||
|
|
},
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// OpenAI API请求结构
|
|||
|
|
type openAIRequest struct {
|
|||
|
|
Model string `json:"model"`
|
|||
|
|
Messages []message `json:"messages"`
|
|||
|
|
MaxTokens int `json:"max_tokens"`
|
|||
|
|
Temperature float64 `json:"temperature"`
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
type message struct {
|
|||
|
|
Role string `json:"role"`
|
|||
|
|
Content string `json:"content"`
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
type openAIResponse struct {
|
|||
|
|
Choices []struct {
|
|||
|
|
Message message `json:"message"`
|
|||
|
|
} `json:"choices"`
|
|||
|
|
Error *struct {
|
|||
|
|
Message string `json:"message"`
|
|||
|
|
Type string `json:"type"`
|
|||
|
|
} `json:"error,omitempty"`
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// callOpenAI 调用OpenAI API
|
|||
|
|
func (s *aiService) callOpenAI(ctx context.Context, prompt string) (string, error) {
|
|||
|
|
if s.apiKey == "" {
|
|||
|
|
return "", fmt.Errorf("OpenAI API key not configured")
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
reqBody := openAIRequest{
|
|||
|
|
Model: "gpt-3.5-turbo",
|
|||
|
|
Messages: []message{
|
|||
|
|
{
|
|||
|
|
Role: "user",
|
|||
|
|
Content: prompt,
|
|||
|
|
},
|
|||
|
|
},
|
|||
|
|
MaxTokens: 1000,
|
|||
|
|
Temperature: 0.7,
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
jsonData, err := json.Marshal(reqBody)
|
|||
|
|
if err != nil {
|
|||
|
|
return "", fmt.Errorf("failed to marshal request: %w", err)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
req, err := http.NewRequestWithContext(ctx, "POST", s.baseURL+"/chat/completions", bytes.NewBuffer(jsonData))
|
|||
|
|
if err != nil {
|
|||
|
|
return "", fmt.Errorf("failed to create request: %w", err)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
req.Header.Set("Content-Type", "application/json")
|
|||
|
|
req.Header.Set("Authorization", "Bearer "+s.apiKey)
|
|||
|
|
|
|||
|
|
resp, err := s.client.Do(req)
|
|||
|
|
if err != nil {
|
|||
|
|
return "", fmt.Errorf("failed to send request: %w", err)
|
|||
|
|
}
|
|||
|
|
defer resp.Body.Close()
|
|||
|
|
|
|||
|
|
body, err := io.ReadAll(resp.Body)
|
|||
|
|
if err != nil {
|
|||
|
|
return "", fmt.Errorf("failed to read response: %w", err)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
var openAIResp openAIResponse
|
|||
|
|
if err := json.Unmarshal(body, &openAIResp); err != nil {
|
|||
|
|
return "", fmt.Errorf("failed to unmarshal response: %w", err)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if openAIResp.Error != nil {
|
|||
|
|
return "", fmt.Errorf("OpenAI API error: %s", openAIResp.Error.Message)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if len(openAIResp.Choices) == 0 {
|
|||
|
|
return "", fmt.Errorf("no response from OpenAI")
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return openAIResp.Choices[0].Message.Content, nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// CorrectWriting 写作批改
|
|||
|
|
func (s *aiService) CorrectWriting(ctx context.Context, content string, taskType string) (*model.WritingCorrection, error) {
|
|||
|
|
prompt := fmt.Sprintf(`请对以下英语写作进行批改,任务类型:%s
|
|||
|
|
|
|||
|
|
写作内容:
|
|||
|
|
%s
|
|||
|
|
|
|||
|
|
请按照以下JSON格式返回批改结果:
|
|||
|
|
{
|
|||
|
|
"overall_score": 85,
|
|||
|
|
"grammar_score": 80,
|
|||
|
|
"vocabulary_score": 90,
|
|||
|
|
"structure_score": 85,
|
|||
|
|
"content_score": 88,
|
|||
|
|
"corrections": [
|
|||
|
|
{
|
|||
|
|
"original": "错误的句子",
|
|||
|
|
"corrected": "修正后的句子",
|
|||
|
|
"explanation": "修改说明",
|
|||
|
|
"error_type": "grammar"
|
|||
|
|
}
|
|||
|
|
],
|
|||
|
|
"suggestions": [
|
|||
|
|
"建议1",
|
|||
|
|
"建议2"
|
|||
|
|
],
|
|||
|
|
"strengths": [
|
|||
|
|
"优点1",
|
|||
|
|
"优点2"
|
|||
|
|
],
|
|||
|
|
"weaknesses": [
|
|||
|
|
"需要改进的地方1",
|
|||
|
|
"需要改进的地方2"
|
|||
|
|
]
|
|||
|
|
}`, taskType, content)
|
|||
|
|
|
|||
|
|
response, err := s.callOpenAI(ctx, prompt)
|
|||
|
|
if err != nil {
|
|||
|
|
return nil, err
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 解析JSON响应
|
|||
|
|
var correction model.WritingCorrection
|
|||
|
|
if err := json.Unmarshal([]byte(response), &correction); err != nil {
|
|||
|
|
// 如果JSON解析失败,返回基本的批改结果
|
|||
|
|
return &model.WritingCorrection{
|
|||
|
|
OverallScore: 75,
|
|||
|
|
Suggestions: []string{"AI批改服务暂时不可用,请稍后重试"},
|
|||
|
|
}, nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return &correction, nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// EvaluateSpeaking 口语评估
|
|||
|
|
func (s *aiService) EvaluateSpeaking(ctx context.Context, audioText string, prompt string) (*model.SpeakingEvaluation, error) {
|
|||
|
|
evalPrompt := fmt.Sprintf(`请对以下英语口语进行评估,题目:%s
|
|||
|
|
|
|||
|
|
口语内容:
|
|||
|
|
%s
|
|||
|
|
|
|||
|
|
请按照以下JSON格式返回评估结果:
|
|||
|
|
{
|
|||
|
|
"overall_score": 85,
|
|||
|
|
"pronunciation_score": 80,
|
|||
|
|
"fluency_score": 90,
|
|||
|
|
"grammar_score": 85,
|
|||
|
|
"vocabulary_score": 88,
|
|||
|
|
"feedback": "整体评价",
|
|||
|
|
"strengths": [
|
|||
|
|
"优点1",
|
|||
|
|
"优点2"
|
|||
|
|
],
|
|||
|
|
"improvements": [
|
|||
|
|
"需要改进的地方1",
|
|||
|
|
"需要改进的地方2"
|
|||
|
|
]
|
|||
|
|
}`, prompt, audioText)
|
|||
|
|
|
|||
|
|
response, err := s.callOpenAI(ctx, evalPrompt)
|
|||
|
|
if err != nil {
|
|||
|
|
return nil, err
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 解析JSON响应
|
|||
|
|
var evaluation model.SpeakingEvaluation
|
|||
|
|
if err := json.Unmarshal([]byte(response), &evaluation); err != nil {
|
|||
|
|
// 如果JSON解析失败,返回基本的评估结果
|
|||
|
|
return &model.SpeakingEvaluation{
|
|||
|
|
OverallScore: 75,
|
|||
|
|
Feedback: "AI评估服务暂时不可用,请稍后重试",
|
|||
|
|
}, nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return &evaluation, nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// GetRecommendations 智能推荐
|
|||
|
|
func (s *aiService) GetRecommendations(ctx context.Context, userLevel string, learningHistory []string) (*model.AIRecommendation, error) {
|
|||
|
|
historyStr := strings.Join(learningHistory, ", ")
|
|||
|
|
prompt := fmt.Sprintf(`基于用户的英语水平(%s)和学习历史(%s),请提供个性化的学习推荐。
|
|||
|
|
|
|||
|
|
请按照以下JSON格式返回推荐结果:
|
|||
|
|
{
|
|||
|
|
"recommended_topics": [
|
|||
|
|
"推荐主题1",
|
|||
|
|
"推荐主题2"
|
|||
|
|
],
|
|||
|
|
"difficulty_level": "intermediate",
|
|||
|
|
"study_plan": [
|
|||
|
|
"学习计划步骤1",
|
|||
|
|
"学习计划步骤2"
|
|||
|
|
],
|
|||
|
|
"focus_areas": [
|
|||
|
|
"重点关注领域1",
|
|||
|
|
"重点关注领域2"
|
|||
|
|
]
|
|||
|
|
}`, userLevel, historyStr)
|
|||
|
|
|
|||
|
|
response, err := s.callOpenAI(ctx, prompt)
|
|||
|
|
if err != nil {
|
|||
|
|
return nil, err
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 解析JSON响应
|
|||
|
|
var recommendation model.AIRecommendation
|
|||
|
|
if err := json.Unmarshal([]byte(response), &recommendation); err != nil {
|
|||
|
|
// 如果JSON解析失败,返回基本的推荐结果
|
|||
|
|
return &model.AIRecommendation{
|
|||
|
|
RecommendedTopics: []string{"基础语法练习", "日常对话练习"},
|
|||
|
|
DifficultyLevel: "beginner",
|
|||
|
|
StudyPlan: []string{"每天练习30分钟", "重点关注基础词汇"},
|
|||
|
|
}, nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return &recommendation, nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// GenerateExercise 生成练习题
|
|||
|
|
func (s *aiService) GenerateExercise(ctx context.Context, content string, exerciseType string) (*model.Exercise, error) {
|
|||
|
|
prompt := fmt.Sprintf(`基于以下内容生成%s类型的练习题:
|
|||
|
|
|
|||
|
|
内容:
|
|||
|
|
%s
|
|||
|
|
|
|||
|
|
请按照以下JSON格式返回练习题:
|
|||
|
|
{
|
|||
|
|
"title": "练习题标题",
|
|||
|
|
"instructions": "练习说明",
|
|||
|
|
"questions": [
|
|||
|
|
{
|
|||
|
|
"question": "问题1",
|
|||
|
|
"options": ["选项A", "选项B", "选项C", "选项D"],
|
|||
|
|
"correct_answer": "正确答案",
|
|||
|
|
"explanation": "解释"
|
|||
|
|
}
|
|||
|
|
]
|
|||
|
|
}`, exerciseType, content)
|
|||
|
|
|
|||
|
|
response, err := s.callOpenAI(ctx, prompt)
|
|||
|
|
if err != nil {
|
|||
|
|
return nil, err
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 解析JSON响应
|
|||
|
|
var exercise model.Exercise
|
|||
|
|
if err := json.Unmarshal([]byte(response), &exercise); err != nil {
|
|||
|
|
// 如果JSON解析失败,返回基本的练习题
|
|||
|
|
return &model.Exercise{
|
|||
|
|
Title: "基础练习",
|
|||
|
|
Instructions: "AI练习生成服务暂时不可用,请稍后重试",
|
|||
|
|
Questions: []model.Question{},
|
|||
|
|
}, nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return &exercise, nil
|
|||
|
|
}
|