Files
breakpilot-lehrer/school-service/internal/services/ai_service.go
Benjamin Boenisch 5a31f52310 Initial commit: breakpilot-lehrer - Lehrer KI Platform
Services: Admin-Lehrer, Backend-Lehrer, Studio v2, Website,
Klausur-Service, School-Service, Voice-Service, Geo-Service,
BreakPilot Drive, Agent-Core

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-11 23:47:26 +01:00

219 lines
5.9 KiB
Go

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
}