feat: implement auto-fix functionality

This commit is contained in:
Udhay-Adithya
2025-09-05 19:11:51 +05:30
parent 6bbbe6ccc3
commit e68674d610
5 changed files with 297 additions and 10 deletions

View File

@@ -142,21 +142,47 @@ CONTEXT
TASK
- Perform root-cause analysis for the error and provide a concise, stepwise debugging plan tailored to the given context.
- Include concrete checks and likely fixes.
- If you can suggest a specific fix, provide an actionable change to the request.
- When suggesting fixes, explain clearly WHAT will be changed and WHY
- Use meaningful placeholder values (like 'your_username' instead of empty strings)
- Make explanations detailed but simple for users to understand
OUTPUT FORMAT (STRICT)
- Return ONLY a single JSON object. No markdown, no extra text.
- The JSON MUST contain both keys:
{
"explnation": string,
"explnation": string, // Detailed explanation of the issue and what the fix will do
"action": {
"action": "update_request" | "update_header" | "update_body" | "set_env" | "delete" | "other",
"target": "endpoint" | "header" | "body" | "env" | "collection" | "test",
"path": string, // dot path like 'headers.Authorization' or 'body.user.id'
"value": string // specific fix or instruction
}
"action": "update_field" | "add_header" | "update_header" | "delete_header" | "update_body" | "update_url" | "update_method" | "no_action",
"target": "httpRequestModel",
"field": "url" | "method" | "headers" | "body" | "params" | "auth",
"path": string, // specific path like "Authorization" for headers, or "user.id" for body fields
"value": string | object // the new value to set, use meaningful placeholders
} | null
}
ACTION GUIDELINES
- Use "update_field" for simple field updates (url, method)
- Use "add_header" to add a new header with meaningful values
- Use "update_header" to modify existing header value
- Use "delete_header" to remove a header
- Use "update_body" for body modifications with proper JSON structure
- For parameters, use object format: {"param_name": "meaningful_placeholder"}
- Set action to null if no specific fix can be suggested
- Always explain WHAT will be changed and provide meaningful placeholder values
PARAMETER EXAMPLES
- Username: "your_username" or "john_doe"
- Password: "your_password" or "secret123"
- Email: "user@example.com"
- API Key: "your_api_key_here"
- Token: "your_jwt_token"
EXPLANATION EXAMPLES
- "I'll add the missing 'username' and 'password' query parameters with placeholder values that you can replace with your actual credentials"
- "I'll update the Authorization header to include a Bearer token placeholder"
- "I'll modify the request URL to include the correct API endpoint path"
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?","action":null}

View File

