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>
219 lines
5.9 KiB
Go
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
|
|
}
|