feat(ai-diagnostics): add REST API routes [3.3]
This commit is contained in:
parent
d575b725cb
commit
1cba69948f
51
.windsurf/workflows/repo_update-agent-docs.md
Normal file
51
.windsurf/workflows/repo_update-agent-docs.md
Normal file
@ -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
|
||||
101
scripts/sync-workflows.sh
Executable file
101
scripts/sync-workflows.sh
Executable file
@ -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"
|
||||
@ -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<string, unknown>;
|
||||
}>;
|
||||
suggestedActions: string[];
|
||||
executionTimeMs: number;
|
||||
|
||||
544
services/platform-service/src/modules/ai-diagnostics/routes.ts
Normal file
544
services/platform-service/src/modules/ai-diagnostics/routes.ts
Normal file
@ -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<void> {
|
||||
// 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<typeof QueryRequestSchema> }>,
|
||||
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<ErrorClusterDoc & { latestInsight?: DiagnosticInsightDoc }> = 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<typeof AnalyzeClusterRequestSchema>;
|
||||
}>,
|
||||
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<typeof FeedbackRequestSchema> }>,
|
||||
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),
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
@ -256,9 +256,11 @@ export const ExtractedEntitiesSchema = z.object({
|
||||
export type ExtractedEntities = z.infer<typeof ExtractedEntitiesSchema>;
|
||||
|
||||
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<typeof SupportingDataSchema>;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user