diff --git a/lib/dashbot/core/constants/dashbot_prompts.dart b/lib/dashbot/core/constants/dashbot_prompts.dart index 857e7d2b..3d43986e 100644 --- a/lib/dashbot/core/constants/dashbot_prompts.dart +++ b/lib/dashbot/core/constants/dashbot_prompts.dart @@ -453,6 +453,7 @@ RETURN THE JSON ONLY. // Provide insights and suggestions after importing an OpenAPI spec String openApiInsightsPrompt({ required String specSummary, + Map? specMeta, }) { return """ @@ -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: 1–2 line summary → 4–6 bullets → 2–3 next steps. + - Use the meta JSON when present to be specific about routes, tags, and content types. +- Keep it detailed and actionable: 6–10 line summary → 4–6 bullets → 2–3 next steps. OUTPUT FORMAT (STRICT) - Return ONLY a single JSON object. diff --git a/lib/dashbot/core/services/openapi_import_service.dart b/lib/dashbot/core/services/openapi_import_service.dart index ea262610..71c30a48 100644 --- a/lib/dashbot/core/services/openapi_import_service.dart +++ b/lib/dashbot/core/services/openapi_import_service.dart @@ -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 = {}; + (spec.paths ?? const {}).forEach((_, item) { + final ops = { + '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 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 = {}; + final tags = {}; + final routes = >[]; + final reqContentTypes = {}; + final respContentTypes = {}; + + (spec.paths ?? const {}).forEach((path, item) { + final ops = { + '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 = []; + 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 buildOperationPicker(OpenApi spec) { + static Map buildOperationPicker(OpenApi spec, + {String? insights}) { final servers = spec.servers ?? const []; int endpointsCount = 0; final methods = {}; @@ -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), } }; } diff --git a/lib/dashbot/features/chat/viewmodel/chat_viewmodel.dart b/lib/dashbot/features/chat/viewmodel/chat_viewmodel.dart index 8653491b..9f56ca92 100644 --- a/lib/dashbot/features/chat/viewmodel/chat_viewmodel.dart +++ b/lib/dashbot/features/chat/viewmodel/chat_viewmodel.dart @@ -675,7 +675,42 @@ class ChatViewmodel extends StateNotifier { 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 { .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 { final normalized = path.startsWith('/') ? path : '/$path'; return '{{$key}}$normalized'; } - - Future _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((