From 1cba69948f25bc964a569ec3287a6d4cc1afca8c Mon Sep 17 00:00:00 2001 From: saravanakumardb1 Date: Tue, 3 Mar 2026 11:57:00 -0800 Subject: [PATCH] feat(ai-diagnostics): add REST API routes [3.3] --- .windsurf/workflows/repo_update-agent-docs.md | 51 ++ scripts/sync-workflows.sh | 101 ++++ .../modules/ai-diagnostics/query-executor.ts | 4 +- .../src/modules/ai-diagnostics/routes.ts | 544 ++++++++++++++++++ .../src/modules/ai-diagnostics/types.ts | 4 +- 5 files changed, 701 insertions(+), 3 deletions(-) create mode 100644 .windsurf/workflows/repo_update-agent-docs.md create mode 100755 scripts/sync-workflows.sh create mode 100644 services/platform-service/src/modules/ai-diagnostics/routes.ts diff --git a/.windsurf/workflows/repo_update-agent-docs.md b/.windsurf/workflows/repo_update-agent-docs.md new file mode 100644 index 00000000..51bbe0f5 --- /dev/null +++ b/.windsurf/workflows/repo_update-agent-docs.md @@ -0,0 +1,51 @@ +--- +description: Regenerate AI agent docs (AGENTS.md, CLAUDE.md, .cursorrules, etc.) across all repos +--- + +# Update Agent Docs Across Workspace + +Regenerates all 8 AI agent configuration files across all repos in the workspace. + +## Files Generated Per Repo + +| File | Tool | +|------|------| +| `AGENTS.md` | Universal (OpenAI Codex, Claude, Copilot, etc.) | +| `CLAUDE.md` | Claude Code | +| `.cursorrules` | Cursor AI | +| `.github/copilot-instructions.md` | GitHub Copilot | +| `.windsurfrules` | Windsurf / Cascade | +| `.clinerules` | Cline / Roo Code | +| `.aider.conf.yml` | Aider | +| `.editorconfig` | All editors | + +## Steps + +1. Run the update script: + +```bash +cd /Users/sd9235/code/mygh/learning_ai_common_plat +./scripts/update-agent-docs.sh +``` + +2. Review changes per repo: + +```bash +cd /Users/sd9235/code/mygh/learning_voice_ai_agent && git diff --stat +cd /Users/sd9235/code/mygh/learning_multimodal_memory_agents && git diff --stat +# ... etc for all repos +``` + +3. Commit changes (if any): + +```bash +cd /Users/sd9235/code/mygh/learning_voice_ai_agent +[ -n "$(git status --porcelain)" ] && git add -A && git commit -m "chore(docs): update agent configuration files" +``` + +## Notes + +- The script scans each repo's structure and regenerates docs based on current state +- Only commits if there are actual changes +- Safe to run repeatedly (idempotent) +- Requires `learning_ai_common_plat` to be the source of truth for templates diff --git a/scripts/sync-workflows.sh b/scripts/sync-workflows.sh new file mode 100755 index 00000000..26d7f715 --- /dev/null +++ b/scripts/sync-workflows.sh @@ -0,0 +1,101 @@ +#!/bin/zsh +# sync-workflows.sh — Sync common repo_* workflows from common-plat to all repos +# Usage: ./sync-workflows.sh [--dry-run] + +set -eo pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +REPOS_ROOT="/Users/sd9235/code/mygh" +COMMON_PLAT="$REPOS_ROOT/learning_ai_common_plat" + +REPOS=( + "learning_voice_ai_agent" + "learning_multimodal_memory_agents" + "learning_ai_clock" + "learning_ai_peakpulse" + "learning_ai_fastgap" + "learning_ai_jarvis_jr" +) + +WORKFLOWS_TO_SYNC=( + "repo_backup-main-branch.md" + "repo_backup-and-push.md" + "repo_sync-repos.md" + "repo_commit-workspace.md" + "repo_update-agent-docs.md" + "refresh-chat-history.md" +) + +DRY_RUN=false +if [ "$1" = "--dry-run" ]; then + DRY_RUN=true +fi + +sync_count=0 +skip_count=0 + +for repo in "${REPOS[@]}"; do + repo_path="$REPOS_ROOT/$repo" + + if [ ! -d "$repo_path" ]; then + echo "⚠ Skipping $repo (not found)" + continue + fi + + # Create .windsurf/workflows if missing + wf_dir="$repo_path/.windsurf/workflows" + if [ ! -d "$wf_dir" ]; then + if [ "$DRY_RUN" = true ]; then + echo "[dry-run] mkdir -p $wf_dir" + else + mkdir -p "$wf_dir" + echo "✓ Created $wf_dir" + fi + fi + + for wf in "${WORKFLOWS_TO_SYNC[@]}"; do + src="$COMMON_PLAT/.windsurf/workflows/$wf" + dest="$wf_dir/$wf" + + if [ ! -f "$src" ]; then + echo "⚠ Source not found: $src" + continue + fi + + if [ "$DRY_RUN" = true ]; then + if [ -f "$dest" ]; then + # Compare files + if diff -q "$src" "$dest" > /dev/null 2>&1; then + echo "[dry-run] $repo/$wf (identical, skip)" + skip_count=$((skip_count + 1)) + else + echo "[dry-run] $repo/$wf (would update)" + sync_count=$((sync_count + 1)) + fi + else + echo "[dry-run] $repo/$wf (would copy)" + sync_count=$((sync_count + 1)) + fi + else + if [ -f "$dest" ]; then + if diff -q "$src" "$dest" > /dev/null 2>&1; then + echo " = $repo/$wf (identical)" + skip_count=$((skip_count + 1)) + else + cp "$src" "$dest" + echo " ✓ $repo/$wf (updated)" + sync_count=$((sync_count + 1)) + fi + else + cp "$src" "$dest" + echo " ✓ $repo/$wf (new)" + sync_count=$((sync_count + 1)) + fi + fi + done +done + +echo "" +echo "=== Sync complete ===" +echo " Updated: $sync_count" +echo " Skipped (identical): $skip_count" diff --git a/services/platform-service/src/modules/ai-diagnostics/query-executor.ts b/services/platform-service/src/modules/ai-diagnostics/query-executor.ts index 81d9ce4d..5189d21a 100644 --- a/services/platform-service/src/modules/ai-diagnostics/query-executor.ts +++ b/services/platform-service/src/modules/ai-diagnostics/query-executor.ts @@ -14,11 +14,11 @@ export interface QueryExecutionResult { aiResponse: string; confidence: number; supportingData: Array<{ - type: 'cluster' | 'insight' | 'trend' | 'comparison'; + type: 'cluster' | 'insight' | 'trend' | 'comparison' | 'session' | 'telemetry'; id: string; title: string; relevanceScore: number; - data: unknown; + data?: Record; }>; suggestedActions: string[]; executionTimeMs: number; diff --git a/services/platform-service/src/modules/ai-diagnostics/routes.ts b/services/platform-service/src/modules/ai-diagnostics/routes.ts new file mode 100644 index 00000000..cff24334 --- /dev/null +++ b/services/platform-service/src/modules/ai-diagnostics/routes.ts @@ -0,0 +1,544 @@ +import type { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify'; +import { z } from 'zod'; +import { UnauthorizedError } from '../../lib/errors.js'; +import * as repository from './repository.js'; +import { parseQuery, validateQuery, generateQuerySuggestions } from './query-parser.js'; +import { executeQuery } from './query-executor.js'; +import { analyzeRootCause, generatePatternSummary } from './llm-analyzer.js'; +import { aggregateClusterContext } from './telemetry-linking.js'; +import type { ErrorClusterDoc, DiagnosticInsightDoc } from './types.js'; + +// Auth helper +function requireAdmin(req: { jwtPayload?: { role?: string } }): void { + if (req.jwtPayload?.role !== 'admin') { + throw new UnauthorizedError('Admin access required'); + } +} + +// ============================================================================ +// Request/Response Schemas +// ============================================================================ + +const QueryRequestSchema = z.object({ + query: z.string().min(1), + productId: z.string().optional(), + timeRange: z.object({ + start: z.string(), + end: z.string(), + }).optional(), +}); + +const FeedbackRequestSchema = z.object({ + insightId: z.string(), + clusterId: z.string(), + rating: z.enum(['helpful', 'not_helpful']), + note: z.string().optional(), +}); + +const AnalyzeClusterRequestSchema = z.object({ + analysisType: z.enum(['root_cause', 'pattern', 'comparison', 'trend']).default('root_cause'), + modelPreference: z.enum(['gpt-4o-mini', 'gpt-4o']).default('gpt-4o-mini'), +}); + +const SearchRequestSchema = z.object({ + query: z.string(), + productId: z.string().optional(), + limit: z.number().default(10), + threshold: z.number().default(0.75), +}); + +// ============================================================================ +// Routes +// ============================================================================ + +export default async function aiDiagnosticsRoutes(fastify: FastifyInstance): Promise { + // All routes require admin authentication + fastify.addHook('onRequest', async (request) => { + requireAdmin(request); + }); + + // ========================================================================== + // POST /ai-diagnostics/query - Natural language diagnostic query + // ========================================================================== + fastify.post('/query', { + schema: { + body: { + type: 'object', + properties: { + query: { type: 'string' }, + productId: { type: 'string' }, + timeRange: { + type: 'object', + properties: { + start: { type: 'string' }, + end: { type: 'string' }, + }, + }, + }, + required: ['query'], + }, + }, + handler: async ( + request: FastifyRequest<{ Body: z.infer }>, + reply: FastifyReply + ) => { + try { + const body = QueryRequestSchema.parse(request.body); + const userId = request.jwtPayload?.sub || 'anonymous'; + + // Parse the query + const parsedQuery = parseQuery(body.query); + + // Add time range if provided + if (body.timeRange) { + parsedQuery.entities.timeRange = body.timeRange; + } + + // Add product filter + if (body.productId) { + parsedQuery.entities.products = [body.productId]; + } + + // Validate query + const validation = validateQuery(parsedQuery); + if (!validation.valid) { + return reply.status(400).send({ + error: 'Invalid query', + details: validation.errors, + }); + } + + // Execute query + const result = await executeQuery(parsedQuery, { + productId: body.productId, + userId, + }); + + // Save query for history + await repository.saveNaturalLanguageQuery({ + id: `nq_${Date.now()}_${Math.random().toString(36).slice(2, 11)}`, + userId, + productId: body.productId, + rawQuery: body.query, + parsedIntent: parsedQuery.intent, + extractedEntities: parsedQuery.entities, + aiResponse: result.aiResponse, + confidence: result.confidence, + supportingData: result.supportingData, + dataSources: result.supportingData.map((s) => s.type), + executionTimeMs: result.executionTimeMs, + createdAt: new Date().toISOString(), + ttl: 30 * 86400, // 30 days + }); + + return reply.send({ + success: true, + result, + warnings: validation.warnings, + }); + } catch (error) { + request.log.error({ error }, 'Query execution failed'); + return reply.status(500).send({ + error: 'Query execution failed', + message: error instanceof Error ? error.message : String(error), + }); + } + }, + }); + + // ========================================================================== + // GET /ai-diagnostics/clusters - List error clusters + // ========================================================================== + fastify.get('/clusters', { + schema: { + querystring: { + type: 'object', + properties: { + productId: { type: 'string' }, + status: { type: 'string', enum: ['active', 'investigating', 'resolved', 'ignored'] }, + limit: { type: 'number', default: 50 }, + includeInsights: { type: 'boolean', default: false }, + }, + required: ['productId'], + }, + }, + handler: async ( + request: FastifyRequest<{ + Querystring: { + productId: string; + status?: 'active' | 'investigating' | 'resolved' | 'ignored'; + limit?: number; + includeInsights?: boolean; + }; + }>, + reply: FastifyReply + ) => { + try { + const { productId, status, limit = 50, includeInsights = false } = request.query; + + const clusters = await repository.findClustersByProduct(productId, { + status, + limit, + }); + + let clustersWithInsights: Array = clusters; + + if (includeInsights) { + clustersWithInsights = await Promise.all( + clusters.map(async (cluster) => { + const insight = await repository.getLatestInsightForCluster( + cluster.id, + productId + ); + return { ...cluster, latestInsight: insight || undefined }; + }) + ); + } + + return reply.send({ + clusters: clustersWithInsights, + total: clusters.length, + }); + } catch (error) { + request.log.error({ error }, 'Failed to fetch clusters'); + return reply.status(500).send({ + error: 'Failed to fetch clusters', + message: error instanceof Error ? error.message : String(error), + }); + } + }, + }); + + // ========================================================================== + // GET /ai-diagnostics/clusters/:id - Get cluster detail + // ========================================================================== + fastify.get('/clusters/:id', { + schema: { + params: { + type: 'object', + properties: { + id: { type: 'string' }, + }, + required: ['id'], + }, + querystring: { + type: 'object', + properties: { + productId: { type: 'string' }, + }, + required: ['productId'], + }, + }, + handler: async ( + request: FastifyRequest<{ + Params: { id: string }; + Querystring: { productId: string }; + }>, + reply: FastifyReply + ) => { + try { + const { id } = request.params; + const { productId } = request.query; + + const cluster = await repository.getErrorClusterById(id, productId); + + if (!cluster) { + return reply.status(404).send({ error: 'Cluster not found' }); + } + + // Get latest insight + const latestInsight = await repository.getLatestInsightForCluster(id, productId); + + // Get related clusters + const relatedClusters = await repository.findRelatedClusters(id, productId, { + limit: 5, + }); + + return reply.send({ + cluster, + latestInsight, + relatedClusters, + }); + } catch (error) { + request.log.error({ error }, 'Failed to fetch cluster'); + return reply.status(500).send({ + error: 'Failed to fetch cluster', + message: error instanceof Error ? error.message : String(error), + }); + } + }, + }); + + // ========================================================================== + // POST /ai-diagnostics/clusters/:id/analyze - Trigger fresh analysis + // ========================================================================== + fastify.post('/clusters/:id/analyze', { + schema: { + params: { + type: 'object', + properties: { + id: { type: 'string' }, + }, + required: ['id'], + }, + querystring: { + type: 'object', + properties: { + productId: { type: 'string' }, + }, + required: ['productId'], + }, + body: { + type: 'object', + properties: { + analysisType: { type: 'string', enum: ['root_cause', 'pattern', 'comparison', 'trend'] }, + modelPreference: { type: 'string', enum: ['gpt-4o-mini', 'gpt-4o'] }, + }, + }, + }, + handler: async ( + request: FastifyRequest<{ + Params: { id: string }; + Querystring: { productId: string }; + Body: z.infer; + }>, + reply: FastifyReply + ) => { + try { + const { id } = request.params; + const { productId } = request.query; + const body = AnalyzeClusterRequestSchema.parse(request.body || {}); + + // Get cluster + const cluster = await repository.getErrorClusterById(id, productId); + if (!cluster) { + return reply.status(404).send({ error: 'Cluster not found' }); + } + + // Aggregate context + const contextSummary = await aggregateClusterContext(cluster, []); + + // Run analysis + const analysis = await analyzeRootCause({ + cluster, + context: contextSummary, + sampleStackTraces: [cluster.stackSignature], + relatedClusters: [], + analysisType: body.analysisType, + }); + + // Save insight + const insightId = `di_${Date.now()}_${Math.random().toString(36).slice(2, 11)}`; + const insight: DiagnosticInsightDoc = { + id: insightId, + clusterId: id, + productId, + ...analysis, + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + ttl: 90 * 86400, + } as DiagnosticInsightDoc; + + await repository.createDiagnosticInsight(insight); + + // Update cluster with insight reference + cluster.lastAnalyzedAt = new Date().toISOString(); + cluster.insightId = insightId; + await repository.updateErrorCluster(cluster); + + return reply.send({ + success: true, + insight, + }); + } catch (error) { + request.log.error({ error }, 'Analysis failed'); + return reply.status(500).send({ + error: 'Analysis failed', + message: error instanceof Error ? error.message : String(error), + }); + } + }, + }); + + // ========================================================================== + // GET /ai-diagnostics/clusters/:id/analysis - Get pre-computed insight + // ========================================================================== + fastify.get('/clusters/:id/analysis', { + schema: { + params: { + type: 'object', + properties: { + id: { type: 'string' }, + }, + required: ['id'], + }, + querystring: { + type: 'object', + properties: { + productId: { type: 'string' }, + }, + required: ['productId'], + }, + }, + handler: async ( + request: FastifyRequest<{ + Params: { id: string }; + Querystring: { productId: string }; + }>, + reply: FastifyReply + ) => { + try { + const { id } = request.params; + const { productId } = request.query; + + const insight = await repository.getLatestInsightForCluster(id, productId); + + if (!insight) { + return reply.status(404).send({ + error: 'No analysis found', + message: 'Run POST /analyze to generate analysis', + }); + } + + return reply.send({ insight }); + } catch (error) { + request.log.error({ error }, 'Failed to fetch analysis'); + return reply.status(500).send({ + error: 'Failed to fetch analysis', + message: error instanceof Error ? error.message : String(error), + }); + } + }, + }); + + // ========================================================================== + // POST /ai-diagnostics/feedback - Submit insight rating + // ========================================================================== + fastify.post('/feedback', { + schema: { + body: { + type: 'object', + properties: { + insightId: { type: 'string' }, + clusterId: { type: 'string' }, + rating: { type: 'string', enum: ['helpful', 'not_helpful'] }, + note: { type: 'string' }, + }, + required: ['insightId', 'clusterId', 'rating'], + }, + }, + handler: async ( + request: FastifyRequest<{ Body: z.infer }>, + reply: FastifyReply + ) => { + try { + const body = FeedbackRequestSchema.parse(request.body); + const userId = request.jwtPayload?.sub || 'anonymous'; + + await repository.updateInsightFeedback( + body.insightId, + body.clusterId, + { + helpful: body.rating === 'helpful', + note: body.note, + } + ); + + return reply.send({ + success: true, + message: 'Feedback recorded', + }); + } catch (error) { + request.log.error({ error }, 'Failed to record feedback'); + return reply.status(500).send({ + error: 'Failed to record feedback', + message: error instanceof Error ? error.message : String(error), + }); + } + }, + }); + + // ========================================================================== + // GET /ai-diagnostics/suggestions - Get AI-suggested investigations + // ========================================================================== + fastify.get('/suggestions', { + schema: { + querystring: { + type: 'object', + properties: { + productId: { type: 'string' }, + limit: { type: 'number', default: 5 }, + }, + required: ['productId'], + }, + }, + handler: async ( + request: FastifyRequest<{ + Querystring: { productId: string; limit?: number }; + }>, + reply: FastifyReply + ) => { + try { + const { productId, limit = 5 } = request.query; + + // Get active alerts + const alerts = await repository.getActiveAlerts(productId); + + // Get top error types for suggestions + const topTypes = await repository.getTopErrorTypes(productId, 5); + const platforms = ['ios', 'android', 'web']; + + const suggestions = generateQuerySuggestions( + topTypes.map((t) => t.errorType), + platforms + ); + + return reply.send({ + alerts: alerts.slice(0, limit), + suggestions: suggestions.slice(0, limit), + }); + } catch (error) { + request.log.error({ error }, 'Failed to get suggestions'); + return reply.status(500).send({ + error: 'Failed to get suggestions', + message: error instanceof Error ? error.message : String(error), + }); + } + }, + }); + + // ========================================================================== + // GET /ai-diagnostics/query-history - Get user's query history + // ========================================================================== + fastify.get('/query-history', { + schema: { + querystring: { + type: 'object', + properties: { + limit: { type: 'number', default: 20 }, + }, + }, + }, + handler: async ( + request: FastifyRequest<{ + Querystring: { limit?: number }; + }>, + reply: FastifyReply + ) => { + try { + const userId = request.jwtPayload?.sub || 'anonymous'; + const { limit = 20 } = request.query; + + const history = await repository.getQueryHistory(userId, { limit }); + + return reply.send({ history }); + } catch (error) { + request.log.error({ error }, 'Failed to fetch query history'); + return reply.status(500).send({ + error: 'Failed to fetch query history', + message: error instanceof Error ? error.message : String(error), + }); + } + }, + }); +} diff --git a/services/platform-service/src/modules/ai-diagnostics/types.ts b/services/platform-service/src/modules/ai-diagnostics/types.ts index c82fc6e7..a6d7ab5a 100644 --- a/services/platform-service/src/modules/ai-diagnostics/types.ts +++ b/services/platform-service/src/modules/ai-diagnostics/types.ts @@ -256,9 +256,11 @@ export const ExtractedEntitiesSchema = z.object({ export type ExtractedEntities = z.infer; export const SupportingDataSchema = z.object({ - type: z.enum(['cluster', 'telemetry', 'session', 'insight']), + type: z.enum(['cluster', 'telemetry', 'session', 'insight', 'trend', 'comparison']), id: z.string(), relevanceScore: z.number(), + title: z.string().optional(), + data: z.record(z.unknown()).optional(), }); export type SupportingData = z.infer;