package services import ( "bytes" "context" "encoding/json" "fmt" "io" "net/http" "time" ) // AIService handles AI-related operations via LLM Gateway type AIService struct { llmGatewayURL string httpClient *http.Client } // NewAIService creates a new AIService func NewAIService(llmGatewayURL string) *AIService { return &AIService{ llmGatewayURL: llmGatewayURL, httpClient: &http.Client{ Timeout: 120 * time.Second, // AI requests can take longer }, } } // ChatMessage represents a message in the chat format type ChatMessage struct { Role string `json:"role"` Content string `json:"content"` } // ChatRequest represents a request to the LLM Gateway type ChatRequest struct { Messages []ChatMessage `json:"messages"` Model string `json:"model,omitempty"` } // ChatResponse represents a response from the LLM Gateway type ChatResponse struct { Choices []struct { Message struct { Content string `json:"content"` } `json:"message"` } `json:"choices"` Error string `json:"error,omitempty"` } // GenerateExamVariant generates a variant of an exam using AI func (s *AIService) GenerateExamVariant(ctx context.Context, originalContent string, variationType string) (string, error) { var prompt string switch variationType { case "rewrite": prompt = fmt.Sprintf(`Du bist ein erfahrener Lehrer. Erstelle eine Nachschreibeversion der folgenden Klausur/Arbeit. WICHTIG: - Behalte den EXAKTEN Schwierigkeitsgrad bei - Formuliere die Textaufgaben um (andere Worte, gleiche Bedeutung) - Bei Rechenaufgaben: Andere Zahlen, gleicher Rechenweg, gleiche Schwierigkeit - Behalte die Punkteverteilung und Struktur bei - Die neue Version soll für Schüler sein, die bei der Originalklausur gefehlt haben Original-Klausur: %s Erstelle nun die Nachschreibeversion:`, originalContent) case "alternative": prompt = fmt.Sprintf(`Du bist ein erfahrener Lehrer. Erstelle eine komplett alternative Version der folgenden Klausur zum gleichen Thema. WICHTIG: - Gleiches Thema und Lernziele - Komplett neue Aufgaben (nicht nur umformuliert) - Gleicher Schwierigkeitsgrad - Gleiche Punkteverteilung und Struktur - Andere Beispiele und Szenarien verwenden Original-Klausur (als Referenz für Thema und Schwierigkeitsgrad): %s Erstelle nun die alternative Version:`, originalContent) case "similar": prompt = fmt.Sprintf(`Du bist ein erfahrener Lehrer. Erstelle eine ähnliche Übungsarbeit basierend auf der folgenden Klausur. WICHTIG: - Geeignet als Übungsmaterial für die Klausurvorbereitung - Ähnliche Aufgabentypen wie im Original - Gleicher Schwierigkeitsgrad - Kann als Hausaufgabe oder zur Selbstübung verwendet werden Original-Klausur: %s Erstelle nun die Übungsarbeit:`, originalContent) default: return "", fmt.Errorf("unknown variation type: %s", variationType) } return s.sendChatRequest(ctx, prompt) } // ImproveExamContent improves exam content (grammar, clarity) func (s *AIService) ImproveExamContent(ctx context.Context, content string) (string, error) { prompt := fmt.Sprintf(`Du bist ein erfahrener Lehrer. Verbessere den folgenden Klausurtext: WICHTIG: - Korrigiere Rechtschreibung und Grammatik - Verbessere die Klarheit der Aufgabenstellungen - Ändere NICHT den Schwierigkeitsgrad oder Inhalt - Ändere NICHT die Punkteverteilung - Behalte die Struktur bei Original: %s Verbesserte Version:`, content) return s.sendChatRequest(ctx, prompt) } // GenerateGradeFeedback generates feedback for a student based on their grades func (s *AIService) GenerateGradeFeedback(ctx context.Context, studentName string, grades map[string]float64, semester int) (string, error) { gradesJSON, _ := json.Marshal(grades) prompt := fmt.Sprintf(`Du bist ein erfahrener Klassenlehrer. Erstelle eine kurze, konstruktive Zeugnis-Bemerkung für folgenden Schüler: Schüler: %s Halbjahr: %d Noten: %s WICHTIG: - Maximal 3-4 Sätze - Konstruktiv und ermutigend formulieren - Stärken hervorheben - Bei Bedarf dezente Verbesserungsvorschläge - Professioneller, sachlicher Ton (Zeugnis-tauglich) - Keine übertriebenen Lobpreisungen Erstelle die Bemerkung:`, studentName, semester, string(gradesJSON)) return s.sendChatRequest(ctx, prompt) } // sendChatRequest sends a request to the LLM Gateway func (s *AIService) sendChatRequest(ctx context.Context, prompt string) (string, error) { reqBody := ChatRequest{ Messages: []ChatMessage{ {Role: "user", Content: prompt}, }, } jsonBody, err := json.Marshal(reqBody) if err != nil { return "", fmt.Errorf("failed to marshal request: %w", err) } req, err := http.NewRequestWithContext(ctx, "POST", s.llmGatewayURL+"/v1/chat/completions", bytes.NewBuffer(jsonBody)) if err != nil { return "", fmt.Errorf("failed to create request: %w", err) } req.Header.Set("Content-Type", "application/json") resp, err := s.httpClient.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) } if resp.StatusCode != http.StatusOK { return "", fmt.Errorf("LLM Gateway returned status %d: %s", resp.StatusCode, string(body)) } var chatResp ChatResponse if err := json.Unmarshal(body, &chatResp); err != nil { return "", fmt.Errorf("failed to parse response: %w", err) } if chatResp.Error != "" { return "", fmt.Errorf("LLM Gateway error: %s", chatResp.Error) } if len(chatResp.Choices) == 0 { return "", fmt.Errorf("no response from LLM") } return chatResp.Choices[0].Message.Content, nil } // HealthCheck checks if the LLM Gateway is available func (s *AIService) HealthCheck(ctx context.Context) error { req, err := http.NewRequestWithContext(ctx, "GET", s.llmGatewayURL+"/health", nil) if err != nil { return err } resp, err := s.httpClient.Do(req) if err != nil { return err } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return fmt.Errorf("LLM Gateway unhealthy: status %d", resp.StatusCode) } return nil }