diff --git a/src/components/AIQueryInterface.astro b/src/components/AIQueryInterface.astro index b513a6c..46d9f54 100644 --- a/src/components/AIQueryInterface.astro +++ b/src/components/AIQueryInterface.astro @@ -1,756 +1,24 @@ --- // src/components/AIQueryInterface.astro - +import AIQueryForm from './ui/AIQueryForm.astro'; +import AIResults from './ui/AIResults.astro'; +import AuditTrailView from './ui/AuditTrailView.astro'; import { getToolsData } from '../utils/dataService.js'; const data = await getToolsData(); const tools = data.tools; -const phases = data.phases; -const domainAgnosticSoftware = data['domain-agnostic-software'] || []; --- - \ No newline at end of file diff --git a/src/components/ui/AIQueryForm.astro b/src/components/ui/AIQueryForm.astro new file mode 100644 index 0000000..d9e6f08 --- /dev/null +++ b/src/components/ui/AIQueryForm.astro @@ -0,0 +1,131 @@ +--- +// src/components/ui/AIQueryForm.astro +--- + +
+
+

+ + + + + Forensic AI +

+

+ Beschreiben Sie Ihr forensisches Szenario und erhalten Sie maßgeschneiderte Workflow-Empfehlungen + basierend auf bewährten DFIR-Workflows und der verfügbaren Software-Datenbank. +

+
+ +
+ +
+ + + + + + Workflow-Empfehlung + + +
+
+
+ + + + + + Spezifische Software oder Methode + +
+ + +
+
+ +
0/2000
+
+ +
+
+
+
+ + + + + +
+
+

Intelligente Hilfe

+

+ Während Sie tippen, analysiert die KI Ihre Eingabe und schlägt gezielten Fragen vor. +

+
+ Aktiviert ab: + 40+ Zeichen +
+
+
+
+ + +
+
+ + +
+

+ + + + + + Ihre Anfrage wird über die API von mistral.ai übertragen. + Datenschutzrichtlinien +

