From 0c7c502b0395d6b8dabc8045e0115c6227b2a52d Mon Sep 17 00:00:00 2001 From: overcuriousity Date: Sat, 16 Aug 2025 18:15:20 +0200 Subject: [PATCH 01/25] first iteration - buggy --- src/components/AIQueryInterface.astro | 772 +++--------- src/pages/api/ai/embeddings-status.ts | 8 +- src/pages/api/ai/enhance-input.ts | 81 +- src/pages/api/ai/query.ts | 64 +- src/utils/aiPipeline.ts | 1666 ++++++------------------- src/utils/aiService.ts | 150 +++ src/utils/auditService.ts | 371 ++---- src/utils/clientUtils.ts | 98 +- src/utils/confidenceScoring.ts | 239 ++++ src/utils/embeddings.ts | 337 ++--- src/utils/jsonUtils.ts | 132 ++ src/utils/toolSelector.ts | 458 +++++++ 12 files changed, 1939 insertions(+), 2437 deletions(-) create mode 100644 src/utils/aiService.ts create mode 100644 src/utils/confidenceScoring.ts create mode 100644 src/utils/jsonUtils.ts create mode 100644 src/utils/toolSelector.ts diff --git a/src/components/AIQueryInterface.astro b/src/components/AIQueryInterface.astro index b513a6c..1c7d737 100644 --- a/src/components/AIQueryInterface.astro +++ b/src/components/AIQueryInterface.astro @@ -3,6 +3,7 @@ import { getToolsData } from '../utils/dataService.js'; + const data = await getToolsData(); const tools = data.tools; const phases = data.phases; @@ -216,6 +217,95 @@ const domainAgnosticSoftware = data['domain-agnostic-software'] || []; \ No newline at end of file diff --git a/src/pages/api/ai/embeddings-status.ts b/src/pages/api/ai/embeddings-status.ts index 9d9b9c8..0fc5a95 100644 --- a/src/pages/api/ai/embeddings-status.ts +++ b/src/pages/api/ai/embeddings-status.ts @@ -1,17 +1,19 @@ -// src/pages/api/ai/embeddings-status.ts +// src/pages/api/ai/embeddings-status.ts - Updated import type { APIRoute } from 'astro'; +import { embeddingsService } from '../../../utils/embeddings.js'; export const prerender = false; export const GET: APIRoute = async () => { try { - const { embeddingsService } = await import('../../../utils/embeddings.js'); await embeddingsService.waitForInitialization(); const stats = embeddingsService.getStats(); const status = stats.enabled && stats.initialized ? 'ready' : stats.enabled && !stats.initialized ? 'initializing' : 'disabled'; + console.log(`[EMBEDDINGS-STATUS-API] Service status: ${status}, stats:`, stats); + return new Response(JSON.stringify({ success: true, embeddings: stats, @@ -23,6 +25,8 @@ export const GET: APIRoute = async () => { }); } catch (error) { + console.error('[EMBEDDINGS-STATUS-API] Error checking embeddings status:', error); + return new Response(JSON.stringify({ success: false, embeddings: { enabled: false, initialized: false, count: 0 }, diff --git a/src/pages/api/ai/enhance-input.ts b/src/pages/api/ai/enhance-input.ts index 9eccb1c..9fe7dd8 100644 --- a/src/pages/api/ai/enhance-input.ts +++ b/src/pages/api/ai/enhance-input.ts @@ -1,23 +1,13 @@ -// src/pages/api/ai/enhance-input.ts - Enhanced AI service compatibility +// src/pages/api/ai/enhance-input.ts - Updated to use refactored services import type { APIRoute } from 'astro'; import { withAPIAuth } from '../../../utils/auth.js'; import { apiError, apiServerError, createAuthErrorResponse } from '../../../utils/api.js'; import { enqueueApiCall } from '../../../utils/rateLimitedQueue.js'; +import { aiService } from '../../../utils/aiService.js'; +import { JSONParser } from '../../../utils/jsonUtils.js'; export const prerender = false; -function getEnv(key: string): string { - const value = process.env[key]; - if (!value) { - throw new Error(`Missing environment variable: ${key}`); - } - return value; -} - -const AI_ENDPOINT = getEnv('AI_ANALYZER_ENDPOINT'); -const AI_ANALYZER_API_KEY = getEnv('AI_ANALYZER_API_KEY'); -const AI_ANALYZER_MODEL = getEnv('AI_ANALYZER_MODEL'); - const rateLimitStore = new Map(); const RATE_LIMIT_WINDOW = 60 * 1000; const RATE_LIMIT_MAX = 5; @@ -49,7 +39,7 @@ function checkRateLimit(userId: string): boolean { return true; } -function cleanupExpiredRateLimits() { +function cleanupExpiredRateLimits(): void { const now = Date.now(); for (const [userId, limit] of rateLimitStore.entries()) { if (now > limit.resetTime) { @@ -94,39 +84,6 @@ ${input} `.trim(); } -async function callAIService(prompt: string): Promise { - const endpoint = AI_ENDPOINT; - const apiKey = AI_ANALYZER_API_KEY; - const model = AI_ANALYZER_MODEL; - - let headers: Record = { - 'Content-Type': 'application/json' - }; - - if (apiKey) { - headers['Authorization'] = `Bearer ${apiKey}`; - console.log('[ENHANCE API] Using API key authentication'); - } else { - console.log('[ENHANCE API] No API key - making request without authentication'); - } - - const requestBody = { - model, - messages: [{ role: 'user', content: prompt }], - max_tokens: 300, - temperature: 0.7, - top_p: 0.9, - frequency_penalty: 0.2, - presence_penalty: 0.1 - }; - - return fetch(`${endpoint}/v1/chat/completions`, { - method: 'POST', - headers, - body: JSON.stringify(requestBody) - }); -} - export const POST: APIRoute = async ({ request }) => { try { const authResult = await withAPIAuth(request, 'ai'); @@ -155,28 +112,26 @@ export const POST: APIRoute = async ({ request }) => { const systemPrompt = createEnhancementPrompt(sanitizedInput); const taskId = `enhance_${userId}_${Date.now()}_${Math.random().toString(36).substr(2, 4)}`; - const aiResponse = await enqueueApiCall(() => callAIService(systemPrompt), taskId); + console.log(`[ENHANCE-API] Processing enhancement request for user: ${userId}`); + + const aiResponse = await enqueueApiCall(() => + aiService.callAI(systemPrompt, { + maxTokens: 300, + temperature: 0.7 + }), taskId); - if (!aiResponse.ok) { - const errorText = await aiResponse.text(); - console.error('[ENHANCE API] AI enhancement error:', errorText, 'Status:', aiResponse.status); - return apiServerError.unavailable('Enhancement service unavailable'); - } - - const aiData = await aiResponse.json(); - const aiContent = aiData.choices?.[0]?.message?.content; - - if (!aiContent) { + if (!aiResponse.content) { return apiServerError.unavailable('No enhancement response'); } let questions; try { - const cleanedContent = aiContent + const cleanedContent = aiResponse.content .replace(/^```json\s*/i, '') .replace(/\s*```\s*$/, '') .trim(); - questions = JSON.parse(cleanedContent); + + questions = JSONParser.safeParseJSON(cleanedContent, []); if (!Array.isArray(questions)) { throw new Error('Response is not an array'); @@ -198,11 +153,11 @@ export const POST: APIRoute = async ({ request }) => { } } catch (error) { - console.error('Failed to parse enhancement response:', aiContent); + console.error('[ENHANCE-API] Failed to parse enhancement response:', aiResponse.content); questions = []; } - console.log(`[ENHANCE API] User: ${userId}, Forensics Questions: ${questions.length}, Input length: ${sanitizedInput.length}`); + console.log(`[ENHANCE-API] User: ${userId}, Questions generated: ${questions.length}, Input length: ${sanitizedInput.length}`); return new Response(JSON.stringify({ success: true, @@ -215,7 +170,7 @@ export const POST: APIRoute = async ({ request }) => { }); } catch (error) { - console.error('Enhancement error:', error); + console.error('[ENHANCE-API] Enhancement error:', error); return apiServerError.internal('Enhancement processing failed'); } }; \ No newline at end of file diff --git a/src/pages/api/ai/query.ts b/src/pages/api/ai/query.ts index 85379fd..ade956a 100644 --- a/src/pages/api/ai/query.ts +++ b/src/pages/api/ai/query.ts @@ -1,4 +1,4 @@ -// src/pages/api/ai/query.ts +// src/pages/api/ai/query.ts - Updated to use refactored services import type { APIRoute } from 'astro'; import { withAPIAuth } from '../../../utils/auth.js'; import { apiError, apiServerError, createAuthErrorResponse } from '../../../utils/api.js'; @@ -20,15 +20,14 @@ const MAIN_RATE_LIMIT_MAX = parseInt(process.env.AI_RATE_LIMIT_MAX_REQUESTS || ' const MICRO_TASK_TOTAL_LIMIT = parseInt(process.env.AI_MICRO_TASK_TOTAL_LIMIT || '50', 10); function sanitizeInput(input: string): string { - let sanitized = input + return input .replace(/```[\s\S]*?```/g, '[CODE_BLOCK_REMOVED]') .replace(/\<\/?[^>]+(>|$)/g, '') .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]') - .trim(); - - sanitized = sanitized.slice(0, 2000).replace(/\s+/g, ' '); - return sanitized; + .trim() + .slice(0, 2000) + .replace(/\s+/g, ' '); } function checkRateLimit(userId: string): { allowed: boolean; reason?: string; microTasksRemaining?: number } { @@ -77,7 +76,7 @@ function incrementMicroTaskCount(userId: string, aiCallsMade: number): void { } } -function cleanupExpiredRateLimits() { +function cleanupExpiredRateLimits(): void { const now = Date.now(); const maxStoreSize = 1000; @@ -117,51 +116,52 @@ export const POST: APIRoute = async ({ request }) => { const body = await request.json(); const { query, mode = 'workflow', taskId: clientTaskId } = body; - console.log(`[MICRO-TASK API] Received request - TaskId: ${clientTaskId}, Mode: ${mode}, Query length: ${query?.length || 0}`); - console.log(`[MICRO-TASK API] Micro-task calls remaining: ${rateLimitResult.microTasksRemaining}`); + console.log(`[AI-API] Received request - TaskId: ${clientTaskId}, Mode: ${mode}, Query length: ${query?.length || 0}`); + console.log(`[AI-API] Micro-task calls remaining: ${rateLimitResult.microTasksRemaining}`); if (!query || typeof query !== 'string') { - console.log(`[MICRO-TASK API] Invalid query for task ${clientTaskId}`); + console.log(`[AI-API] Invalid query for task ${clientTaskId}`); return apiError.badRequest('Query required'); } if (!['workflow', 'tool'].includes(mode)) { - console.log(`[MICRO-TASK API] Invalid mode for task ${clientTaskId}: ${mode}`); + console.log(`[AI-API] Invalid mode for task ${clientTaskId}: ${mode}`); return apiError.badRequest('Invalid mode. Must be "workflow" or "tool"'); } const sanitizedQuery = sanitizeInput(query); if (sanitizedQuery.includes('[FILTERED]')) { - console.log(`[MICRO-TASK API] Filtered input detected for task ${clientTaskId}`); + console.log(`[AI-API] Filtered input detected for task ${clientTaskId}`); return apiError.badRequest('Invalid input detected'); } const taskId = clientTaskId || `ai_${userId}_${Date.now()}_${Math.random().toString(36).substr(2, 6)}`; - console.log(`[MICRO-TASK API] About to enqueue micro-task pipeline ${taskId}`); + console.log(`[AI-API] Enqueueing pipeline task ${taskId}`); const result = await enqueueApiCall(() => aiPipeline.processQuery(sanitizedQuery, mode) , taskId); if (!result || !result.recommendation) { - return apiServerError.unavailable('No response from micro-task AI pipeline'); + return apiServerError.unavailable('No response from AI pipeline'); } const stats = result.processingStats; const estimatedAICallsMade = stats.microTasksCompleted + stats.microTasksFailed; incrementMicroTaskCount(userId, estimatedAICallsMade); - console.log(`[MICRO-TASK API] Pipeline completed for ${taskId}:`); - console.log(` - Mode: ${mode}`); - console.log(` - User: ${userId}`); - console.log(` - Query length: ${sanitizedQuery.length}`); - console.log(` - Processing time: ${stats.processingTimeMs}ms`); - console.log(` - Micro-tasks completed: ${stats.microTasksCompleted}`); - console.log(` - Micro-tasks failed: ${stats.microTasksFailed}`); - console.log(` - Estimated AI calls: ${estimatedAICallsMade}`); - console.log(` - Embeddings used: ${stats.embeddingsUsed}`); - console.log(` - Final items: ${stats.finalSelectedItems}`); + console.log(`[AI-API] Pipeline completed for ${taskId}:`, { + mode, + user: userId, + queryLength: sanitizedQuery.length, + processingTime: stats.processingTimeMs, + microTasksCompleted: stats.microTasksCompleted, + microTasksFailed: stats.microTasksFailed, + estimatedAICalls: estimatedAICallsMade, + embeddingsUsed: stats.embeddingsUsed, + finalItems: stats.finalSelectedItems + }); const currentLimit = rateLimitStore.get(userId); const remainingMicroTasks = currentLimit ? @@ -175,7 +175,7 @@ export const POST: APIRoute = async ({ request }) => { query: sanitizedQuery, processingStats: { ...result.processingStats, - pipelineType: 'micro-task', + pipelineType: 'refactored', microTasksSuccessRate: stats.microTasksCompleted / (stats.microTasksCompleted + stats.microTasksFailed), averageTaskTime: stats.processingTimeMs / (stats.microTasksCompleted + stats.microTasksFailed), estimatedAICallsMade @@ -191,18 +191,16 @@ export const POST: APIRoute = async ({ request }) => { }); } catch (error) { - console.error('[MICRO-TASK API] Pipeline error:', error); + console.error('[AI-API] Pipeline error:', error); if (error.message.includes('embeddings')) { - return apiServerError.unavailable('Embeddings service error - using AI fallback'); - } else if (error.message.includes('micro-task')) { - return apiServerError.unavailable('Micro-task pipeline error - some analysis steps failed'); - } else if (error.message.includes('selector')) { - return apiServerError.unavailable('AI selector service error'); + return apiServerError.unavailable('Embeddings service error'); + } else if (error.message.includes('AI')) { + return apiServerError.unavailable('AI service error'); } else if (error.message.includes('rate limit')) { - return apiError.rateLimit('AI service rate limits exceeded during micro-task processing'); + return apiError.rateLimit('AI service rate limits exceeded'); } else { - return apiServerError.internal('Micro-task AI pipeline error'); + return apiServerError.internal('AI pipeline error'); } } }; \ No newline at end of file diff --git a/src/utils/aiPipeline.ts b/src/utils/aiPipeline.ts index fbd64a9..bf61097 100644 --- a/src/utils/aiPipeline.ts +++ b/src/utils/aiPipeline.ts @@ -1,18 +1,17 @@ -// src/utils/aiPipeline.ts - +// src/utils/aiPipeline.ts - Refactored import { getCompressedToolsDataForAI } from './dataService.js'; -import { embeddingsService, type EmbeddingData, type SimilarityResult } from './embeddings.js'; -import { AI_PROMPTS, getPrompt } from '../config/prompts.js'; -import { isToolHosted } from './toolHelpers.js'; +import { aiService } from './aiService.js'; +import { toolSelector, type SelectionContext } from './toolSelector.js'; +import { confidenceScoring, type AnalysisContext } from './confidenceScoring.js'; +import { embeddingsService } from './embeddings.js'; import { auditService, type AuditEntry } from './auditService.js'; -import dotenv from 'dotenv'; +import { getPrompt } from '../config/prompts.js'; +import 'dotenv/config'; -dotenv.config(); - -interface AIConfig { - endpoint: string; - apiKey: string; - model: string; +interface PipelineConfig { + microTaskDelay: number; + maxContextTokens: number; + maxPromptTokens: number; } interface MicroTaskResult { @@ -36,7 +35,7 @@ interface AnalysisResult { }; } -interface AnalysisContext { +interface PipelineContext { userQuery: string; mode: string; filteredData: any; @@ -61,774 +60,187 @@ interface AnalysisContext { }>; seenToolNames: Set; embeddingsSimilarities: Map; - aiSelectedTools?: any[]; - aiSelectedConcepts?: any[]; } -interface ConfidenceMetrics { - overall: number; - semanticRelevance: number; - taskSuitability: number; - uncertaintyFactors: string[]; - strengthIndicators: string[]; -} - -class ImprovedMicroTaskAIPipeline { - private config: AIConfig; - private maxSelectedItems: number; - private embeddingCandidates: number; - private similarityThreshold: number; - private microTaskDelay: number; - private embeddingSelectionLimit: number; - private embeddingConceptsLimit: number; - private noEmbeddingsToolLimit: number; - private noEmbeddingsConceptLimit: number; - private embeddingsMinTools: number; - private embeddingsMaxReductionRatio: number; - private methodSelectionRatio: number; - private softwareSelectionRatio: number; - private maxContextTokens: number; - private maxPromptTokens: number; - private confidenceConfig: { - semanticWeight: number; - suitabilityWeight: number; - minimumThreshold: number; - mediumThreshold: number; - highThreshold: number; - }; +class AIPipeline { + private config: PipelineConfig; constructor() { this.config = { - endpoint: this.getRequiredEnv('AI_ANALYZER_ENDPOINT'), - apiKey: this.getRequiredEnv('AI_ANALYZER_API_KEY'), - model: this.getRequiredEnv('AI_ANALYZER_MODEL') + microTaskDelay: parseInt(process.env.AI_MICRO_TASK_DELAY_MS || '500', 10), + maxContextTokens: parseInt(process.env.AI_MAX_CONTEXT_TOKENS || '4000', 10), + maxPromptTokens: parseInt(process.env.AI_MAX_PROMPT_TOKENS || '1500', 10) }; - this.maxSelectedItems = this.getEnvInt('AI_MAX_SELECTED_ITEMS', 25); - this.embeddingCandidates = this.getEnvInt('AI_EMBEDDING_CANDIDATES', 50); - this.similarityThreshold = this.getEnvFloat('AI_SIMILARITY_THRESHOLD', 0.3); - this.microTaskDelay = this.getEnvInt('AI_MICRO_TASK_DELAY_MS', 500); - this.embeddingSelectionLimit = this.getEnvInt('AI_EMBEDDING_SELECTION_LIMIT', 30); - this.embeddingConceptsLimit = this.getEnvInt('AI_EMBEDDING_CONCEPTS_LIMIT', 15); - this.noEmbeddingsToolLimit = this.getEnvInt('AI_NO_EMBEDDINGS_TOOL_LIMIT', 25); - this.noEmbeddingsConceptLimit = this.getEnvInt('AI_NO_EMBEDDINGS_CONCEPT_LIMIT', 10); - this.embeddingsMinTools = this.getEnvInt('AI_EMBEDDINGS_MIN_TOOLS', 8); - this.embeddingsMaxReductionRatio = this.getEnvFloat('AI_EMBEDDINGS_MAX_REDUCTION_RATIO', 0.75); - this.methodSelectionRatio = this.getEnvFloat('AI_METHOD_SELECTION_RATIO', 0.4); - this.softwareSelectionRatio = this.getEnvFloat('AI_SOFTWARE_SELECTION_RATIO', 0.5); - this.maxContextTokens = this.getEnvInt('AI_MAX_CONTEXT_TOKENS', 4000); - this.maxPromptTokens = this.getEnvInt('AI_MAX_PROMPT_TOKENS', 1500); - - this.confidenceConfig = { - semanticWeight: this.getEnvFloat('CONFIDENCE_SEMANTIC_WEIGHT', 0.3), - suitabilityWeight: this.getEnvFloat('CONFIDENCE_SUITABILITY_WEIGHT', 0.7), - minimumThreshold: this.getEnvInt('CONFIDENCE_MINIMUM_THRESHOLD', 40), - mediumThreshold: this.getEnvInt('CONFIDENCE_MEDIUM_THRESHOLD', 60), - highThreshold: this.getEnvInt('CONFIDENCE_HIGH_THRESHOLD', 80) - }; - - console.log('[AI-PIPELINE] Initialized with audit service integration'); + console.log('[AI-PIPELINE] Initialized orchestration pipeline'); } - private getRequiredEnv(key: string): string { - const value = process.env[key]; - if (!value) { - throw new Error(`Missing required environment variable: ${key}`); - } - return value; - } - - private getEnvInt(key: string, defaultValue: number): number { - const value = process.env[key]; - return value ? parseInt(value, 10) : defaultValue; - } - - private getEnvFloat(key: string, defaultValue: number): number { - const value = process.env[key]; - return value ? parseFloat(value) : defaultValue; - } - - // SIMPLIFIED AUDIT INTEGRATION - Use auditService instead of local implementation - private addAuditEntry( - context: AnalysisContext, - phase: string, - action: string, - input: any, - output: any, - confidence: number, - startTime: number, - metadata: Record = {} - ): void { - auditService.addEntry(phase, action, input, output, confidence, startTime, metadata); - } - - private calculateSelectionConfidence(result: any, candidateCount: number): number { - if (!result?.selectedTools) return 30; + async processQuery(userQuery: string, mode: string): Promise { + const startTime = Date.now(); + let completedTasks = 0; + let failedTasks = 0; - const selectionRatio = result.selectedTools.length / candidateCount; - const hasReasoning = result.reasoning && result.reasoning.length > 50; - - let confidence = 60; - - if (selectionRatio > 0.05 && selectionRatio < 0.3) confidence += 20; - else if (selectionRatio <= 0.05) confidence -= 10; - else confidence -= 15; - - if (hasReasoning) confidence += 15; - if (result.selectedConcepts?.length > 0) confidence += 5; - - return Math.min(95, Math.max(25, confidence)); - } + console.log('[AI-PIPELINE] Starting', mode, 'analysis pipeline'); - private estimateTokens(text: string): number { - return Math.ceil(text.length / 4); - } + // Initialize audit trail + auditService.clearAuditTrail(); - private addToContextHistory(context: AnalysisContext, newEntry: string): void { - const entryTokens = this.estimateTokens(newEntry); - - context.contextHistory.push(newEntry); - context.currentContextLength += entryTokens; - - while (context.currentContextLength > this.maxContextTokens && context.contextHistory.length > 1) { - const removed = context.contextHistory.shift()!; - context.currentContextLength -= this.estimateTokens(removed); - } - } - - private safeParseJSON(jsonString: string, fallback: any = null): any { try { - let cleaned = jsonString.trim(); + const toolsData = await getCompressedToolsDataForAI(); - const jsonBlockPatterns = [ - /```json\s*([\s\S]*?)\s*```/i, - /```\s*([\s\S]*?)\s*```/i, - /\{[\s\S]*\}/, - ]; - - for (const pattern of jsonBlockPatterns) { - const match = cleaned.match(pattern); - if (match) { - cleaned = match[1] || match[0]; - break; - } - } - - if (!cleaned.endsWith('}') && !cleaned.endsWith(']')) { - console.warn('[AI-PIPELINE] JSON appears truncated, attempting recovery'); - - let braceCount = 0; - let bracketCount = 0; - let inString = false; - let escaped = false; - let lastCompleteStructure = ''; - - for (let i = 0; i < cleaned.length; i++) { - const char = cleaned[i]; - - if (escaped) { - escaped = false; - continue; - } - - if (char === '\\') { - escaped = true; - continue; - } - - if (char === '"' && !escaped) { - inString = !inString; - continue; - } - - if (!inString) { - if (char === '{') braceCount++; - if (char === '}') braceCount--; - if (char === '[') bracketCount++; - if (char === ']') bracketCount--; - - if (braceCount === 0 && bracketCount === 0 && (char === '}' || char === ']')) { - lastCompleteStructure = cleaned.substring(0, i + 1); - } - } - } - - if (lastCompleteStructure) { - cleaned = lastCompleteStructure; - } else { - if (braceCount > 0) cleaned += '}'; - if (bracketCount > 0) cleaned += ']'; - } - } - - const parsed = JSON.parse(cleaned); - - if (parsed && typeof parsed === 'object') { - if (!parsed.selectedTools) parsed.selectedTools = []; - if (!parsed.selectedConcepts) parsed.selectedConcepts = []; - if (!Array.isArray(parsed.selectedTools)) parsed.selectedTools = []; - if (!Array.isArray(parsed.selectedConcepts)) parsed.selectedConcepts = []; - } - - return parsed; - - } catch (error) { - console.warn('[AI-PIPELINE] JSON parsing failed:', error.message); - - if (jsonString.includes('selectedTools') || jsonString.includes('selectedConcepts')) { - const selectedTools: string[] = []; - const selectedConcepts: string[] = []; - - const toolsMatch = jsonString.match(/"selectedTools"\s*:\s*\[([\s\S]*?)\]/i); - if (toolsMatch) { - const toolMatches = toolsMatch[1].match(/"([^"]+)"/g); - if (toolMatches) { - selectedTools.push(...toolMatches.map(match => match.replace(/"/g, ''))); - } - } - - const conceptsMatch = jsonString.match(/"selectedConcepts"\s*:\s*\[([\s\S]*?)\]/i); - if (conceptsMatch) { - const conceptMatches = conceptsMatch[1].match(/"([^"]+)"/g); - if (conceptMatches) { - selectedConcepts.push(...conceptMatches.map(match => match.replace(/"/g, ''))); - } - } - - if (selectedTools.length === 0 && selectedConcepts.length === 0) { - const allMatches = jsonString.match(/"([^"]+)"/g); - if (allMatches) { - const possibleNames = allMatches - .map(match => match.replace(/"/g, '')) - .filter(name => - name.length > 2 && - !['selectedTools', 'selectedConcepts', 'reasoning'].includes(name) && - !name.includes(':') && - !name.match(/^\d+$/) - ) - .slice(0, 15); - - selectedTools.push(...possibleNames); - } - } - - if (selectedTools.length > 0 || selectedConcepts.length > 0) { - console.log('[AI-PIPELINE] JSON recovery successful:', selectedTools.length, 'tools,', selectedConcepts.length, 'concepts'); - return { - selectedTools, - selectedConcepts, - reasoning: 'Recovered from malformed JSON response' - }; - } - } - - return fallback; - } - } - - private addToolToSelection( - context: AnalysisContext, - tool: any, - phase: string, - priority: string, - justification?: string, - taskRelevance?: number, - limitations?: string[] - ): boolean { - context.seenToolNames.add(tool.name); - if (!context.selectedTools) context.selectedTools = []; - - context.selectedTools.push({ - tool, - phase, - priority, - justification, - taskRelevance, - limitations - }); - - return true; - } - - private generatePhaseQueryTemplates(phases: any[]): Record { - const templates: Record = {}; - - phases.forEach((phase: any) => { - if (phase?.id && phase?.name) { - const phaseKeywords = [ - 'forensic', - phase.name.toLowerCase(), - ...(phase.description ? phase.description.toLowerCase().split(' ').filter((word: string) => word.length > 3) : []), - ...(phase.key_activities || []).map((activity: string) => activity.toLowerCase()), - ...(phase.typical_tools || []).map((tool: string) => tool.toLowerCase()) - ].join(' '); - - templates[phase.id] = phaseKeywords; - } - }); - - return templates; - } - - private async getIntelligentCandidates( - userQuery: string, - toolsData: any, - mode: string, - context: AnalysisContext - ) { - let candidateTools: any[] = []; - let candidateConcepts: any[] = []; - let selectionMethod = 'unknown'; - - context.embeddingsSimilarities = new Map(); - - try { - await embeddingsService.waitForInitialization(); - } catch (error) { - console.error('[AI-PIPELINE] Embeddings initialization failed:', error); - } - - if (embeddingsService.isEnabled()) { - const embeddingsStart = Date.now(); - const similarItems = await embeddingsService.findSimilar( + const context: PipelineContext = { userQuery, - this.embeddingCandidates, - this.similarityThreshold - ) as SimilarityResult[]; + mode, + filteredData: {}, + contextHistory: [], + maxContextLength: this.config.maxContextTokens, + currentContextLength: 0, + seenToolNames: new Set(), + embeddingsSimilarities: new Map() + }; + + // Phase 1: Get intelligent tool candidates + console.log('[AI-PIPELINE] Phase 1: Tool candidate selection'); + const candidateData = await toolSelector.getIntelligentCandidates(userQuery, toolsData, mode, context); - console.log('[AI-PIPELINE] Embeddings found', similarItems.length, 'similar items'); + context.filteredData = candidateData; - similarItems.forEach(item => { - context.embeddingsSimilarities.set(item.name, item.similarity); - }); + this.addAuditEntry( + 'initialization', + 'pipeline-start', + { userQuery, mode, toolsDataLoaded: !!toolsData }, + { candidateTools: candidateData.tools.length, candidateConcepts: candidateData.concepts.length }, + 90, + startTime, + { selectionMethod: candidateData.selectionMethod } + ); + + // Phase 2: Contextual analysis micro-tasks + console.log('[AI-PIPELINE] Phase 2: Contextual analysis'); - const toolsMap = new Map(toolsData.tools.map((tool: any) => [tool.name, tool])); - const conceptsMap = new Map(toolsData.concepts.map((concept: any) => [concept.name, concept])); + const analysisResult = await this.analyzeScenario(context); + if (analysisResult.success) completedTasks++; else failedTasks++; + await this.delay(this.config.microTaskDelay); + + const approachResult = await this.generateApproach(context); + if (approachResult.success) completedTasks++; else failedTasks++; + await this.delay(this.config.microTaskDelay); + + const considerationsResult = await this.generateCriticalConsiderations(context); + if (considerationsResult.success) completedTasks++; else failedTasks++; + await this.delay(this.config.microTaskDelay); + + // Phase 3: Tool-specific analysis + console.log('[AI-PIPELINE] Phase 3: Tool-specific analysis'); - const similarTools = similarItems - .filter((item: any) => item.type === 'tool') - .map((item: any) => toolsMap.get(item.name)) - .filter((tool: any): tool is NonNullable => tool !== undefined && tool !== null); - - const similarConcepts = similarItems - .filter((item: any) => item.type === 'concept') - .map((item: any) => conceptsMap.get(item.name)) - .filter((concept: any): concept is NonNullable => concept !== undefined && concept !== null); - - const totalAvailableTools = toolsData.tools.length; - const reductionRatio = similarTools.length / totalAvailableTools; - - if (similarTools.length >= this.embeddingsMinTools && reductionRatio <= this.embeddingsMaxReductionRatio) { - candidateTools = similarTools; - candidateConcepts = similarConcepts; - selectionMethod = 'embeddings_candidates'; - - console.log('[AI-PIPELINE] Using embeddings filtering:', totalAvailableTools, '→', similarTools.length, 'tools'); + if (mode === 'workflow') { + await this.processWorkflowMode(context, toolsData, completedTasks, failedTasks); } else { - console.log('[AI-PIPELINE] Embeddings filtering insufficient, using full dataset'); - candidateTools = toolsData.tools; - candidateConcepts = toolsData.concepts; - selectionMethod = 'full_dataset'; + await this.processToolMode(context, completedTasks, failedTasks); } + + // Phase 4: Knowledge and finalization + console.log('[AI-PIPELINE] Phase 4: Knowledge synthesis'); + const knowledgeResult = await this.selectBackgroundKnowledge(context); + if (knowledgeResult.success) completedTasks++; else failedTasks++; + await this.delay(this.config.microTaskDelay); + + const finalResult = await this.generateFinalRecommendations(context); + if (finalResult.success) completedTasks++; else failedTasks++; + + // Build final recommendation + const recommendation = this.buildRecommendation(context, mode, finalResult.content); + this.addAuditEntry( - context, - 'retrieval', - 'embeddings-search', - { query: userQuery, threshold: this.similarityThreshold, candidates: this.embeddingCandidates }, - { - candidatesFound: similarItems.length, - reductionRatio: reductionRatio, - usingEmbeddings: selectionMethod === 'embeddings_candidates', - totalAvailable: totalAvailableTools, - filtered: similarTools.length - }, - selectionMethod === 'embeddings_candidates' ? 85 : 60, - embeddingsStart, - { selectionMethod, embeddingsEnabled: true } + 'completion', + 'pipeline-end', + { completedTasks, failedTasks }, + { finalRecommendation: !!recommendation, auditEntriesGenerated: auditService.getCurrentAuditTrail().length }, + completedTasks > failedTasks ? 85 : 60, + startTime, + { totalProcessingTimeMs: Date.now() - startTime } ); - } else { - console.log('[AI-PIPELINE] Embeddings disabled, using full dataset'); - candidateTools = toolsData.tools; - candidateConcepts = toolsData.concepts; - selectionMethod = 'full_dataset'; - } - const finalSelection = await this.aiSelectionWithFullData( - userQuery, - candidateTools, - candidateConcepts, - mode, - selectionMethod, - context - ); - - return { - tools: finalSelection.selectedTools, - concepts: finalSelection.selectedConcepts, - domains: toolsData.domains, - phases: toolsData.phases, - 'domain-agnostic-software': toolsData['domain-agnostic-software'] - }; - } + const processingStats = { + embeddingsUsed: embeddingsService.isEnabled(), + candidatesFromEmbeddings: candidateData.tools.length, + finalSelectedItems: (context.selectedTools?.length || 0) + (context.backgroundKnowledge?.length || 0), + processingTimeMs: Date.now() - startTime, + microTasksCompleted: completedTasks, + microTasksFailed: failedTasks, + contextContinuityUsed: true + }; - private async aiSelectionWithFullData( - userQuery: string, - candidateTools: any[], - candidateConcepts: any[], - mode: string, - selectionMethod: string, - context: AnalysisContext - ) { - const selectionStart = Date.now(); - - const candidateMethods = candidateTools.filter((tool: any) => tool && tool.type === 'method'); - const candidateSoftware = candidateTools.filter((tool: any) => tool && tool.type === 'software'); - - console.log('[AI-PIPELINE] Tool selection candidates:', candidateMethods.length, 'methods,', candidateSoftware.length, 'software,', candidateConcepts.length, 'concepts'); - - const methodsWithFullData = candidateMethods.map(this.createToolData); - const softwareWithFullData = candidateSoftware.map(this.createToolData); - const conceptsWithFullData = candidateConcepts.map(this.createConceptData); + console.log('[AI-PIPELINE] Pipeline completed successfully:', { + mode, + processingTimeMs: processingStats.processingTimeMs, + completedTasks, + failedTasks, + finalItems: processingStats.finalSelectedItems + }); - let toolsToSend: any[]; - let conceptsToSend: any[]; - - if (selectionMethod === 'embeddings_candidates') { - const totalLimit = this.embeddingSelectionLimit; - const methodLimit = Math.ceil(totalLimit * this.methodSelectionRatio); - const softwareLimit = Math.floor(totalLimit * this.softwareSelectionRatio); - - toolsToSend = [ - ...methodsWithFullData.slice(0, methodLimit), - ...softwareWithFullData.slice(0, softwareLimit) - ]; - - const remainingCapacity = totalLimit - toolsToSend.length; - if (remainingCapacity > 0) { - if (methodsWithFullData.length > methodLimit) { - toolsToSend.push(...methodsWithFullData.slice(methodLimit, methodLimit + remainingCapacity)); - } else if (softwareWithFullData.length > softwareLimit) { - toolsToSend.push(...softwareWithFullData.slice(softwareLimit, softwareLimit + remainingCapacity)); - } - } - - conceptsToSend = conceptsWithFullData.slice(0, this.embeddingConceptsLimit); - } else { - const maxTools = this.noEmbeddingsToolLimit; - const maxConcepts = this.noEmbeddingsConceptLimit; - const methodLimit = Math.ceil(maxTools * 0.4); - const softwareLimit = Math.floor(maxTools * 0.5); - - toolsToSend = [ - ...methodsWithFullData.slice(0, methodLimit), - ...softwareWithFullData.slice(0, softwareLimit) - ]; - - const remainingCapacity = maxTools - toolsToSend.length; - if (remainingCapacity > 0) { - if (methodsWithFullData.length > methodLimit) { - toolsToSend.push(...methodsWithFullData.slice(methodLimit, methodLimit + remainingCapacity)); - } else if (softwareWithFullData.length > softwareLimit) { - toolsToSend.push(...softwareWithFullData.slice(softwareLimit, softwareLimit + remainingCapacity)); - } - } - - conceptsToSend = conceptsWithFullData.slice(0, maxConcepts); - } + // Finalize audit trail + const finalAuditTrail = auditService.finalizeAuditTrail(); - const basePrompt = getPrompt('toolSelection', mode, userQuery, selectionMethod, this.maxSelectedItems); - const prompt = getPrompt('toolSelectionWithData', basePrompt, toolsToSend, conceptsToSend); - - const estimatedTokens = this.estimateTokens(prompt); - console.log('[AI-PIPELINE] Sending to AI:', toolsToSend.filter((t: any) => t.type === 'method').length, 'methods,', toolsToSend.filter((t: any) => t.type === 'software').length, 'software,', conceptsToSend.length, 'concepts'); - - if (estimatedTokens > 35000) { - console.warn('[AI-PIPELINE] WARNING: Prompt tokens may exceed model limits:', estimatedTokens); - } - - try { - const response = await this.callAI(prompt, 2500); - const result = this.safeParseJSON(response, null); - - if (!result || !Array.isArray(result.selectedTools) || !Array.isArray(result.selectedConcepts)) { - console.error('[AI-PIPELINE] AI selection returned invalid structure'); - - this.addAuditEntry( - context, - 'selection', - 'ai-tool-selection-failed', - { candidateCount: candidateTools.length, mode }, - { error: 'Invalid JSON structure' }, - 10, - selectionStart, - { aiModel: this.config.model, selectionMethod } - ); - - throw new Error('AI selection failed to return valid tool and concept selection'); - } - - const totalSelected = result.selectedTools.length + result.selectedConcepts.length; - if (totalSelected === 0) { - throw new Error('AI selection returned empty selection'); - } - - const toolsMap = new Map(candidateTools.map((tool: any) => [tool.name, tool])); - const conceptsMap = new Map(candidateConcepts.map((concept: any) => [concept.name, concept])); - - const selectedTools = result.selectedTools - .map((name: string) => toolsMap.get(name)) - .filter((tool: any): tool is NonNullable => tool !== undefined && tool !== null); - - const selectedConcepts = result.selectedConcepts - .map((name: string) => conceptsMap.get(name)) - .filter((concept: any): concept is NonNullable => concept !== undefined && concept !== null); - - const selectedMethods = selectedTools.filter((t: any) => t && t.type === 'method'); - const selectedSoftware = selectedTools.filter((t: any) => t && t.type === 'software'); - - console.log('[AI-PIPELINE] AI selected:', selectedMethods.length, 'methods,', selectedSoftware.length, 'software,', selectedConcepts.length, 'concepts'); - - const confidence = this.calculateSelectionConfidence(result, candidateTools.length + candidateConcepts.length); - - this.addAuditEntry( - context, - 'selection', - 'ai-tool-selection', - { candidateCount: candidateTools.length, mode }, - { - selectedMethodCount: selectedMethods.length, - selectedSoftwareCount: selectedSoftware.length, - selectedConceptCount: selectedConcepts.length, - reasoning: result.reasoning?.slice(0, 200), - methodBalance: `${((selectedMethods.length / (selectedTools.length || 1)) * 100).toFixed(0)}%` + return { + recommendation: { + ...recommendation, + auditTrail: auditService.isEnabled() ? finalAuditTrail : undefined }, - confidence, - selectionStart, - { aiModel: this.config.model, selectionMethod } - ); - - return { selectedTools, selectedConcepts }; + processingStats + }; } catch (error) { - console.error('[AI-PIPELINE] AI selection failed:', error); - - this.addAuditEntry( - context, - 'selection', - 'ai-tool-selection-error', - { candidateCount: candidateTools.length, mode }, - { error: error.message }, - 5, - selectionStart, - { aiModel: this.config.model, selectionMethod } - ); + console.error('[AI-PIPELINE] Pipeline failed:', error); throw error; } } - private createToolData = (tool: any) => ({ - name: tool.name, - type: tool.type, - description: tool.description, - domains: tool.domains, - phases: tool.phases, - platforms: tool.platforms || [], - tags: tool.tags || [], - skillLevel: tool.skillLevel, - license: tool.license, - accessType: tool.accessType, - projectUrl: tool.projectUrl, - knowledgebase: tool.knowledgebase, - related_concepts: tool.related_concepts || [], - related_software: tool.related_software || [] - }); - - private createConceptData = (concept: any) => ({ - name: concept.name, - type: 'concept', - description: concept.description, - domains: concept.domains, - phases: concept.phases, - tags: concept.tags || [], - skillLevel: concept.skillLevel, - related_concepts: concept.related_concepts || [], - related_software: concept.related_software || [] - }); - - private async delay(ms: number): Promise { - return new Promise(resolve => setTimeout(resolve, ms)); - } - - private async callMicroTaskAI( - prompt: string, - context: AnalysisContext, - maxTokens: number = 500 - ): Promise { - const startTime = Date.now(); + private async processWorkflowMode( + context: PipelineContext, + toolsData: any, + completedTasks: number, + failedTasks: number + ): Promise { + const phases = toolsData.phases || []; - let contextPrompt = prompt; - if (context.contextHistory.length > 0) { - const contextSection = `BISHERIGE ANALYSE:\n${context.contextHistory.join('\n\n')}\n\nAKTUELLE AUFGABE:\n`; - const combinedPrompt = contextSection + prompt; - - if (this.estimateTokens(combinedPrompt) <= this.maxPromptTokens) { - contextPrompt = combinedPrompt; - } - } - - try { - const response = await this.callAI(contextPrompt, maxTokens); - - const result = { - taskType: 'micro-task', - content: response.trim(), - processingTimeMs: Date.now() - startTime, - success: true - }; - - this.addAuditEntry( - context, - 'micro-task', - 'ai-analysis', - { promptLength: contextPrompt.length, maxTokens }, - { responseLength: response.length }, - response.length > 50 ? 80 : 60, - startTime, - { aiModel: this.config.model, contextUsed: context.contextHistory.length > 0 } + // Select tools for each phase + for (const phase of phases) { + const phaseTools = context.filteredData.tools.filter((tool: any) => + tool && tool.phases && Array.isArray(tool.phases) && tool.phases.includes(phase.id) ); - return result; - - } catch (error) { - const result = { - taskType: 'micro-task', - content: '', - processingTimeMs: Date.now() - startTime, - success: false, - error: error.message - }; + const selections = await toolSelector.selectToolsForPhase(context.userQuery, phase, phaseTools, context); - this.addAuditEntry( - context, - 'micro-task', - 'ai-analysis-failed', - { promptLength: contextPrompt.length, maxTokens }, - { error: error.message }, - 5, - startTime, - { aiModel: this.config.model } - ); + selections.forEach((sel: any) => { + const tool = phaseTools.find((t: any) => t && t.name === sel.toolName); + if (tool) { + const priority = this.derivePriorityFromScore(sel.taskRelevance); + this.addToolToSelection(context, tool, phase.id, priority, sel.justification, sel.taskRelevance, sel.limitations); + } + }); - return result; + if (selections.length > 0) completedTasks++; else failedTasks++; + await this.delay(this.config.microTaskDelay); + } + + // Complete underrepresented phases + await this.completeUnderrepresentedPhases(context, toolsData); + } + + private async processToolMode(context: PipelineContext, completedTasks: number, failedTasks: number): Promise { + const topTools = context.filteredData.tools.slice(0, 3); + + for (let i = 0; i < topTools.length; i++) { + const evaluationResult = await this.evaluateSpecificTool(context, topTools[i], i + 1); + if (evaluationResult.success) completedTasks++; else failedTasks++; + await this.delay(this.config.microTaskDelay); } } - private calculateRecommendationConfidence( - tool: any, - context: AnalysisContext, - taskRelevance: number = 70, - limitations: string[] = [] - ): ConfidenceMetrics { - const rawSemanticRelevance = context.embeddingsSimilarities.has(tool.name) ? - context.embeddingsSimilarities.get(tool.name)! * 100 : 50; - - let enhancedTaskSuitability = taskRelevance; - - if (context.mode === 'workflow') { - const toolSelection = context.selectedTools?.find((st: any) => st.tool && st.tool.name === tool.name); - if (toolSelection && tool.phases && Array.isArray(tool.phases) && tool.phases.includes(toolSelection.phase)) { - const phaseBonus = Math.min(15, 100 - taskRelevance); - enhancedTaskSuitability = Math.min(100, taskRelevance + phaseBonus); - } - } - - const overall = ( - rawSemanticRelevance * this.confidenceConfig.semanticWeight + - enhancedTaskSuitability * this.confidenceConfig.suitabilityWeight - ); - - const uncertaintyFactors = this.identifyUncertaintyFactors(tool, context, limitations, overall); - const strengthIndicators = this.identifyStrengthIndicators(tool, context, overall); - - return { - overall: Math.round(overall), - semanticRelevance: Math.round(rawSemanticRelevance), - taskSuitability: Math.round(enhancedTaskSuitability), - uncertaintyFactors, - strengthIndicators - }; - } - - private identifyUncertaintyFactors( - tool: any, - context: AnalysisContext, - limitations: string[], - confidence: number - ): string[] { - const factors: string[] = []; - - if (limitations?.length > 0) { - factors.push(...limitations.slice(0, 2)); - } - - const similarity = context.embeddingsSimilarities.get(tool.name) || 0.5; - if (similarity < 0.7) { - factors.push('Geringe semantische Ähnlichkeit zur Anfrage'); - } - - if (tool.skillLevel === 'expert' && /schnell|rapid|triage|urgent|sofort/i.test(context.userQuery)) { - factors.push('Experten-Tool für zeitkritisches Szenario'); - } - - if (tool.skillLevel === 'novice' && /komplex|erweitert|tiefgehend|advanced|forensisch/i.test(context.userQuery)) { - factors.push('Einsteiger-Tool für komplexe Analyse'); - } - - if (tool.type === 'software' && !isToolHosted(tool) && tool.accessType === 'download') { - factors.push('Installation und Setup erforderlich'); - } - - if (tool.license === 'Proprietary') { - factors.push('Kommerzielle Software - Lizenzkosten zu beachten'); - } - - if (confidence < 60) { - factors.push('Moderate Gesamtbewertung - alternative Ansätze empfohlen'); - } - - return factors.slice(0, 4); - } - - private identifyStrengthIndicators(tool: any, context: AnalysisContext, confidence: number): string[] { - const indicators: string[] = []; - - const similarity = context.embeddingsSimilarities.get(tool.name) || 0.5; - if (similarity >= 0.7) { - indicators.push('Sehr gute semantische Übereinstimmung mit Ihrer Anfrage'); - } - - if (tool.knowledgebase === true) { - indicators.push('Umfassende Dokumentation und Wissensbasis verfügbar'); - } - - if (isToolHosted(tool)) { - indicators.push('Sofort verfügbar über gehostete Lösung'); - } - - if (tool.skillLevel === 'intermediate' || tool.skillLevel === 'advanced') { - indicators.push('Ausgewogenes Verhältnis zwischen Funktionalität und Benutzerfreundlichkeit'); - } - - if (tool.type === 'method' && /methodik|vorgehen|prozess|ansatz/i.test(context.userQuery)) { - indicators.push('Methodischer Ansatz passt zu Ihrer prozeduralen Anfrage'); - } - - return indicators.slice(0, 4); - } - - private async analyzeScenario(context: AnalysisContext): Promise { - console.log('[AI-PIPELINE] Starting scenario analysis micro-task'); + private async analyzeScenario(context: PipelineContext): Promise { + console.log('[AI-PIPELINE] Micro-task: Scenario analysis'); const isWorkflow = context.mode === 'workflow'; const prompt = getPrompt('scenarioAnalysis', isWorkflow, context.userQuery); @@ -847,8 +259,8 @@ class ImprovedMicroTaskAIPipeline { return result; } - private async generateApproach(context: AnalysisContext): Promise { - console.log('[AI-PIPELINE] Starting investigation approach micro-task'); + private async generateApproach(context: PipelineContext): Promise { + console.log('[AI-PIPELINE] Micro-task: Investigation approach'); const isWorkflow = context.mode === 'workflow'; const prompt = getPrompt('investigationApproach', isWorkflow, context.userQuery); @@ -862,8 +274,8 @@ class ImprovedMicroTaskAIPipeline { return result; } - private async generateCriticalConsiderations(context: AnalysisContext): Promise { - console.log('[AI-PIPELINE] Starting critical considerations micro-task'); + private async generateCriticalConsiderations(context: PipelineContext): Promise { + console.log('[AI-PIPELINE] Micro-task: Critical considerations'); const isWorkflow = context.mode === 'workflow'; const prompt = getPrompt('criticalConsiderations', isWorkflow, context.userQuery); @@ -877,265 +289,8 @@ class ImprovedMicroTaskAIPipeline { return result; } - private async selectToolsForPhase(context: AnalysisContext, phase: any): Promise { - console.log('[AI-PIPELINE] Starting phase tool selection micro-task for:', phase.id); - const phaseTools = context.filteredData.tools.filter((tool: any) => - tool && tool.phases && Array.isArray(tool.phases) && tool.phases.includes(phase.id) - ); - - if (phaseTools.length === 0) { - console.log('[AI-PIPELINE] No tools available for phase:', phase.id); - return { - taskType: 'tool-selection', - content: JSON.stringify([]), - processingTimeMs: 0, - success: true - }; - } - - const phaseMethods = phaseTools.filter((t: any) => t && t.type === 'method'); - const phaseSoftware = phaseTools.filter((t: any) => t && t.type === 'software'); - - console.log('[AI-PIPELINE] Phase tools available:', phaseMethods.length, 'methods,', phaseSoftware.length, 'software'); - - const prompt = getPrompt('phaseToolSelection', context.userQuery, phase, phaseTools); - const result = await this.callMicroTaskAI(prompt, context, 1000); - - if (result.success) { - const selections = this.safeParseJSON(result.content, []); - - if (Array.isArray(selections)) { - const validSelections = selections.filter((sel: any) => { - const matchingTool = phaseTools.find((tool: any) => tool && tool.name === sel.toolName); - if (!matchingTool) { - console.warn('[AI-PIPELINE] Invalid tool selection for phase:', phase.id, sel.toolName); - } - return !!matchingTool; - }); - - console.log('[AI-PIPELINE] Valid selections for phase:', phase.id, validSelections.length); - - validSelections.forEach((sel: any) => { - const tool = phaseTools.find((t: any) => t && t.name === sel.toolName); - if (tool) { - const taskRelevance = typeof sel.taskRelevance === 'number' ? - sel.taskRelevance : parseInt(String(sel.taskRelevance)) || 70; - - const priority = this.derivePriorityFromScore(taskRelevance); - - this.addToolToSelection(context, tool, phase.id, priority, sel.justification, taskRelevance, sel.limitations); - } - }); - - this.addAuditEntry( - context, - 'micro-task', - 'phase-tool-selection', - { phase: phase.id, availableTools: phaseTools.length }, - { - validSelections: validSelections.length, - selectedTools: validSelections.map((s: any) => ({ - name: s.toolName, - taskRelevance: s.taskRelevance, - derivedPriority: this.derivePriorityFromScore(s.taskRelevance) - })) - }, - validSelections.length > 0 ? 75 : 30, - Date.now() - result.processingTimeMs, - { phaseName: phase.name } - ); - } - } - - return result; - } - - private async completeUnderrepresentedPhases( - context: AnalysisContext, - toolsData: any, - originalQuery: string - ): Promise { - const phases = toolsData.phases || []; - const selectedPhases = new Map(); - - context.selectedTools?.forEach((st: any) => { - const count = selectedPhases.get(st.phase) || 0; - selectedPhases.set(st.phase, count + 1); - }); - - console.log('[AI-PIPELINE] Phase coverage analysis complete'); - - const phaseQueryTemplates = this.generatePhaseQueryTemplates(phases); - - const underrepresentedPhases = phases.filter((phase: any) => { - const count = selectedPhases.get(phase.id) || 0; - return count <= 1; - }); - - if (underrepresentedPhases.length === 0) { - console.log('[AI-PIPELINE] All phases adequately represented'); - return; - } - - console.log('[AI-PIPELINE] Completing underrepresented phases:', underrepresentedPhases.map((p: any) => p.id).join(', ')); - - for (const phase of underrepresentedPhases) { - await this.completePhaseWithSemanticSearch(context, phase, phaseQueryTemplates, toolsData, originalQuery); - await this.delay(this.microTaskDelay); - } - } - - private async completePhaseWithSemanticSearch( - context: AnalysisContext, - phase: any, - phaseQueryTemplates: Record, - toolsData: any, - originalQuery: string - ): Promise { - const phaseStart = Date.now(); - const phaseQuery = phaseQueryTemplates[phase.id] || `forensic ${phase.name.toLowerCase()} tools methods`; - - console.log('[AI-PIPELINE] Starting enhanced phase completion micro-task for:', phase.id); - - try { - const phaseResults = await embeddingsService.findSimilar(phaseQuery, 20, 0.2); - - if (phaseResults.length === 0) { - console.log('[AI-PIPELINE] No semantic results for phase:', phase.id); - return; - } - - const toolsMap = new Map(toolsData.tools.map((tool: any) => [tool.name, tool])); - const conceptsMap = new Map(toolsData.concepts.map((concept: any) => [concept.name, concept])); - - const phaseTools = phaseResults - .filter((result: any) => result.type === 'tool') - .map((result: any) => toolsMap.get(result.name)) - .filter((tool: any): tool is NonNullable => - tool !== undefined && - tool !== null && - tool.phases && - Array.isArray(tool.phases) && - tool.phases.includes(phase.id) && - !context.seenToolNames.has(tool.name) - ) - .slice(0, 5); - - const phaseConcepts = phaseResults - .filter((result: any) => result.type === 'concept') - .map((result: any) => conceptsMap.get(result.name)) - .filter((concept: any): concept is NonNullable => concept !== undefined && concept !== null) - .slice(0, 2); - - if (phaseTools.length === 0) { - console.log('[AI-PIPELINE] No suitable tools for phase completion:', phase.id); - return; - } - - const selectionPrompt = AI_PROMPTS.generatePhaseCompletionPrompt(originalQuery, phase, phaseTools, phaseConcepts); - const selectionResult = await this.callMicroTaskAI(selectionPrompt, context, 800); - - if (!selectionResult.success) { - console.error('[AI-PIPELINE] Phase completion selection failed for:', phase.id); - return; - } - - const selection = this.safeParseJSON(selectionResult.content, { - selectedTools: [], - selectedConcepts: [], - completionReasoning: '' - }); - - const validTools = selection.selectedTools - .map((name: string) => phaseTools.find((t: any) => t && t.name === name)) - .filter((tool: any): tool is NonNullable => tool !== undefined && tool !== null) - .slice(0, 2); - - if (validTools.length === 0) { - console.log('[AI-PIPELINE] No valid tools selected for phase completion:', phase.id); - return; - } - - for (const tool of validTools) { - console.log('[AI-PIPELINE] Generating reasoning for phase completion tool:', tool.name); - - const reasoningPrompt = getPrompt( - 'phaseCompletionReasoning', - originalQuery, - phase, - tool.name, - tool, - selection.completionReasoning || 'Nachergänzung zur Vervollständigung der Phasenabdeckung' - ); - - const reasoningResult = await this.callMicroTaskAI(reasoningPrompt, context, 400); - - let detailedJustification: string; - if (reasoningResult.success) { - detailedJustification = reasoningResult.content.trim(); - } else { - detailedJustification = `Nachträglich hinzugefügt zur Vervollständigung der ${phase.name}-Phase. Die ursprüngliche KI-Auswahl war zu spezifisch und hat wichtige Tools für diese Phase übersehen.`; - } - - this.addToolToSelection( - context, - tool, - phase.id, - 'medium', - detailedJustification, - 75, - ['Nachträgliche Ergänzung via semantische Phasensuche'] - ); - - console.log('[AI-PIPELINE] Added phase completion tool with reasoning:', tool.name); - } - - this.addAuditEntry( - context, - 'validation', - 'phase-completion', - { - phase: phase.id, - phaseQuery, - candidatesFound: phaseTools.length, - selectionReasoning: selection.completionReasoning - }, - { - toolsAdded: validTools.length, - addedTools: validTools.map((t: any) => ({ - name: t.name, - type: t.type, - reasoning: 'Generated via micro-task' - })) - }, - validTools.length > 0 ? 80 : 40, - phaseStart, - { - phaseCompletion: true, - semanticSearch: true, - microTaskReasoning: true, - contextualExplanation: true - } - ); - - } catch (error) { - console.error('[AI-PIPELINE] Enhanced phase completion failed for:', phase.id, error); - - this.addAuditEntry( - context, - 'validation', - 'phase-completion-failed', - { phase: phase.id, phaseQuery }, - { error: error.message }, - 10, - phaseStart, - { phaseCompletion: true, failed: true } - ); - } - } - - private async evaluateSpecificTool(context: AnalysisContext, tool: any, rank: number): Promise { - console.log('[AI-PIPELINE] Starting tool evaluation micro-task for:', tool.name); + private async evaluateSpecificTool(context: PipelineContext, tool: any, rank: number): Promise { + console.log('[AI-PIPELINE] Micro-task: Tool evaluation for:', tool.name); const existingSelection = context.selectedTools?.find((st: any) => st.tool && st.tool.name === tool.name); const taskRelevance = existingSelection?.taskRelevance || 70; const priority = this.derivePriorityFromScore(taskRelevance); @@ -1159,31 +314,14 @@ class ImprovedMicroTaskAIPipeline { rank, task_relevance: taskRelevance } - }, 'evaluation', priority, evaluation.detailed_explanation, - taskRelevance, evaluation.limitations); - - this.addAuditEntry( - context, - 'micro-task', - 'tool-evaluation', - { toolName: tool.name, rank, existingTaskRelevance: taskRelevance }, - { - hasExplanation: !!evaluation.detailed_explanation, - hasImplementationApproach: !!evaluation.implementation_approach, - prosCount: evaluation.pros?.length || 0, - limitationsCount: evaluation.limitations?.length || 0 - }, - 70, - Date.now() - result.processingTimeMs, - { toolType: tool.type } - ); + }, 'evaluation', priority, evaluation.detailed_explanation, taskRelevance, evaluation.limitations); } return result; } - private async selectBackgroundKnowledge(context: AnalysisContext): Promise { - console.log('[AI-PIPELINE] Starting background knowledge selection micro-task'); + private async selectBackgroundKnowledge(context: PipelineContext): Promise { + console.log('[AI-PIPELINE] Micro-task: Background knowledge selection'); const availableConcepts = context.filteredData.concepts; if (availableConcepts.length === 0) { @@ -1209,262 +347,108 @@ class ImprovedMicroTaskAIPipeline { concept: availableConcepts.find((c: any) => c.name === sel.conceptName), relevance: sel.relevance })); - - this.addAuditEntry( - context, - 'micro-task', - 'background-knowledge-selection', - { availableConcepts: availableConcepts.length }, - { selectedConcepts: context.backgroundKnowledge?.length || 0 }, - context.backgroundKnowledge && context.backgroundKnowledge.length > 0 ? 75 : 40, - Date.now() - result.processingTimeMs, - {} - ); } } return result; } - private async generateFinalRecommendations(context: AnalysisContext): Promise { - console.log('[AI-PIPELINE] Starting final recommendations micro-task'); + private async generateFinalRecommendations(context: PipelineContext): Promise { + console.log('[AI-PIPELINE] Micro-task: Final recommendations'); const selectedToolNames = context.selectedTools?.map((st: any) => st.tool && st.tool.name).filter(Boolean) || []; const prompt = getPrompt('finalRecommendations', context.mode === 'workflow', context.userQuery, selectedToolNames); - const result = await this.callMicroTaskAI(prompt, context, 350); - return result; + return this.callMicroTaskAI(prompt, context, 350); } - private async callAI(prompt: string, maxTokens: number = 1500): Promise { - const endpoint = this.config.endpoint; - const apiKey = this.config.apiKey; - const model = this.config.model; + private async completeUnderrepresentedPhases(context: PipelineContext, toolsData: any): Promise { + const phases = toolsData.phases || []; + const selectedPhases = new Map(); - let headers: Record = { - 'Content-Type': 'application/json' - }; + context.selectedTools?.forEach((st: any) => { + const count = selectedPhases.get(st.phase) || 0; + selectedPhases.set(st.phase, count + 1); + }); - if (apiKey) { - headers['Authorization'] = `Bearer ${apiKey}`; + const underrepresentedPhases = phases.filter((phase: any) => { + const count = selectedPhases.get(phase.id) || 0; + return count <= 1; + }); + + if (underrepresentedPhases.length === 0) { + console.log('[AI-PIPELINE] All phases adequately represented'); + return; } - const requestBody = { - model, - messages: [{ role: 'user', content: prompt }], - max_tokens: maxTokens, - temperature: 0.3 - }; + console.log('[AI-PIPELINE] Completing underrepresented phases:', underrepresentedPhases.map((p: any) => p.id).join(', ')); + + for (const phase of underrepresentedPhases) { + await this.completePhaseWithSemanticSearch(context, phase, toolsData); + await this.delay(this.config.microTaskDelay); + } + } + + private async completePhaseWithSemanticSearch(context: PipelineContext, phase: any, toolsData: any): Promise { + const phaseStart = Date.now(); + const phaseQuery = `forensic ${phase.name.toLowerCase()} tools methods`; + + console.log('[AI-PIPELINE] Phase completion for:', phase.id); try { - const response = await fetch(`${endpoint}/v1/chat/completions`, { - method: 'POST', - headers, - body: JSON.stringify(requestBody) - }); - - if (!response.ok) { - const errorText = await response.text(); - console.error('[AI-PIPELINE] AI API Error:', response.status, errorText); - throw new Error(`AI API error: ${response.status} - ${errorText}`); - } - - const data = await response.json(); - const content = data.choices?.[0]?.message?.content; + const phaseResults = await embeddingsService.findSimilar(phaseQuery, 20, 0.2); - if (!content) { - console.error('[AI-PIPELINE] No response content from AI model'); - throw new Error('No response from AI model'); + if (phaseResults.length === 0) { + console.log('[AI-PIPELINE] No semantic results for phase:', phase.id); + return; + } + + const toolsMap = new Map(toolsData.tools.map((tool: any) => [tool.name, tool])); + + const phaseTools = phaseResults + .filter((result: any) => result.type === 'tool') + .map((result: any) => toolsMap.get(result.name)) + .filter((tool: any): tool is NonNullable => + tool !== undefined && + tool !== null && + tool.phases && + Array.isArray(tool.phases) && + tool.phases.includes(phase.id) && + !context.seenToolNames.has(tool.name) + ) + .slice(0, 2); + + if (phaseTools.length === 0) { + console.log('[AI-PIPELINE] No suitable tools for phase completion:', phase.id); + return; + } + + // Add tools with justification + for (const tool of phaseTools) { + const justification = `Nachträglich hinzugefügt zur Vervollständigung der ${phase.name}-Phase. Die ursprüngliche KI-Auswahl war zu spezifisch und hat wichtige Tools für diese Phase übersehen.`; + + this.addToolToSelection( + context, + tool, + phase.id, + 'medium', + justification, + 75, + ['Nachträgliche Ergänzung via semantische Phasensuche'] + ); + + console.log('[AI-PIPELINE] Added phase completion tool:', tool.name); } - - return content; } catch (error) { - console.error('[AI-PIPELINE] AI service call failed:', error.message); - throw error; + console.error('[AI-PIPELINE] Phase completion failed for:', phase.id, error); } } - private derivePriorityFromScore(taskRelevance: number): string { - if (taskRelevance >= 80) return 'high'; - if (taskRelevance >= 60) return 'medium'; - return 'low'; - } - - private async performAISelection( - filteredData: any, - userQuery: string, - mode: string, - context: AnalysisContext - ): Promise<{ tools: any[], concepts: any[] }> { - const result = await this.aiSelectionWithFullData( - userQuery, - filteredData.tools, - filteredData.concepts, - mode, - embeddingsService.isEnabled() ? 'embeddings_candidates' : 'full_dataset', - context - ); - - console.log('[AI-PIPELINE] AI selection complete:', result.selectedTools.length, 'tools,', result.selectedConcepts.length, 'concepts'); - - return { - tools: result.selectedTools, - concepts: result.selectedConcepts - }; - } - - async processQuery(userQuery: string, mode: string): Promise { - const startTime = Date.now(); - let completeTasks = 0; - let failedTasks = 0; - - console.log('[AI-PIPELINE] Starting', mode, 'query processing'); - - // CLEAR AUDIT TRAIL for new analysis - auditService.clearAuditTrail(); - - try { - const toolsData = await getCompressedToolsDataForAI(); - - const context: AnalysisContext = { - userQuery, - mode, - filteredData: {}, - contextHistory: [], - maxContextLength: this.maxContextTokens, - currentContextLength: 0, - seenToolNames: new Set(), - embeddingsSimilarities: new Map(), - aiSelectedTools: [], - aiSelectedConcepts: [] - }; - - const filteredData = await this.getIntelligentCandidates(userQuery, toolsData, mode, context); - const aiSelection = await this.performAISelection(filteredData, userQuery, mode, context); - - context.aiSelectedTools = aiSelection.tools; - context.aiSelectedConcepts = aiSelection.concepts; - - context.filteredData = { - tools: aiSelection.tools, - concepts: aiSelection.concepts, - domains: filteredData.domains, - phases: filteredData.phases, - 'domain-agnostic-software': filteredData['domain-agnostic-software'] - }; - - this.addAuditEntry( - context, - 'initialization', - 'pipeline-start', - { userQuery, mode, toolsDataLoaded: !!toolsData }, - { candidateTools: filteredData.tools.length, candidateConcepts: filteredData.concepts.length }, - 90, - startTime, - { auditEnabled: auditService.isEnabled() } - ); - - const analysisResult = await this.analyzeScenario(context); - if (analysisResult.success) completeTasks++; else failedTasks++; - await this.delay(this.microTaskDelay); - - const approachResult = await this.generateApproach(context); - if (approachResult.success) completeTasks++; else failedTasks++; - await this.delay(this.microTaskDelay); - - const considerationsResult = await this.generateCriticalConsiderations(context); - if (considerationsResult.success) completeTasks++; else failedTasks++; - await this.delay(this.microTaskDelay); - - if (mode === 'workflow') { - const phases = toolsData.phases || []; - - for (const phase of phases) { - const toolSelectionResult = await this.selectToolsForPhase(context, phase); - if (toolSelectionResult.success) completeTasks++; else failedTasks++; - await this.delay(this.microTaskDelay); - } - - await this.completeUnderrepresentedPhases(context, toolsData, userQuery); - - } else { - const topTools = filteredData.tools.slice(0, 3); - for (let i = 0; i < topTools.length; i++) { - const evaluationResult = await this.evaluateSpecificTool(context, topTools[i], i + 1); - if (evaluationResult.success) completeTasks++; else failedTasks++; - await this.delay(this.microTaskDelay); - } - } - - const knowledgeResult = await this.selectBackgroundKnowledge(context); - if (knowledgeResult.success) completeTasks++; else failedTasks++; - await this.delay(this.microTaskDelay); - - const finalResult = await this.generateFinalRecommendations(context); - if (finalResult.success) completeTasks++; else failedTasks++; - - const recommendation = this.buildRecommendation(context, mode, finalResult.content); - - this.addAuditEntry( - context, - 'completion', - 'pipeline-end', - { completedTasks: completeTasks, failedTasks }, - { finalRecommendation: !!recommendation, auditEntriesGenerated: auditService.getCurrentAuditTrail().length }, - completeTasks > failedTasks ? 85 : 60, - startTime, - { totalProcessingTimeMs: Date.now() - startTime } - ); - - const processingStats = { - embeddingsUsed: embeddingsService.isEnabled(), - candidatesFromEmbeddings: filteredData.tools.length, - finalSelectedItems: (context.selectedTools?.length || 0) + (context.backgroundKnowledge?.length || 0), - processingTimeMs: Date.now() - startTime, - microTasksCompleted: completeTasks, - microTasksFailed: failedTasks, - contextContinuityUsed: true - }; - - console.log('[AI-PIPELINE] Processing complete. Tasks completed:', completeTasks, 'failed:', failedTasks); - - // FINALIZE AUDIT TRAIL and get final trail - const finalAuditTrail = auditService.finalizeAuditTrail(); - - return { - recommendation: { - ...recommendation, - auditTrail: auditService.isEnabled() ? finalAuditTrail : undefined - }, - processingStats - }; - - } catch (error) { - console.error('[AI-PIPELINE] Processing failed:', error); - throw error; - } - } - - private buildRecommendation(context: AnalysisContext, mode: string, finalContent: string): any { + private buildRecommendation(context: PipelineContext, mode: string, finalContent: string): any { const isWorkflow = mode === 'workflow'; console.log('[AI-PIPELINE] Building recommendation for', mode, 'mode with', context.selectedTools?.length || 0, 'tools'); - if (context.selectedTools && context.selectedTools.length > 0) { - const methods = context.selectedTools.filter((st: any) => st.tool && st.tool.type === 'method'); - const software = context.selectedTools.filter((st: any) => st.tool && st.tool.type === 'software'); - - console.log('[AI-PIPELINE] Final selection breakdown:', methods.length, 'methods,', software.length, 'software'); - console.log('[AI-PIPELINE] Method names:', methods.map((m: any) => m.tool.name).join(', ')); - console.log('[AI-PIPELINE] Software names:', software.map((s: any) => s.tool.name).join(', ')); - - context.selectedTools.forEach((st: any, index: number) => { - console.log('[AI-PIPELINE] Selected tool', index + 1, ':', st.tool.name, '(' + st.tool.type + ') - Phase:', st.phase, ', Priority:', st.priority); - }); - } else { - console.warn('[AI-PIPELINE] WARNING: No tools in selectedTools array!'); - } - const base = { [isWorkflow ? 'scenario_analysis' : 'problem_analysis']: isWorkflow ? context.scenarioAnalysis : context.problemAnalysis, @@ -1478,29 +462,19 @@ class ImprovedMicroTaskAIPipeline { if (isWorkflow) { const recommendedToolsWithConfidence = context.selectedTools?.map((st: any) => { - const confidence = this.calculateRecommendationConfidence( + const analysisContext: AnalysisContext = { + userQuery: context.userQuery, + mode: context.mode, + embeddingsSimilarities: context.embeddingsSimilarities, + selectedTools: context.selectedTools + }; + + const confidence = confidenceScoring.calculateRecommendationConfidence( st.tool, - context, + analysisContext, st.taskRelevance || 70, st.limitations || [] ); - - this.addAuditEntry( - context, - 'validation', - 'confidence-scoring', - { toolName: st.tool.name, toolType: st.tool.type, phase: st.phase }, - { - overall: confidence.overall, - components: { - semantic: confidence.semanticRelevance, - suitability: confidence.taskSuitability, - } - }, - confidence.overall, - Date.now(), - { uncertaintyCount: confidence.uncertaintyFactors.length, strengthCount: confidence.strengthIndicators.length } - ); return { name: st.tool.name, @@ -1509,8 +483,7 @@ class ImprovedMicroTaskAIPipeline { priority: st.priority, justification: st.justification || `Empfohlen für ${st.phase}`, confidence: confidence, - recommendationStrength: confidence.overall >= this.confidenceConfig.highThreshold ? 'strong' : - confidence.overall >= this.confidenceConfig.mediumThreshold ? 'moderate' : 'weak' + recommendationStrength: confidenceScoring.getConfidenceLevel(confidence.overall) }; }) || []; @@ -1521,26 +494,19 @@ class ImprovedMicroTaskAIPipeline { }; } else { const recommendedToolsWithConfidence = context.selectedTools?.map((st: any) => { - const confidence = this.calculateRecommendationConfidence( + const analysisContext: AnalysisContext = { + userQuery: context.userQuery, + mode: context.mode, + embeddingsSimilarities: context.embeddingsSimilarities, + selectedTools: context.selectedTools + }; + + const confidence = confidenceScoring.calculateRecommendationConfidence( st.tool, - context, + analysisContext, st.taskRelevance || 70, st.limitations || [] ); - - this.addAuditEntry( - context, - 'validation', - 'confidence-scoring', - { toolName: st.tool.name, toolType: st.tool.type, rank: st.tool.evaluation?.rank || 1 }, - { - overall: confidence.overall, - suitabilityAlignment: st.priority === 'high' && confidence.overall >= this.confidenceConfig.highThreshold - }, - confidence.overall, - Date.now(), - { strengthCount: confidence.strengthIndicators.length } - ); return { name: st.tool.name, @@ -1553,8 +519,7 @@ class ImprovedMicroTaskAIPipeline { cons: st.tool.evaluation?.limitations || [], alternatives: st.tool.evaluation?.alternatives || '', confidence: confidence, - recommendationStrength: confidence.overall >= this.confidenceConfig.highThreshold ? 'strong' : - confidence.overall >= this.confidenceConfig.mediumThreshold ? 'moderate' : 'weak' + recommendationStrength: confidenceScoring.getConfidenceLevel(confidence.overall) }; }) || []; @@ -1565,8 +530,127 @@ class ImprovedMicroTaskAIPipeline { }; } } + + // Helper methods + private async callMicroTaskAI(prompt: string, context: PipelineContext, maxTokens: number = 500): Promise { + const startTime = Date.now(); + + let contextPrompt = prompt; + if (context.contextHistory.length > 0) { + const contextSection = `BISHERIGE ANALYSE:\n${context.contextHistory.join('\n\n')}\n\nAKTUELLE AUFGABE:\n`; + const combinedPrompt = contextSection + prompt; + + if (aiService.estimateTokens(combinedPrompt) <= this.config.maxPromptTokens) { + contextPrompt = combinedPrompt; + } + } + + try { + const response = await aiService.callMicroTaskAI(contextPrompt, maxTokens); + + return { + taskType: 'micro-task', + content: response.content, + processingTimeMs: Date.now() - startTime, + success: true + }; + + } catch (error) { + return { + taskType: 'micro-task', + content: '', + processingTimeMs: Date.now() - startTime, + success: false, + error: error.message + }; + } + } + + private addToContextHistory(context: PipelineContext, newEntry: string): void { + const entryTokens = aiService.estimateTokens(newEntry); + + context.contextHistory.push(newEntry); + context.currentContextLength += entryTokens; + + while (context.currentContextLength > this.config.maxContextTokens && context.contextHistory.length > 1) { + const removed = context.contextHistory.shift()!; + context.currentContextLength -= aiService.estimateTokens(removed); + } + } + + private addToolToSelection( + context: PipelineContext, + tool: any, + phase: string, + priority: string, + justification?: string, + taskRelevance?: number, + limitations?: string[] + ): boolean { + context.seenToolNames.add(tool.name); + if (!context.selectedTools) context.selectedTools = []; + + context.selectedTools.push({ + tool, + phase, + priority, + justification, + taskRelevance, + limitations + }); + + return true; + } + + private derivePriorityFromScore(taskRelevance: number): string { + if (taskRelevance >= 80) return 'high'; + if (taskRelevance >= 60) return 'medium'; + return 'low'; + } + + private safeParseJSON(jsonString: string, fallback: any = null): any { + try { + let cleaned = jsonString.trim(); + + // Remove code block markers + const jsonBlockPatterns = [ + /```json\s*([\s\S]*?)\s*```/i, + /```\s*([\s\S]*?)\s*```/i, + /\{[\s\S]*\}/, + ]; + + for (const pattern of jsonBlockPatterns) { + const match = cleaned.match(pattern); + if (match) { + cleaned = match[1] || match[0]; + break; + } + } + + return JSON.parse(cleaned); + + } catch (error) { + console.warn('[AI-PIPELINE] JSON parsing failed:', error.message); + return fallback; + } + } + + private async delay(ms: number): Promise { + return new Promise(resolve => setTimeout(resolve, ms)); + } + + private addAuditEntry( + phase: string, + action: string, + input: any, + output: any, + confidence: number, + startTime: number, + metadata: Record = {} + ): void { + auditService.addEntry(phase, action, input, output, confidence, startTime, metadata); + } } -const aiPipeline = new ImprovedMicroTaskAIPipeline(); - -export { aiPipeline, type AnalysisResult }; \ No newline at end of file +export const aiPipeline = new AIPipeline(); +export type { AnalysisResult }; \ No newline at end of file diff --git a/src/utils/aiService.ts b/src/utils/aiService.ts new file mode 100644 index 0000000..2e2d754 --- /dev/null +++ b/src/utils/aiService.ts @@ -0,0 +1,150 @@ +// src/utils/aiService.ts +import 'dotenv/config'; + +export interface AIServiceConfig { + endpoint: string; + apiKey: string; + model: string; +} + +export interface AICallOptions { + maxTokens?: number; + temperature?: number; + timeout?: number; +} + +export interface AIResponse { + content: string; + usage?: { + promptTokens: number; + completionTokens: number; + totalTokens: number; + }; +} + +class AIService { + private config: AIServiceConfig; + private defaultOptions: AICallOptions; + + constructor() { + this.config = { + endpoint: this.getRequiredEnv('AI_ANALYZER_ENDPOINT'), + apiKey: this.getRequiredEnv('AI_ANALYZER_API_KEY'), + model: this.getRequiredEnv('AI_ANALYZER_MODEL') + }; + + this.defaultOptions = { + maxTokens: 1500, + temperature: 0.3, + timeout: 30000 + }; + + console.log('[AI-SERVICE] Initialized with model:', this.config.model); + } + + private getRequiredEnv(key: string): string { + const value = process.env[key]; + if (!value) { + throw new Error(`Missing required environment variable: ${key}`); + } + return value; + } + + async callAI(prompt: string, options: AICallOptions = {}): Promise { + const mergedOptions = { ...this.defaultOptions, ...options }; + + console.log('[AI-SERVICE] Making API call:', { + promptLength: prompt.length, + maxTokens: mergedOptions.maxTokens, + temperature: mergedOptions.temperature + }); + + const headers: Record = { + 'Content-Type': 'application/json' + }; + + if (this.config.apiKey) { + headers['Authorization'] = `Bearer ${this.config.apiKey}`; + } + + const requestBody = { + model: this.config.model, + messages: [{ role: 'user', content: prompt }], + max_tokens: mergedOptions.maxTokens, + temperature: mergedOptions.temperature + }; + + try { + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), mergedOptions.timeout); + + const response = await fetch(`${this.config.endpoint}/v1/chat/completions`, { + method: 'POST', + headers, + body: JSON.stringify(requestBody), + signal: controller.signal + }); + + clearTimeout(timeoutId); + + if (!response.ok) { + const errorText = await response.text(); + console.error('[AI-SERVICE] API Error:', response.status, errorText); + throw new Error(`AI API error: ${response.status} - ${errorText}`); + } + + const data = await response.json(); + const content = data.choices?.[0]?.message?.content; + + if (!content) { + console.error('[AI-SERVICE] No response content from AI model'); + throw new Error('No response from AI model'); + } + + console.log('[AI-SERVICE] API call successful:', { + responseLength: content.length, + usage: data.usage + }); + + return { + content: content.trim(), + usage: data.usage + }; + + } catch (error) { + if (error.name === 'AbortError') { + console.error('[AI-SERVICE] Request timeout'); + throw new Error('AI request timeout'); + } + + console.error('[AI-SERVICE] API call failed:', error.message); + throw error; + } + } + + async callMicroTaskAI(prompt: string, maxTokens: number = 500): Promise { + return this.callAI(prompt, { + maxTokens, + temperature: 0.3, + timeout: 15000 + }); + } + + estimateTokens(text: string): number { + return Math.ceil(text.length / 4); + } + + validatePromptLength(prompt: string, maxTokens: number = 35000): void { + const estimatedTokens = this.estimateTokens(prompt); + if (estimatedTokens > maxTokens) { + console.warn('[AI-SERVICE] WARNING: Prompt may exceed model limits:', estimatedTokens); + throw new Error(`Prompt too long: ${estimatedTokens} tokens (max: ${maxTokens})`); + } + } + + getConfig(): AIServiceConfig { + return { ...this.config }; + } +} + +export const aiService = new AIService(); \ No newline at end of file diff --git a/src/utils/auditService.ts b/src/utils/auditService.ts index 8f3e9d7..5e7b638 100644 --- a/src/utils/auditService.ts +++ b/src/utils/auditService.ts @@ -1,4 +1,4 @@ -// src/utils/auditService.ts +// src/utils/auditService.ts - Refactored import 'dotenv/config'; function env(key: string, fallback: string | undefined = undefined): string | undefined { @@ -11,7 +11,6 @@ function env(key: string, fallback: string | undefined = undefined): string | un return fallback; } -// CONSOLIDATED AUDIT INTERFACES - Single source of truth export interface AuditEntry { timestamp: number; phase: string; @@ -30,63 +29,9 @@ interface AuditConfig { maxEntries: number; } -interface CompressedAuditEntry { - timestamp: number; - phase: string; - action: string; - inputSummary: string; - outputSummary: string; - confidence: number; - processingTimeMs: number; - metadata: Record; -} - -export interface ProcessedAuditTrail { - totalTime: number; - avgConfidence: number; - stepCount: number; - highConfidenceSteps: number; - lowConfidenceSteps: number; - phases: Array<{ - name: string; - icon: string; - displayName: string; - avgConfidence: number; - totalTime: number; - entries: CompressedAuditEntry[]; - }>; - summary: { - analysisQuality: 'excellent' | 'good' | 'fair' | 'poor'; - keyInsights: string[]; - potentialIssues: string[]; - }; -} - class AuditService { private config: AuditConfig; private activeAuditTrail: AuditEntry[] = []; - - private readonly phaseConfig = { - 'initialization': { icon: '🚀', displayName: 'Initialisierung' }, - 'retrieval': { icon: '🔍', displayName: 'Datensuche' }, - 'selection': { icon: '🎯', displayName: 'Tool-Auswahl' }, - 'micro-task': { icon: '⚡', displayName: 'Detail-Analyse' }, - 'validation': { icon: '✓', displayName: 'Validierung' }, - 'completion': { icon: '✅', displayName: 'Finalisierung' } - }; - - private readonly actionTranslations = { - 'pipeline-start': 'Analyse gestartet', - 'embeddings-search': 'Ähnliche Tools gesucht', - 'ai-tool-selection': 'Tools automatisch ausgewählt', - 'ai-analysis': 'KI-Analyse durchgeführt', - 'phase-tool-selection': 'Phasen-Tools evaluiert', - 'tool-evaluation': 'Tool-Bewertung erstellt', - 'background-knowledge-selection': 'Hintergrundwissen ausgewählt', - 'confidence-scoring': 'Vertrauenswertung berechnet', - 'phase-completion': 'Phasenergänzung durchgeführt', - 'pipeline-end': 'Analyse abgeschlossen' - }; constructor() { this.config = this.loadConfig(); @@ -110,7 +55,6 @@ class AuditService { }; } - // CONSOLIDATED AUDIT ENTRY CREATION - Single method for all audit operations addEntry( phase: string, action: string, @@ -134,15 +78,19 @@ class AuditService { }; this.activeAuditTrail.push(entry); + + // Enforce max entries limit + if (this.activeAuditTrail.length > this.config.maxEntries) { + this.activeAuditTrail.shift(); + } + console.log(`[AUDIT-SERVICE] ${phase}/${action}: ${confidence}% confidence, ${entry.processingTimeMs}ms`); } - // GET CURRENT AUDIT TRAIL - For integration with AI pipeline getCurrentAuditTrail(): AuditEntry[] { return [...this.activeAuditTrail]; } - // CLEAR AUDIT TRAIL - Start fresh for new analysis clearAuditTrail(): void { if (this.activeAuditTrail.length > 0) { console.log(`[AUDIT-SERVICE] Cleared ${this.activeAuditTrail.length} audit entries`); @@ -150,7 +98,6 @@ class AuditService { } } - // FINALIZE AUDIT TRAIL - Complete analysis and return final trail finalizeAuditTrail(): AuditEntry[] { const finalTrail = [...this.activeAuditTrail]; console.log(`[AUDIT-SERVICE] Finalized audit trail with ${finalTrail.length} entries`); @@ -158,102 +105,6 @@ class AuditService { return finalTrail; } - processAuditTrail(rawAuditTrail: AuditEntry[]): ProcessedAuditTrail | null { - if (!this.config.enabled) { - console.log('[AUDIT-SERVICE] Processing disabled'); - return null; - } - - if (!rawAuditTrail || !Array.isArray(rawAuditTrail) || rawAuditTrail.length === 0) { - console.log('[AUDIT-SERVICE] No audit trail data to process'); - return null; - } - - try { - console.log(`[AUDIT-SERVICE] Processing ${rawAuditTrail.length} audit entries`); - - const totalTime = rawAuditTrail.reduce((sum, entry) => sum + (entry.processingTimeMs || 0), 0); - const validConfidenceEntries = rawAuditTrail.filter(entry => typeof entry.confidence === 'number'); - const avgConfidence = validConfidenceEntries.length > 0 - ? Math.round(validConfidenceEntries.reduce((sum, entry) => sum + entry.confidence, 0) / validConfidenceEntries.length) - : 0; - - const highConfidenceSteps = rawAuditTrail.filter(entry => (entry.confidence || 0) >= 80).length; - const lowConfidenceSteps = rawAuditTrail.filter(entry => (entry.confidence || 0) < 60).length; - - const groupedEntries = rawAuditTrail.reduce((groups, entry) => { - const phase = entry.phase || 'unknown'; - if (!groups[phase]) groups[phase] = []; - groups[phase].push(entry); - return groups; - }, {} as Record); - - const phases = Object.entries(groupedEntries).map(([phase, entries]) => { - const phaseConfig = this.phaseConfig[phase] || { icon: '📋', displayName: phase }; - const validEntries = entries.filter(entry => entry && typeof entry === 'object'); - - const phaseAvgConfidence = validEntries.length > 0 - ? Math.round(validEntries.reduce((sum, entry) => sum + (entry.confidence || 0), 0) / validEntries.length) - : 0; - - const phaseTotalTime = validEntries.reduce((sum, entry) => sum + (entry.processingTimeMs || 0), 0); - - return { - name: phase, - icon: phaseConfig.icon, - displayName: phaseConfig.displayName, - avgConfidence: phaseAvgConfidence, - totalTime: phaseTotalTime, - entries: validEntries - .map(e => this.compressEntry(e)) - .filter((e): e is CompressedAuditEntry => e !== null) - }; - }).filter(phase => phase.entries.length > 0); - - const summary = this.generateSummary(rawAuditTrail, avgConfidence, lowConfidenceSteps); - - const result: ProcessedAuditTrail = { - totalTime, - avgConfidence, - stepCount: rawAuditTrail.length, - highConfidenceSteps, - lowConfidenceSteps, - phases, - summary - }; - - console.log(`[AUDIT-SERVICE] Successfully processed audit trail: ${result.phases.length} phases, ${result.avgConfidence}% avg confidence`); - return result; - - } catch (error) { - console.error('[AUDIT-SERVICE] Error processing audit trail:', error); - return null; - } - } - - private compressEntry(entry: AuditEntry): CompressedAuditEntry | null { - if (!entry || typeof entry !== 'object') { - console.warn('[AUDIT-SERVICE] Invalid audit entry skipped'); - return null; - } - - try { - return { - timestamp: entry.timestamp || Date.now(), - phase: entry.phase || 'unknown', - action: entry.action || 'unknown', - inputSummary: this.summarizeData(entry.input), - outputSummary: this.summarizeData(entry.output), - confidence: entry.confidence || 0, - processingTimeMs: entry.processingTimeMs || 0, - metadata: entry.metadata || {} - }; - } catch (error) { - console.error('[AUDIT-SERVICE] Error compressing entry:', error); - return null; - } - } - private compressData(data: any): any { if (this.config.detailLevel === 'verbose') { return data; @@ -264,30 +115,6 @@ class AuditService { } } - private summarizeData(data: any): string { - if (data === null || data === undefined) return 'null'; - if (typeof data === 'string') { - return data.length > 100 ? data.slice(0, 100) + '...' : data; - } - if (typeof data === 'number' || typeof data === 'boolean') { - return data.toString(); - } - if (Array.isArray(data)) { - if (data.length === 0) return '[]'; - if (data.length <= 3) return JSON.stringify(data); - return `[${data.slice(0, 3).map(i => typeof i === 'string' ? i : JSON.stringify(i)).join(', ')}, ...+${data.length - 3}]`; - } - if (typeof data === 'object') { - const keys = Object.keys(data); - if (keys.length === 0) return '{}'; - if (keys.length <= 3) { - return '{' + keys.map(k => `${k}: ${typeof data[k] === 'string' ? data[k].slice(0, 20) + (data[k].length > 20 ? '...' : '') : JSON.stringify(data[k])}`).join(', ') + '}'; - } - return `{${keys.slice(0, 3).join(', ')}, ...+${keys.length - 3} keys}`; - } - return String(data); - } - private summarizeForStorage(data: any): any { if (typeof data === 'string' && data.length > 500) { return data.slice(0, 500) + '...[truncated]'; @@ -308,71 +135,6 @@ class AuditService { return data; } - private generateSummary(entries: AuditEntry[], avgConfidence: number, lowConfidenceSteps: number): { - analysisQuality: 'excellent' | 'good' | 'fair' | 'poor'; - keyInsights: string[]; - potentialIssues: string[]; - } { - let analysisQuality: 'excellent' | 'good' | 'fair' | 'poor'; - if (avgConfidence >= 85 && lowConfidenceSteps === 0) { - analysisQuality = 'excellent'; - } else if (avgConfidence >= 70 && lowConfidenceSteps <= 1) { - analysisQuality = 'good'; - } else if (avgConfidence >= 60 && lowConfidenceSteps <= 3) { - analysisQuality = 'fair'; - } else { - analysisQuality = 'poor'; - } - - const keyInsights: string[] = []; - const embeddingsUsed = entries.some(e => e.action === 'embeddings-search'); - if (embeddingsUsed) { - keyInsights.push('Semantische Suche wurde erfolgreich eingesetzt'); - } - - const toolSelectionEntries = entries.filter(e => e.action === 'ai-tool-selection'); - if (toolSelectionEntries.length > 0) { - const avgSelectionConfidence = toolSelectionEntries.reduce((sum, e) => sum + e.confidence, 0) / toolSelectionEntries.length; - if (avgSelectionConfidence >= 80) { - keyInsights.push('Hohe Konfidenz bei der Tool-Auswahl'); - } - } - - const potentialIssues: string[] = []; - if (lowConfidenceSteps > 2) { - potentialIssues.push(`${lowConfidenceSteps} Analyseschritte mit niedriger Konfidenz`); - } - - const longSteps = entries.filter(e => e.processingTimeMs > 5000); - if (longSteps.length > 0) { - potentialIssues.push(`${longSteps.length} Schritte benötigten mehr als 5 Sekunden`); - } - - return { - analysisQuality, - keyInsights, - potentialIssues - }; - } - - getActionDisplayName(action: string): string { - return this.actionTranslations[action] || action; - } - - formatDuration(ms: number): string { - if (ms < 1000) return '< 1s'; - if (ms < 60000) return `${Math.ceil(ms / 1000)}s`; - const minutes = Math.floor(ms / 60000); - const seconds = Math.ceil((ms % 60000) / 1000); - return seconds > 0 ? `${minutes}m ${seconds}s` : `${minutes}m`; - } - - getConfidenceColor(confidence: number): string { - if (confidence >= 80) return 'var(--color-accent)'; - if (confidence >= 60) return 'var(--color-warning)'; - return 'var(--color-error)'; - } - isEnabled(): boolean { return this.config.enabled; } @@ -380,7 +142,122 @@ class AuditService { getConfig(): AuditConfig { return { ...this.config }; } + + // Statistics and analysis methods + getAuditStatistics(auditTrail: AuditEntry[]): { + totalTime: number; + avgConfidence: number; + stepCount: number; + highConfidenceSteps: number; + lowConfidenceSteps: number; + phaseBreakdown: Record; + } { + if (!auditTrail || auditTrail.length === 0) { + return { + totalTime: 0, + avgConfidence: 0, + stepCount: 0, + highConfidenceSteps: 0, + lowConfidenceSteps: 0, + phaseBreakdown: {} + }; + } + + const totalTime = auditTrail.reduce((sum, entry) => sum + (entry.processingTimeMs || 0), 0); + const validConfidenceEntries = auditTrail.filter(entry => typeof entry.confidence === 'number'); + const avgConfidence = validConfidenceEntries.length > 0 + ? Math.round(validConfidenceEntries.reduce((sum, entry) => sum + entry.confidence, 0) / validConfidenceEntries.length) + : 0; + + const highConfidenceSteps = auditTrail.filter(entry => (entry.confidence || 0) >= 80).length; + const lowConfidenceSteps = auditTrail.filter(entry => (entry.confidence || 0) < 60).length; + + // Phase breakdown + const phaseBreakdown: Record = {}; + + auditTrail.forEach(entry => { + const phase = entry.phase || 'unknown'; + if (!phaseBreakdown[phase]) { + phaseBreakdown[phase] = { count: 0, avgConfidence: 0, totalTime: 0 }; + } + + phaseBreakdown[phase].count++; + phaseBreakdown[phase].totalTime += entry.processingTimeMs || 0; + }); + + // Calculate average confidence per phase + Object.keys(phaseBreakdown).forEach(phase => { + const phaseEntries = auditTrail.filter(entry => entry.phase === phase); + const validEntries = phaseEntries.filter(entry => typeof entry.confidence === 'number'); + + if (validEntries.length > 0) { + phaseBreakdown[phase].avgConfidence = Math.round( + validEntries.reduce((sum, entry) => sum + entry.confidence, 0) / validEntries.length + ); + } + }); + + return { + totalTime, + avgConfidence, + stepCount: auditTrail.length, + highConfidenceSteps, + lowConfidenceSteps, + phaseBreakdown + }; + } + + validateAuditTrail(auditTrail: AuditEntry[]): { + isValid: boolean; + issues: string[]; + warnings: string[]; + } { + const issues: string[] = []; + const warnings: string[] = []; + + if (!Array.isArray(auditTrail)) { + issues.push('Audit trail is not an array'); + return { isValid: false, issues, warnings }; + } + + if (auditTrail.length === 0) { + warnings.push('Audit trail is empty'); + } + + auditTrail.forEach((entry, index) => { + if (!entry || typeof entry !== 'object') { + issues.push(`Entry ${index} is not a valid object`); + return; + } + + // Required fields validation + const requiredFields = ['timestamp', 'phase', 'action']; + requiredFields.forEach(field => { + if (!(field in entry)) { + issues.push(`Entry ${index} missing required field: ${field}`); + } + }); + + // Data type validation + if (typeof entry.confidence !== 'number' || entry.confidence < 0 || entry.confidence > 100) { + warnings.push(`Entry ${index} has invalid confidence value: ${entry.confidence}`); + } + + if (typeof entry.processingTimeMs !== 'number' || entry.processingTimeMs < 0) { + warnings.push(`Entry ${index} has invalid processing time: ${entry.processingTimeMs}`); + } + + if (typeof entry.timestamp !== 'number' || entry.timestamp <= 0) { + issues.push(`Entry ${index} has invalid timestamp: ${entry.timestamp}`); + } + }); + + return { + isValid: issues.length === 0, + issues, + warnings + }; + } } -export const auditService = new AuditService(); -export type { CompressedAuditEntry }; \ No newline at end of file +export const auditService = new AuditService(); \ No newline at end of file diff --git a/src/utils/clientUtils.ts b/src/utils/clientUtils.ts index f0a6796..3ce458c 100644 --- a/src/utils/clientUtils.ts +++ b/src/utils/clientUtils.ts @@ -1,9 +1,9 @@ -// src/utils/clientUtils.ts - +// src/utils/clientUtils.ts - Consolidated (removes duplicates from toolHelpers.ts) +// Tool helper functions (moved here to avoid circular imports) export function createToolSlug(toolName: string): string { if (!toolName || typeof toolName !== 'string') { - console.warn('[toolHelpers] Invalid toolName provided to createToolSlug:', toolName); + console.warn('[CLIENT-UTILS] Invalid toolName provided to createToolSlug:', toolName); return ''; } @@ -30,6 +30,86 @@ export function isToolHosted(tool: any): boolean { tool.projectUrl.trim() !== ""; } +// Text and display utilities +export function sanitizeText(text: string): string { + if (typeof text !== 'string') return ''; + + return text + .replace(/^#{1,6}\s+/gm, '') + .replace(/^\s*[-*+]\s+/gm, '') + .replace(/^\s*\d+\.\s+/gm, '') + .replace(/\*\*(.+?)\*\*/g, '$1') + .replace(/\*(.+?)\*/g, '$1') + .replace(/\[([^\]]+)\]\([^)]+\)/g, '$1') + .replace(/```[\s\S]*?```/g, '[CODE BLOCK]') + .replace(/`([^`]+)`/g, '$1') + .replace(/<[^>]+>/g, '') + .replace(/\n\s*\n\s*\n/g, '\n\n') + .trim(); +} + +export function escapeHtml(text: string): string { + if (typeof text !== 'string') return String(text); + const div = document.createElement('div'); + div.textContent = text; + return div.innerHTML; +} + +export function truncateText(text: string, maxLength: number): string { + if (!text || text.length <= maxLength) return text; + return text.slice(0, maxLength) + '...'; +} + +// Data summarization utilities +export function summarizeData(data: any): string { + if (data === null || data === undefined) return 'null'; + if (typeof data === 'string') { + return data.length > 100 ? data.slice(0, 100) + '...' : data; + } + if (typeof data === 'number' || typeof data === 'boolean') { + return data.toString(); + } + if (Array.isArray(data)) { + if (data.length === 0) return '[]'; + if (data.length <= 3) return JSON.stringify(data); + return `[${data.slice(0, 3).map(i => typeof i === 'string' ? i : JSON.stringify(i)).join(', ')}, ...+${data.length - 3}]`; + } + if (typeof data === 'object') { + const keys = Object.keys(data); + if (keys.length === 0) return '{}'; + if (keys.length <= 3) { + return '{' + keys.map(k => `${k}: ${typeof data[k] === 'string' ? data[k].slice(0, 20) + (data[k].length > 20 ? '...' : '') : JSON.stringify(data[k])}`).join(', ') + '}'; + } + return `{${keys.slice(0, 3).join(', ')}, ...+${keys.length - 3} keys}`; + } + return String(data); +} + +// Time formatting utilities +export function formatDuration(ms: number): string { + if (ms < 1000) return '< 1s'; + if (ms < 60000) return `${Math.ceil(ms / 1000)}s`; + const minutes = Math.floor(ms / 60000); + const seconds = Math.ceil((ms % 60000) / 1000); + return seconds > 0 ? `${minutes}m ${seconds}s` : `${minutes}m`; +} + +// DOM utilities +export function showElement(element: HTMLElement | null): void { + if (element) { + element.style.display = 'block'; + element.classList.remove('hidden'); + } +} + +export function hideElement(element: HTMLElement | null): void { + if (element) { + element.style.display = 'none'; + element.classList.add('hidden'); + } +} + +// Autocomplete functionality (kept from original clientUtils.ts as it's UI-specific) interface AutocompleteOptions { minLength?: number; maxResults?: number; @@ -202,7 +282,7 @@ export class AutocompleteManager { defaultRender(item: any): string { const text = typeof item === 'string' ? item : item.name || item.label || item.toString(); - return `
${this.escapeHtml(text)}
`; + return `
${escapeHtml(text)}
`; } renderDropdown(): void { @@ -284,8 +364,8 @@ export class AutocompleteManager { align-items: center; gap: 0.25rem; "> - ${this.escapeHtml(item)} - - - -
+ `; @@ -1918,32 +1917,38 @@ class AIQueryInterface { const priority = tool.recommendation ? tool.recommendation.priority : tool.priority; const confidenceTooltip = tool.confidence ? this.renderConfidenceTooltip(tool.confidence) : ''; + const cardClass = this.getToolClass(tool, 'recommendation'); return ` -
-
-

- ${tool.icon ? `${tool.icon}` : ''} +
+ +
+

+ ${tool.icon ? `${tool.icon}` : ''} ${tool.name}

-
- +
+ ${priority} ${confidenceTooltip}
-
+
"${sanitizeText(tool.justification || (tool.recommendation && tool.recommendation.justification) || `Empfohlen für ${tool.phase}`)}"
-
-
+
+
${this.renderToolBadges(tool)}
-
+
${tool.type === 'method' ? 'Methode' : tool.platforms.slice(0, 2).join(', ')} ${tool.skillLevel}
@@ -2021,15 +2026,15 @@ class AIQueryInterface { const confidenceTooltip = recommendation.confidence ? this.renderConfidenceTooltip(recommendation.confidence) : ''; return ` -
+
${rank}
-
-

${tool.name}

-
- +
+

${tool.name}

+
+ ${this.getSuitabilityText(recommendation.suitability_score)} ${confidenceTooltip} @@ -2037,13 +2042,13 @@ class AIQueryInterface {
-
-

Warum diese Methode?

-
${sanitizeText(recommendation.detailed_explanation)}
+
+

Warum diese Methode?

+
${sanitizeText(recommendation.detailed_explanation)}
${recommendation.implementation_approach ? ` -

Anwendungsansatz

-
${sanitizeText(recommendation.implementation_approach)}
+

Anwendungsansatz

+
${sanitizeText(recommendation.implementation_approach)}
` : ''}
@@ -2058,66 +2063,69 @@ class AIQueryInterface { if (!confidence || typeof confidence.overall !== 'number') return ''; const confidenceColor = getConfidenceColor(confidence.overall); + const tooltipId = `tooltip-${Math.random().toString(36).substr(2, 9)}`; return ` - -
- ${confidence.overall}% +
+ ${confidence.overall}% -