feat: add ai insights for curl import

This commit is contained in:
Udhay-Adithya
2025-09-21 14:54:51 +05:30
parent 401d2f4b69
commit 5fba4bb572
4 changed files with 404 additions and 16 deletions

View File

@@ -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.
</system_prompt>
""";
}
// Provide insights after parsing a cURL command (AI-generated)
String curlInsightsPrompt({
required String curlSummary,
Map<String, dynamic>? diff,
Map<String, dynamic>? current,
}) {
return """
<system_prompt>
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 12 line paragraph summary.
- Then provide 58 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 23 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.
</system_prompt>
""";

View File

@@ -32,7 +32,7 @@ class CurlImportService {
headers['Authorization'] = 'Basic $basic';
}
return {
final payload = <String, dynamic>{
'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<String, dynamic> buildActionMessageFromPayload(
Map<String, dynamic> 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<String, dynamic> actionPayload, {
String? note,
Map<String, dynamic>? 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<Map<String, dynamic>> actions})
buildResponseFromParsed(Curl curl) {
buildResponseFromParsed(Curl curl, {Map<String, dynamic>? current}) {
final payload = buildActionPayloadFromCurl(curl);
// Build a small note for flags that are not represented in the request model
final notes = <String>[];
@@ -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<Map<String, dynamic>>().toList();
@@ -104,7 +136,7 @@ class CurlImportService {
String? error,
String? jsonMessage,
List<Map<String, dynamic>>? actions
}) processPastedCurl(String input) {
}) processPastedCurl(String input, {Map<String, dynamic>? 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<String, dynamic> p) {
final method = (p['method'] as String? ?? 'GET').toUpperCase();
final url = p['url'] as String? ?? '';
final headers = (p['headers'] as Map?)?.cast<String, dynamic>() ?? {};
final params = (p['params'] as Map?)?.cast<String, dynamic>() ?? {};
final body = p['body'] as String?;
final form = p['form'] == true;
final formData =
((p['formData'] as List?) ?? const []).whereType<Map>().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<String, dynamic> _diffWithCurrent(
Map<String, dynamic> p, Map<String, dynamic>? current) {
if (current == null || current.isEmpty) return {};
final diff = <String, dynamic>{};
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<String, String> normMap(dynamic m) {
final map = (m as Map?)?.cast<String, dynamic>() ?? {};
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 = <String>[];
final headerUpdates = <String>[];
final headerRemoves = <String>[];
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 = <String>[];
final paramUpdates = <String>[];
final paramRemoves = <String>[];
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<String, dynamic> payload,
{Map<String, dynamic>? 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<String, dynamic>;
buf.writeln('- Method: ${d['from']}${d['to']}');
}
if (diff.containsKey('url')) {
final d = diff['url'] as Map<String, dynamic>;
buf.writeln('- URL: ${d['from']}${d['to']}');
}
if (diff.containsKey('headers')) {
final d = (diff['headers'] as Map).cast<String, dynamic>();
List<String> 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<String, dynamic>();
List<String> 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<String, dynamic>();
final t = (d['type'] as Map).cast<String, dynamic>();
final s = (d['size'] as Map).cast<String, dynamic>();
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<String, dynamic> p) =>
_summaryForCurlPayload(p);
static Map<String, dynamic> diffForPayload(
Map<String, dynamic> p, Map<String, dynamic>? current) =>
_diffWithCurrent(p, current);
}

View File

@@ -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',

View File

@@ -440,8 +440,74 @@ class ChatViewmodel extends StateNotifier<ChatState> {
ChatMessageType.importCurl);
return;
}
final built = CurlImportService.buildResponseFromParsed(curl);
final msg = jsonDecode(built.jsonMessage) as Map<String, dynamic>;
final currentCtx = _currentRequestContext();
// Prepare base message first (without AI insights)
var built = CurlImportService.buildResponseFromParsed(
curl,
current: currentCtx,
);
var msg = jsonDecode(built.jsonMessage) as Map<String, dynamic>;
// Ask AI for cURL insights
try {
final ai = _selectedAIModel;
if (ai != null) {
final summary = CurlImportService.summaryForPayload(
jsonDecode(built.jsonMessage)['actions'][0]['value']
as Map<String, dynamic>,
);
final diff = CurlImportService.diffForPayload(
jsonDecode(built.jsonMessage)['actions'][0]['value']
as Map<String, dynamic>,
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<String, dynamic>)
: <String, dynamic>{};
final enriched = CurlImportService.buildActionMessageFromPayload(
payload,
current: currentCtx,
insights: insights,
);
msg = enriched;
built = (
jsonMessage: jsonEncode(enriched),
actions: (enriched['actions'] as List)
.whereType<Map<String, dynamic>>()
.toList(),
);
}
}
} catch (e) {
debugPrint('[cURL] insights error: $e');
}
final rqId = _currentRequest?.id ?? 'global';
_addMessage(
rqId,
@@ -465,6 +531,41 @@ class ChatViewmodel extends StateNotifier<ChatState> {
}
}
Map<String, dynamic>? _currentRequestContext() {
final rq = _currentRequest?.httpRequestModel;
if (rq == null) return null;
final headers = <String, String>{};
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 = <String, String>{};
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<void> handleOpenApiAttachment(ChatAttachment att) async {
try {
final content = utf8.decode(att.data);