+
+ + +
+ +
+
+
\ No newline at end of file diff --git a/src/components/ui/AIResults.astro b/src/components/ui/AIResults.astro new file mode 100644 index 0000000..d0fc3c3 --- /dev/null +++ b/src/components/ui/AIResults.astro @@ -0,0 +1,78 @@ +--- +// src/components/ui/AIResults.astro +--- + + + + + + + + + \ No newline at end of file diff --git a/src/components/ui/AuditTrailView.astro b/src/components/ui/AuditTrailView.astro new file mode 100644 index 0000000..76e8c36 --- /dev/null +++ b/src/components/ui/AuditTrailView.astro @@ -0,0 +1,370 @@ +--- +// src/components/ui/AuditTrailView.astro +/// +--- + +
+ + \ No newline at end of file diff --git a/src/config/appConfig.ts b/src/config/appConfig.ts new file mode 100644 index 0000000..0edde20 --- /dev/null +++ b/src/config/appConfig.ts @@ -0,0 +1,74 @@ +// src/config/appConfig.ts +import dotenv from 'dotenv'; + +dotenv.config(); + +function getRequiredEnv(key: string): string { + const value = process.env[key]; + if (!value) { + throw new Error(`Missing required environment variable: ${key}`); + } + return value; +} + +function getEnvInt(key: string, defaultValue: number): number { + const value = process.env[key]; + return value ? parseInt(value, 10) : defaultValue; +} + +function getEnvFloat(key: string, defaultValue: number): number { + const value = process.env[key]; + return value ? parseFloat(value) : defaultValue; +} + +function getEnvBool(key: string, defaultValue: boolean): boolean { + const value = process.env[key]; + return value ? value === 'true' : defaultValue; +} + +export const config = { + ai: { + endpoint: getRequiredEnv('AI_ANALYZER_ENDPOINT'), + apiKey: getRequiredEnv('AI_ANALYZER_API_KEY'), + model: getRequiredEnv('AI_ANALYZER_MODEL'), + maxSelectedItems: getEnvInt('AI_MAX_SELECTED_ITEMS', 25), + embeddingCandidates: getEnvInt('AI_EMBEDDING_CANDIDATES', 50), + similarityThreshold: getEnvFloat('AI_SIMILARITY_THRESHOLD', 0.3), + microTaskDelay: getEnvInt('AI_MICRO_TASK_DELAY_MS', 500), + embeddingSelectionLimit: getEnvInt('AI_EMBEDDING_SELECTION_LIMIT', 30), + embeddingConceptsLimit: getEnvInt('AI_EMBEDDING_CONCEPTS_LIMIT', 15), + noEmbeddingsToolLimit: getEnvInt('AI_NO_EMBEDDINGS_TOOL_LIMIT', 25), + noEmbeddingsConceptLimit: getEnvInt('AI_NO_EMBEDDINGS_CONCEPT_LIMIT', 10), + embeddingsMinTools: getEnvInt('AI_EMBEDDINGS_MIN_TOOLS', 8), + embeddingsMaxReductionRatio: getEnvFloat('AI_EMBEDDINGS_MAX_REDUCTION_RATIO', 0.75), + methodSelectionRatio: getEnvFloat('AI_METHOD_SELECTION_RATIO', 0.4), + softwareSelectionRatio: getEnvFloat('AI_SOFTWARE_SELECTION_RATIO', 0.5), + maxContextTokens: getEnvInt('AI_MAX_CONTEXT_TOKENS', 4000), + maxPromptTokens: getEnvInt('AI_MAX_PROMPT_TOKENS', 1500), + taskTimeout: getEnvInt('AI_TASK_TIMEOUT_MS', 300000) + }, + + audit: { + enabled: getEnvBool('FORENSIC_AUDIT_ENABLED', false), + detailLevel: (process.env.FORENSIC_AUDIT_DETAIL_LEVEL || 'standard') as 'minimal' | 'standard' | 'verbose', + retentionHours: getEnvInt('FORENSIC_AUDIT_RETENTION_HOURS', 72), + maxEntries: getEnvInt('FORENSIC_AUDIT_MAX_ENTRIES', 50) + }, + + confidence: { + semanticWeight: getEnvFloat('CONFIDENCE_SEMANTIC_WEIGHT', 0.3), + suitabilityWeight: getEnvFloat('CONFIDENCE_SUITABILITY_WEIGHT', 0.7), + minimumThreshold: getEnvInt('CONFIDENCE_MINIMUM_THRESHOLD', 40), + mediumThreshold: getEnvInt('CONFIDENCE_MEDIUM_THRESHOLD', 60), + highThreshold: getEnvInt('CONFIDENCE_HIGH_THRESHOLD', 80) + }, + + rateLimit: { + delayMs: getEnvInt('AI_RATE_LIMIT_DELAY_MS', 2000), + window: 60 * 1000, // 1 minute + maxRequests: getEnvInt('AI_RATE_LIMIT_MAX_REQUESTS', 4), + microTaskTotalLimit: getEnvInt('AI_MICRO_TASK_TOTAL_LIMIT', 50) + } +} as const; + +export type AppConfig = typeof config; \ No newline at end of file diff --git a/src/env.d.ts b/src/env.d.ts index 8bd00b4..801818b 100644 --- a/src/env.d.ts +++ b/src/env.d.ts @@ -1,47 +1,134 @@ /// +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[]; + }; +} + +interface CompressedAuditEntry { + timestamp: number; + phase: string; + action: string; + inputSummary: string; + outputSummary: string; + confidence: number; + processingTimeMs: number; + metadata: Record; +} + declare global { interface Window { + // Theme utilities themeUtils: { initTheme: () => void; toggleTheme: () => void; getStoredTheme: () => string; + getSystemTheme: () => string; + applyTheme: (theme: string) => void; + updateThemeToggle: (theme: string) => void; }; + + // Tool utilities - THESE WERE MISSING toolsData: any[]; + createToolSlug: (toolName: string) => string; + findToolByIdentifier: (tools: any[], identifier: string) => any | undefined; + isToolHosted: (tool: any) => boolean; + + // Modal and UI utilities showToolDetails: (toolName: string, modalType?: string) => void; hideToolDetails: (modalType?: string) => void; hideAllToolDetails: () => void; toggleKbEntry: (entryId: string) => void; toggleDomainAgnosticSection: (sectionId: string) => void; + modalHideInProgress?: boolean; + + // AI interface utilities restoreAIResults?: () => void; switchToAIView?: () => void; + + // Filter and search utilities - THESE WERE MISSING clearTagFilters?: () => void; clearAllFilters?: () => void; - - createToolSlug: (toolName: string) => string; - findToolByIdentifier: (tools: any[], identifier: string) => any | undefined; - isToolHosted: (tool: any) => boolean; + applyScenarioSearch?: (scenarioId: string) => void; + selectPhase?: (phase: string) => void; + selectApproach?: (approach: string) => void; + prioritizeSearchResults: (tools: any[], searchTerm: string) => any[]; + applyFilters?: () => void; // THIS WAS MISSING + // Navigation utilities + navigateToGrid?: (toolName: string) => void; + navigateToMatrix?: (toolName: string) => void; + toggleAllScenarios?: () => void; + showShareDialog?: (shareButton: Element) => void; + + // Scroll utilities + scrollToElement: (element: Element | null, options?: ScrollIntoViewOptions) => void; + scrollToElementById: (elementId: string, options?: ScrollIntoViewOptions) => void; + scrollToElementBySelector: (selector: string, options?: ScrollIntoViewOptions) => void; + + // Authentication utilities checkClientAuth: (context?: string) => Promise<{authenticated: boolean; authRequired: boolean; expires?: string}>; requireClientAuth: (callback?: () => void, returnUrl?: string, context?: string) => Promise; showIfAuthenticated: (selector: string, context?: string) => Promise; setupAuthButtons: (selector?: string) => void; - scrollToElement: (element: Element | null, options?: ScrollIntoViewOptions) => void; - scrollToElementById: (elementId: string, options?: ScrollIntoViewOptions) => void; - scrollToElementBySelector: (selector: string, options?: ScrollIntoViewOptions) => void; - - applyScenarioSearch?: (scenarioId: string) => void; - selectPhase?: (phase: string) => void; - selectApproach?: (approach: string) => void; - navigateToGrid?: (toolName: string) => void; - navigateToMatrix?: (toolName: string) => void; - toggleAllScenarios?: () => void; - showShareDialog?: (shareButton: Element) => void; - modalHideInProgress?: boolean; - + // Sharing utilities shareArticle: (button: HTMLElement, url: string, title: string) => Promise; shareCurrentArticle: (button: HTMLElement) => Promise; + + // Audit trail utilities + auditService?: { + processAuditTrail: (rawAuditTrail: any[]) => ProcessedAuditTrail | null; + formatDuration: (ms: number) => string; + getConfidenceColor: (confidence: number) => string; + getActionDisplayName: (action: string) => string; + isEnabled: () => boolean; + }; + + AuditUtils?: { + formatDuration: (ms: number) => string; + getConfidenceColor: (confidence: number) => string; + escapeHtml: (text: string) => string; + sanitizeText: (text: string) => string; + }; + + AuditTrailRenderer?: new ( + containerId: string, + options?: { + title?: string; + collapsible?: boolean; + defaultExpanded?: boolean; + } + ) => { + containerId: string; + options: { + title: string; + collapsible: boolean; + defaultExpanded: boolean; + }; + render: (processedAudit: ProcessedAuditTrail | null) => void; + }; + } + + // EventTarget extension for better typing + interface EventTarget { + closest?: (selector: string) => Element | null; } } @@ -49,4 +136,4 @@ declare module 'js-yaml' { export function load(str: string): any; } -export {}; \ No newline at end of file +export {}; diff --git a/src/layouts/BaseLayout.astro b/src/layouts/BaseLayout.astro index d8aa06b..741b154 100644 --- a/src/layouts/BaseLayout.astro +++ b/src/layouts/BaseLayout.astro @@ -1,4 +1,5 @@ --- +/// import Navigation from '../components/Navigation.astro'; import Footer from '../components/Footer.astro'; import '../styles/global.css'; @@ -25,133 +26,123 @@ const { title, description = 'ForensicPathways - A comprehensive directory of di diff --git a/src/pages/api/ai/enhance-input.ts b/src/pages/api/ai/enhance-input.ts index 392ca86..b57c631 100644 --- a/src/pages/api/ai/enhance-input.ts +++ b/src/pages/api/ai/enhance-input.ts @@ -1,24 +1,15 @@ -// src/pages/api/ai/enhance-input.ts - Enhanced AI service compatibility +// src/pages/api/ai/enhance-input.ts 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 '../../../services/ai/aiService.js'; +import { config } from '../../../config/appConfig.js'; +import { logger } from '../../../services/logger.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; @@ -95,39 +86,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'); @@ -145,39 +103,52 @@ export const POST: APIRoute = async ({ request }) => { const { input } = body; if (!input || typeof input !== 'string' || input.length < 40) { + logger.api('POST', '/api/ai/enhance-input', 400, { + error: 'Input too short', + userId, + inputLength: input?.length || 0 + }); return apiError.badRequest('Input too short for enhancement (minimum 40 characters)'); } const sanitizedInput = sanitizeInput(input); if (sanitizedInput.length < 40) { + logger.api('POST', '/api/ai/enhance-input', 400, { + error: 'Input too short after sanitization', + userId + }); return apiError.badRequest('Input too short after sanitization'); } const systemPrompt = createEnhancementPrompt(sanitizedInput); const taskId = `enhance_${userId}_${Date.now()}_${Math.random().toString(36).substr(2, 4)}`; - const aiResponse = await enqueueApiCall(() => callAIService(systemPrompt), taskId); + logger.info('enhance-input', 'Processing enhancement request', { + userId, + inputLength: sanitizedInput.length, + taskId + }); - if (!aiResponse.ok) { - const errorText = await aiResponse.text(); - console.error('[ENHANCE API] AI enhancement error:', errorText, 'Status:', aiResponse.status); + const aiResponse = await enqueueApiCall(() => + aiService.call({ + prompt: systemPrompt, + maxTokens: 300, + temperature: 0.7, + context: 'input-enhancement' + }) + , taskId); + + if (!aiResponse.content) { + logger.error('enhance-input', 'No enhancement response received', undefined, { + userId, + taskId + }); return apiServerError.unavailable('Enhancement service unavailable'); } - const aiData = await aiResponse.json(); - const aiContent = aiData.choices?.[0]?.message?.content; - - if (!aiContent) { - return apiServerError.unavailable('No enhancement response'); - } - let questions; try { - const cleanedContent = aiContent - .replace(/^```json\s*/i, '') - .replace(/\s*```\s*$/, '') - .trim(); - questions = JSON.parse(cleanedContent); + questions = aiService.parseJSONResponse(aiResponse.content, []); if (!Array.isArray(questions)) { throw new Error('Response is not an array'); @@ -199,11 +170,22 @@ export const POST: APIRoute = async ({ request }) => { } } catch (error) { - console.error('Failed to parse enhancement response:', aiContent); + logger.warn('enhance-input', 'Failed to parse enhancement response', { + userId, + taskId, + error: error.message, + responsePreview: aiResponse.content.slice(0, 100) + }); questions = []; } - console.log(`[ENHANCE API] User: ${userId}, Forensics Questions: ${questions.length}, Input length: ${sanitizedInput.length}`); + logger.api('POST', '/api/ai/enhance-input', 200, { + userId, + taskId, + inputLength: sanitizedInput.length, + questionsGenerated: questions.length, + processingTime: `${aiResponse.requestTimeMs}ms` + }); return new Response(JSON.stringify({ success: true, @@ -216,7 +198,9 @@ export const POST: APIRoute = async ({ request }) => { }); } catch (error) { - console.error('Enhancement error:', error); + logger.error('enhance-input', 'Enhancement processing failed', error as Error, { + endpoint: '/api/ai/enhance-input' + }); 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 2e94071..478fe3e 100644 --- a/src/pages/api/ai/query.ts +++ b/src/pages/api/ai/query.ts @@ -4,7 +4,9 @@ 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 { aiPipeline } from '../../../utils/aiPipeline.js'; +import { pipelineOrchestrator } from '../../../services/ai/pipelineOrchestrator.js'; +import { config } from '../../../config/appConfig.js'; +import { logger } from '../../../services/logger.js'; export const prerender = false; @@ -16,10 +18,6 @@ interface RateLimitData { const rateLimitStore = new Map(); -const RATE_LIMIT_WINDOW = 60 * 1000; // 1 minute -const MAIN_RATE_LIMIT_MAX = parseInt(process.env.AI_RATE_LIMIT_MAX_REQUESTS || '4', 10); -const MICRO_TASK_TOTAL_LIMIT = parseInt(process.env.AI_MICRO_TASK_TOTAL_LIMIT || '50', 10); - function sanitizeInput(input: string): string { let sanitized = input .replace(/```[\s\S]*?```/g, '[CODE_BLOCK_REMOVED]') @@ -39,26 +37,26 @@ function checkRateLimit(userId: string): { allowed: boolean; reason?: string; mi if (!userLimit || now > userLimit.resetTime) { rateLimitStore.set(userId, { count: 1, - resetTime: now + RATE_LIMIT_WINDOW, + resetTime: now + config.rateLimit.window, microTaskCount: 0 }); return { allowed: true, - microTasksRemaining: MICRO_TASK_TOTAL_LIMIT + microTasksRemaining: config.rateLimit.microTaskTotalLimit }; } - if (userLimit.count >= MAIN_RATE_LIMIT_MAX) { + if (userLimit.count >= config.rateLimit.maxRequests) { return { allowed: false, - reason: `Main rate limit exceeded. Max ${MAIN_RATE_LIMIT_MAX} requests per minute.` + reason: `Main rate limit exceeded. Max ${config.rateLimit.maxRequests} requests per minute.` }; } - if (userLimit.microTaskCount >= MICRO_TASK_TOTAL_LIMIT) { + if (userLimit.microTaskCount >= config.rateLimit.microTaskTotalLimit) { return { allowed: false, - reason: `Micro-task limit exceeded. Max ${MICRO_TASK_TOTAL_LIMIT} AI calls per minute.` + reason: `Micro-task limit exceeded. Max ${config.rateLimit.microTaskTotalLimit} AI calls per minute.` }; } @@ -66,7 +64,7 @@ function checkRateLimit(userId: string): { allowed: boolean; reason?: string; mi return { allowed: true, - microTasksRemaining: MICRO_TASK_TOTAL_LIMIT - userLimit.microTaskCount + microTasksRemaining: config.rateLimit.microTaskTotalLimit - userLimit.microTaskCount }; } @@ -74,7 +72,11 @@ function incrementMicroTaskCount(userId: string, aiCallsMade: number): void { const userLimit = rateLimitStore.get(userId); if (userLimit) { userLimit.microTaskCount += aiCallsMade; - console.log(`[RATE LIMIT] User ${userId} now at ${userLimit.microTaskCount}/${MICRO_TASK_TOTAL_LIMIT} micro-task calls`); + logger.debug('rate-limit', `User micro-task usage updated`, { + userId, + currentCount: userLimit.microTaskCount, + limit: config.rateLimit.microTaskTotalLimit + }); } } @@ -95,7 +97,10 @@ function cleanupExpiredRateLimits() { const toRemove = entries.slice(0, entries.length - maxStoreSize); toRemove.forEach(([userId]) => rateLimitStore.delete(userId)); - console.log(`[RATE LIMIT] Cleanup: removed ${toRemove.length} old entries`); + logger.info('rate-limit', `Cleanup completed`, { + removed: toRemove.length, + remaining: rateLimitStore.size + }); } } @@ -118,55 +123,60 @@ 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}`); - if (!query || typeof query !== 'string') { - console.log(`[MICRO-TASK API] Invalid query for task ${clientTaskId}`); + logger.api('POST', '/api/ai/query', 400, { error: 'Invalid query', userId }); return apiError.badRequest('Query required'); } if (!['workflow', 'tool'].includes(mode)) { - console.log(`[MICRO-TASK API] Invalid mode for task ${clientTaskId}: ${mode}`); + logger.api('POST', '/api/ai/query', 400, { error: 'Invalid mode', userId, 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}`); + logger.api('POST', '/api/ai/query', 400, { error: 'Filtered input', userId }); 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}`); + logger.api('POST', '/api/ai/query', 200, { + taskId, + mode, + queryLength: sanitizedQuery.length, + userId, + microTasksRemaining: rateLimitResult.microTasksRemaining + }); const result = await enqueueApiCall(() => - aiPipeline.processQuery(sanitizedQuery, mode) + pipelineOrchestrator.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}`); + logger.api('POST', '/api/ai/query', 200, { + taskId, + mode, + userId, + queryLength: sanitizedQuery.length, + processingTime: `${stats.processingTimeMs}ms`, + microTasksCompleted: stats.microTasksCompleted, + microTasksFailed: stats.microTasksFailed, + estimatedAICallsMade, + embeddingsUsed: stats.embeddingsUsed, + finalItems: stats.finalSelectedItems + }); const currentLimit = rateLimitStore.get(userId); const remainingMicroTasks = currentLimit ? - MICRO_TASK_TOTAL_LIMIT - currentLimit.microTaskCount : MICRO_TASK_TOTAL_LIMIT; + config.rateLimit.microTaskTotalLimit - currentLimit.microTaskCount : config.rateLimit.microTaskTotalLimit; return new Response(JSON.stringify({ success: true, @@ -182,9 +192,9 @@ export const POST: APIRoute = async ({ request }) => { estimatedAICallsMade }, rateLimitInfo: { - mainRequestsRemaining: MAIN_RATE_LIMIT_MAX - (currentLimit?.count || 0), + mainRequestsRemaining: config.rateLimit.maxRequests - (currentLimit?.count || 0), microTaskCallsRemaining: remainingMicroTasks, - resetTime: Date.now() + RATE_LIMIT_WINDOW + resetTime: Date.now() + config.rateLimit.window } }), { status: 200, @@ -192,7 +202,9 @@ export const POST: APIRoute = async ({ request }) => { }); } catch (error) { - console.error('[MICRO-TASK API] Pipeline error:', error); + logger.error('api', 'Pipeline processing failed', error as Error, { + endpoint: '/api/ai/query' + }); if (error.message.includes('embeddings')) { return apiServerError.unavailable('Embeddings service error - using AI fallback'); diff --git a/src/services/ai/aiService.ts b/src/services/ai/aiService.ts new file mode 100644 index 0000000..63129ad --- /dev/null +++ b/src/services/ai/aiService.ts @@ -0,0 +1,228 @@ +// src/services/ai/aiService.ts +import { config } from '../../config/appConfig.js'; +import { logger } from '../logger.js'; + +export interface AIRequest { + prompt: string; + maxTokens?: number; + temperature?: number; + context?: string; +} + +export interface AIResponse { + content: string; + requestTimeMs: number; + tokenCount?: number; +} + +export class AIService { + private readonly endpoint: string; + private readonly apiKey: string; + private readonly model: string; + + constructor() { + this.endpoint = config.ai.endpoint; + this.apiKey = config.ai.apiKey; + this.model = config.ai.model; + + logger.info('ai-service', 'AI Service initialized', { + model: this.model, + hasApiKey: !!this.apiKey + }); + } + + async call(request: AIRequest): Promise { + const startTime = Date.now(); + const timerId = logger.time('ai-service', `call-${request.context || 'unknown'}`); + + try { + const headers: Record = { + 'Content-Type': 'application/json' + }; + + if (this.apiKey) { + headers['Authorization'] = `Bearer ${this.apiKey}`; + } + + const requestBody = { + model: this.model, + messages: [{ role: 'user', content: request.prompt }], + max_tokens: request.maxTokens || 1500, + temperature: request.temperature || 0.3 + }; + + logger.debug('ai-service', 'Making AI request', { + context: request.context, + promptLength: request.prompt.length, + maxTokens: requestBody.max_tokens, + temperature: requestBody.temperature + }); + + const response = await fetch(`${this.endpoint}/v1/chat/completions`, { + method: 'POST', + headers, + body: JSON.stringify(requestBody) + }); + + const requestTimeMs = Date.now() - startTime; + logger.timeEnd(timerId); + + if (!response.ok) { + const errorText = await response.text(); + logger.error('ai-service', 'AI API request failed', new Error(`HTTP ${response.status}`), { + status: response.status, + context: request.context, + errorText: errorText.slice(0, 200) + }); + throw new Error(`AI API error: ${response.status} - ${errorText}`); + } + + const data = await response.json(); + const content = data.choices?.[0]?.message?.content; + + if (!content) { + logger.error('ai-service', 'No response content from AI model', undefined, { + context: request.context, + choices: data.choices?.length || 0 + }); + throw new Error('No response from AI model'); + } + + logger.info('ai-service', 'AI request completed', { + context: request.context, + responseLength: content.length, + duration: `${requestTimeMs}ms`, + success: true + }); + + return { + content: content.trim(), + requestTimeMs, + tokenCount: data.usage?.total_tokens + }; + + } catch (error) { + const requestTimeMs = Date.now() - startTime; + logger.error('ai-service', 'AI service call failed', error as Error, { + context: request.context, + duration: `${requestTimeMs}ms` + }); + throw error; + } + } + + async callMicroTask(prompt: string, context: string, maxTokens: number = 500): Promise { + return this.call({ + prompt, + maxTokens, + temperature: 0.3, + context: `micro-task:${context}` + }); + } + + estimateTokens(text: string): number { + // Simple estimation: ~4 characters per token + return Math.ceil(text.length / 4); + } + + validatePromptSize(prompt: string, maxTokens: number = config.ai.maxPromptTokens): boolean { + const estimatedTokens = this.estimateTokens(prompt); + if (estimatedTokens > maxTokens) { + logger.warn('ai-service', 'Prompt exceeds token limit', { + estimated: estimatedTokens, + limit: maxTokens, + promptLength: prompt.length + }); + return false; + } + return true; + } + + // Utility for parsing JSON responses with error handling + parseJSONResponse(response: string, fallback: T): T { + try { + let cleaned = response.trim(); + + // Remove markdown code blocks + 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; + } + } + + // Handle truncated JSON + if (!cleaned.endsWith('}') && !cleaned.endsWith(']')) { + logger.warn('ai-service', '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); + + logger.debug('ai-service', 'JSON parsing successful', { + originalLength: response.length, + cleanedLength: cleaned.length + }); + + return parsed; + + } catch (error) { + logger.warn('ai-service', 'JSON parsing failed, using fallback', { + error: (error as Error).message, + responsePreview: response.slice(0, 100) + }); + return fallback; + } + } +} + +export const aiService = new AIService(); \ No newline at end of file diff --git a/src/services/ai/confidenceService.ts b/src/services/ai/confidenceService.ts new file mode 100644 index 0000000..6593e84 --- /dev/null +++ b/src/services/ai/confidenceService.ts @@ -0,0 +1,228 @@ +// src/services/ai/confidenceService.ts +import { config } from '../../config/appConfig.js'; +import { isToolHosted } from '../../utils/toolHelpers.js'; +import { logger } from '../logger.js'; + +export interface ConfidenceMetrics { + overall: number; + semanticRelevance: number; + taskSuitability: number; + uncertaintyFactors: string[]; + strengthIndicators: string[]; +} + +export interface AnalysisContext { + userQuery: string; + mode: string; + embeddingsSimilarities: Map; + selectedTools?: Array<{ + tool: any; + phase: string; + priority: string; + }>; +} + +export class ConfidenceService { + private readonly semanticWeight: number; + private readonly suitabilityWeight: number; + private readonly minimumThreshold: number; + private readonly mediumThreshold: number; + private readonly highThreshold: number; + + constructor() { + this.semanticWeight = config.confidence.semanticWeight; + this.suitabilityWeight = config.confidence.suitabilityWeight; + this.minimumThreshold = config.confidence.minimumThreshold; + this.mediumThreshold = config.confidence.mediumThreshold; + this.highThreshold = config.confidence.highThreshold; + + logger.info('confidence', 'Confidence service initialized', { + semanticWeight: this.semanticWeight, + suitabilityWeight: this.suitabilityWeight + }); + } + + calculateRecommendationConfidence( + tool: any, + context: AnalysisContext, + taskRelevance: number = 70, + limitations: string[] = [] + ): ConfidenceMetrics { + // Calculate semantic relevance from embeddings + const rawSemanticRelevance = context.embeddingsSimilarities.has(tool.name) ? + context.embeddingsSimilarities.get(tool.name)! * 100 : 50; + + // Enhanced task suitability based on context + 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); + } + } + + // Calculate overall confidence + const overall = ( + rawSemanticRelevance * this.semanticWeight + + enhancedTaskSuitability * this.suitabilityWeight + ); + + const uncertaintyFactors = this.identifyUncertaintyFactors( + tool, context, limitations, overall + ); + + const strengthIndicators = this.identifyStrengthIndicators( + tool, context, overall + ); + + const result: ConfidenceMetrics = { + overall: Math.round(overall), + semanticRelevance: Math.round(rawSemanticRelevance), + taskSuitability: Math.round(enhancedTaskSuitability), + uncertaintyFactors, + strengthIndicators + }; + + logger.confidence( + tool.name, + result.overall, + result.semanticRelevance, + result.taskSuitability + ); + + return result; + } + + private identifyUncertaintyFactors( + tool: any, + context: AnalysisContext, + limitations: string[], + confidence: number + ): string[] { + const factors: string[] = []; + + // Include provided limitations + if (limitations?.length > 0) { + factors.push(...limitations.slice(0, 2)); + } + + // Semantic similarity concerns + const similarity = context.embeddingsSimilarities.get(tool.name) || 0.5; + if (similarity < 0.7) { + factors.push('Geringe semantische Ähnlichkeit zur Anfrage'); + } + + // Skill level vs query urgency mismatch + 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'); + } + + // Installation/setup requirements + if (tool.type === 'software' && !isToolHosted(tool) && + tool.accessType === 'download') { + factors.push('Installation und Setup erforderlich'); + } + + // License considerations + if (tool.license === 'Proprietary') { + factors.push('Kommerzielle Software - Lizenzkosten zu beachten'); + } + + // Overall confidence concerns + 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[] = []; + + // High semantic similarity + const similarity = context.embeddingsSimilarities.get(tool.name) || 0.5; + if (similarity >= 0.7) { + indicators.push('Sehr gute semantische Übereinstimmung mit Ihrer Anfrage'); + } + + // Documentation availability + if (tool.knowledgebase === true) { + indicators.push('Umfassende Dokumentation und Wissensbasis verfügbar'); + } + + // Immediate availability + if (isToolHosted(tool)) { + indicators.push('Sofort verfügbar über gehostete Lösung'); + } + + // Balanced complexity + if (tool.skillLevel === 'intermediate' || tool.skillLevel === 'advanced') { + indicators.push('Ausgewogenes Verhältnis zwischen Funktionalität und Benutzerfreundlichkeit'); + } + + // Method alignment with query type + 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); + } + + getConfidenceLevel(confidence: number): 'low' | 'medium' | 'high' { + if (confidence >= this.highThreshold) return 'high'; + if (confidence >= this.mediumThreshold) return 'medium'; + return 'low'; + } + + getConfidenceColor(confidence: number): string { + if (confidence >= this.highThreshold) return 'var(--color-accent)'; + if (confidence >= this.mediumThreshold) return 'var(--color-warning)'; + return 'var(--color-error)'; + } + + calculateSelectionConfidence( + result: any, + candidateCount: number + ): number { + if (!result?.selectedTools) return 30; + + const selectionRatio = result.selectedTools.length / candidateCount; + const hasReasoning = result.reasoning && result.reasoning.length > 50; + + let confidence = 60; + + // Selection ratio scoring + if (selectionRatio > 0.05 && selectionRatio < 0.3) { + confidence += 20; + } else if (selectionRatio <= 0.05) { + confidence -= 10; + } else { + confidence -= 15; + } + + // Quality indicators + if (hasReasoning) confidence += 15; + if (result.selectedConcepts?.length > 0) confidence += 5; + + return Math.min(95, Math.max(25, confidence)); + } +} + +export const confidenceService = new ConfidenceService(); \ No newline at end of file diff --git a/src/services/ai/pipelineOrchestrator.ts b/src/services/ai/pipelineOrchestrator.ts new file mode 100644 index 0000000..b437755 --- /dev/null +++ b/src/services/ai/pipelineOrchestrator.ts @@ -0,0 +1,510 @@ +// src/services/ai/pipelineOrchestrator.ts +import { getCompressedToolsDataForAI } from '../../utils/dataService.js'; +import { getPrompt } from '../../config/prompts.js'; +import { config } from '../../config/appConfig.js'; +import { aiService } from './aiService.js'; +import { toolSelector } from './toolSelector.js'; +import { confidenceService } from './confidenceService.js'; +import { auditService } from '../../utils/auditService.js'; +import { logger } from '../logger.js'; + +export interface AnalysisResult { + recommendation: any; + processingStats: { + embeddingsUsed: boolean; + candidatesFromEmbeddings: number; + finalSelectedItems: number; + processingTimeMs: number; + microTasksCompleted: number; + microTasksFailed: number; + contextContinuityUsed: boolean; + }; +} + +export interface AnalysisContext { + userQuery: string; + mode: string; + filteredData: any; + selectedTools?: Array<{ + tool: any; + phase: string; + priority: string; + justification?: string; + taskRelevance?: number; + limitations?: string[]; + }>; + backgroundKnowledge?: Array<{ + concept: any; + relevance: string; + }>; + seenToolNames: Set; + embeddingsSimilarities: Map; + scenarioAnalysis?: string; + problemAnalysis?: string; + investigationApproach?: string; + criticalConsiderations?: string; +} + +export class PipelineOrchestrator { + private readonly microTaskDelay: number; + + constructor() { + this.microTaskDelay = config.ai.microTaskDelay; + logger.info('pipeline', 'Pipeline orchestrator initialized'); + } + + async processQuery(userQuery: string, mode: string): Promise { + const startTime = Date.now(); + let completeTasks = 0; + let failedTasks = 0; + + logger.pipeline('initialization', 'processing started', { userQuery: userQuery.slice(0, 50), mode }); + + try { + const toolsData = await getCompressedToolsDataForAI(); + + const context: AnalysisContext = { + userQuery, + mode, + filteredData: {}, + seenToolNames: new Set(), + embeddingsSimilarities: new Map() + }; + + // Step 1: Get candidates and select tools + const candidateData = await toolSelector.getCandidates(userQuery, toolsData); + const toolSelection = await toolSelector.selectTools( + userQuery, + candidateData.tools, + candidateData.concepts, + mode, + candidateData.selectionMethod + ); + + context.filteredData = { + tools: toolSelection.selectedTools, + concepts: toolSelection.selectedConcepts, + domains: toolsData.domains, + phases: toolsData.phases, + 'domain-agnostic-software': toolsData['domain-agnostic-software'] + }; + + // Step 2: Scenario/Problem Analysis + const analysisResult = await this.analyzeScenario(context); + if (analysisResult.success) completeTasks++; else failedTasks++; + await this.delay(this.microTaskDelay); + + // Step 3: Investigation Approach + const approachResult = await this.generateApproach(context); + if (approachResult.success) completeTasks++; else failedTasks++; + await this.delay(this.microTaskDelay); + + // Step 4: Critical Considerations + const considerationsResult = await this.generateCriticalConsiderations(context); + if (considerationsResult.success) completeTasks++; else failedTasks++; + await this.delay(this.microTaskDelay); + + // Step 5: Mode-specific processing + if (mode === 'workflow') { + const workflowTasks = await this.processWorkflowMode(context, toolsData); + completeTasks += workflowTasks.completed; + failedTasks += workflowTasks.failed; + } else { + const toolTasks = await this.processToolMode(context); + completeTasks += toolTasks.completed; + failedTasks += toolTasks.failed; + } + + // Step 6: Background Knowledge + const knowledgeResult = await this.selectBackgroundKnowledge(context); + if (knowledgeResult.success) completeTasks++; else failedTasks++; + await this.delay(this.microTaskDelay); + + // Step 7: Final Recommendations + const finalResult = await this.generateFinalRecommendations(context); + if (finalResult.success) completeTasks++; else failedTasks++; + + const recommendation = this.buildRecommendation(context, mode, finalResult.content); + + const processingStats = { + embeddingsUsed: candidateData.selectionMethod === 'embeddings_candidates', + candidatesFromEmbeddings: candidateData.tools.length, + finalSelectedItems: (context.selectedTools?.length || 0) + (context.backgroundKnowledge?.length || 0), + processingTimeMs: Date.now() - startTime, + microTasksCompleted: completeTasks, + microTasksFailed: failedTasks, + contextContinuityUsed: true + }; + + logger.pipeline('completion', 'processing completed', { + duration: `${processingStats.processingTimeMs}ms`, + tasksCompleted: completeTasks, + tasksFailed: failedTasks, + finalItems: processingStats.finalSelectedItems + }); + + return { + recommendation: { + ...recommendation, + auditTrail: auditService.isEnabled() ? [] : undefined // Audit trail handled by auditService + }, + processingStats + }; + + } catch (error) { + const duration = Date.now() - startTime; + logger.error('pipeline', 'Processing failed', error as Error, { + duration: `${duration}ms`, + completedTasks: completeTasks, + failedTasks: failedTasks + }); + throw error; + } + } + + private async analyzeScenario(context: AnalysisContext): Promise<{ success: boolean; content: string }> { + try { + const isWorkflow = context.mode === 'workflow'; + const prompt = getPrompt('scenarioAnalysis', isWorkflow, context.userQuery); + + const response = await aiService.callMicroTask(prompt, 'scenario-analysis', 400); + + if (isWorkflow) { + context.scenarioAnalysis = response.content; + } else { + context.problemAnalysis = response.content; + } + + logger.pipeline('micro-task', `${isWorkflow ? 'scenario' : 'problem'} analysis completed`); + return { success: true, content: response.content }; + + } catch (error) { + logger.error('pipeline', 'Scenario analysis failed', error as Error); + return { success: false, content: '' }; + } + } + + private async generateApproach(context: AnalysisContext): Promise<{ success: boolean; content: string }> { + try { + const isWorkflow = context.mode === 'workflow'; + const prompt = getPrompt('investigationApproach', isWorkflow, context.userQuery); + + const response = await aiService.callMicroTask(prompt, 'investigation-approach', 400); + context.investigationApproach = response.content; + + logger.pipeline('micro-task', 'investigation approach completed'); + return { success: true, content: response.content }; + + } catch (error) { + logger.error('pipeline', 'Investigation approach failed', error as Error); + return { success: false, content: '' }; + } + } + + private async generateCriticalConsiderations(context: AnalysisContext): Promise<{ success: boolean; content: string }> { + try { + const isWorkflow = context.mode === 'workflow'; + const prompt = getPrompt('criticalConsiderations', isWorkflow, context.userQuery); + + const response = await aiService.callMicroTask(prompt, 'critical-considerations', 350); + context.criticalConsiderations = response.content; + + logger.pipeline('micro-task', 'critical considerations completed'); + return { success: true, content: response.content }; + + } catch (error) { + logger.error('pipeline', 'Critical considerations failed', error as Error); + return { success: false, content: '' }; + } + } + + private async processWorkflowMode(context: AnalysisContext, toolsData: any): Promise<{ completed: number; failed: number }> { + let completed = 0; + let failed = 0; + + const phases = toolsData.phases || []; + + for (const phase of phases) { + try { + const result = await this.selectToolsForPhase(context, phase); + if (result.success) completed++; else failed++; + await this.delay(this.microTaskDelay); + } catch (error) { + logger.error('pipeline', `Phase ${phase.id} tool selection failed`, error as Error); + failed++; + } + } + + logger.pipeline('workflow', 'phase processing completed', { + phases: phases.length, + completed, + failed + }); + + return { completed, failed }; + } + + private async processToolMode(context: AnalysisContext): Promise<{ completed: number; failed: number }> { + let completed = 0; + let failed = 0; + + const topTools = context.filteredData.tools.slice(0, 3); + + for (let i = 0; i < topTools.length; i++) { + try { + const result = await this.evaluateSpecificTool(context, topTools[i], i + 1); + if (result.success) completed++; else failed++; + await this.delay(this.microTaskDelay); + } catch (error) { + logger.error('pipeline', `Tool evaluation failed for ${topTools[i]?.name}`, error as Error); + failed++; + } + } + + logger.pipeline('tool-mode', 'tool evaluation completed', { + toolsEvaluated: topTools.length, + completed, + failed + }); + + return { completed, failed }; + } + + private async selectToolsForPhase(context: AnalysisContext, phase: any): Promise<{ success: boolean }> { + const phaseTools = context.filteredData.tools.filter((tool: any) => + tool && tool.phases && Array.isArray(tool.phases) && tool.phases.includes(phase.id) + ); + + if (phaseTools.length === 0) { + logger.debug('pipeline', `No tools available for phase ${phase.id}`); + return { success: true }; + } + + try { + const prompt = getPrompt('phaseToolSelection', context.userQuery, phase, phaseTools); + const response = await aiService.callMicroTask(prompt, `phase-${phase.id}`, 1000); + + const selections = aiService.parseJSONResponse(response.content, []); + + if (Array.isArray(selections)) { + const validSelections = selections.filter((sel: any) => { + return phaseTools.some((tool: any) => tool && tool.name === sel.toolName); + }); + + 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; + + this.addToolToSelection(context, tool, phase.id, this.derivePriorityFromScore(taskRelevance), + sel.justification, taskRelevance, sel.limitations); + } + }); + + logger.pipeline('phase-selection', `Phase ${phase.id} completed`, { + available: phaseTools.length, + selected: validSelections.length + }); + } + + return { success: true }; + + } catch (error) { + logger.error('pipeline', `Phase ${phase.id} tool selection failed`, error as Error); + return { success: false }; + } + } + + private async evaluateSpecificTool(context: AnalysisContext, tool: any, rank: number): Promise<{ success: boolean }> { + try { + const existingSelection = context.selectedTools?.find((st: any) => st.tool && st.tool.name === tool.name); + const taskRelevance = existingSelection?.taskRelevance || 70; + const priority = this.derivePriorityFromScore(taskRelevance); + + const prompt = getPrompt('toolEvaluation', context.userQuery, tool, rank, taskRelevance); + const response = await aiService.callMicroTask(prompt, `tool-eval-${tool.name}`, 1000); + + const evaluation = aiService.parseJSONResponse(response.content, { + detailed_explanation: 'Evaluation failed', + implementation_approach: '', + pros: [], + limitations: [], + alternatives: '' + }); + + this.addToolToSelection(context, { + ...tool, + evaluation: { + ...evaluation, + rank, + task_relevance: taskRelevance + } + }, 'evaluation', priority, evaluation.detailed_explanation, taskRelevance, evaluation.limitations); + + logger.pipeline('tool-evaluation', `Tool ${tool.name} evaluated`, { rank, taskRelevance }); + return { success: true }; + + } catch (error) { + logger.error('pipeline', `Tool evaluation failed for ${tool.name}`, error as Error); + return { success: false }; + } + } + + private async selectBackgroundKnowledge(context: AnalysisContext): Promise<{ success: boolean }> { + const availableConcepts = context.filteredData.concepts; + + if (availableConcepts.length === 0) { + return { success: true }; + } + + try { + const selectedToolNames = context.selectedTools?.map((st: any) => st.tool && st.tool.name).filter(Boolean) || []; + const prompt = getPrompt('backgroundKnowledgeSelection', context.userQuery, context.mode, selectedToolNames, availableConcepts); + const response = await aiService.callMicroTask(prompt, 'background-knowledge', 700); + + const selections = aiService.parseJSONResponse(response.content, []); + + if (Array.isArray(selections)) { + context.backgroundKnowledge = selections.filter((sel: any) => + sel.conceptName && availableConcepts.some((concept: any) => concept.name === sel.conceptName) + ).map((sel: any) => ({ + concept: availableConcepts.find((c: any) => c.name === sel.conceptName), + relevance: sel.relevance + })); + + logger.pipeline('background-knowledge', 'selection completed', { + available: availableConcepts.length, + selected: context.backgroundKnowledge?.length || 0 + }); + } + + return { success: true }; + + } catch (error) { + logger.error('pipeline', 'Background knowledge selection failed', error as Error); + return { success: false }; + } + } + + private async generateFinalRecommendations(context: AnalysisContext): Promise<{ success: boolean; content: string }> { + try { + 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 response = await aiService.callMicroTask(prompt, 'final-recommendations', 350); + + logger.pipeline('final', 'recommendations generated'); + return { success: true, content: response.content }; + + } catch (error) { + logger.error('pipeline', 'Final recommendations failed', error as Error); + return { success: false, content: '' }; + } + } + + private addToolToSelection( + context: AnalysisContext, + tool: any, + phase: string, + priority: string, + justification?: string, + taskRelevance?: number, + limitations?: string[] + ): void { + context.seenToolNames.add(tool.name); + if (!context.selectedTools) context.selectedTools = []; + + context.selectedTools.push({ + tool, + phase, + priority, + justification, + taskRelevance, + limitations + }); + } + + private derivePriorityFromScore(taskRelevance: number): string { + if (taskRelevance >= 80) return 'high'; + if (taskRelevance >= 60) return 'medium'; + return 'low'; + } + + private async delay(ms: number): Promise { + return new Promise(resolve => setTimeout(resolve, ms)); + } + + private buildRecommendation(context: AnalysisContext, mode: string, finalContent: string): any { + const isWorkflow = mode === 'workflow'; + + const base = { + [isWorkflow ? 'scenario_analysis' : 'problem_analysis']: + isWorkflow ? context.scenarioAnalysis : context.problemAnalysis, + investigation_approach: context.investigationApproach, + critical_considerations: context.criticalConsiderations, + background_knowledge: context.backgroundKnowledge?.map((bk: any) => ({ + concept_name: bk.concept.name, + relevance: bk.relevance + })) || [] + }; + + if (isWorkflow) { + const recommendedToolsWithConfidence = context.selectedTools?.map((st: any) => { + const confidence = confidenceService.calculateRecommendationConfidence( + st.tool, + context, + st.taskRelevance || 70, + st.limitations || [] + ); + + return { + name: st.tool.name, + type: st.tool.type, + phase: st.phase, + priority: st.priority, + justification: st.justification || `Empfohlen für ${st.phase}`, + confidence, + recommendationStrength: confidenceService.getConfidenceLevel(confidence.overall) + }; + }) || []; + + return { + ...base, + recommended_tools: recommendedToolsWithConfidence, + workflow_suggestion: finalContent + }; + } else { + const recommendedToolsWithConfidence = context.selectedTools?.map((st: any) => { + const confidence = confidenceService.calculateRecommendationConfidence( + st.tool, + context, + st.taskRelevance || 70, + st.limitations || [] + ); + + return { + name: st.tool.name, + type: st.tool.type, + rank: st.tool.evaluation?.rank || 1, + suitability_score: st.priority, + detailed_explanation: st.tool.evaluation?.detailed_explanation || '', + implementation_approach: st.tool.evaluation?.implementation_approach || '', + pros: st.tool.evaluation?.pros || [], + cons: st.tool.evaluation?.limitations || [], + alternatives: st.tool.evaluation?.alternatives || '', + confidence, + recommendationStrength: confidenceService.getConfidenceLevel(confidence.overall) + }; + }) || []; + + return { + ...base, + recommended_tools: recommendedToolsWithConfidence, + additional_considerations: finalContent + }; + } + } +} + +export const pipelineOrchestrator = new PipelineOrchestrator(); \ No newline at end of file diff --git a/src/services/ai/toolSelector.ts b/src/services/ai/toolSelector.ts new file mode 100644 index 0000000..d53f6eb --- /dev/null +++ b/src/services/ai/toolSelector.ts @@ -0,0 +1,354 @@ +// src/services/ai/toolSelector.ts +import { config } from '../../config/appConfig.js'; +import { aiService } from './aiService.js'; +import { embeddingsService } from '../../utils/embeddings.js'; +import { getPrompt } from '../../config/prompts.js'; +import { logger } from '../logger.js'; + +export interface ToolSelection { + selectedTools: any[]; + selectedConcepts: any[]; + selectionMethod: string; + confidence: number; +} + +export interface CandidateData { + tools: any[]; + concepts: any[]; + selectionMethod: string; +} + +export class ToolSelector { + private readonly embeddingCandidates: number; + private readonly similarityThreshold: number; + private readonly embeddingSelectionLimit: number; + private readonly embeddingConceptsLimit: number; + private readonly noEmbeddingsToolLimit: number; + private readonly noEmbeddingsConceptLimit: number; + private readonly embeddingsMinTools: number; + private readonly embeddingsMaxReductionRatio: number; + private readonly methodSelectionRatio: number; + private readonly softwareSelectionRatio: number; + private readonly maxSelectedItems: number; + + constructor() { + this.embeddingCandidates = config.ai.embeddingCandidates; + this.similarityThreshold = config.ai.similarityThreshold; + this.embeddingSelectionLimit = config.ai.embeddingSelectionLimit; + this.embeddingConceptsLimit = config.ai.embeddingConceptsLimit; + this.noEmbeddingsToolLimit = config.ai.noEmbeddingsToolLimit; + this.noEmbeddingsConceptLimit = config.ai.noEmbeddingsConceptLimit; + this.embeddingsMinTools = config.ai.embeddingsMinTools; + this.embeddingsMaxReductionRatio = config.ai.embeddingsMaxReductionRatio; + this.methodSelectionRatio = config.ai.methodSelectionRatio; + this.softwareSelectionRatio = config.ai.softwareSelectionRatio; + this.maxSelectedItems = config.ai.maxSelectedItems; + + logger.info('tool-selector', 'Tool selector initialized', { + embeddingCandidates: this.embeddingCandidates, + methodRatio: `${(this.methodSelectionRatio * 100).toFixed(0)}%`, + softwareRatio: `${(this.softwareSelectionRatio * 100).toFixed(0)}%` + }); + } + + async getCandidates( + userQuery: string, + toolsData: any + ): Promise { + try { + await embeddingsService.waitForInitialization(); + } catch (error) { + logger.warn('tool-selector', 'Embeddings initialization failed, using full dataset'); + } + + if (embeddingsService.isEnabled()) { + return this.getEmbeddingCandidates(userQuery, toolsData); + } else { + return this.getFullDatasetCandidates(toolsData); + } + } + + private async getEmbeddingCandidates( + userQuery: string, + toolsData: any + ): Promise { + const startTime = Date.now(); + + const similarItems = await embeddingsService.findSimilar( + userQuery, + this.embeddingCandidates, + this.similarityThreshold + ); + + logger.embedding('similarity search completed', { + query: userQuery.slice(0, 50), + candidates: this.embeddingCandidates, + threshold: this.similarityThreshold, + results: similarItems.length, + duration: `${Date.now() - startTime}ms` + }); + + 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 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; + + const useEmbeddings = similarTools.length >= this.embeddingsMinTools && + reductionRatio <= this.embeddingsMaxReductionRatio; + + if (useEmbeddings) { + logger.selection('embeddings', similarTools.length, similarConcepts.length, { + reduction: `${totalAvailableTools} → ${similarTools.length}`, + ratio: reductionRatio.toFixed(2) + }); + + return { + tools: similarTools, + concepts: similarConcepts, + selectionMethod: 'embeddings_candidates' + }; + } else { + logger.info('tool-selector', 'Embeddings filtering insufficient, using full dataset', { + toolsFound: similarTools.length, + minRequired: this.embeddingsMinTools, + reductionRatio: reductionRatio.toFixed(2), + maxRatio: this.embeddingsMaxReductionRatio + }); + + return this.getFullDatasetCandidates(toolsData); + } + } + + private getFullDatasetCandidates(toolsData: any): CandidateData { + logger.selection('full-dataset', toolsData.tools.length, toolsData.concepts.length); + + return { + tools: toolsData.tools, + concepts: toolsData.concepts, + selectionMethod: 'full_dataset' + }; + } + + async selectTools( + userQuery: string, + candidateTools: any[], + candidateConcepts: any[], + mode: string, + selectionMethod: string + ): Promise { + const startTime = Date.now(); + + const candidateMethods = candidateTools.filter((tool: any) => + tool && tool.type === 'method' + ); + const candidateSoftware = candidateTools.filter((tool: any) => + tool && tool.type === 'software' + ); + + logger.info('tool-selector', 'Starting AI tool selection', { + mode, + selectionMethod, + methods: candidateMethods.length, + software: candidateSoftware.length, + concepts: candidateConcepts.length + }); + + const { toolsToSend, conceptsToSend } = this.prepareSelectionData( + candidateMethods, + candidateSoftware, + candidateConcepts, + selectionMethod + ); + + const basePrompt = getPrompt( + 'toolSelection', + mode, + userQuery, + selectionMethod, + this.maxSelectedItems + ); + + const prompt = getPrompt( + 'toolSelectionWithData', + basePrompt, + toolsToSend, + conceptsToSend + ); + + if (!aiService.validatePromptSize(prompt, 35000)) { + throw new Error('Prompt too large for AI model'); + } + + const response = await aiService.call({ + prompt, + maxTokens: 2500, + context: `tool-selection-${mode}` + }); + + const result = aiService.parseJSONResponse(response.content, { + selectedTools: [], + selectedConcepts: [], + reasoning: '' + }); + + if (!Array.isArray(result.selectedTools) || !Array.isArray(result.selectedConcepts)) { + throw new Error('AI selection returned invalid structure'); + } + + const totalSelected = result.selectedTools.length + result.selectedConcepts.length; + if (totalSelected === 0) { + throw new Error('AI selection returned empty selection'); + } + + const selectedTools = this.mapSelectedItems(result.selectedTools, candidateTools); + const selectedConcepts = this.mapSelectedItems(result.selectedConcepts, candidateConcepts); + + const selectedMethods = selectedTools.filter((t: any) => t && t.type === 'method'); + const selectedSoftware = selectedTools.filter((t: any) => t && t.type === 'software'); + + const confidence = this.calculateSelectionConfidence(result, candidateTools.length + candidateConcepts.length); + + const duration = Date.now() - startTime; + + logger.selection('ai-completed', selectedTools.length, selectedConcepts.length, { + methods: selectedMethods.length, + software: selectedSoftware.length, + confidence: `${confidence}%`, + duration: `${duration}ms`, + selectionMethod + }); + + return { + selectedTools, + selectedConcepts, + selectionMethod, + confidence + }; + } + + private prepareSelectionData( + candidateMethods: any[], + candidateSoftware: any[], + candidateConcepts: any[], + selectionMethod: string + ): { toolsToSend: any[], conceptsToSend: any[] } { + const isEmbeddingsBased = selectionMethod === 'embeddings_candidates'; + + const totalLimit = isEmbeddingsBased ? + this.embeddingSelectionLimit : + this.noEmbeddingsToolLimit; + + const conceptLimit = isEmbeddingsBased ? + this.embeddingConceptsLimit : + this.noEmbeddingsConceptLimit; + + const methodLimit = Math.ceil(totalLimit * this.methodSelectionRatio); + const softwareLimit = Math.floor(totalLimit * this.softwareSelectionRatio); + + const toolsToSend = [ + ...candidateMethods.slice(0, methodLimit).map(this.createToolData), + ...candidateSoftware.slice(0, softwareLimit).map(this.createToolData) + ]; + + // Fill remaining capacity + const remainingCapacity = totalLimit - toolsToSend.length; + if (remainingCapacity > 0) { + if (candidateMethods.length > methodLimit) { + toolsToSend.push( + ...candidateMethods + .slice(methodLimit, methodLimit + remainingCapacity) + .map(this.createToolData) + ); + } else if (candidateSoftware.length > softwareLimit) { + toolsToSend.push( + ...candidateSoftware + .slice(softwareLimit, softwareLimit + remainingCapacity) + .map(this.createToolData) + ); + } + } + + const conceptsToSend = candidateConcepts + .slice(0, conceptLimit) + .map(this.createConceptData); + + logger.debug('tool-selector', 'Selection data prepared', { + toolsToSend: toolsToSend.length, + conceptsToSend: conceptsToSend.length, + methodLimit, + softwareLimit, + conceptLimit, + isEmbeddingsBased + }); + + return { toolsToSend, conceptsToSend }; + } + + 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 mapSelectedItems(selectedNames: string[], candidates: any[]): any[] { + const candidatesMap = new Map(candidates.map((item: any) => [item.name, item])); + + return selectedNames + .map((name: string) => candidatesMap.get(name)) + .filter((item: any): item is NonNullable => item !== undefined && item !== null); + } + + private calculateSelectionConfidence(result: any, candidateCount: number): number { + if (!result?.selectedTools) return 30; + + 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)); + } +} + +export const toolSelector = new ToolSelector(); \ No newline at end of file diff --git a/src/services/logger.ts b/src/services/logger.ts new file mode 100644 index 0000000..75f17ad --- /dev/null +++ b/src/services/logger.ts @@ -0,0 +1,159 @@ +// src/services/logger.ts + +export enum LogLevel { + ERROR = 0, + WARN = 1, + INFO = 2, + DEBUG = 3 +} + +interface LogContext { + [key: string]: any; +} + +class Logger { + private logLevel: LogLevel; + private enabledContexts: Set; + + constructor() { + this.logLevel = this.getLogLevel(); + this.enabledContexts = this.getEnabledContexts(); + } + + private getLogLevel(): LogLevel { + const level = process.env.LOG_LEVEL?.toUpperCase() || 'INFO'; + switch (level) { + case 'ERROR': return LogLevel.ERROR; + case 'WARN': return LogLevel.WARN; + case 'DEBUG': return LogLevel.DEBUG; + default: return LogLevel.INFO; + } + } + + private getEnabledContexts(): Set { + const contexts = process.env.LOG_CONTEXTS?.split(',') || []; + return new Set(contexts.map(c => c.trim().toLowerCase())); + } + + private shouldLog(level: LogLevel, context: string): boolean { + if (level > this.logLevel) return false; + if (this.enabledContexts.size > 0 && !this.enabledContexts.has(context.toLowerCase())) { + return false; + } + return true; + } + + private formatMessage(level: string, context: string, message: string, data?: LogContext): string { + const timestamp = new Date().toISOString(); + const contextStr = context.toUpperCase().padEnd(12); + let logMessage = `[${timestamp}] ${level.padEnd(5)} [${contextStr}] ${message}`; + + if (data && Object.keys(data).length > 0) { + const dataStr = Object.entries(data) + .map(([key, value]) => `${key}=${typeof value === 'object' ? JSON.stringify(value) : value}`) + .join(' '); + logMessage += ` | ${dataStr}`; + } + + return logMessage; + } + + error(context: string, message: string, error?: Error, data?: LogContext): void { + if (!this.shouldLog(LogLevel.ERROR, context)) return; + + const logData = { ...data }; + if (error) { + logData.error = error.message; + logData.stack = error.stack; + } + + console.error(this.formatMessage('ERROR', context, message, logData)); + } + + warn(context: string, message: string, data?: LogContext): void { + if (!this.shouldLog(LogLevel.WARN, context)) return; + console.warn(this.formatMessage('WARN', context, message, data)); + } + + info(context: string, message: string, data?: LogContext): void { + if (!this.shouldLog(LogLevel.INFO, context)) return; + console.log(this.formatMessage('INFO', context, message, data)); + } + + debug(context: string, message: string, data?: LogContext): void { + if (!this.shouldLog(LogLevel.DEBUG, context)) return; + console.log(this.formatMessage('DEBUG', context, message, data)); + } + + // Specialized logging methods for common contexts + pipeline(phase: string, action: string, details?: LogContext): void { + this.info('pipeline', `${phase}/${action}`, details); + } + + audit(phase: string, action: string, confidence: number, timeMs: number, metadata?: LogContext): void { + this.info('audit', `${phase}/${action} completed`, { + confidence: `${confidence}%`, + duration: `${timeMs}ms`, + ...metadata + }); + } + + api(method: string, path: string, status: number, details?: LogContext): void { + const level = status >= 400 ? LogLevel.ERROR : status >= 300 ? LogLevel.WARN : LogLevel.INFO; + if (level === LogLevel.ERROR) { + this.error('api', `${method} ${path}`, undefined, { status, ...details }); + } else if (level === LogLevel.WARN) { + this.warn('api', `${method} ${path}`, { status, ...details }); + } else { + this.info('api', `${method} ${path}`, { status, ...details }); + } + } + + embedding(action: string, details?: LogContext): void { + this.info('embedding', action, details); + } + + selection(method: string, toolCount: number, conceptCount: number, details?: LogContext): void { + this.info('selection', `${method} selection completed`, { + tools: toolCount, + concepts: conceptCount, + ...details + }); + } + + confidence(toolName: string, overall: number, semantic: number, suitability: number): void { + this.debug('confidence', `${toolName} scored`, { + overall: `${overall}%`, + semantic: `${semantic}%`, + suitability: `${suitability}%` + }); + } + + queue(action: string, details?: LogContext): void { + this.info('queue', action, details); + } + + // Performance timing + time(context: string, label: string): string { + const timerId = `${context}:${label}:${Date.now()}`; + console.time(timerId); + return timerId; + } + + timeEnd(timerId: string, message?: string): void { + console.timeEnd(timerId); + if (message) { + const [context] = timerId.split(':'); + this.debug(context, message); + } + } +} + +export const logger = new Logger(); + +// Export convenience functions for common patterns +export const logPipeline = logger.pipeline.bind(logger); +export const logAudit = logger.audit.bind(logger); +export const logAPI = logger.api.bind(logger); +export const logError = logger.error.bind(logger); +export const logInfo = logger.info.bind(logger); \ No newline at end of file diff --git a/src/utils/aiPipeline.ts b/src/utils/aiPipeline.ts deleted file mode 100644 index 2bf712d..0000000 --- a/src/utils/aiPipeline.ts +++ /dev/null @@ -1,1624 +0,0 @@ -// src/utils/aiPipeline.ts - -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 dotenv from 'dotenv'; - -dotenv.config(); - -interface AIConfig { - endpoint: string; - apiKey: string; - model: string; -} - -interface MicroTaskResult { - taskType: string; - content: string; - processingTimeMs: number; - success: boolean; - error?: string; -} - -interface AnalysisResult { - recommendation: any; - processingStats: { - embeddingsUsed: boolean; - candidatesFromEmbeddings: number; - finalSelectedItems: number; - processingTimeMs: number; - microTasksCompleted: number; - microTasksFailed: number; - contextContinuityUsed: boolean; - }; -} - -interface AuditEntry { - timestamp: number; - phase: string; - action: string; - input: any; - output: any; - confidence: number; - processingTimeMs: number; - metadata: Record; -} - -interface AnalysisContext { - userQuery: string; - mode: string; - filteredData: any; - contextHistory: string[]; - maxContextLength: number; - currentContextLength: number; - scenarioAnalysis?: string; - problemAnalysis?: string; - investigationApproach?: string; - criticalConsiderations?: string; - selectedTools?: Array<{ - tool: any; - phase: string; - priority: string; - justification?: string; - taskRelevance?: number; - limitations?: string[]; - }>; - backgroundKnowledge?: Array<{ - concept: any; - relevance: string; - }>; - seenToolNames: Set; - auditTrail: AuditEntry[]; - 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 auditConfig: { - enabled: boolean; - detailLevel: string; - }; - private confidenceConfig: { - semanticWeight: number; - suitabilityWeight: number; - minimumThreshold: number; - mediumThreshold: number; - highThreshold: number; - }; - - constructor() { - this.config = { - endpoint: this.getRequiredEnv('AI_ANALYZER_ENDPOINT'), - apiKey: this.getRequiredEnv('AI_ANALYZER_API_KEY'), - model: this.getRequiredEnv('AI_ANALYZER_MODEL') - }; - - 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.auditConfig = { - enabled: process.env.FORENSIC_AUDIT_ENABLED === 'true', - detailLevel: process.env.FORENSIC_AUDIT_DETAIL_LEVEL || 'standard' - }; - - 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) - }; - - this.logPipelineInit(); - } - - 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; - } - - private logPipelineInit(): void { - console.log('[AI-PIPELINE] Initialized with audit:', this.auditConfig.enabled); - console.log('[AI-PIPELINE] Method/Software balance:', - `${(this.methodSelectionRatio * 100).toFixed(0)}%/${(this.softwareSelectionRatio * 100).toFixed(0)}%`); - } - - private addAuditEntry( - context: AnalysisContext, - phase: string, - action: string, - input: any, - output: any, - confidence: number, - startTime: number, - metadata: Record = {} - ): void { - if (!this.auditConfig.enabled) return; - - const entry: AuditEntry = { - timestamp: Date.now(), - phase, - action, - input, - output, - confidence: Math.round(confidence), - processingTimeMs: Date.now() - startTime, - metadata - }; - - context.auditTrail.push(entry); - this.logAuditEntry(phase, action, confidence, entry.processingTimeMs); - } - - private logAuditEntry(phase: string, action: string, confidence: number, timeMs: number): void { - console.log(`[AUDIT] ${phase}/${action}: ${confidence}% confidence, ${timeMs}ms`); - } - - private calculateSelectionConfidence(result: any, candidateCount: number): number { - if (!result?.selectedTools) return 30; - - 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)); - } - - private estimateTokens(text: string): number { - return Math.ceil(text.length / 4); - } - - 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 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( - userQuery, - this.embeddingCandidates, - this.similarityThreshold - ) as SimilarityResult[]; - - console.log('[AI-PIPELINE] Embeddings found', similarItems.length, 'similar items'); - - similarItems.forEach(item => { - context.embeddingsSimilarities.set(item.name, item.similarity); - }); - - 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 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'); - } else { - console.log('[AI-PIPELINE] Embeddings filtering insufficient, using full dataset'); - candidateTools = toolsData.tools; - candidateConcepts = toolsData.concepts; - selectionMethod = 'full_dataset'; - } - - if (this.auditConfig.enabled) { - 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 } - ); - } - } 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'] - }; - } - - 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); - - 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); - } - - 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'); - - if (this.auditConfig.enabled) { - 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'); - - if (this.auditConfig.enabled) { - 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)}%` - }, - confidence, - selectionStart, - { aiModel: this.config.model, selectionMethod } - ); - } - - return { selectedTools, selectedConcepts }; - - } catch (error) { - console.error('[AI-PIPELINE] AI selection failed:', error); - - if (this.auditConfig.enabled) { - this.addAuditEntry( - context, - 'selection', - 'ai-tool-selection-error', - { candidateCount: candidateTools.length, mode }, - { error: error.message }, - 5, - selectionStart, - { aiModel: this.config.model, selectionMethod } - ); - } - 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(); - - 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 } - ); - - return result; - - } catch (error) { - const result = { - taskType: 'micro-task', - content: '', - processingTimeMs: Date.now() - startTime, - success: false, - error: error.message - }; - - this.addAuditEntry( - context, - 'micro-task', - 'ai-analysis-failed', - { promptLength: contextPrompt.length, maxTokens }, - { error: error.message }, - 5, - startTime, - { aiModel: this.config.model } - ); - - return result; - } - } - - 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'); - const isWorkflow = context.mode === 'workflow'; - const prompt = getPrompt('scenarioAnalysis', isWorkflow, context.userQuery); - - const result = await this.callMicroTaskAI(prompt, context, 400); - - if (result.success) { - if (isWorkflow) { - context.scenarioAnalysis = result.content; - } else { - context.problemAnalysis = result.content; - } - - this.addToContextHistory(context, `${isWorkflow ? 'Szenario' : 'Problem'}-Analyse: ${result.content.slice(0, 200)}...`); - } - - return result; - } - - private async generateApproach(context: AnalysisContext): Promise { - console.log('[AI-PIPELINE] Starting investigation approach micro-task'); - const isWorkflow = context.mode === 'workflow'; - const prompt = getPrompt('investigationApproach', isWorkflow, context.userQuery); - - const result = await this.callMicroTaskAI(prompt, context, 400); - - if (result.success) { - context.investigationApproach = result.content; - this.addToContextHistory(context, `${isWorkflow ? 'Untersuchungs' : 'Lösungs'}ansatz: ${result.content.slice(0, 200)}...`); - } - - return result; - } - - private async generateCriticalConsiderations(context: AnalysisContext): Promise { - console.log('[AI-PIPELINE] Starting critical considerations micro-task'); - const isWorkflow = context.mode === 'workflow'; - const prompt = getPrompt('criticalConsiderations', isWorkflow, context.userQuery); - - const result = await this.callMicroTaskAI(prompt, context, 350); - - if (result.success) { - context.criticalConsiderations = result.content; - this.addToContextHistory(context, `Kritische Überlegungen: ${result.content.slice(0, 200)}...`); - } - - 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; - } - - // Step 1: AI selection of tools for completion - 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; - } - - // Step 2: Generate detailed reasoning for each selected tool - 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); - const existingSelection = context.selectedTools?.find((st: any) => st.tool && st.tool.name === tool.name); - const taskRelevance = existingSelection?.taskRelevance || 70; - const priority = this.derivePriorityFromScore(taskRelevance); - - const prompt = getPrompt('toolEvaluation', context.userQuery, tool, rank, taskRelevance); - const result = await this.callMicroTaskAI(prompt, context, 1000); - - if (result.success) { - const evaluation = this.safeParseJSON(result.content, { - detailed_explanation: 'Evaluation failed', - implementation_approach: '', - pros: [], - limitations: [], - alternatives: '' - }); - - this.addToolToSelection(context, { - ...tool, - evaluation: { - ...evaluation, - 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 } - ); - } - - return result; - } - - private async selectBackgroundKnowledge(context: AnalysisContext): Promise { - console.log('[AI-PIPELINE] Starting background knowledge selection micro-task'); - const availableConcepts = context.filteredData.concepts; - - if (availableConcepts.length === 0) { - return { - taskType: 'background-knowledge', - content: JSON.stringify([]), - processingTimeMs: 0, - success: true - }; - } - - const selectedToolNames = context.selectedTools?.map((st: any) => st.tool && st.tool.name).filter(Boolean) || []; - const prompt = getPrompt('backgroundKnowledgeSelection', context.userQuery, context.mode, selectedToolNames, availableConcepts); - const result = await this.callMicroTaskAI(prompt, context, 700); - - if (result.success) { - const selections = this.safeParseJSON(result.content, []); - - if (Array.isArray(selections)) { - context.backgroundKnowledge = selections.filter((sel: any) => - sel.conceptName && availableConcepts.some((concept: any) => concept.name === sel.conceptName) - ).map((sel: any) => ({ - 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'); - 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; - } - - private async callAI(prompt: string, maxTokens: number = 1500): Promise { - const endpoint = this.config.endpoint; - const apiKey = this.config.apiKey; - const model = this.config.model; - - let headers: Record = { - 'Content-Type': 'application/json' - }; - - if (apiKey) { - headers['Authorization'] = `Bearer ${apiKey}`; - } - - const requestBody = { - model, - messages: [{ role: 'user', content: prompt }], - max_tokens: maxTokens, - temperature: 0.3 - }; - - 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; - - if (!content) { - console.error('[AI-PIPELINE] No response content from AI model'); - throw new Error('No response from AI model'); - } - - return content; - - } catch (error) { - console.error('[AI-PIPELINE] AI service call failed:', error.message); - throw 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'); - - try { - const toolsData = await getCompressedToolsDataForAI(); - - const context: AnalysisContext = { - userQuery, - mode, - filteredData: {}, - contextHistory: [], - maxContextLength: this.maxContextTokens, - currentContextLength: 0, - seenToolNames: new Set(), - auditTrail: [], - 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: this.auditConfig.enabled } - ); - - 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: context.auditTrail.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); - - return { - recommendation: { - ...recommendation, - auditTrail: this.auditConfig.enabled ? context.auditTrail : undefined - }, - processingStats - }; - - } catch (error) { - console.error('[AI-PIPELINE] Processing failed:', error); - throw error; - } - } - - private buildRecommendation(context: AnalysisContext, 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, - investigation_approach: context.investigationApproach, - critical_considerations: context.criticalConsiderations, - background_knowledge: context.backgroundKnowledge?.map((bk: any) => ({ - concept_name: bk.concept.name, - relevance: bk.relevance - })) || [] - }; - - const processedAuditTrail = this.auditConfig.enabled && context.auditTrail ? context.auditTrail : []; - - if (isWorkflow) { - const recommendedToolsWithConfidence = context.selectedTools?.map((st: any) => { - const confidence = this.calculateRecommendationConfidence( - st.tool, - context, - 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, - type: st.tool.type, - phase: st.phase, - 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' - }; - }) || []; - - return { - ...base, - recommended_tools: recommendedToolsWithConfidence, - workflow_suggestion: finalContent, - auditTrail: processedAuditTrail - }; - } else { - const recommendedToolsWithConfidence = context.selectedTools?.map((st: any) => { - const confidence = this.calculateRecommendationConfidence( - st.tool, - context, - 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, - type: st.tool.type, - rank: st.tool.evaluation?.rank || 1, - suitability_score: st.priority, - detailed_explanation: st.tool.evaluation?.detailed_explanation || '', - implementation_approach: st.tool.evaluation?.implementation_approach || '', - pros: st.tool.evaluation?.pros || [], - 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' - }; - }) || []; - - return { - ...base, - recommended_tools: recommendedToolsWithConfidence, - additional_considerations: finalContent, - auditTrail: processedAuditTrail - }; - } - } -} - -const aiPipeline = new ImprovedMicroTaskAIPipeline(); - -export { aiPipeline, type AnalysisResult }; \ No newline at end of file diff --git a/src/utils/aiUtils.ts b/src/utils/aiUtils.ts new file mode 100644 index 0000000..8ef79c3 --- /dev/null +++ b/src/utils/aiUtils.ts @@ -0,0 +1,184 @@ +// src/utils/aiUtils.ts +import { isToolHosted, createToolSlug } from './toolHelpers.js'; + +export interface ModeConfig { + placeholder: string; + description: string; + submitText: string; + loadingText: string; +} + +export function getModeConfig(): Record { + return { + workflow: { + placeholder: "Beschreiben Sie Ihr forensisches Szenario... z.B. 'Verdacht auf Ransomware-Angriff auf Windows-Domänencontroller'", + description: "Beschreiben Sie Ihre Untersuchungssituation und erhalten Empfehlungen für alle Phasen der Untersuchung.", + submitText: "Empfehlungen generieren", + loadingText: "Analysiere Szenario und generiere Empfehlungen..." + }, + tool: { + placeholder: "Beschreiben Sie Ihr Problem... z.B. 'Analyse von Android-Backups mit WhatsApp-Nachrichten'", + description: "Beschreiben Sie Ihre Untersuchungssituation und erhalten Empfehlungen für eine spezifische Aufgabenstellung.", + submitText: "Empfehlungen finden", + loadingText: "Analysiere Anforderungen und suche passende Methode..." + } + }; +} + +export function generateShareURL(toolName: string, view: string, modal: string | null = null): string { + const toolSlug = createToolSlug(toolName); + const baseUrl = window.location.origin + window.location.pathname; + const params = new URLSearchParams(); + params.set('tool', toolSlug); + params.set('view', view); + if (modal) { + params.set('modal', modal); + } + return `${baseUrl}?${params.toString()}`; +} + +export function getErrorMessage(error: Error): string { + const errorMap = { + '429': 'Zu viele Anfragen. Bitte warten Sie einen Moment.', + '401': 'Authentifizierung erforderlich. Bitte melden Sie sich an.', + '503': 'KI-Service vorübergehend nicht verfügbar.' + }; + + for (const [code, message] of Object.entries(errorMap)) { + if (error.message.includes(code)) return message; + } + + return `Fehler: ${error.message}`; +} + +export function renderToolBadges(tool: any): string { + const isMethod = tool.type === 'method'; + const hasValidProjectUrl = isToolHosted(tool); + + let badges = ''; + if (isMethod) { + badges += 'Methode'; + } else if (hasValidProjectUrl) { + badges += 'CC24-Server'; + } else if (tool.license !== 'Proprietary') { + badges += 'Open Source'; + } + + if (tool.knowledgebase === true) { + badges += '📖'; + } + + return badges; +} + +export function getToolClass(tool: any, context: string = 'card'): string { + const isMethod = tool.type === 'method'; + const hasValidProjectUrl = isToolHosted(tool); + + if (context === 'recommendation') { + if (isMethod) return 'method'; + if (hasValidProjectUrl) return 'hosted'; + if (tool.license !== 'Proprietary') return 'oss'; + return ''; + } else { + if (isMethod) return 'card-method'; + if (hasValidProjectUrl) return 'card-hosted'; + if (tool.license !== 'Proprietary') return 'card-oss'; + return ''; + } +} + +export function getSuitabilityText(score: string): string { + const texts = { + high: 'GUT GEEIGNET', + medium: 'GEEIGNET', + low: 'VIELLEICHT GEEIGNET' + }; + return texts[score] || 'GEEIGNET'; +} + +export function truncateText(text: string, maxLength: number): string { + if (!text || text.length <= maxLength) return text; + return text.slice(0, maxLength) + '...'; +} + +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 renderConfidenceTooltip(confidence: any): string { + if (!confidence || typeof confidence.overall !== 'number') return ''; + + const confidenceColor = getConfidenceColor(confidence.overall); + + return ` + +
+ ${confidence.overall}% + + +
+ `; +} + +function getConfidenceColor(confidence: number): string { + if (confidence >= 80) return 'var(--color-accent)'; + if (confidence >= 60) return 'var(--color-warning)'; + return 'var(--color-error)'; +} \ No newline at end of file diff --git a/src/utils/auditService.ts b/src/utils/auditService.ts index b2ad986..63f0777 100644 --- a/src/utils/auditService.ts +++ b/src/utils/auditService.ts @@ -1,17 +1,8 @@ // src/utils/auditService.ts -import 'dotenv/config'; +import { config } from '../config/appConfig.js'; +import { logger } from '../services/logger.js'; -function env(key: string, fallback: string | undefined = undefined): string | undefined { - if (typeof process !== 'undefined' && process.env?.[key] !== undefined) { - return process.env[key]; - } - if (typeof import.meta !== 'undefined' && (import.meta as any).env?.[key] !== undefined) { - return (import.meta as any).env[key]; - } - return fallback; -} - -interface AuditEntry { +export interface AuditEntry { timestamp: number; phase: string; action: string; @@ -22,14 +13,7 @@ interface AuditEntry { metadata: Record; } -interface AuditConfig { - enabled: boolean; - detailLevel: 'minimal' | 'standard' | 'verbose'; - retentionHours: number; - maxEntries: number; -} - -interface CompressedAuditEntry { +export interface CompressedAuditEntry { timestamp: number; phase: string; action: string; @@ -40,7 +24,7 @@ interface CompressedAuditEntry { metadata: Record; } -interface ProcessedAuditTrail { +export interface ProcessedAuditTrail { totalTime: number; avgConfidence: number; stepCount: number; @@ -62,7 +46,8 @@ interface ProcessedAuditTrail { } class AuditService { - private config: AuditConfig; + private readonly enabled: boolean; + private readonly detailLevel: 'minimal' | 'standard' | 'verbose'; private tempEntries: AuditEntry[] = []; private readonly phaseConfig = { @@ -87,50 +72,13 @@ class AuditService { }; constructor() { - this.config = this.loadConfig(); - } - - private loadConfig(): AuditConfig { - const enabledFlag = env('FORENSIC_AUDIT_ENABLED', 'false'); - const detailLevel = env('FORENSIC_AUDIT_DETAIL_LEVEL', 'standard') as 'minimal' | 'standard' | 'verbose'; - const retentionHours = parseInt(env('FORENSIC_AUDIT_RETENTION_HOURS', '72') || '72', 10); - const maxEntries = parseInt(env('FORENSIC_AUDIT_MAX_ENTRIES', '50') || '50', 10); - - console.log('[AUDIT SERVICE] Configuration loaded:', { - enabled: enabledFlag === 'true', - detailLevel, - retentionHours, - maxEntries, - context: typeof process !== 'undefined' ? 'server' : 'client' - }); - - return { - enabled: enabledFlag === 'true', - detailLevel, - retentionHours, - maxEntries - }; - } - - getDebugInfo(): { - config: AuditConfig; - environment: Record; - context: string; - } { - const context = typeof process !== 'undefined' ? 'server' : 'client'; + this.enabled = config.audit.enabled; + this.detailLevel = config.audit.detailLevel; - return { - config: this.config, - environment: { - FORENSIC_AUDIT_ENABLED: env('FORENSIC_AUDIT_ENABLED'), - FORENSIC_AUDIT_DETAIL_LEVEL: env('FORENSIC_AUDIT_DETAIL_LEVEL'), - FORENSIC_AUDIT_RETENTION_HOURS: env('FORENSIC_AUDIT_RETENTION_HOURS'), - FORENSIC_AUDIT_MAX_ENTRIES: env('FORENSIC_AUDIT_MAX_ENTRIES'), - processEnvKeys: typeof process !== 'undefined' ? Object.keys(process.env).filter(k => k.includes('AUDIT')) : [], - importMetaEnvAvailable: typeof import.meta !== 'undefined' && !!(import.meta as any).env - }, - context - }; + logger.info('audit-service', 'Audit service initialized', { + enabled: this.enabled, + detailLevel: this.detailLevel + }); } addEntry( @@ -142,7 +90,7 @@ class AuditService { startTime: number, metadata: Record = {} ): void { - if (!this.config.enabled) return; + if (!this.enabled) return; const entry: AuditEntry = { timestamp: Date.now(), @@ -156,76 +104,73 @@ class AuditService { }; this.tempEntries.push(entry); - console.log(`[AUDIT] ${phase}/${action}: ${confidence}% confidence, ${entry.processingTimeMs}ms`); + logger.audit(phase, action, confidence, entry.processingTimeMs, metadata); } - mergeAndClear(auditTrail: AuditEntry[]): void { - if (!this.config.enabled || this.tempEntries.length === 0) return; + if (!this.enabled || this.tempEntries.length === 0) return; auditTrail.unshift(...this.tempEntries); const entryCount = this.tempEntries.length; this.tempEntries = []; - console.log(`[AUDIT] Merged ${entryCount} entries into audit trail`); + logger.debug('audit-service', `Merged audit entries`, { entryCount }); } - processAuditTrail(rawAuditTrail: AuditEntry[]): ProcessedAuditTrail | null { - if (!this.config.enabled) { - console.log('[AUDIT] Service disabled, returning null'); - return null; + if (!this.enabled) { + return null; } if (!rawAuditTrail || !Array.isArray(rawAuditTrail) || rawAuditTrail.length === 0) { - console.log('[AUDIT] No audit trail data provided'); - return null; + logger.debug('audit-service', 'No audit trail data to process'); + return null; } try { - console.log('[AUDIT] Processing', rawAuditTrail.length, 'audit entries'); + logger.debug('audit-service', 'Processing audit trail', { entries: rawAuditTrail.length }); - 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 + 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 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 groupedEntries = rawAuditTrail.reduce((groups, entry) => { const phase = entry.phase || 'unknown'; if (!groups[phase]) groups[phase] = []; groups[phase].push(entry); return groups; - }, {} as Record); + }, {} as Record); - const phases = Object.entries(groupedEntries).map(([phase, entries]) => { + 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; + ? 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) + 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); + }).filter(phase => phase.entries.length > 0); - const summary = this.generateSummary(rawAuditTrail, avgConfidence, lowConfidenceSteps); + const summary = this.generateSummary(rawAuditTrail, avgConfidence, lowConfidenceSteps); - const result: ProcessedAuditTrail = { + const result: ProcessedAuditTrail = { totalTime, avgConfidence, stepCount: rawAuditTrail.length, @@ -233,25 +178,30 @@ class AuditService { lowConfidenceSteps, phases, summary - }; + }; - console.log('[AUDIT] Successfully processed audit trail:', result); - return result; + logger.debug('audit-service', 'Audit trail processed successfully', { + totalTime: `${totalTime}ms`, + avgConfidence: `${avgConfidence}%`, + phases: phases.length + }); + + return result; } catch (error) { - console.error('[AUDIT] Error processing audit trail:', error); - return null; + logger.error('audit-service', 'Error processing audit trail', error as Error); + return null; } } private compressEntry(entry: AuditEntry): CompressedAuditEntry | null { if (!entry || typeof entry !== 'object') { - console.warn('[AUDIT] Invalid audit entry:', entry); - return null; + logger.warn('audit-service', 'Invalid audit entry', { entry }); + return null; } try { - return { + return { timestamp: entry.timestamp || Date.now(), phase: entry.phase || 'unknown', action: entry.action || 'unknown', @@ -260,17 +210,17 @@ class AuditService { confidence: entry.confidence || 0, processingTimeMs: entry.processingTimeMs || 0, metadata: entry.metadata || {} - }; + }; } catch (error) { - console.error('[AUDIT] Error compressing entry:', error); - return null; + logger.error('audit-service', 'Error compressing audit entry', error as Error); + return null; } } private compressData(data: any): any { - if (this.config.detailLevel === 'verbose') { + if (this.detailLevel === 'verbose') { return data; - } else if (this.config.detailLevel === 'standard') { + } else if (this.detailLevel === 'standard') { return this.summarizeForStorage(data); } else { return this.minimalSummary(data); @@ -387,25 +337,12 @@ class AuditService { } isEnabled(): boolean { - return this.config.enabled; + return this.enabled; } - getConfig(): AuditConfig { - return { ...this.config }; + getConfig(): typeof config.audit { + return config.audit; } } -export const auditService = new AuditService(); -export type { ProcessedAuditTrail, CompressedAuditEntry }; - -export const debugAuditService = { - getDebugInfo() { - return auditService.getDebugInfo(); - }, - isEnabled() { - return auditService.isEnabled(); - }, - getConfig() { - return auditService.getConfig(); - } -}; \ 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 0d766e1..41c152f 100644 --- a/src/utils/clientUtils.ts +++ b/src/utils/clientUtils.ts @@ -1,36 +1,13 @@ // src/utils/clientUtils.ts -// Client-side utilities that mirror server-side toolHelpers.ts +// Client-side utilities - now only contains AutocompleteManager to avoid duplication +// Other utilities consolidated into globalUtils.ts -export function createToolSlug(toolName: string): string { - if (!toolName || typeof toolName !== 'string') { - console.warn('[toolHelpers] Invalid toolName provided to createToolSlug:', toolName); - return ''; - } - - return toolName.toLowerCase() - .replace(/[^a-z0-9\s-]/g, '') // Remove special characters - .replace(/\s+/g, '-') // Replace spaces with hyphens - .replace(/-+/g, '-') // Remove duplicate hyphens - .replace(/^-|-$/g, ''); // Remove leading/trailing hyphens -} +import { createToolSlug, findToolByIdentifier, isToolHosted } from './toolHelpers.js'; -export function findToolByIdentifier(tools: any[], identifier: string): any | undefined { - if (!identifier || !Array.isArray(tools)) return undefined; - - return tools.find((tool: any) => - tool.name === identifier || - createToolSlug(tool.name) === identifier.toLowerCase() - ); -} +// Re-export tool helper functions to maintain API compatibility +export { createToolSlug, findToolByIdentifier, isToolHosted }; -export function isToolHosted(tool: any): boolean { - return tool.projectUrl !== undefined && - tool.projectUrl !== null && - tool.projectUrl !== "" && - tool.projectUrl.trim() !== ""; -} - -// Consolidated Autocomplete Functionality +// Consolidated Autocomplete Functionality - this is unique and not duplicated elsewhere interface AutocompleteOptions { minLength?: number; maxResults?: number; diff --git a/src/utils/globalUtils.ts b/src/utils/globalUtils.ts new file mode 100644 index 0000000..cef66e3 --- /dev/null +++ b/src/utils/globalUtils.ts @@ -0,0 +1,314 @@ +// src/utils/globalUtils.ts +// Consolidated global utilities to eliminate duplication and provide consistent interface + +import { createToolSlug, findToolByIdentifier, isToolHosted } from './toolHelpers.js'; +import { auditService } from './auditService.js'; + +// Utility functions for UI interactions +export const UIUtils = { + scrollToElement(element: Element | null, options: ScrollIntoViewOptions = {}) { + if (!element) return; + + setTimeout(() => { + const headerHeight = document.querySelector('nav')?.offsetHeight || 80; + const elementRect = element.getBoundingClientRect(); + const absoluteElementTop = elementRect.top + window.pageYOffset; + const targetPosition = absoluteElementTop - headerHeight - 20; + + window.scrollTo({ + top: targetPosition, + behavior: 'smooth', + ...options + }); + }, 100); + }, + + scrollToElementById(elementId: string, options: ScrollIntoViewOptions = {}) { + const element = document.getElementById(elementId); + if (element) { + this.scrollToElement(element, options); + } + }, + + scrollToElementBySelector(selector: string, options: ScrollIntoViewOptions = {}) { + const element = document.querySelector(selector); + if (element) { + this.scrollToElement(element, options); + } + }, + + prioritizeSearchResults(tools: any[], searchTerm: string) { + if (!searchTerm || !searchTerm.trim()) { + return tools; + } + + const lowerSearchTerm = searchTerm.toLowerCase().trim(); + + return tools.sort((a, b) => { + const aTagsLower = (a.tags || []).map((tag: string) => tag.toLowerCase()); + const bTagsLower = (b.tags || []).map((tag: string) => tag.toLowerCase()); + + const aExactTag = aTagsLower.includes(lowerSearchTerm); + const bExactTag = bTagsLower.includes(lowerSearchTerm); + + if (aExactTag && !bExactTag) return -1; + if (!aExactTag && bExactTag) return 1; + + return a.name.localeCompare(b.name); + }); + } +}; + +// Theme management +export const ThemeManager = { + THEME_KEY: 'dfir-theme', + + getSystemTheme(): 'dark' | 'light' { + return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; + }, + + getStoredTheme(): string { + return localStorage.getItem(this.THEME_KEY) || 'auto'; + }, + + applyTheme(theme: string) { + const effectiveTheme = theme === 'auto' ? this.getSystemTheme() : theme; + document.documentElement.setAttribute('data-theme', effectiveTheme); + }, + + updateThemeToggle(theme: string) { + document.querySelectorAll('[data-theme-toggle]').forEach(button => { + button.setAttribute('data-current-theme', theme); + }); + }, + + initTheme() { + const storedTheme = this.getStoredTheme(); + this.applyTheme(storedTheme); + this.updateThemeToggle(storedTheme); + }, + + toggleTheme() { + const current = this.getStoredTheme(); + const themes = ['light', 'dark', 'auto']; + const currentIndex = themes.indexOf(current); + const nextIndex = (currentIndex + 1) % themes.length; + const nextTheme = themes[nextIndex]; + + localStorage.setItem(this.THEME_KEY, nextTheme); + this.applyTheme(nextTheme); + this.updateThemeToggle(nextTheme); + } +}; + +// Authentication utilities +export const AuthManager = { + async checkClientAuth(context: string = 'general') { + try { + const response = await fetch('/api/auth/status'); + const data = await response.json(); + + switch (context) { + case 'contributions': + return { + authenticated: data.contributionAuthenticated, + authRequired: data.contributionAuthRequired, + expires: data.expires + }; + case 'ai': + return { + authenticated: data.aiAuthenticated, + authRequired: data.aiAuthRequired, + expires: data.expires + }; + default: + return { + authenticated: data.authenticated, + authRequired: data.contributionAuthRequired || data.aiAuthRequired, + expires: data.expires + }; + } + } catch (error) { + console.error('Auth check failed:', error); + return { + authenticated: false, + authRequired: true + }; + } + }, + + async requireClientAuth(callback?: () => void, returnUrl?: string, context: string = 'general') { + const authStatus = await this.checkClientAuth(context); + + if (authStatus.authRequired && !authStatus.authenticated) { + const targetUrl = returnUrl || window.location.href; + window.location.href = `/api/auth/login?returnTo=${encodeURIComponent(targetUrl)}`; + return false; + } else { + if (typeof callback === 'function') { + callback(); + } + return true; + } + }, + + async showIfAuthenticated(selector: string, context: string = 'general') { + const authStatus = await this.checkClientAuth(context); + const element = document.querySelector(selector); + + if (element) { + (element as HTMLElement).style.display = (!authStatus.authRequired || authStatus.authenticated) + ? 'inline-flex' + : 'none'; + } + }, + + setupAuthButtons(selector: string = '[data-contribute-button]') { + document.addEventListener('click', async (e) => { + if (!e.target) return; + + const target = e.target as HTMLElement; + const button = target.closest(selector) as HTMLAnchorElement; + if (!button) return; + + e.preventDefault(); + + await this.requireClientAuth(() => { + window.location.href = button.href; + }, button.href, 'contributions'); + }); + } +}; + +// Sharing utilities +export const SharingUtils = { + async copyUrlToClipboard(url: string, button: HTMLElement) { + try { + await navigator.clipboard.writeText(url); + + const originalHTML = button.innerHTML; + button.innerHTML = ` + + + + Kopiert! + `; + button.style.color = 'var(--color-accent)'; + + setTimeout(() => { + button.innerHTML = originalHTML; + button.style.color = ''; + }, 2000); + } catch (err) { + const textArea = document.createElement('textarea'); + textArea.value = url; + document.body.appendChild(textArea); + textArea.select(); + document.execCommand('copy'); + document.body.removeChild(textArea); + + const originalHTML = button.innerHTML; + button.innerHTML = 'Kopiert!'; + setTimeout(() => { + button.innerHTML = originalHTML; + }, 2000); + } + }, + + async shareArticle(button: HTMLElement, url: string, title: string) { + const fullUrl = window.location.origin + url; + await this.copyUrlToClipboard(fullUrl, button); + }, + + async shareCurrentArticle(button: HTMLElement) { + await this.copyUrlToClipboard(window.location.href, button); + } +}; + +// Audit utilities for window exposure +export const AuditUtils = { + 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)'; + }, + + escapeHtml(text: string): string { + if (typeof text !== 'string') return String(text); + const div = document.createElement('div'); + div.textContent = text; + return div.innerHTML; + }, + + 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(); + } +}; + +// Tool utilities (re-export from toolHelpers) +export const ToolUtils = { + createToolSlug, + findToolByIdentifier, + isToolHosted +}; + +// Centralized initialization function +export function initializeGlobalUtils() { + // Expose theme utilities + window.themeUtils = ThemeManager; + + // Expose UI utilities + window.scrollToElement = UIUtils.scrollToElement.bind(UIUtils); + window.scrollToElementById = UIUtils.scrollToElementById.bind(UIUtils); + window.scrollToElementBySelector = UIUtils.scrollToElementBySelector.bind(UIUtils); + window.prioritizeSearchResults = UIUtils.prioritizeSearchResults.bind(UIUtils); + + // Expose auth utilities + window.checkClientAuth = AuthManager.checkClientAuth.bind(AuthManager); + window.requireClientAuth = AuthManager.requireClientAuth.bind(AuthManager); + window.showIfAuthenticated = AuthManager.showIfAuthenticated.bind(AuthManager); + window.setupAuthButtons = AuthManager.setupAuthButtons.bind(AuthManager); + + // Expose sharing utilities + window.shareArticle = SharingUtils.shareArticle.bind(SharingUtils); + window.shareCurrentArticle = SharingUtils.shareCurrentArticle.bind(SharingUtils); + + // Expose tool utilities + window.createToolSlug = ToolUtils.createToolSlug; + window.findToolByIdentifier = ToolUtils.findToolByIdentifier; + window.isToolHosted = ToolUtils.isToolHosted; + + // Expose audit utilities + window.AuditUtils = AuditUtils; + window.auditService = auditService; +} + +// Listen for system theme changes +export function setupThemeChangeListener() { + window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => { + if (ThemeManager.getStoredTheme() === 'auto') { + ThemeManager.applyTheme('auto'); + } + }); +} \ No newline at end of file diff --git a/src/utils/rateLimitedQueue.ts b/src/utils/rateLimitedQueue.ts index d2f078c..5e2eb59 100644 --- a/src/utils/rateLimitedQueue.ts +++ b/src/utils/rateLimitedQueue.ts @@ -1,14 +1,7 @@ // src/utils/rateLimitedQueue.ts -import dotenv from "dotenv"; - -dotenv.config(); - -const RATE_LIMIT_DELAY_MS = - Number.parseInt(process.env.AI_RATE_LIMIT_DELAY_MS ?? "2000", 10) || 2000; - -const TASK_TIMEOUT_MS = - Number.parseInt(process.env.AI_TASK_TIMEOUT_MS ?? "300000", 10) || 300000; +import { config } from '../config/appConfig.js'; +import { logger } from '../services/logger.js'; export type Task = () => Promise; @@ -32,8 +25,8 @@ export interface QueueStatus { class RateLimitedQueue { private tasks: QueuedTask[] = []; private isProcessing = false; - private delayMs = RATE_LIMIT_DELAY_MS; - private taskTimeoutMs = TASK_TIMEOUT_MS; + private readonly delayMs: number; + private readonly taskTimeoutMs: number; private lastProcessedAt = 0; private currentlyProcessingTaskId: string | null = null; @@ -41,9 +34,17 @@ class RateLimitedQueue { private readonly TASK_RETENTION_MS = 300000; // 5 minutes constructor() { + this.delayMs = config.rateLimit.delayMs; + this.taskTimeoutMs = config.ai.taskTimeout; + this.cleanupInterval = setInterval(() => { this.cleanupOldTasks(); }, 30000); + + logger.info('queue', 'Rate limited queue initialized', { + delayMs: this.delayMs, + taskTimeoutMs: this.taskTimeoutMs + }); } private cleanupOldTasks(): void { @@ -64,7 +65,10 @@ class RateLimitedQueue { const cleaned = initialLength - this.tasks.length; if (cleaned > 0) { - console.log(`[QUEUE] Cleaned up ${cleaned} old tasks, ${this.tasks.length} remaining`); + logger.queue('cleanup completed', { + cleaned, + remaining: this.tasks.length + }); } } @@ -72,6 +76,7 @@ class RateLimitedQueue { if (this.cleanupInterval) { clearInterval(this.cleanupInterval); } + logger.info('queue', 'Rate limited queue shut down'); } add(task: Task, taskId?: string): Promise { @@ -95,6 +100,11 @@ class RateLimitedQueue { }; this.tasks.push(queuedTask); + + logger.queue('task queued', { + taskId: id, + queueLength: this.tasks.length + }); setTimeout(() => { this.processQueue(); @@ -162,6 +172,7 @@ class RateLimitedQueue { if (this.isProcessing) return; this.isProcessing = true; + logger.queue('processing started'); try { while (true) { @@ -176,6 +187,11 @@ class RateLimitedQueue { this.currentlyProcessingTaskId = nextTask.id; this.lastProcessedAt = Date.now(); + logger.queue('task processing started', { + taskId: nextTask.id, + queuePosition: 1 + }); + try { await Promise.race([ nextTask.task(), @@ -189,25 +205,34 @@ class RateLimitedQueue { nextTask.status = "completed"; nextTask.completedAt = Date.now(); - console.log(`[QUEUE] Task ${nextTask.id} completed`); + + logger.queue('task completed', { + taskId: nextTask.id, + duration: `${Date.now() - nextTask.startedAt!}ms` + }); + } catch (error) { const err = error as Error; nextTask.status = err.message.includes("timed out") ? "timedout" : "failed"; nextTask.completedAt = Date.now(); - console.error(`[QUEUE] Task ${nextTask.id} failed:`, error); + + logger.error('queue', `Task ${nextTask.id} failed`, err, { + duration: `${Date.now() - nextTask.startedAt!}ms`, + status: nextTask.status + }); } this.currentlyProcessingTaskId = null; const hasMoreQueued = this.tasks.some((t) => t.status === "queued"); if (hasMoreQueued) { - console.log(`[QUEUE] Waiting ${this.delayMs}ms before next task`); + logger.debug('queue', `Waiting ${this.delayMs}ms before next task`); await new Promise((r) => setTimeout(r, this.delayMs)); } } } finally { this.isProcessing = false; - console.log(`[QUEUE] Queue processing finished`); + logger.queue('processing finished'); } } @@ -230,4 +255,4 @@ export function shutdownQueue(): void { queue.shutdown(); } -export default queue; +export default queue; \ No newline at end of file