From 5fba4bb5723e7c202a98c59f8b0f65c90484a7b4 Mon Sep 17 00:00:00 2001 From: Udhay-Adithya Date: Sun, 21 Sep 2025 14:54:51 +0530 Subject: [PATCH] feat: add ai insights for curl import --- .../core/constants/dashbot_prompts.dart | 42 +++ .../core/services/curl_import_service.dart | 271 +++++++++++++++++- .../core/services/openapi_import_service.dart | 2 +- .../chat/viewmodel/chat_viewmodel.dart | 105 ++++++- 4 files changed, 404 insertions(+), 16 deletions(-) diff --git a/lib/dashbot/core/constants/dashbot_prompts.dart b/lib/dashbot/core/constants/dashbot_prompts.dart index 3d43986e..c2d6df57 100644 --- a/lib/dashbot/core/constants/dashbot_prompts.dart +++ b/lib/dashbot/core/constants/dashbot_prompts.dart @@ -484,6 +484,48 @@ OUTPUT FORMAT (STRICT) REFUSAL TEMPLATE (when off-topic), JSON only: {"explnation":"I am Dashbot, an AI assistant focused specifically on API development tasks within API Dash. My capabilities are limited to explaining API responses, debugging requests, generating documentation, creating tests, visualizing API data, and generating integration code. Therefore, I cannot answer questions outside of this scope. How can I assist you with an API-related task?","actions":[]} +RETURN THE JSON ONLY. + +"""; + } + + // Provide insights after parsing a cURL command (AI-generated) + String curlInsightsPrompt({ + required String curlSummary, + Map? diff, + Map? current, + }) { + return """ + +YOU ARE Dashbot, an API Insights Assistant specialized in analyzing cURL commands within API Dash. + +STRICT OFF-TOPIC POLICY +- If a request is unrelated to APIs, refuse. Return JSON with only "explnation" and an empty "actions": []. + +CONTEXT (CURL SUMMARY) +${curlSummary.trim()} + +CONTEXT (DIFF VS CURRENT REQUEST, JSON) +${diff ?? '{}'} + +CONTEXT (CURRENT REQUEST SNAPSHOT, JSON) +${current ?? '{}'} + +TASK +- Provide practical, user-friendly insights based on the cURL: + - Start with a short 1–2 line paragraph summary. + - Then provide 5–8 concise bullet points with key insights (method/url change, headers added/updated, params, body type/size, auth/security notes). + - Provide a short preview of changes if applied (bulleted), and any caveats (overwriting headers/body, missing tokens). + - End with 2–3 next steps (apply to selected/new, verify tokens, test with env variables). + - Prefer bullet lists for readability over long paragraphs. + +OUTPUT FORMAT (STRICT) +- Return ONLY a single JSON object. +- Keys: {"explnation": string, "actions": []} + +REFUSAL TEMPLATE (when off-topic), JSON only: +{"explnation":"I am Dashbot, an AI assistant focused specifically on API development tasks within API Dash. My capabilities are limited to explaining API responses, debugging requests, generating documentation, creating tests, visualizing API data, and generating integration code. Therefore, I cannot answer questions outside of this scope. How can I assist you with an API-related task?","actions":[]} + RETURN THE JSON ONLY. """; diff --git a/lib/dashbot/core/services/curl_import_service.dart b/lib/dashbot/core/services/curl_import_service.dart index 8d2c5d6f..e070159d 100644 --- a/lib/dashbot/core/services/curl_import_service.dart +++ b/lib/dashbot/core/services/curl_import_service.dart @@ -32,7 +32,7 @@ class CurlImportService { headers['Authorization'] = 'Basic $basic'; } - return { + final payload = { 'method': curl.method, 'url': curl.uri.toString(), 'headers': headers, @@ -46,20 +46,43 @@ class CurlImportService { }) .toList(), }; + + // Include query params for insights only + try { + final qp = curl.uri.queryParameters; + if (qp.isNotEmpty) { + payload['params'] = qp; + } + } catch (_) {} + + return payload; } /// Build the message object with two actions: apply to selected or new. static Map buildActionMessageFromPayload( - Map actionPayload, - {String? note}) { - final explanation = StringBuffer( - 'Parsed the cURL command. Where do you want to apply the changes? Choose one of the options below.'); - if (note != null && note.isNotEmpty) { - explanation.writeln(''); - explanation.write('Note: $note'); + Map actionPayload, { + String? note, + Map? current, + String? insights, + }) { + final base = _insightsExplanation( + actionPayload, + current: current, + header: 'cURL parsed. Here is a quick summary and diff:', + ); + final buf = StringBuffer()..writeln(base); + if (insights != null && insights.isNotEmpty) { + buf + ..writeln() + ..writeln(insights.trim()); } - return { - 'explnation': explanation.toString(), + buf + ..writeln() + ..writeln( + 'Where do you want to apply the changes? Choose one of the options below.'); + final explanation = buf.toString(); + final map = { + 'explnation': explanation, 'actions': [ { 'action': 'apply_curl', @@ -77,11 +100,19 @@ class CurlImportService { } ] }; + if (note != null && note.isNotEmpty) { + map['note'] = note; + } + map['meta'] = { + 'curl_summary': _summaryForCurlPayload(actionPayload), + 'diff': _diffWithCurrent(actionPayload, current), + }; + return map; } /// Convenience: from parsed [Curl] to (json, actions list). static ({String jsonMessage, List> actions}) - buildResponseFromParsed(Curl curl) { + buildResponseFromParsed(Curl curl, {Map? current}) { final payload = buildActionPayloadFromCurl(curl); // Build a small note for flags that are not represented in the request model final notes = []; @@ -92,6 +123,7 @@ class CurlImportService { final msg = buildActionMessageFromPayload( payload, note: notes.isEmpty ? null : notes.join('; '), + current: current, ); final actions = (msg['actions'] as List).whereType>().toList(); @@ -104,7 +136,7 @@ class CurlImportService { String? error, String? jsonMessage, List>? actions - }) processPastedCurl(String input) { + }) processPastedCurl(String input, {Map? current}) { try { final curl = tryParseCurl(input); if (curl == null) { @@ -115,7 +147,7 @@ class CurlImportService { actions: null ); } - final built = buildResponseFromParsed(curl); + final built = buildResponseFromParsed(curl, current: current); return ( error: null, jsonMessage: built.jsonMessage, @@ -126,4 +158,217 @@ class CurlImportService { return (error: 'Parsing failed: $safe', jsonMessage: null, actions: null); } } + + // ----- Insights helpers ----- + + static String _summaryForCurlPayload(Map p) { + final method = (p['method'] as String? ?? 'GET').toUpperCase(); + final url = p['url'] as String? ?? ''; + final headers = (p['headers'] as Map?)?.cast() ?? {}; + final params = (p['params'] as Map?)?.cast() ?? {}; + final body = p['body'] as String?; + final form = p['form'] == true; + final formData = + ((p['formData'] as List?) ?? const []).whereType().toList(); + final bodyType = form || formData.isNotEmpty + ? 'form-data' + : (body != null && body.trim().isNotEmpty + ? (_looksLikeJson(body) ? 'json' : 'text') + : 'none'); + final size = body?.length ?? 0; + return [ + 'Request Summary:', + '- Method: $method', + '- URL: $url', + if (params.isNotEmpty) '- Query Params: ${params.length}', + '- Headers: ${headers.length}', + '- Body: $bodyType${size > 0 ? ' ($size chars)' : ''}', + ].join('\n'); + } + + static Map _diffWithCurrent( + Map p, Map? current) { + if (current == null || current.isEmpty) return {}; + final diff = {}; + + String up(String? s) => (s ?? '').toUpperCase(); + String curMethod = up(current['method'] as String?); + String newMethod = up(p['method'] as String?); + if (curMethod != newMethod) { + diff['method'] = {'from': curMethod, 'to': newMethod}; + } + + final curUrl = (current['url'] as String?) ?? ''; + final newUrl = (p['url'] as String?) ?? ''; + if (curUrl != newUrl) { + diff['url'] = {'from': curUrl, 'to': newUrl}; + } + + Map normMap(dynamic m) { + final map = (m as Map?)?.cast() ?? {}; + return Map.fromEntries(map.entries.map( + (e) => MapEntry(e.key.toLowerCase(), (e.value ?? '').toString()))); + } + + final curHeaders = normMap(current['headers']); + final newHeaders = normMap(p['headers']); + final headerAdds = []; + final headerUpdates = []; + final headerRemoves = []; + for (final k in newHeaders.keys) { + if (!curHeaders.containsKey(k)) { + headerAdds.add(k); + } else if (curHeaders[k] != newHeaders[k]) { + headerUpdates.add(k); + } + } + for (final k in curHeaders.keys) { + if (!newHeaders.containsKey(k)) headerRemoves.add(k); + } + if (headerAdds.isNotEmpty || + headerUpdates.isNotEmpty || + headerRemoves.isNotEmpty) { + diff['headers'] = { + 'add': headerAdds, + 'update': headerUpdates, + 'remove': headerRemoves, + }; + } + + final curParams = normMap(current['params']); + final newParams = normMap(p['params']); + final paramAdds = []; + final paramUpdates = []; + final paramRemoves = []; + if (newParams.isNotEmpty || curParams.isNotEmpty) { + for (final k in newParams.keys) { + if (!curParams.containsKey(k)) { + paramAdds.add(k); + } else if (curParams[k] != newParams[k]) { + paramUpdates.add(k); + } + } + for (final k in curParams.keys) { + if (!newParams.containsKey(k)) paramRemoves.add(k); + } + if (paramAdds.isNotEmpty || + paramUpdates.isNotEmpty || + paramRemoves.isNotEmpty) { + diff['params'] = { + 'add': paramAdds, + 'update': paramUpdates, + 'remove': paramRemoves, + }; + } + } + + final curBody = (current['body'] as String?) ?? ''; + final newBody = (p['body'] as String?) ?? ''; + final curForm = current['form'] == true || + ((current['formData'] as List?)?.isNotEmpty ?? false); + final newForm = + p['form'] == true || ((p['formData'] as List?)?.isNotEmpty ?? false); + final curType = curForm + ? 'form-data' + : (curBody.trim().isEmpty + ? 'none' + : (_looksLikeJson(curBody) ? 'json' : 'text')); + final newType = newForm + ? 'form-data' + : (newBody.trim().isEmpty + ? 'none' + : (_looksLikeJson(newBody) ? 'json' : 'text')); + if (curType != newType || curBody != newBody) { + diff['body'] = { + 'type': {'from': curType, 'to': newType}, + 'size': {'from': curBody.length, 'to': newBody.length}, + }; + } + return diff; + } + + static String _insightsExplanation(Map payload, + {Map? current, String? header}) { + final buf = StringBuffer(); + if (header != null && header.isNotEmpty) { + buf.writeln(header); + } + buf.writeln(); + // High-level summary + buf.writeln(_summaryForCurlPayload(payload)); + + // Diff section + final diff = _diffWithCurrent(payload, current); + if (diff.isNotEmpty) { + buf.writeln(); + buf.writeln('If applied to the selected request, changes:'); + if (diff.containsKey('method')) { + final d = diff['method'] as Map; + buf.writeln('- Method: ${d['from']} → ${d['to']}'); + } + if (diff.containsKey('url')) { + final d = diff['url'] as Map; + buf.writeln('- URL: ${d['from']} → ${d['to']}'); + } + if (diff.containsKey('headers')) { + final d = (diff['headers'] as Map).cast(); + List parts = []; + if ((d['add'] as List).isNotEmpty) { + parts.add('add ${(d['add'] as List).length}'); + } + if ((d['update'] as List).isNotEmpty) { + parts.add('update ${(d['update'] as List).length}'); + } + if ((d['remove'] as List).isNotEmpty) { + parts.add('remove ${(d['remove'] as List).length}'); + } + if (parts.isNotEmpty) { + buf.writeln('- Headers: ${parts.join(', ')}'); + } + } + if (diff.containsKey('params')) { + final d = (diff['params'] as Map).cast(); + List parts = []; + if ((d['add'] as List).isNotEmpty) { + parts.add('add ${(d['add'] as List).length}'); + } + if ((d['update'] as List).isNotEmpty) { + parts.add('update ${(d['update'] as List).length}'); + } + if ((d['remove'] as List).isNotEmpty) { + parts.add('remove ${(d['remove'] as List).length}'); + } + if (parts.isNotEmpty) { + buf.writeln('- Query Params: ${parts.join(', ')}'); + } + } + if (diff.containsKey('body')) { + final d = (diff['body'] as Map).cast(); + final t = (d['type'] as Map).cast(); + final s = (d['size'] as Map).cast(); + buf.writeln( + '- Body: ${t['from']} → ${t['to']} (${s['from']} → ${s['to']} chars)'); + } + } + return buf.toString(); + } + + static bool _looksLikeJson(String s) { + final t = s.trim(); + if (t.isEmpty) return false; + if (!(t.startsWith('{') || t.startsWith('['))) return false; + try { + jsonDecode(t); + return true; + } catch (_) { + return false; + } + } + + // Public helpers to reuse where needed + static String summaryForPayload(Map p) => + _summaryForCurlPayload(p); + static Map diffForPayload( + Map p, Map? current) => + _diffWithCurrent(p, current); } diff --git a/lib/dashbot/core/services/openapi_import_service.dart b/lib/dashbot/core/services/openapi_import_service.dart index 71c30a48..6e707d8c 100644 --- a/lib/dashbot/core/services/openapi_import_service.dart +++ b/lib/dashbot/core/services/openapi_import_service.dart @@ -307,7 +307,7 @@ class OpenApiImportService { return { 'explnation': insights == null || insights.isEmpty ? '$explanation\n\n${summary.toString()}' - : '$explanation\n\n${summary.toString()}\n\\n$insights', + : '$explanation\n\n${summary.toString()}\n $insights', 'actions': [ { 'action': 'import_now_openapi', diff --git a/lib/dashbot/features/chat/viewmodel/chat_viewmodel.dart b/lib/dashbot/features/chat/viewmodel/chat_viewmodel.dart index e0cbb90b..db78a47d 100644 --- a/lib/dashbot/features/chat/viewmodel/chat_viewmodel.dart +++ b/lib/dashbot/features/chat/viewmodel/chat_viewmodel.dart @@ -440,8 +440,74 @@ class ChatViewmodel extends StateNotifier { ChatMessageType.importCurl); return; } - final built = CurlImportService.buildResponseFromParsed(curl); - final msg = jsonDecode(built.jsonMessage) as Map; + final currentCtx = _currentRequestContext(); + // Prepare base message first (without AI insights) + var built = CurlImportService.buildResponseFromParsed( + curl, + current: currentCtx, + ); + var msg = jsonDecode(built.jsonMessage) as Map; + + // Ask AI for cURL insights + try { + final ai = _selectedAIModel; + if (ai != null) { + final summary = CurlImportService.summaryForPayload( + jsonDecode(built.jsonMessage)['actions'][0]['value'] + as Map, + ); + final diff = CurlImportService.diffForPayload( + jsonDecode(built.jsonMessage)['actions'][0]['value'] + as Map, + currentCtx, + ); + final sys = dash.DashbotPrompts().curlInsightsPrompt( + curlSummary: summary, + diff: diff, + current: currentCtx, + ); + final res = await _repo.sendChat( + request: ai.copyWith( + systemPrompt: sys, + userPrompt: + 'Provide concise, actionable insights about this cURL import.', + stream: false, + ), + ); + String? insights; + if (res != null && res.isNotEmpty) { + try { + final parsed = MessageJson.safeParse(res); + if (parsed['explnation'] is String) { + insights = parsed['explnation']; + } + } catch (_) { + insights = res; + } + } + if (insights != null && insights.isNotEmpty) { + // Rebuild message including insights in explanation + final payload = (msg['actions'] as List).isNotEmpty + ? (((msg['actions'] as List).first as Map)['value'] + as Map) + : {}; + final enriched = CurlImportService.buildActionMessageFromPayload( + payload, + current: currentCtx, + insights: insights, + ); + msg = enriched; + built = ( + jsonMessage: jsonEncode(enriched), + actions: (enriched['actions'] as List) + .whereType>() + .toList(), + ); + } + } + } catch (e) { + debugPrint('[cURL] insights error: $e'); + } final rqId = _currentRequest?.id ?? 'global'; _addMessage( rqId, @@ -465,6 +531,41 @@ class ChatViewmodel extends StateNotifier { } } + Map? _currentRequestContext() { + final rq = _currentRequest?.httpRequestModel; + if (rq == null) return null; + final headers = {}; + for (final h in rq.headers ?? const []) { + final k = (h.name).toString(); + final v = (h.value ?? '').toString(); + if (k.isNotEmpty) headers[k] = v; + } + final params = {}; + for (final p in rq.params ?? const []) { + final k = (p.name).toString(); + final v = (p.value ?? '').toString(); + if (k.isNotEmpty) params[k] = v; + } + final body = rq.body ?? ''; + final formData = (rq.formData ?? const []) + .map((f) => { + 'name': f.name, + 'value': f.value, + 'type': f.type.name, + }) + .toList(); + final isForm = rq.bodyContentType == ContentType.formdata; + return { + 'method': rq.method.name.toUpperCase(), + 'url': rq.url, + 'headers': headers, + 'params': params, + 'body': body, + 'form': isForm, + 'formData': formData, + }; + } + Future handleOpenApiAttachment(ChatAttachment att) async { try { final content = utf8.decode(att.data);