feat: enhance OpenAPI insights generation with structured metadata and summary

This commit is contained in:
Udhay-Adithya
2025-09-21 00:59:30 +05:30
parent ce3248dec0
commit 3a4a871676
3 changed files with 178 additions and 41 deletions

View File

@@ -453,6 +453,7 @@ RETURN THE JSON ONLY.
// Provide insights and suggestions after importing an OpenAPI spec
String openApiInsightsPrompt({
required String specSummary,
Map<String, dynamic>? specMeta,
}) {
return """
<system_prompt>
@@ -464,13 +465,17 @@ STRICT OFF-TOPIC POLICY
CONTEXT (OPENAPI SUMMARY)
${specSummary.trim()}
CONTEXT (OPENAPI META, JSON)
${specMeta ?? '{}'}
TASK
- Provide practical, user-friendly insights based on the API spec:
- Identify noteworthy endpoints (e.g., CRUD sets, auth/login, health/status) and common patterns.
- Point out authentication/security requirements (e.g., API keys, OAuth scopes) if present.
- Suggest a few starter calls (e.g., list/search) and a short onboarding path.
- Call out potential pitfalls (rate limits, pagination, required headers, content types).
- Keep it concise and actionable: 12 line summary → 46 bullets → 23 next steps.
- Use the meta JSON when present to be specific about routes, tags, and content types.
- Keep it detailed and actionable: 610 line summary → 46 bullets → 23 next steps.
OUTPUT FORMAT (STRICT)
- Return ONLY a single JSON object.

View File

@@ -4,6 +4,132 @@ import 'package:openapi_spec/openapi_spec.dart';
/// Service to parse OpenAPI specifications and produce
/// a standard action message map understood by Dashbot.
class OpenApiImportService {
/// Produce a concise, human-readable summary for a spec.
static String summaryForSpec(OpenApi spec) {
final servers = spec.servers ?? const [];
int endpointsCount = 0;
final methods = <String>{};
(spec.paths ?? const {}).forEach((_, item) {
final ops = <String, Operation?>{
'GET': item.get,
'POST': item.post,
'PUT': item.put,
'DELETE': item.delete,
'PATCH': item.patch,
'HEAD': item.head,
'OPTIONS': item.options,
'TRACE': item.trace,
};
ops.forEach((m, op) {
if (op == null) return;
endpointsCount += 1;
methods.add(m);
});
});
final title = spec.info.title.isNotEmpty ? spec.info.title : 'Untitled API';
final version = spec.info.version;
final server = servers.isNotEmpty ? servers.first.url : null;
final summary = StringBuffer()
..writeln('- Title: $title (v$version)')
..writeln('- Server: ${server ?? '/'}')
..writeln('- Endpoints discovered: $endpointsCount')
..writeln('- Methods: ${methods.join(', ')}');
return summary.toString();
}
/// Extract structured metadata from an OpenAPI spec for analytics/insights.
/// Returns a JSON-serializable map capturing key details like title, version,
/// servers, total endpoints, methods, tags and a concise list of routes.
static Map<String, dynamic> extractSpecMeta(OpenApi spec,
{int maxRoutes = 40}) {
final servers = (spec.servers ?? const [])
.map((s) => s.url)
.where((u) => (u ?? '').trim().isNotEmpty)
.map((u) => u!)
.toList(growable: false);
final title = spec.info.title.isNotEmpty ? spec.info.title : 'Untitled API';
final version = spec.info.version;
int endpointsCount = 0;
final methods = <String>{};
final tags = <String>{};
final routes = <Map<String, dynamic>>[];
final reqContentTypes = <String>{};
final respContentTypes = <String>{};
(spec.paths ?? const {}).forEach((path, item) {
final ops = <String, Operation?>{
'GET': item.get,
'POST': item.post,
'PUT': item.put,
'DELETE': item.delete,
'PATCH': item.patch,
'HEAD': item.head,
'OPTIONS': item.options,
'TRACE': item.trace,
};
final presentMethods = <String>[];
ops.forEach((method, op) {
if (op == null) return;
endpointsCount += 1;
methods.add(method);
presentMethods.add(method);
// tags
for (final t in op.tags ?? const []) {
final tt = t?.trim() ?? '';
if (tt.isNotEmpty) tags.add(tt);
}
// content types
final reqCts = op.requestBody?.content?.keys;
if (reqCts != null) reqContentTypes.addAll(reqCts);
final resps = op.responses ?? const {};
for (final r in resps.values) {
final ct = r.content?.keys;
if (ct != null) respContentTypes.addAll(ct);
}
});
if (presentMethods.isNotEmpty) {
routes.add({
'path': path,
'methods': presentMethods,
});
}
});
// Heuristic noteworthy route detection
final noteworthy = routes
.where((r) {
final p = (r['path'] as String).toLowerCase();
return p.contains('auth') ||
p.contains('login') ||
p.contains('status') ||
p.contains('health') ||
p.contains('user') ||
p.contains('search');
})
.take(10)
.toList();
// Trim routes list to keep payload light
final trimmedRoutes =
routes.length > maxRoutes ? routes.take(maxRoutes).toList() : routes;
return {
'title': title,
'version': version,
'servers': servers,
'baseUrl': servers.isNotEmpty ? servers.first : null,
'endpointsCount': endpointsCount,
'methods': methods.toList()..sort(),
'tags': tags.toList()..sort(),
'routes': trimmedRoutes,
'noteworthyRoutes': noteworthy,
'requestContentTypes': reqContentTypes.toList()..sort(),
'responseContentTypes': respContentTypes.toList()..sort(),
};
}
/// Try to parse a JSON or YAML OpenAPI spec string.
/// Returns null if parsing fails.
static OpenApi? tryParseSpec(String source) {
@@ -135,10 +261,8 @@ class OpenApiImportService {
};
}
/// Build a list of operations from the spec, and if multiple are found,
/// return a JSON with a single "Import Now" style action to open
/// a selection dialog in the UI, avoiding rendering dozens of buttons.
static Map<String, dynamic> buildOperationPicker(OpenApi spec) {
static Map<String, dynamic> buildOperationPicker(OpenApi spec,
{String? insights}) {
final servers = spec.servers ?? const [];
int endpointsCount = 0;
final methods = <String>{};
@@ -168,7 +292,6 @@ class OpenApiImportService {
};
}
// Build a short spec summary for downstream insights prompt
final title = spec.info.title.isNotEmpty ? spec.info.title : 'Untitled API';
final version = spec.info.version;
final server = servers.isNotEmpty ? servers.first.url : null;
@@ -178,8 +301,13 @@ class OpenApiImportService {
..writeln('- Endpoints discovered: $endpointsCount')
..writeln('- Methods: ${methods.join(', ')}');
final explanation =
StringBuffer('OpenAPI parsed. Click Import Now to choose operations.')
.toString();
return {
'explnation': 'OpenAPI parsed. Click Import Now to choose operations.',
'explnation': insights == null || insights.isEmpty
? '$explanation\n\n${summary.toString()}'
: '$explanation\n\n${summary.toString()}\n\\n$insights',
'actions': [
{
'action': 'import_now_openapi',
@@ -194,6 +322,7 @@ class OpenApiImportService {
],
'meta': {
'openapi_summary': summary.toString(),
'openapi_meta': extractSpecMeta(spec),
}
};
}

View File

@@ -675,7 +675,42 @@ class ChatViewmodel extends StateNotifier<ChatState> {
ChatMessageType.importOpenApi);
return;
}
final picker = OpenApiImportService.buildOperationPicker(spec);
// Build a short summary + structured meta for the insights prompt
final summary = OpenApiImportService.summaryForSpec(spec);
String? insights;
try {
final ai = _selectedAIModel;
if (ai != null) {
final meta = OpenApiImportService.extractSpecMeta(spec);
final sys = dash.DashbotPrompts()
.openApiInsightsPrompt(specSummary: summary, specMeta: meta);
final res = await _repo.sendChat(
request: ai.copyWith(
systemPrompt: sys,
userPrompt:
'Provide concise, actionable insights about these endpoints.',
stream: false,
),
);
if (res != null && res.isNotEmpty) {
// Ensure we only pass the explnation string to embed into explanation
try {
final map = MessageJson.safeParse(res);
if (map['explnation'] is String) insights = map['explnation'];
} catch (_) {
insights = res; // fallback raw text
}
}
}
} catch (e) {
debugPrint('[OpenAPI] insights error: $e');
}
final picker = OpenApiImportService.buildOperationPicker(
spec,
insights: insights,
);
final rqId = _currentRequest?.id ?? 'global';
_addMessage(
rqId,
@@ -691,16 +726,7 @@ class ChatViewmodel extends StateNotifier<ChatState> {
.toList(),
),
);
// If meta summary is present, generate insights via AI
try {
final meta = picker['meta'];
final summary = (meta is Map && meta['openapi_summary'] is String)
? meta['openapi_summary'] as String
: '';
if (summary.isNotEmpty) {
await _generateOpenApiInsights(summary);
}
} catch (_) {}
// Do not generate a separate insights prompt; summary is inline now.
} catch (e) {
debugPrint('[OpenAPI] Exception: $e');
final safe = e.toString().replaceAll('"', "'");
@@ -1035,29 +1061,6 @@ class ChatViewmodel extends StateNotifier<ChatState> {
final normalized = path.startsWith('/') ? path : '/$path';
return '{{$key}}$normalized';
}
Future<void> _generateOpenApiInsights(String summary) async {
final ai = _selectedAIModel;
if (ai == null) return;
try {
final sys = dash.DashbotPrompts().openApiInsightsPrompt(
specSummary: summary,
);
final res = await _repo.sendChat(
request: ai.copyWith(
systemPrompt: sys,
userPrompt:
'Provide concise, actionable insights about these endpoints.',
stream: false,
),
);
if (res != null && res.isNotEmpty) {
_appendSystem(res, ChatMessageType.importOpenApi);
}
} catch (e) {
debugPrint('[Chat] Insights error: $e');
}
}
}
final chatViewmodelProvider = StateNotifierProvider<ChatViewmodel, ChatState>((