diff --git a/admin-compliance/app/api/sdk/v1/use-case/[[...path]]/route.ts b/admin-compliance/app/api/sdk/v1/use-case/[[...path]]/route.ts index 497fe3f..90d9846 100644 --- a/admin-compliance/app/api/sdk/v1/use-case/[[...path]]/route.ts +++ b/admin-compliance/app/api/sdk/v1/use-case/[[...path]]/route.ts @@ -33,7 +33,7 @@ async function proxyRequest( const fetchOptions: RequestInit = { method, headers, - signal: AbortSignal.timeout(30000), + signal: AbortSignal.timeout(60000), } if (['POST', 'PUT', 'PATCH'].includes(method)) { diff --git a/ai-compliance-sdk/internal/usecase/compiler_llm.go b/ai-compliance-sdk/internal/usecase/compiler_llm.go index 31246d3..b425977 100644 --- a/ai-compliance-sdk/internal/usecase/compiler_llm.go +++ b/ai-compliance-sdk/internal/usecase/compiler_llm.go @@ -23,63 +23,83 @@ func NewLLMQuestionGenerator(registry *llm.ProviderRegistry) *LLMQuestionGenerat // llmQuestion is the JSON structure we expect from the LLM. type llmQuestion struct { + MCName string `json:"mc_name"` Question string `json:"question"` PassCriteria []string `json:"pass_criteria"` FailCriteria []string `json:"fail_criteria"` Severity string `json:"severity"` } -// GenerateQuestions generates questions for a list of MCs using the LLM. +// maxLLMMCs limits how many MCs we send to the LLM in one batch. +const maxLLMMCs = 10 + +// GenerateQuestions generates questions for MCs using a single batched LLM call. func (g *LLMQuestionGenerator) GenerateQuestions(mcs []MCInfo, regulations []string) ([]Question, error) { if g.registry == nil { return nil, fmt.Errorf("no LLM provider configured") } - ctx, cancel := context.WithTimeout(context.Background(), 120*time.Second) + // Limit batch size + batch := mcs + if len(batch) > maxLLMMCs { + batch = batch[:maxLLMMCs] + } + + ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second) defer cancel() + prompt := buildBatchPrompt(batch, regulations) + + resp, err := g.registry.Chat(ctx, &llm.ChatRequest{ + Messages: []llm.Message{ + {Role: "system", Content: systemPrompt}, + {Role: "user", Content: prompt}, + }, + Temperature: 0.3, + MaxTokens: 2000, + }) + if err != nil { + return nil, fmt.Errorf("LLM call failed: %w", err) + } + + parsed := parseLLMResponse(resp.Message.Content) + if len(parsed) == 0 { + return nil, fmt.Errorf("LLM returned no valid questions") + } + + // Map parsed questions back to MCs + mcByName := make(map[string]MCInfo) + for _, mc := range batch { + mcByName[mc.CanonicalName] = mc + } + var questions []Question - qNum := 1 - - for _, mc := range mcs { - prompt := buildPrompt(mc, regulations) - - resp, err := g.registry.Chat(ctx, &llm.ChatRequest{ - Messages: []llm.Message{ - {Role: "system", Content: systemPrompt}, - {Role: "user", Content: prompt}, - }, - Temperature: 0.3, - MaxTokens: 500, - }) - if err != nil { - // Fallback to deterministic generation - questions = append(questions, GenerateFromMC(mc)...) - qNum += len(GenerateFromMC(mc)) - continue - } - - parsed := parseLLMResponse(resp.Message.Content) - for _, lq := range parsed { - q := Question{ - ID: fmt.Sprintf("Q%d", qNum), - MCID: mc.MasterControlID, - MCName: mc.CanonicalName, - Text: lq.Question, - QuestionType: "yes_no", - Severity: normalizeSeverity(lq.Severity), - Regulation: mc.RegSource, - PassCriteria: lq.PassCriteria, - FailCriteria: lq.FailCriteria, + for _, lq := range parsed { + mc, ok := mcByName[lq.MCName] + if !ok { + // Try fuzzy match + for name, m := range mcByName { + if strings.Contains(lq.MCName, name) || strings.Contains(name, lq.MCName) { + mc = m + ok = true + break + } } - questions = append(questions, q) - qNum++ } - // Cap total questions - if qNum > 50 { - break + q := Question{ + Text: lq.Question, + QuestionType: "yes_no", + Severity: normalizeSeverity(lq.Severity), + PassCriteria: lq.PassCriteria, + FailCriteria: lq.FailCriteria, } + if ok { + q.MCID = mc.MasterControlID + q.MCName = mc.CanonicalName + q.Regulation = mc.RegSource + } + questions = append(questions, q) } return questions, nil @@ -88,12 +108,30 @@ func (g *LLMQuestionGenerator) GenerateQuestions(mcs []MCInfo, regulations []str const systemPrompt = `Du bist ein Compliance-Experte. Generiere praezise Prueffragen fuer Compliance-Audits. Antworte NUR mit einem JSON-Array. Jedes Element hat: +- "mc_name": Der canonical_name des Master Controls (exakt wie im Input) - "question": Eine klare Ja/Nein-Frage auf Deutsch - "pass_criteria": Array mit 1-2 Kriterien fuer "bestanden" - "fail_criteria": Array mit 1-2 Kriterien fuer "nicht bestanden" - "severity": "HIGH", "MEDIUM" oder "LOW" -Keine Erklaerungen, nur das JSON-Array.` +Generiere 1 Frage pro Master Control. Keine Erklaerungen, nur das JSON-Array.` + +func buildBatchPrompt(mcs []MCInfo, regulations []string) string { + regStr := strings.Join(regulations, ", ") + + var sb strings.Builder + sb.WriteString(fmt.Sprintf("Regulierungen: %s\n\nMaster Controls:\n", regStr)) + + for i, mc := range mcs { + readable := strings.ReplaceAll(mc.CanonicalName, "_", " ") + sb.WriteString(fmt.Sprintf("%d. mc_name=%q (%d Controls, Quelle: %s)\n", + i+1, mc.CanonicalName, mc.TotalControls, mc.RegSource)) + _ = readable + } + + sb.WriteString("\nGeneriere je 1 Prueffrage pro Master Control.") + return sb.String() +} func buildPrompt(mc MCInfo, regulations []string) string { readable := strings.ReplaceAll(mc.CanonicalName, "_", " ")