cleanup, prompt centralization
This commit is contained in:
		
							parent
							
								
									b17458d153
								
							
						
					
					
						commit
						dc9f52fb7c
					
				@ -1130,16 +1130,12 @@ class AIQueryInterface {
 | 
				
			|||||||
    const lowConfidenceSteps = auditTrail.filter(entry => (entry.confidence || 0) < 60).length;
 | 
					    const lowConfidenceSteps = auditTrail.filter(entry => (entry.confidence || 0) < 60).length;
 | 
				
			||||||
    const mediumConfidenceSteps = auditTrail.length - highConfidenceSteps - lowConfidenceSteps;
 | 
					    const mediumConfidenceSteps = auditTrail.length - highConfidenceSteps - lowConfidenceSteps;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // FIX 1: Count actual AI decision actions only
 | 
					 | 
				
			||||||
    const aiDecisionCount = auditTrail.filter(entry => entry.action === 'ai-decision').length;
 | 
					    const aiDecisionCount = auditTrail.filter(entry => entry.action === 'ai-decision').length;
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
    // FIX 2: Count actual similarity search actions, not metadata flags
 | 
					 | 
				
			||||||
    const embeddingsUsageCount = auditTrail.filter(entry => entry.action === 'similarity-search').length;
 | 
					    const embeddingsUsageCount = auditTrail.filter(entry => entry.action === 'similarity-search').length;
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
    // FIX 3: Maintain tool selection count (this was correct)
 | 
					 | 
				
			||||||
    const toolSelectionCount = auditTrail.filter(entry => entry.action === 'selection-decision').length;
 | 
					    const toolSelectionCount = auditTrail.filter(entry => entry.action === 'selection-decision').length;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Additional diagnostic counts for debugging
 | 
					 | 
				
			||||||
    const microTaskCount = auditTrail.filter(entry => 
 | 
					    const microTaskCount = auditTrail.filter(entry => 
 | 
				
			||||||
      entry.action === 'ai-decision' && entry.metadata?.microTaskType
 | 
					      entry.action === 'ai-decision' && entry.metadata?.microTaskType
 | 
				
			||||||
    ).length;
 | 
					    ).length;
 | 
				
			||||||
@ -1152,7 +1148,6 @@ class AIQueryInterface {
 | 
				
			|||||||
      entry.action === 'phase-enhancement'
 | 
					      entry.action === 'phase-enhancement'
 | 
				
			||||||
    ).length;
 | 
					    ).length;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Enhanced insights with diagnostic information
 | 
					 | 
				
			||||||
    const keyInsights = [];
 | 
					    const keyInsights = [];
 | 
				
			||||||
    const potentialIssues = [];
 | 
					    const potentialIssues = [];
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
@ -1172,7 +1167,6 @@ class AIQueryInterface {
 | 
				
			|||||||
      keyInsights.push(`${microTaskCount} spezialisierte Micro-Task-Analysen durchgeführt`);
 | 
					      keyInsights.push(`${microTaskCount} spezialisierte Micro-Task-Analysen durchgeführt`);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Detect mode-specific patterns for validation
 | 
					 | 
				
			||||||
    if (phaseToolSelectionCount > 0 || phaseEnhancementCount > 0) {
 | 
					    if (phaseToolSelectionCount > 0 || phaseEnhancementCount > 0) {
 | 
				
			||||||
      keyInsights.push('Workflow-Modus: Phasenspezifische Analyse durchgeführt');
 | 
					      keyInsights.push('Workflow-Modus: Phasenspezifische Analyse durchgeführt');
 | 
				
			||||||
    } else if (microTaskCount >= 3) {
 | 
					    } else if (microTaskCount >= 3) {
 | 
				
			||||||
@ -1216,10 +1210,9 @@ class AIQueryInterface {
 | 
				
			|||||||
      keyInsights.push('Mehrheit der Analyseschritte mit hoher Sicherheit');
 | 
					      keyInsights.push('Mehrheit der Analyseschritte mit hoher Sicherheit');
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Validate expected counts based on mode detection
 | 
					 | 
				
			||||||
    const isWorkflowMode = phaseToolSelectionCount > 0 || phaseEnhancementCount > 0;
 | 
					    const isWorkflowMode = phaseToolSelectionCount > 0 || phaseEnhancementCount > 0;
 | 
				
			||||||
    const expectedMinAI = isWorkflowMode ? 11 : 8; // Workflow: 5 common + 6 phase selections, Tool: 5 common + 3 evaluations
 | 
					    const expectedMinAI = isWorkflowMode ? 11 : 8; 
 | 
				
			||||||
    const expectedMinEmbeddings = 1; // Both modes should have initial search
 | 
					    const expectedMinEmbeddings = 1; 
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (aiDecisionCount < expectedMinAI) {
 | 
					    if (aiDecisionCount < expectedMinAI) {
 | 
				
			||||||
      potentialIssues.push(`${expectedMinAI - aiDecisionCount} fehlende KI-Entscheidungen für ${isWorkflowMode ? 'Workflow' : 'Tool'}-Modus`);
 | 
					      potentialIssues.push(`${expectedMinAI - aiDecisionCount} fehlende KI-Entscheidungen für ${isWorkflowMode ? 'Workflow' : 'Tool'}-Modus`);
 | 
				
			||||||
@ -1250,7 +1243,6 @@ class AIQueryInterface {
 | 
				
			|||||||
      analysisQuality,
 | 
					      analysisQuality,
 | 
				
			||||||
      keyInsights,
 | 
					      keyInsights,
 | 
				
			||||||
      potentialIssues,
 | 
					      potentialIssues,
 | 
				
			||||||
      // Debug information
 | 
					 | 
				
			||||||
      debugCounts: {
 | 
					      debugCounts: {
 | 
				
			||||||
        microTaskCount,
 | 
					        microTaskCount,
 | 
				
			||||||
        phaseToolSelectionCount, 
 | 
					        phaseToolSelectionCount, 
 | 
				
			||||||
 | 
				
			|||||||
@ -16,9 +16,41 @@ STRICTNESS:
 | 
				
			|||||||
`.trim();
 | 
					`.trim();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const AI_PROMPTS = {
 | 
					export const AI_PROMPTS = {
 | 
				
			||||||
  // ---------------------------------------------------------------------------
 | 
					  enhancementQuestions: (input: string) => {
 | 
				
			||||||
  // Tool/Concept selection (AI pre-pick from embedding-curated set)
 | 
					    return `Sie sind DFIR-Experte. Ein Nutzer beschreibt unten ein Szenario/Problem.
 | 
				
			||||||
  // ---------------------------------------------------------------------------
 | 
					
 | 
				
			||||||
 | 
					ZIEL:
 | 
				
			||||||
 | 
					- Stellen Sie NUR dann 1–3 präzise Rückfragen, wenn entscheidende forensische Lücken die weitere Analyse/Toolauswahl PHASENREIHENFOLGE oder EVIDENCE-STRATEGIE wesentlich beeinflussen würden.
 | 
				
			||||||
 | 
					- Wenn ausreichend abgedeckt: Geben Sie eine leere Liste [] zurück.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					PRIORITÄT DER THEMEN (in dieser Reihenfolge prüfen):
 | 
				
			||||||
 | 
					1) Available Evidence & Artefakte (z.B. RAM-Dump, Disk-Image, Logs, PCAP, Registry, Cloud/Audit-Logs)
 | 
				
			||||||
 | 
					2) Scope/Systems (konkrete Plattformen/Assets/Identitäten/Netzsegmente)
 | 
				
			||||||
 | 
					3) Investigation Objectives (Ziele: IOC-Extraktion, Timeline, Impact, Attribution)
 | 
				
			||||||
 | 
					4) Timeline/Timeframe (kritische Zeitfenster, Erhalt flüchtiger Daten)
 | 
				
			||||||
 | 
					5) Legal & Compliance (Chain of Custody, Aufbewahrung, DSGVO/Branchenvorgaben)
 | 
				
			||||||
 | 
					6) Technical Constraints (Ressourcen, Zugriffsrechte, Tooling/EDR)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					FRAGEN-QUALITÄT:
 | 
				
			||||||
 | 
					- Forensisch spezifisch und entscheidungsrelevant (keine Allgemeinplätze).
 | 
				
			||||||
 | 
					- Eine Frage pro Thema, keine Dopplungen.
 | 
				
			||||||
 | 
					- Antwortbar vom Nutzer (keine Spekulation, keine “Beweise senden”-Aufforderungen).
 | 
				
			||||||
 | 
					- Maximal 18 Wörter, endet mit "?".
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					VALIDIERUNG:
 | 
				
			||||||
 | 
					- Stellen Sie NUR Fragen zu Themen, die im Nutzertext NICHT hinreichend konkret beantwortet sind (keine Wiederholung bereits gegebener Details).
 | 
				
			||||||
 | 
					- Wenn alle priorisierten Themen ausreichend sind → [].
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					ANTWORTFORMAT (NUR JSON, KEIN ZUSÄTZLICHER TEXT):
 | 
				
			||||||
 | 
					[
 | 
				
			||||||
 | 
					  "präzise Frage 1?",
 | 
				
			||||||
 | 
					  "präzise Frage 2?",
 | 
				
			||||||
 | 
					  "präzise Frage 3?"
 | 
				
			||||||
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					NUTZER-EINGABE:
 | 
				
			||||||
 | 
					${input}`.trim();
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
  toolSelection: (mode: string, userQuery: string, maxSelectedItems: number) => {
 | 
					  toolSelection: (mode: string, userQuery: string, maxSelectedItems: number) => {
 | 
				
			||||||
    const modeInstruction =
 | 
					    const modeInstruction =
 | 
				
			||||||
      mode === 'workflow'
 | 
					      mode === 'workflow'
 | 
				
			||||||
@ -80,9 +112,6 @@ ${RELEVANCE_RUBRIC}
 | 
				
			|||||||
${STRICTNESS}`;
 | 
					${STRICTNESS}`;
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // ---------------------------------------------------------------------------
 | 
					 | 
				
			||||||
  // Contextual analyses (keine JSON-Ausgabe nötig; kurzer Fließtext)
 | 
					 | 
				
			||||||
  // ---------------------------------------------------------------------------
 | 
					 | 
				
			||||||
  scenarioAnalysis: (isWorkflow: boolean, userQuery: string) => {
 | 
					  scenarioAnalysis: (isWorkflow: boolean, userQuery: string) => {
 | 
				
			||||||
    const analysisType = isWorkflow ? 'Szenario' : 'Problem';
 | 
					    const analysisType = isWorkflow ? 'Szenario' : 'Problem';
 | 
				
			||||||
    const focus = isWorkflow
 | 
					    const focus = isWorkflow
 | 
				
			||||||
@ -124,9 +153,6 @@ Fokus: ${focus}
 | 
				
			|||||||
Antwort: Fließtext, max 100 Wörter.`;
 | 
					Antwort: Fließtext, max 100 Wörter.`;
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // ---------------------------------------------------------------------------
 | 
					 | 
				
			||||||
  // Phase-specific selection (workflow mode substep)
 | 
					 | 
				
			||||||
  // ---------------------------------------------------------------------------
 | 
					 | 
				
			||||||
  phaseToolSelection: (userQuery: string, phase: any, phaseTools: any[]) => {
 | 
					  phaseToolSelection: (userQuery: string, phase: any, phaseTools: any[]) => {
 | 
				
			||||||
    const methods = phaseTools.filter(t => t.type === 'method');
 | 
					    const methods = phaseTools.filter(t => t.type === 'method');
 | 
				
			||||||
    const tools = phaseTools.filter(t => t.type === 'software');
 | 
					    const tools = phaseTools.filter(t => t.type === 'software');
 | 
				
			||||||
@ -183,9 +209,6 @@ ANTWORT (NUR JSON):
 | 
				
			|||||||
]`;
 | 
					]`;
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // ---------------------------------------------------------------------------
 | 
					 | 
				
			||||||
  // Per-item evaluation (used in tool mode & elsewhere)
 | 
					 | 
				
			||||||
  // ---------------------------------------------------------------------------
 | 
					 | 
				
			||||||
  toolEvaluation: (userQuery: string, tool: any, rank: number) => {
 | 
					  toolEvaluation: (userQuery: string, tool: any, rank: number) => {
 | 
				
			||||||
    const itemType = tool.type === 'method' ? 'Methode' : 'Tool';
 | 
					    const itemType = tool.type === 'method' ? 'Methode' : 'Tool';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -215,9 +238,6 @@ ANTWORT (NUR JSON):
 | 
				
			|||||||
}`;
 | 
					}`;
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // ---------------------------------------------------------------------------
 | 
					 | 
				
			||||||
  // Background knowledge (concepts)
 | 
					 | 
				
			||||||
  // ---------------------------------------------------------------------------
 | 
					 | 
				
			||||||
  backgroundKnowledgeSelection: (userQuery: string, mode: string, selectedToolNames: string[], availableConcepts: any[]) => {
 | 
					  backgroundKnowledgeSelection: (userQuery: string, mode: string, selectedToolNames: string[], availableConcepts: any[]) => {
 | 
				
			||||||
    return `Wähle 2–4 Konzepte, die das Verständnis/den Einsatz der ausgewählten Tools verbessern.
 | 
					    return `Wähle 2–4 Konzepte, die das Verständnis/den Einsatz der ausgewählten Tools verbessern.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -242,9 +262,6 @@ ANTWORT (NUR JSON):
 | 
				
			|||||||
]`;
 | 
					]`;
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // ---------------------------------------------------------------------------
 | 
					 | 
				
			||||||
  // Phase completion (underrepresented phase fix)
 | 
					 | 
				
			||||||
  // ---------------------------------------------------------------------------
 | 
					 | 
				
			||||||
  phaseCompletionReasoning: (
 | 
					  phaseCompletionReasoning: (
 | 
				
			||||||
    originalQuery: string,
 | 
					    originalQuery: string,
 | 
				
			||||||
    phase: any,
 | 
					    phase: any,
 | 
				
			||||||
@ -306,9 +323,6 @@ ANTWORT (NUR JSON):
 | 
				
			|||||||
}`;
 | 
					}`;
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // ---------------------------------------------------------------------------
 | 
					 | 
				
			||||||
  // Final synthesis (short prose, no JSON needed)
 | 
					 | 
				
			||||||
  // ---------------------------------------------------------------------------
 | 
					 | 
				
			||||||
  finalRecommendations: (isWorkflow: boolean, userQuery: string, selectedToolNames: string[]) => {
 | 
					  finalRecommendations: (isWorkflow: boolean, userQuery: string, selectedToolNames: string[]) => {
 | 
				
			||||||
    const focus = isWorkflow
 | 
					    const focus = isWorkflow
 | 
				
			||||||
      ? 'Knappe Workflow-Schritte & Best Practices; neutral formulieren'
 | 
					      ? 'Knappe Workflow-Schritte & Best Practices; neutral formulieren'
 | 
				
			||||||
@ -324,7 +338,7 @@ Antwort: Fließtext, max ${isWorkflow ? '100' : '80'} Wörter. Keine Liste.`;
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
} as const;
 | 
					} as const;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Overloads
 | 
					export function getPrompt(key: 'enhancementQuestions', input: string): string;
 | 
				
			||||||
export function getPrompt(key: 'toolSelection', mode: string, userQuery: string, maxSelectedItems: number): string;
 | 
					export function getPrompt(key: 'toolSelection', mode: string, userQuery: string, maxSelectedItems: number): string;
 | 
				
			||||||
export function getPrompt(key: 'toolSelectionWithData', basePrompt: string, toolsToSend: any[], conceptsToSend: any[]): string;
 | 
					export function getPrompt(key: 'toolSelectionWithData', basePrompt: string, toolsToSend: any[], conceptsToSend: any[]): string;
 | 
				
			||||||
export function getPrompt(key: 'scenarioAnalysis', isWorkflow: boolean, userQuery: string): string;
 | 
					export function getPrompt(key: 'scenarioAnalysis', isWorkflow: boolean, userQuery: string): string;
 | 
				
			||||||
@ -337,7 +351,6 @@ export function getPrompt(key: 'phaseCompletionReasoning', originalQuery: string
 | 
				
			|||||||
export function getPrompt(key: 'finalRecommendations', isWorkflow: boolean, userQuery: string, selectedToolNames: string[]): string;
 | 
					export function getPrompt(key: 'finalRecommendations', isWorkflow: boolean, userQuery: string, selectedToolNames: string[]): string;
 | 
				
			||||||
export function getPrompt(key: 'generatePhaseCompletionPrompt', originalQuery: string, phase: any, candidateTools: any[], candidateConcepts: any[]): string;
 | 
					export function getPrompt(key: 'generatePhaseCompletionPrompt', originalQuery: string, phase: any, candidateTools: any[], candidateConcepts: any[]): string;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Dispatcher
 | 
					 | 
				
			||||||
export function getPrompt(promptKey: keyof typeof AI_PROMPTS, ...args: any[]): string {
 | 
					export function getPrompt(promptKey: keyof typeof AI_PROMPTS, ...args: any[]): string {
 | 
				
			||||||
  try {
 | 
					  try {
 | 
				
			||||||
    const f = AI_PROMPTS[promptKey];
 | 
					    const f = AI_PROMPTS[promptKey];
 | 
				
			||||||
@ -348,4 +361,4 @@ export function getPrompt(promptKey: keyof typeof AI_PROMPTS, ...args: any[]): s
 | 
				
			|||||||
    console.error(`[PROMPTS] Error generating prompt ${promptKey}:`, err);
 | 
					    console.error(`[PROMPTS] Error generating prompt ${promptKey}:`, err);
 | 
				
			||||||
    return 'Error: Failed to generate prompt';
 | 
					    return 'Error: Failed to generate prompt';
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -273,15 +273,13 @@ import BaseLayout from '../layouts/BaseLayout.astro';
 | 
				
			|||||||
</BaseLayout>
 | 
					</BaseLayout>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<script>
 | 
					<script>
 | 
				
			||||||
  // Load simple-boost from local node_modules instead of CDN
 | 
					  // TODO: cleanup
 | 
				
			||||||
  import('simple-boost').then(() => {
 | 
					  import('simple-boost').then(() => {
 | 
				
			||||||
    console.log('Simple-boost loaded successfully from local dependencies');
 | 
					    console.log('Simple-boost loaded successfully from local dependencies');
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
    // Setup dynamic amount updating
 | 
					 | 
				
			||||||
    setupDynamicAmounts();
 | 
					    setupDynamicAmounts();
 | 
				
			||||||
  }).catch(error => {
 | 
					  }).catch(error => {
 | 
				
			||||||
    console.error('Failed to load simple-boost:', error);
 | 
					    console.error('Failed to load simple-boost:', error);
 | 
				
			||||||
    // Fallback: try to load from node_modules path
 | 
					 | 
				
			||||||
    const script = document.createElement('script');
 | 
					    const script = document.createElement('script');
 | 
				
			||||||
    script.type = 'module';
 | 
					    script.type = 'module';
 | 
				
			||||||
    script.src = '/node_modules/simple-boost/dist/simple-boost.js';
 | 
					    script.src = '/node_modules/simple-boost/dist/simple-boost.js';
 | 
				
			||||||
@ -294,7 +292,6 @@ import BaseLayout from '../layouts/BaseLayout.astro';
 | 
				
			|||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  function setupDynamicAmounts() {
 | 
					  function setupDynamicAmounts() {
 | 
				
			||||||
    // EUR boost button
 | 
					 | 
				
			||||||
    const eurBoost = document.getElementById('eur-boost');
 | 
					    const eurBoost = document.getElementById('eur-boost');
 | 
				
			||||||
    const eurInput = document.getElementById('eur-amount') as HTMLInputElement;
 | 
					    const eurInput = document.getElementById('eur-amount') as HTMLInputElement;
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
@ -305,7 +302,6 @@ import BaseLayout from '../layouts/BaseLayout.astro';
 | 
				
			|||||||
        console.log('EUR amount set to:', amount);
 | 
					        console.log('EUR amount set to:', amount);
 | 
				
			||||||
      });
 | 
					      });
 | 
				
			||||||
      
 | 
					      
 | 
				
			||||||
      // Update on input change for better UX
 | 
					 | 
				
			||||||
      eurInput.addEventListener('input', () => {
 | 
					      eurInput.addEventListener('input', () => {
 | 
				
			||||||
        const amount = parseFloat(eurInput.value) || 0.5;
 | 
					        const amount = parseFloat(eurInput.value) || 0.5;
 | 
				
			||||||
        eurBoost.setAttribute('amount', amount.toString());
 | 
					        eurBoost.setAttribute('amount', amount.toString());
 | 
				
			||||||
@ -315,7 +311,6 @@ import BaseLayout from '../layouts/BaseLayout.astro';
 | 
				
			|||||||
</script>
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<style>
 | 
					<style>
 | 
				
			||||||
  /* Custom styling for simple-boost buttons to match your theme */
 | 
					 | 
				
			||||||
  simple-boost {
 | 
					  simple-boost {
 | 
				
			||||||
    --simple-boost-primary: var(--color-warning);
 | 
					    --simple-boost-primary: var(--color-warning);
 | 
				
			||||||
    --simple-boost-primary-hover: var(--color-accent);
 | 
					    --simple-boost-primary-hover: var(--color-accent);
 | 
				
			||||||
 | 
				
			|||||||
@ -5,13 +5,53 @@ import { apiError, apiServerError, createAuthErrorResponse } from '../../../util
 | 
				
			|||||||
import { enqueueApiCall } from '../../../utils/rateLimitedQueue.js';
 | 
					import { enqueueApiCall } from '../../../utils/rateLimitedQueue.js';
 | 
				
			||||||
import { aiService } from '../../../utils/aiService.js';
 | 
					import { aiService } from '../../../utils/aiService.js';
 | 
				
			||||||
import { JSONParser } from '../../../utils/jsonUtils.js';
 | 
					import { JSONParser } from '../../../utils/jsonUtils.js';
 | 
				
			||||||
 | 
					import { getPrompt } from '../../../config/prompts.js';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const prerender = false;
 | 
					export const prerender = false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const rateLimitStore = new Map<string, { count: number; resetTime: number }>();
 | 
					const RATE_LIMIT_WINDOW_MS =
 | 
				
			||||||
const RATE_LIMIT_WINDOW = 60 * 1000;
 | 
					  Number.isFinite(parseInt(process.env.RATE_LIMIT_WINDOW_MS ?? '', 10))
 | 
				
			||||||
const RATE_LIMIT_MAX = 5;
 | 
					    ? parseInt(process.env.RATE_LIMIT_WINDOW_MS!, 10)
 | 
				
			||||||
 | 
					    : 60_000; 
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const RATE_LIMIT_MAX =
 | 
				
			||||||
 | 
					  Number.isFinite(parseInt(process.env.AI_RATE_LIMIT_MAX_REQUESTS ?? '', 10))
 | 
				
			||||||
 | 
					    ? parseInt(process.env.AI_RATE_LIMIT_MAX_REQUESTS!, 10)
 | 
				
			||||||
 | 
					    : 5;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const INPUT_MIN_CHARS = 40;
 | 
				
			||||||
 | 
					const INPUT_MAX_CHARS = 1000;
 | 
				
			||||||
 | 
					const Q_MIN_LEN = 15;
 | 
				
			||||||
 | 
					const Q_MAX_LEN = 160;
 | 
				
			||||||
 | 
					const Q_MAX_COUNT = 3;
 | 
				
			||||||
 | 
					const AI_TEMPERATURE = 0.3;
 | 
				
			||||||
 | 
					const CLEANER_TEMPERATURE = 0.0;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const rateLimitStore = new Map<string, { count: number; resetTime: number }>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function checkRateLimit(userId: string): boolean {
 | 
				
			||||||
 | 
					  const now = Date.now();
 | 
				
			||||||
 | 
					  const entry = rateLimitStore.get(userId);
 | 
				
			||||||
 | 
					  if (!entry || now > entry.resetTime) {
 | 
				
			||||||
 | 
					    rateLimitStore.set(userId, { count: 1, resetTime: now + RATE_LIMIT_WINDOW_MS });
 | 
				
			||||||
 | 
					    return true;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  if (entry.count >= RATE_LIMIT_MAX) return false;
 | 
				
			||||||
 | 
					  entry.count++;
 | 
				
			||||||
 | 
					  return true;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function cleanupExpiredRateLimits(): void {
 | 
				
			||||||
 | 
					  const now = Date.now();
 | 
				
			||||||
 | 
					  for (const [userId, entry] of rateLimitStore.entries()) {
 | 
				
			||||||
 | 
					    if (now > entry.resetTime) rateLimitStore.delete(userId);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					setInterval(cleanupExpiredRateLimits, 5 * 60 * 1000);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Helpers
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
function sanitizeInput(input: string): string {
 | 
					function sanitizeInput(input: string): string {
 | 
				
			||||||
  return input
 | 
					  return input
 | 
				
			||||||
    .replace(/```[\s\S]*?```/g, '[CODE_BLOCK_REMOVED]')
 | 
					    .replace(/```[\s\S]*?```/g, '[CODE_BLOCK_REMOVED]')
 | 
				
			||||||
@ -19,79 +59,24 @@ function sanitizeInput(input: string): string {
 | 
				
			|||||||
    .replace(/\b(system|assistant|user)\s*[:]/gi, '[ROLE_REMOVED]')
 | 
					    .replace(/\b(system|assistant|user)\s*[:]/gi, '[ROLE_REMOVED]')
 | 
				
			||||||
    .replace(/\b(ignore|forget|disregard)\s+(previous|all|your)\s+(instructions?|context|rules?)/gi, '[INSTRUCTION_REMOVED]')
 | 
					    .replace(/\b(ignore|forget|disregard)\s+(previous|all|your)\s+(instructions?|context|rules?)/gi, '[INSTRUCTION_REMOVED]')
 | 
				
			||||||
    .trim()
 | 
					    .trim()
 | 
				
			||||||
    .slice(0, 1000); 
 | 
					    .slice(0, INPUT_MAX_CHARS);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function checkRateLimit(userId: string): boolean {
 | 
					function stripJsonFences(s: string): string {
 | 
				
			||||||
  const now = Date.now();
 | 
					  return s.replace(/^```json\s*/i, '')
 | 
				
			||||||
  const userLimit = rateLimitStore.get(userId);
 | 
					          .replace(/^```\s*/i, '')
 | 
				
			||||||
  
 | 
					          .replace(/\s*```\s*$/, '')
 | 
				
			||||||
  if (!userLimit || now > userLimit.resetTime) {
 | 
					          .trim();
 | 
				
			||||||
    rateLimitStore.set(userId, { count: 1, resetTime: now + RATE_LIMIT_WINDOW });
 | 
					 | 
				
			||||||
    return true;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
  
 | 
					 | 
				
			||||||
  if (userLimit.count >= RATE_LIMIT_MAX) {
 | 
					 | 
				
			||||||
    return false;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
  
 | 
					 | 
				
			||||||
  userLimit.count++;
 | 
					 | 
				
			||||||
  return true;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
function cleanupExpiredRateLimits(): void {
 | 
					 | 
				
			||||||
  const now = Date.now();
 | 
					 | 
				
			||||||
  for (const [userId, limit] of rateLimitStore.entries()) {
 | 
					 | 
				
			||||||
    if (now > limit.resetTime) {
 | 
					 | 
				
			||||||
      rateLimitStore.delete(userId);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
setInterval(cleanupExpiredRateLimits, 5 * 60 * 1000);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
function createEnhancementPrompt(input: string): string {
 | 
					 | 
				
			||||||
  return `Sie sind ein DFIR-Experte mit Spezialisierung auf forensische Methodik. Ein Nutzer beschreibt ein Szenario oder Problem. Analysieren Sie die Eingabe auf Vollständigkeit für eine wissenschaftlich fundierte Untersuchung.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
ANALYSIEREN SIE DIESE FORENSISCHEN KATEGORIEN:
 | 
					 | 
				
			||||||
1. **Incident Context**: Was ist passiert? Welche Angriffsvektoren oder technischen Probleme liegen vor?
 | 
					 | 
				
			||||||
2. **Affected Systems**: Welche spezifischen Technologien/Plattformen sind betroffen? (Windows/Linux/ICS/SCADA/Mobile/Cloud/Network Infrastructure)
 | 
					 | 
				
			||||||
3. **Available Evidence**: Welche forensischen Datenquellen stehen zur Verfügung? (RAM-Dumps, Disk-Images, Log-Files, Network-Captures, Registry-Hives)
 | 
					 | 
				
			||||||
4. **Investigation Objectives**: Was soll erreicht werden? (IOC-Extraktion, Timeline-Rekonstruktion, Attribution, Impact-Assessment)
 | 
					 | 
				
			||||||
5. **Timeline Constraints**: Wie zeitkritisch ist die Untersuchung?
 | 
					 | 
				
			||||||
6. **Legal & Compliance**: Rechtliche Anforderungen, Chain of Custody, Compliance-Rahmen (DSGVO, sector-specific regulations)
 | 
					 | 
				
			||||||
7. **Technical Constraints**: Verfügbare Ressourcen, Skills, Infrastrukturbeschränkungen
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
WENN die Beschreibung alle kritischen forensischen Aspekte abdeckt: Geben Sie eine leere Liste [] zurück.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
WENN wichtige Details fehlen: Formulieren Sie 2-3 präzise Fragen, die die kritischsten Lücken für eine wissenschaftlich fundierte Analyse schließen.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
QUALITÄTSKRITERIEN FÜR FRAGEN:
 | 
					 | 
				
			||||||
- Forensisch spezifisch, nicht allgemein (NICHT: "Mehr Details?")
 | 
					 | 
				
			||||||
- Methodisch relevant (NICHT: "Wann passierte das?")
 | 
					 | 
				
			||||||
- Priorisiert nach Auswirkung auf die Untersuchungsqualität
 | 
					 | 
				
			||||||
- Die Frage soll maximal 20 Wörter umfassen
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
ANTWORTFORMAT (NUR JSON, KEIN ZUSÄTZLICHER TEXT):
 | 
					 | 
				
			||||||
[
 | 
					 | 
				
			||||||
  "spezifische Frage 1?",
 | 
					 | 
				
			||||||
  "spezifische Frage 2?",
 | 
					 | 
				
			||||||
  "spezifische Frage 3?"
 | 
					 | 
				
			||||||
]
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
NUTZER-EINGABE:
 | 
					 | 
				
			||||||
${input}
 | 
					 | 
				
			||||||
  `.trim();
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Handler
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
export const POST: APIRoute = async ({ request }) => {
 | 
					export const POST: APIRoute = async ({ request }) => {
 | 
				
			||||||
  try {
 | 
					  try {
 | 
				
			||||||
    const authResult = await withAPIAuth(request, 'ai');
 | 
					    const auth = await withAPIAuth(request, 'ai');
 | 
				
			||||||
    if (!authResult.authenticated) {
 | 
					    if (!auth.authenticated) return createAuthErrorResponse();
 | 
				
			||||||
      return createAuthErrorResponse();
 | 
					    const userId = auth.userId;
 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    
 | 
					 | 
				
			||||||
    const userId = authResult.userId;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (!checkRateLimit(userId)) {
 | 
					    if (!checkRateLimit(userId)) {
 | 
				
			||||||
      return apiError.rateLimit('Enhancement rate limit exceeded');
 | 
					      return apiError.rateLimit('Enhancement rate limit exceeded');
 | 
				
			||||||
@ -100,61 +85,38 @@ export const POST: APIRoute = async ({ request }) => {
 | 
				
			|||||||
    const body = await request.json();
 | 
					    const body = await request.json();
 | 
				
			||||||
    const { input } = body;
 | 
					    const { input } = body;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (!input || typeof input !== 'string' || input.length < 40) {
 | 
					    if (!input || typeof input !== 'string' || input.length < INPUT_MIN_CHARS) {
 | 
				
			||||||
      return apiError.badRequest('Input too short for enhancement (minimum 40 characters)');
 | 
					      return apiError.badRequest(`Input too short for enhancement (minimum ${INPUT_MIN_CHARS} characters)`);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const sanitizedInput = sanitizeInput(input);
 | 
					    const sanitizedInput = sanitizeInput(input);
 | 
				
			||||||
    if (sanitizedInput.length < 40) {
 | 
					    if (sanitizedInput.length < INPUT_MIN_CHARS) {
 | 
				
			||||||
      return apiError.badRequest('Input too short after sanitization');
 | 
					      return apiError.badRequest('Input too short after sanitization');
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const systemPrompt = createEnhancementPrompt(sanitizedInput);
 | 
					    const taskId = `enhance_${userId}_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`;
 | 
				
			||||||
    const taskId = `enhance_${userId}_${Date.now()}_${Math.random().toString(36).substr(2, 4)}`;
 | 
					    const questionsPrompt = getPrompt('enhancementQuestions', sanitizedInput);
 | 
				
			||||||
    
 | 
					 | 
				
			||||||
    console.log(`[ENHANCE-API] Processing enhancement request for user: ${userId}`);
 | 
					 | 
				
			||||||
    
 | 
					 | 
				
			||||||
    const aiResponse = await enqueueApiCall(() => 
 | 
					 | 
				
			||||||
      aiService.callAI(systemPrompt, { 
 | 
					 | 
				
			||||||
        temperature: 0.7 
 | 
					 | 
				
			||||||
      }), taskId);
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (!aiResponse.content) {
 | 
					    console.log(`[ENHANCE-API] Processing enhancement request for user: ${userId}`);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const aiResponse = await enqueueApiCall(
 | 
				
			||||||
 | 
					      () => aiService.callAI(questionsPrompt, { temperature: AI_TEMPERATURE }),
 | 
				
			||||||
 | 
					      taskId
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (!aiResponse?.content) {
 | 
				
			||||||
      return apiServerError.unavailable('No enhancement response');
 | 
					      return apiServerError.unavailable('No enhancement response');
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    let questions;
 | 
					    let parsed: unknown = JSONParser.safeParseJSON(stripJsonFences(aiResponse.content), null);
 | 
				
			||||||
    try {
 | 
					 | 
				
			||||||
      const cleanedContent = aiResponse.content
 | 
					 | 
				
			||||||
        .replace(/^```json\s*/i, '')
 | 
					 | 
				
			||||||
        .replace(/\s*```\s*$/, '')
 | 
					 | 
				
			||||||
        .trim();
 | 
					 | 
				
			||||||
      
 | 
					 | 
				
			||||||
      questions = JSONParser.safeParseJSON(cleanedContent, []);
 | 
					 | 
				
			||||||
      
 | 
					 | 
				
			||||||
      if (!Array.isArray(questions)) {
 | 
					 | 
				
			||||||
        throw new Error('Response is not an array');
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
      
 | 
					 | 
				
			||||||
      questions = questions
 | 
					 | 
				
			||||||
        .filter(q => typeof q === 'string' && q.length > 20 && q.length < 200)
 | 
					 | 
				
			||||||
        .filter(q => q.includes('?')) 
 | 
					 | 
				
			||||||
        .filter(q => {
 | 
					 | 
				
			||||||
          const forensicsTerms = ['forensisch', 'log', 'dump', 'image', 'artefakt', 'evidence', 'incident', 'system', 'netzwerk', 'zeitraum', 'verfügbar'];
 | 
					 | 
				
			||||||
          const lowerQ = q.toLowerCase();
 | 
					 | 
				
			||||||
          return forensicsTerms.some(term => lowerQ.includes(term));
 | 
					 | 
				
			||||||
        })
 | 
					 | 
				
			||||||
        .map(q => q.trim())
 | 
					 | 
				
			||||||
        .slice(0, 3); 
 | 
					 | 
				
			||||||
        
 | 
					 | 
				
			||||||
      if (questions.length === 0) {
 | 
					 | 
				
			||||||
        questions = [];
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    } catch (error) {
 | 
					    let questions: string[] = Array.isArray(parsed) ? parsed : [];
 | 
				
			||||||
      console.error('[ENHANCE-API] Failed to parse enhancement response:', aiResponse.content);
 | 
					    questions = questions
 | 
				
			||||||
      questions = [];
 | 
					      .filter(q => typeof q === 'string')
 | 
				
			||||||
    }
 | 
					      .map(q => q.trim())
 | 
				
			||||||
 | 
					      .filter(q => q.endsWith('?'))
 | 
				
			||||||
 | 
					      .filter(q => q.length >= Q_MIN_LEN && q.length <= Q_MAX_LEN)
 | 
				
			||||||
 | 
					      .slice(0, Q_MAX_COUNT);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    console.log(`[ENHANCE-API] User: ${userId}, Questions generated: ${questions.length}, Input length: ${sanitizedInput.length}`);
 | 
					    console.log(`[ENHANCE-API] User: ${userId}, Questions generated: ${questions.length}, Input length: ${sanitizedInput.length}`);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -162,14 +124,14 @@ export const POST: APIRoute = async ({ request }) => {
 | 
				
			|||||||
      success: true,
 | 
					      success: true,
 | 
				
			||||||
      questions,
 | 
					      questions,
 | 
				
			||||||
      taskId,
 | 
					      taskId,
 | 
				
			||||||
      inputComplete: questions.length === 0 
 | 
					      inputComplete: questions.length === 0
 | 
				
			||||||
    }), {
 | 
					    }), {
 | 
				
			||||||
      status: 200,
 | 
					      status: 200,
 | 
				
			||||||
      headers: { 'Content-Type': 'application/json' }
 | 
					      headers: { 'Content-Type': 'application/json' }
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  } catch (error) {
 | 
					  } catch (err) {
 | 
				
			||||||
    console.error('[ENHANCE-API] Enhancement error:', error);
 | 
					    console.error('[ENHANCE-API] Enhancement error:', err);
 | 
				
			||||||
    return apiServerError.internal('Enhancement processing failed');
 | 
					    return apiServerError.internal('Enhancement processing failed');
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
				
			|||||||
@ -470,13 +470,11 @@ class AIPipeline {
 | 
				
			|||||||
    pipelineStart: number,
 | 
					    pipelineStart: number,
 | 
				
			||||||
    toolsDataHash: string
 | 
					    toolsDataHash: string
 | 
				
			||||||
  ): Promise<{ completed: number; failed: number }> {
 | 
					  ): Promise<{ completed: number; failed: number }> {
 | 
				
			||||||
    // Evaluate ALL candidates handed over by the embeddings pre-filter.
 | 
					 | 
				
			||||||
    const candidates = context.filteredData.tools || [];
 | 
					    const candidates = context.filteredData.tools || [];
 | 
				
			||||||
    if (!Array.isArray(candidates) || candidates.length === 0) {
 | 
					    if (!Array.isArray(candidates) || candidates.length === 0) {
 | 
				
			||||||
      return { completed: completedTasks, failed: failedTasks };
 | 
					      return { completed: completedTasks, failed: failedTasks };
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Evaluate every candidate (no slicing here)
 | 
					 | 
				
			||||||
    for (let i = 0; i < candidates.length; i++) {
 | 
					    for (let i = 0; i < candidates.length; i++) {
 | 
				
			||||||
      const evaluationResult = await this.evaluateSpecificTool(context, candidates[i], i + 1, pipelineStart, toolsDataHash);
 | 
					      const evaluationResult = await this.evaluateSpecificTool(context, candidates[i], i + 1, pipelineStart, toolsDataHash);
 | 
				
			||||||
      if (evaluationResult.success) completedTasks++; else failedTasks++;
 | 
					      if (evaluationResult.success) completedTasks++; else failedTasks++;
 | 
				
			||||||
@ -484,15 +482,12 @@ class AIPipeline {
 | 
				
			|||||||
      await this.delay(this.config.microTaskDelay);
 | 
					      await this.delay(this.config.microTaskDelay);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // At this point, context.selectedTools may contain 0..N evaluated items (added by evaluateSpecificTool).
 | 
					 | 
				
			||||||
    // Now we sort them by AI-derived taskRelevance (after moderation) and keep ONLY the top 3 for UI.
 | 
					 | 
				
			||||||
    if (Array.isArray(context.selectedTools) && context.selectedTools.length > 0) {
 | 
					    if (Array.isArray(context.selectedTools) && context.selectedTools.length > 0) {
 | 
				
			||||||
      context.selectedTools.sort((a: any, b: any) => {
 | 
					      context.selectedTools.sort((a: any, b: any) => {
 | 
				
			||||||
        const ar = typeof a.taskRelevance === 'number' ? a.taskRelevance : -1;
 | 
					        const ar = typeof a.taskRelevance === 'number' ? a.taskRelevance : -1;
 | 
				
			||||||
        const br = typeof b.taskRelevance === 'number' ? b.taskRelevance : -1;
 | 
					        const br = typeof b.taskRelevance === 'number' ? b.taskRelevance : -1;
 | 
				
			||||||
        if (br !== ar) return br - ar;
 | 
					        if (br !== ar) return br - ar;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        // tie-breakers without domain heuristics:
 | 
					 | 
				
			||||||
        const aLen = (a.justification || '').length;
 | 
					        const aLen = (a.justification || '').length;
 | 
				
			||||||
        const bLen = (b.justification || '').length;
 | 
					        const bLen = (b.justification || '').length;
 | 
				
			||||||
        if (bLen !== aLen) return bLen - aLen;
 | 
					        if (bLen !== aLen) return bLen - aLen;
 | 
				
			||||||
@ -502,7 +497,6 @@ class AIPipeline {
 | 
				
			|||||||
        return aRank - bRank;
 | 
					        return aRank - bRank;
 | 
				
			||||||
      });
 | 
					      });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      // Keep top 3 only
 | 
					 | 
				
			||||||
      context.selectedTools = context.selectedTools.slice(0, 3);
 | 
					      context.selectedTools = context.selectedTools.slice(0, 3);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -877,7 +871,6 @@ class AIPipeline {
 | 
				
			|||||||
  ): Promise<MicroTaskResult> {
 | 
					  ): Promise<MicroTaskResult> {
 | 
				
			||||||
    const taskStart = Date.now();
 | 
					    const taskStart = Date.now();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Build prompt WITHOUT any baseline score
 | 
					 | 
				
			||||||
    const prompt = getPrompt('toolEvaluation', context.userQuery, tool, rank);
 | 
					    const prompt = getPrompt('toolEvaluation', context.userQuery, tool, rank);
 | 
				
			||||||
    const result = await this.callMicroTaskAI(prompt, context, 'tool-evaluation');
 | 
					    const result = await this.callMicroTaskAI(prompt, context, 'tool-evaluation');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -885,16 +878,13 @@ class AIPipeline {
 | 
				
			|||||||
      return result;
 | 
					      return result;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Parse strictly; do NOT provide a default with a score.
 | 
					 | 
				
			||||||
    const evaluation = JSONParser.safeParseJSON(result.content, null);
 | 
					    const evaluation = JSONParser.safeParseJSON(result.content, null);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Require a numeric score produced by the model; otherwise, don't add this tool.
 | 
					 | 
				
			||||||
    const aiProvided = evaluation && typeof evaluation.taskRelevance === 'number' && Number.isFinite(evaluation.taskRelevance)
 | 
					    const aiProvided = evaluation && typeof evaluation.taskRelevance === 'number' && Number.isFinite(evaluation.taskRelevance)
 | 
				
			||||||
      ? Math.round(evaluation.taskRelevance)
 | 
					      ? Math.round(evaluation.taskRelevance)
 | 
				
			||||||
      : null;
 | 
					      : null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (aiProvided === null) {
 | 
					    if (aiProvided === null) {
 | 
				
			||||||
      // Log the malformed output but avoid injecting a synthetic score.
 | 
					 | 
				
			||||||
      auditService.addAIDecision(
 | 
					      auditService.addAIDecision(
 | 
				
			||||||
        'tool-evaluation',
 | 
					        'tool-evaluation',
 | 
				
			||||||
        prompt,
 | 
					        prompt,
 | 
				
			||||||
@ -920,7 +910,6 @@ class AIPipeline {
 | 
				
			|||||||
    const moderatedTaskRelevance = this.moderateTaskRelevance(aiProvided);
 | 
					    const moderatedTaskRelevance = this.moderateTaskRelevance(aiProvided);
 | 
				
			||||||
    const priority = this.derivePriorityFromScore(moderatedTaskRelevance);
 | 
					    const priority = this.derivePriorityFromScore(moderatedTaskRelevance);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Keep original fields if present; coerce to strings/arrays safely.
 | 
					 | 
				
			||||||
    const detailed_explanation = String(evaluation?.detailed_explanation || '').trim();
 | 
					    const detailed_explanation = String(evaluation?.detailed_explanation || '').trim();
 | 
				
			||||||
    const implementation_approach = String(evaluation?.implementation_approach || '').trim();
 | 
					    const implementation_approach = String(evaluation?.implementation_approach || '').trim();
 | 
				
			||||||
    const pros = Array.isArray(evaluation?.pros) ? evaluation.pros : [];
 | 
					    const pros = Array.isArray(evaluation?.pros) ? evaluation.pros : [];
 | 
				
			||||||
 | 
				
			|||||||
@ -194,18 +194,15 @@ class ToolSelector {
 | 
				
			|||||||
    const softwareWithFullData = candidateSoftware.map(this.createToolData);
 | 
					    const softwareWithFullData = candidateSoftware.map(this.createToolData);
 | 
				
			||||||
    const conceptsWithFullData = candidateConcepts.map(this.createConceptData);
 | 
					    const conceptsWithFullData = candidateConcepts.map(this.createConceptData);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Embeddings are always ON → only use embedding limits
 | 
					 | 
				
			||||||
    const maxTools = Math.min(this.config.embeddingSelectionLimit, candidateTools.length);
 | 
					    const maxTools = Math.min(this.config.embeddingSelectionLimit, candidateTools.length);
 | 
				
			||||||
    const maxConcepts = Math.min(this.config.embeddingConceptsLimit, candidateConcepts.length);
 | 
					    const maxConcepts = Math.min(this.config.embeddingConceptsLimit, candidateConcepts.length);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Respect ratios first, then fill the remaining capacity
 | 
					 | 
				
			||||||
    const methodRatio = Math.max(0, Math.min(1, this.config.methodSelectionRatio));
 | 
					    const methodRatio = Math.max(0, Math.min(1, this.config.methodSelectionRatio));
 | 
				
			||||||
    const softwareRatio = Math.max(0, Math.min(1, this.config.softwareSelectionRatio));
 | 
					    const softwareRatio = Math.max(0, Math.min(1, this.config.softwareSelectionRatio));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    let methodLimit = Math.round(maxTools * methodRatio);
 | 
					    let methodLimit = Math.round(maxTools * methodRatio);
 | 
				
			||||||
    let softwareLimit = Math.round(maxTools * softwareRatio);
 | 
					    let softwareLimit = Math.round(maxTools * softwareRatio);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // If rounded sum exceeds maxTools, scale down proportionally
 | 
					 | 
				
			||||||
    if (methodLimit + softwareLimit > maxTools) {
 | 
					    if (methodLimit + softwareLimit > maxTools) {
 | 
				
			||||||
      const scale = maxTools / (methodLimit + softwareLimit);
 | 
					      const scale = maxTools / (methodLimit + softwareLimit);
 | 
				
			||||||
      methodLimit = Math.floor(methodLimit * scale);
 | 
					      methodLimit = Math.floor(methodLimit * scale);
 | 
				
			||||||
@ -217,7 +214,6 @@ class ToolSelector {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    const toolsToSend: any[] = [...methodsPrimary, ...softwarePrimary];
 | 
					    const toolsToSend: any[] = [...methodsPrimary, ...softwarePrimary];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Fill any remaining capacity from whichever pool still has candidates
 | 
					 | 
				
			||||||
    let mIdx = methodsPrimary.length;
 | 
					    let mIdx = methodsPrimary.length;
 | 
				
			||||||
    let sIdx = softwarePrimary.length;
 | 
					    let sIdx = softwarePrimary.length;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user