@@ -40,6 +40,7 @@ class ChatMessage {
final MessageRole role;
final DateTime timestamp;
final ChatMessageType? messageType;
final ChatAction? action;
const ChatMessage({
required this.id,
@@ -47,6 +48,7 @@ class ChatMessage {
required this.role,
required this.timestamp,
this.messageType,
this.action,
});
ChatMessage copyWith({
@@ -55,6 +57,7 @@ class ChatMessage {
MessageRole? role,
DateTime? timestamp,
ChatMessageType? messageType,
ChatAction? action,
}) {
return ChatMessage(
id: id ?? this.id,
@@ -62,6 +65,7 @@ class ChatMessage {
role: role ?? this.role,
timestamp: timestamp ?? this.timestamp,
messageType: messageType ?? this.messageType,
action: action ?? this.action,
);
}
}
@@ -82,6 +86,43 @@ class ChatResponse {
enum ChatMessageType { explainResponse, debugError, generateTest, general }
// Action model for auto-fix functionality
class ChatAction {
final String action;
final String target;
final String field;
final String? path;
final dynamic value;
const ChatAction({
required this.action,
required this.target,
required this.field,
this.path,
this.value,
});
factory ChatAction.fromJson(Map<String, dynamic> json) {
return ChatAction(
action: json['action'] as String,
target: json['target'] as String,
field: json['field'] as String,
path: json['path'] as String?,
value: json['value'],
);
}
Map<String, dynamic> toJson() {
return {
'action': action,
'target': target,
'field': field,
'path': path,
'value': value,
};
}
}
// Failure classes using fpdart Either pattern
abstract class ChatFailure implements Exception {
final String message;

View File

@@ -65,9 +65,12 @@ class _ChatScreenState extends ConsumerState<ChatScreen> {
);
}
final message = msgs[index];
debugPrint(
'[ChatPage] Message action: ${message.action?.toJson()}');
return ChatBubble(
message: message.content,
role: message.role,
action: message.action,
);
},
);

View File

@@ -4,22 +4,33 @@ import '../../../../core/utils/dashbot_icons.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_markdown/flutter_markdown.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../viewmodel/chat_viewmodel.dart';
import '../../models/chat_models.dart';
class ChatBubble extends StatelessWidget {
class ChatBubble extends ConsumerWidget {
final String message;
final MessageRole role;
final String? promptOverride;
final ChatAction? action;
const ChatBubble({
super.key,
required this.message,
required this.role,
this.promptOverride,
this.action,
});
@override
Widget build(BuildContext context) {
Widget build(BuildContext context, WidgetRef ref) {
if (action != null) {
debugPrint('[ChatBubble] Action received: ${action!.toJson()}');
} else {
final preview =
message.length > 100 ? '${message.substring(0, 100)}...' : message;
debugPrint('[ChatBubble] No action received for message: $preview');
}
if (promptOverride != null &&
role == MessageRole.user &&
message == promptOverride) {
@@ -94,6 +105,27 @@ class ChatBubble extends StatelessWidget {
),
),
if (role == MessageRole.system) ...[
if (action != null) ...[
const SizedBox(height: 4),
ElevatedButton.icon(
onPressed: () async {
final chatViewmodel =
ref.read(chatViewmodelProvider.notifier);
await chatViewmodel.applyAutoFix(action!);
debugPrint('Auto-fix applied successfully!');
},
icon: const Icon(Icons.auto_fix_high, size: 16),
label: const Text('Auto Fix'),
style: ElevatedButton.styleFrom(
backgroundColor: Theme.of(context).colorScheme.primary,
foregroundColor: Theme.of(context).colorScheme.onPrimary,
padding:
const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
textStyle: Theme.of(context).textTheme.labelSmall,
),
),
],
const SizedBox(height: 4),
IconButton(
onPressed: () {
Clipboard.setData(ClipboardData(text: message));

View File

@@ -6,6 +6,7 @@ import 'package:apidash/providers/providers.dart';
import 'package:apidash/models/models.dart';
import 'package:nanoid/nanoid.dart';
import '../../../core/utils/safe_parse_json_message.dart';
import '../../../core/constants/dashbot_prompts.dart' as dash;
import '../models/chat_models.dart';
import '../repository/chat_remote_repository.dart';
@@ -31,7 +32,10 @@ class ChatViewmodel extends StateNotifier<ChatState> {
List<ChatMessage> get currentMessages {
final id = _currentRequest?.id ?? 'global';
return state.chatSessions[id] ?? const [];
debugPrint('[Chat] Getting messages for request ID: $id');
final messages = state.chatSessions[id] ?? const [];
debugPrint('[Chat] Found ${messages.length} messages');
return messages;
}
Future<void> sendMessage({
@@ -102,6 +106,27 @@ class ChatViewmodel extends StateNotifier<ChatState> {
debugPrint(
'[Chat] stream done. total=${state.currentStreamingResponse.length}, anyChunk=$receivedAnyChunk');
if (state.currentStreamingResponse.isNotEmpty) {
ChatAction? parsedAction;
try {
debugPrint(
'[Chat] Attempting to parse response: ${state.currentStreamingResponse}');
final Map<String, dynamic> parsed =
MessageJson.safeParse(state.currentStreamingResponse);
debugPrint('[Chat] Parsed JSON: $parsed');
if (parsed.containsKey('action') && parsed['action'] != null) {
debugPrint('[Chat] Action object found: ${parsed['action']}');
parsedAction =
ChatAction.fromJson(parsed['action'] as Map<String, dynamic>);
debugPrint('[Chat] Parsed action: ${parsedAction.toJson()}');
} else {
debugPrint('[Chat] No action found in response');
}
} catch (e) {
debugPrint('[Chat] Error parsing action: $e');
debugPrint('[Chat] Error details: ${e.toString()}');
// If parsing fails, continue without action
}
_addMessage(
requestId,
ChatMessage(
@@ -110,6 +135,7 @@ class ChatViewmodel extends StateNotifier<ChatState> {
role: MessageRole.system,
timestamp: DateTime.now(),
messageType: type,
action: parsedAction,
),
);
} else if (!receivedAnyChunk) {
@@ -120,6 +146,20 @@ class ChatViewmodel extends StateNotifier<ChatState> {
final fallback =
await _repo.sendChat(request: enriched.copyWith(stream: false));
if (fallback != null && fallback.isNotEmpty) {
ChatAction? fallbackAction;
try {
final Map<String, dynamic> parsed =
MessageJson.safeParse(fallback);
if (parsed.containsKey('action') && parsed['action'] != null) {
fallbackAction = ChatAction.fromJson(
parsed['action'] as Map<String, dynamic>);
debugPrint(
'[Chat] Fallback parsed action: ${fallbackAction.toJson()}');
}
} catch (e) {
debugPrint('[Chat] Fallback error parsing action: $e');
}
_addMessage(
requestId,
ChatMessage(
@@ -128,6 +168,7 @@ class ChatViewmodel extends StateNotifier<ChatState> {
role: MessageRole.system,
timestamp: DateTime.now(),
messageType: type,
action: fallbackAction,
),
);
} else {
@@ -152,8 +193,150 @@ class ChatViewmodel extends StateNotifier<ChatState> {
state = state.copyWith(isGenerating: false);
}
Future<void> applyAutoFix(ChatAction action) async {
final requestId = _currentRequest?.id;
if (requestId == null) return;
try {
switch (action.action) {
case 'update_field':
await _applyFieldUpdate(action);
break;
case 'add_header':
await _applyHeaderUpdate(action, isAdd: true);
break;
case 'update_header':
await _applyHeaderUpdate(action, isAdd: false);
break;
case 'delete_header':
await _applyHeaderDelete(action);
break;
case 'update_body':
await _applyBodyUpdate(action);
break;
case 'update_url':
await _applyUrlUpdate(action);
break;
case 'update_method':
await _applyMethodUpdate(action);
break;
default:
debugPrint('[Chat] Unsupported action: ${action.action}');
}
} catch (e) {
debugPrint('[Chat] Error applying auto-fix: $e');
_appendSystem('Failed to apply auto-fix: $e', ChatMessageType.general);
}
}
Future<void> _applyFieldUpdate(ChatAction action) async {
final requestId = _currentRequest?.id;
if (requestId == null) return;
final collectionNotifier =
_ref.read(collectionStateNotifierProvider.notifier);
switch (action.field) {
case 'url':
collectionNotifier.update(url: action.value as String, id: requestId);
break;
case 'method':
final method = HTTPVerb.values.firstWhere(
(m) => m.name.toLowerCase() == (action.value as String).toLowerCase(),
orElse: () => HTTPVerb.get,
);
collectionNotifier.update(method: method, id: requestId);
break;
case 'params':
if (action.value is Map<String, dynamic>) {
final params = (action.value as Map<String, dynamic>)
.entries
.map(
(e) => NameValueModel(name: e.key, value: e.value.toString()))
.toList();
collectionNotifier.update(params: params, id: requestId);
}
break;
}
}
Future<void> _applyHeaderUpdate(ChatAction action,
{required bool isAdd}) async {
final requestId = _currentRequest?.id;
if (requestId == null || action.path == null) return;
final collectionNotifier =
_ref.read(collectionStateNotifierProvider.notifier);
final currentRequest = _currentRequest;
if (currentRequest?.httpRequestModel == null) return;
final headers = List<NameValueModel>.from(
currentRequest!.httpRequestModel!.headers ?? []);
if (isAdd) {
headers.add(
NameValueModel(name: action.path!, value: action.value as String));
} else {
final index = headers.indexWhere((h) => h.name == action.path);
if (index != -1) {
headers[index] = headers[index].copyWith(value: action.value as String);
}
}
collectionNotifier.update(headers: headers, id: requestId);
}
Future<void> _applyHeaderDelete(ChatAction action) async {
final requestId = _currentRequest?.id;
if (requestId == null || action.path == null) return;
final collectionNotifier =
_ref.read(collectionStateNotifierProvider.notifier);
final currentRequest = _currentRequest;
if (currentRequest?.httpRequestModel == null) return;
final headers = List<NameValueModel>.from(
currentRequest!.httpRequestModel!.headers ?? []);
headers.removeWhere((h) => h.name == action.path);
collectionNotifier.update(headers: headers, id: requestId);
}
Future<void> _applyBodyUpdate(ChatAction action) async {
final requestId = _currentRequest?.id;
if (requestId == null) return;
final collectionNotifier =
_ref.read(collectionStateNotifierProvider.notifier);
collectionNotifier.update(body: action.value as String, id: requestId);
}
Future<void> _applyUrlUpdate(ChatAction action) async {
final requestId = _currentRequest?.id;
if (requestId == null) return;
final collectionNotifier =
_ref.read(collectionStateNotifierProvider.notifier);
collectionNotifier.update(url: action.value as String, id: requestId);
}
Future<void> _applyMethodUpdate(ChatAction action) async {
final requestId = _currentRequest?.id;
if (requestId == null) return;
final collectionNotifier =
_ref.read(collectionStateNotifierProvider.notifier);
final method = HTTPVerb.values.firstWhere(
(m) => m.name.toLowerCase() == (action.value as String).toLowerCase(),
orElse: () => HTTPVerb.get,
);
collectionNotifier.update(method: method, id: requestId);
}
// Helpers
void _addMessage(String requestId, ChatMessage m) {
debugPrint(
'[Chat] Adding message to request ID: $requestId, action: ${m.action?.toJson()}');
final msgs = state.chatSessions[requestId] ?? const [];
state = state.copyWith(
chatSessions: {
@@ -161,6 +344,8 @@ class ChatViewmodel extends StateNotifier<ChatState> {
requestId: [...msgs, m],
},
);
debugPrint(
'[Chat] Message added, total messages for $requestId: ${(state.chatSessions[requestId]?.length ?? 0)}');
}
void _appendSystem(String text, ChatMessageType type) {