diff --git a/backend/internal/domain/market/research.go b/backend/internal/domain/market/research.go index 86312fa..350c258 100644 --- a/backend/internal/domain/market/research.go +++ b/backend/internal/domain/market/research.go @@ -80,7 +80,11 @@ func (h *ResearchHandler) Research(c *gin.Context) { c.JSON(http.StatusServiceUnavailable, apierror.NewResponse(apierror.BadRequest("rate_limited", "KI rate limit erreicht, bitte kurz warten"))) return case ai.ErrSchemaViolation: - slog.ErrorContext(ctx, "research schema violation", "market_id", id, "raw", pe.RawOutput, "inner", pe.Inner) + slog.ErrorContext(ctx, "research schema violation", + "market_id", id, + "prompt_hash", pe.PromptHash, + "raw", pe.RawOutput, + "inner", pe.Inner) c.JSON(http.StatusInternalServerError, apierror.NewResponse(apierror.Internal("Modell hat ungültige Ausgabe geliefert"))) return case ai.ErrInvalidRequest: diff --git a/backend/internal/pkg/ai/errors.go b/backend/internal/pkg/ai/errors.go index 1c0607f..cb71463 100644 --- a/backend/internal/pkg/ai/errors.go +++ b/backend/internal/pkg/ai/errors.go @@ -42,11 +42,12 @@ func (c ErrorCode) String() string { } type ProviderError struct { - Code ErrorCode - Message string - Retryable bool - Inner error - RawOutput string + Code ErrorCode + Message string + Retryable bool + Inner error + RawOutput string + PromptHash string // sha256(system+"\x00"+user)[:12], set on ErrSchemaViolation } func (e *ProviderError) Error() string { diff --git a/backend/internal/pkg/ai/gemini.go b/backend/internal/pkg/ai/gemini.go index e872069..ea322cb 100644 --- a/backend/internal/pkg/ai/gemini.go +++ b/backend/internal/pkg/ai/gemini.go @@ -2,6 +2,8 @@ package ai import ( "context" + "crypto/sha256" + "encoding/hex" "encoding/json" "fmt" "sort" @@ -258,12 +260,14 @@ func (p *GeminiProvider) Chat(ctx context.Context, req *ChatRequest) (*ChatRespo text := resp.Text() if len(req.JSONSchema) > 0 { if verr := ValidateSchema(req.JSONSchema, []byte(text)); verr != nil { + promptHash := promptHashShort(req.SystemPrompt, req.UserMessage) return nil, &ProviderError{ - Code: ErrSchemaViolation, - Message: fmt.Sprintf("response does not match schema: %v", verr), - Retryable: true, - Inner: verr, - RawOutput: text, + Code: ErrSchemaViolation, + Message: fmt.Sprintf("response does not match schema: %v", verr), + Retryable: true, + Inner: verr, + RawOutput: text, + PromptHash: promptHash, } } } @@ -330,6 +334,13 @@ func (p *GeminiProvider) record(ctx context.Context, e UsageEvent) { _ = p.recorder.Record(ctx, e) } +// promptHashShort returns the first 12 hex chars of sha256(system+"\x00"+user). +// Used to correlate schema-violation errors with the prompt that produced them. +func promptHashShort(system, user string) string { + h := sha256.Sum256([]byte(system + "\x00" + user)) + return hex.EncodeToString(h[:])[:12] +} + // schemaFromMap converts a raw JSON-schema map to genai.Schema for structured output. func schemaFromMap(m map[string]any) *genai.Schema { s := &genai.Schema{}