refactor: Move AutoFixService, RequestApplyService, PromptBuilder, and UrlEnvService to core services

- Deleted old implementations of AutoFixService, RequestApplyService, PromptBuilder, and UrlEnvService from the chat feature.
- Introduced new implementations of AutoFixService, RequestApplyService, PromptBuilder, and UrlEnvService in the core services directory.
- Updated import paths to reflect the new structure.
- Ensured all functionalities remain intact after the refactor.
This commit is contained in:
Udhay-Adithya
2025-09-21 11:51:15 +05:30
parent f8d54da074
commit d5915bb321
5 changed files with 7 additions and 7 deletions

View File

@@ -1,9 +1,9 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../services/agent/prompt_builder.dart';
import '../services/base/url_env_service.dart';
import '../services/actions/auto_fix_service.dart';
import '../services/actions/request_apply_service.dart';
import '../../../core/services/agent/prompt_builder.dart';
import '../../../core/services/base/url_env_service.dart';
import '../../../core/services/actions/auto_fix_service.dart';
import '../../../core/services/actions/request_apply_service.dart';
import '../../../../providers/providers.dart';
import 'package:apidash_core/apidash_core.dart';

View File

@@ -1,183 +0,0 @@
import 'package:apidash_core/apidash_core.dart';
import 'package:apidash/models/models.dart';
import '../../models/chat_models.dart';
import 'request_apply_service.dart';
class AutoFixService {
AutoFixService({
required this.requestApply,
required this.updateSelected,
required this.addNewRequest,
required this.readCurrentRequestId,
required this.ensureBaseUrl,
required this.readCurrentRequest,
});
final RequestApplyService requestApply;
final UpdateSelectedFn updateSelected;
final AddNewRequestFn addNewRequest;
final String? Function() readCurrentRequestId;
final Future<String> Function(String baseUrl) ensureBaseUrl;
final RequestModel? Function() readCurrentRequest;
Future<String?> apply(ChatAction action) async {
final requestId = readCurrentRequestId();
switch (action.actionType) {
case ChatActionType.updateField:
await _applyFieldUpdate(action, requestId);
return null;
case ChatActionType.addHeader:
await _applyHeaderUpdate(action, isAdd: true, requestId: requestId);
return null;
case ChatActionType.updateHeader:
await _applyHeaderUpdate(action, isAdd: false, requestId: requestId);
return null;
case ChatActionType.deleteHeader:
await _applyHeaderDelete(action, requestId);
return null;
case ChatActionType.updateBody:
await _applyBodyUpdate(action, requestId);
return null;
case ChatActionType.updateUrl:
await _applyUrlUpdate(action, requestId);
return null;
case ChatActionType.updateMethod:
await _applyMethodUpdate(action, requestId);
return null;
case ChatActionType.applyCurl:
{
final payload = (action.value is Map<String, dynamic>)
? (action.value as Map<String, dynamic>)
: <String, dynamic>{};
final res = await requestApply.applyCurl(
payload: payload,
target: action.field,
requestId: requestId,
updateSelected: updateSelected,
addNewRequest: addNewRequest,
ensureBaseUrl: ensureBaseUrl,
);
return res?.systemMessage;
}
case ChatActionType.applyOpenApi:
{
final payload = (action.value is Map<String, dynamic>)
? (action.value as Map<String, dynamic>)
: <String, dynamic>{};
final res = await requestApply.applyOpenApi(
payload: payload,
field: action.field,
path: action.path,
requestId: requestId,
updateSelected: updateSelected,
addNewRequest: addNewRequest,
ensureBaseUrl: ensureBaseUrl,
);
return res?.systemMessage;
}
case ChatActionType.other:
// defer to specific target logic if needed
return null;
case ChatActionType.showLanguages:
case ChatActionType.noAction:
case ChatActionType.uploadAsset:
return null;
}
}
Future<void> _applyFieldUpdate(ChatAction action, String? requestId) async {
if (requestId == null) return;
switch (action.field) {
case 'url':
updateSelected(id: requestId, url: action.value as String);
break;
case 'method':
final method = HTTPVerb.values.firstWhere(
(m) => m.name.toLowerCase() == (action.value as String).toLowerCase(),
orElse: () => HTTPVerb.get,
);
updateSelected(id: requestId, method: method);
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();
final enabled = List<bool>.filled(params.length, true);
updateSelected(
id: requestId,
params: params,
isParamEnabledList: enabled,
);
}
break;
}
}
Future<void> _applyHeaderUpdate(ChatAction action,
{required bool isAdd, String? requestId}) async {
if (requestId == null || action.path == null) return;
final current = readCurrentRequest();
final http = current?.httpRequestModel;
if (http == null) return;
final headers = List<NameValueModel>.from(http.headers ?? const []);
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);
} else {
headers.add(
NameValueModel(name: action.path!, value: action.value as String));
}
}
updateSelected(
id: requestId,
headers: headers,
isHeaderEnabledList: List<bool>.filled(headers.length, true),
);
}
Future<void> _applyHeaderDelete(ChatAction action, String? requestId) async {
if (requestId == null || action.path == null) return;
final current = readCurrentRequest();
final http = current?.httpRequestModel;
if (http == null) return;
final headers = List<NameValueModel>.from(http.headers ?? const []);
headers.removeWhere((h) => h.name == action.path);
updateSelected(
id: requestId,
headers: headers,
isHeaderEnabledList: List<bool>.filled(headers.length, true),
);
}
Future<void> _applyBodyUpdate(ChatAction action, String? requestId) async {
if (requestId == null) return;
updateSelected(id: requestId, body: action.value as String);
}
Future<void> _applyUrlUpdate(ChatAction action, String? requestId) async {
if (requestId == null) return;
updateSelected(id: requestId, url: action.value as String);
}
Future<void> _applyMethodUpdate(ChatAction action, String? requestId) async {
if (requestId == null) return;
final method = HTTPVerb.values.firstWhere(
(m) => m.name.toLowerCase() == (action.value as String).toLowerCase(),
orElse: () => HTTPVerb.get,
);
updateSelected(id: requestId, method: method);
}
}

View File

@@ -1,251 +0,0 @@
import 'dart:convert';
import 'package:apidash_core/apidash_core.dart';
import '../../models/chat_models.dart';
import '../base/url_env_service.dart';
class ApplyResult {
final String? systemMessage;
final ChatMessageType? messageType;
const ApplyResult({this.systemMessage, this.messageType});
}
typedef UpdateSelectedFn = void Function({
required String id,
HTTPVerb? method,
String? url,
List<NameValueModel>? headers,
List<bool>? isHeaderEnabledList,
String? body,
ContentType? bodyContentType,
List<FormDataModel>? formData,
List<NameValueModel>? params,
List<bool>? isParamEnabledList,
String? postRequestScript,
});
typedef AddNewRequestFn = void Function(HttpRequestModel model, {String? name});
class RequestApplyService {
RequestApplyService({required this.urlEnv});
final UrlEnvService urlEnv;
Future<ApplyResult?> applyCurl({
required Map<String, dynamic> payload,
required String? target, // 'apply_to_selected' | 'apply_to_new'
required String? requestId,
required UpdateSelectedFn updateSelected,
required AddNewRequestFn addNewRequest,
required Future<String> Function(String baseUrl) ensureBaseUrl,
}) async {
String methodStr = (payload['method'] as String?)?.toLowerCase() ?? 'get';
final method = HTTPVerb.values.firstWhere(
(m) => m.name == methodStr,
orElse: () => HTTPVerb.get,
);
final url = payload['url'] as String? ?? '';
final baseUrl = urlEnv.inferBaseUrl(url);
final headersMap =
(payload['headers'] as Map?)?.cast<String, dynamic>() ?? {};
final headers = headersMap.entries
.map((e) => NameValueModel(name: e.key, value: e.value.toString()))
.toList();
final body = payload['body'] as String?;
final formFlag = payload['form'] == true;
final formDataListRaw = (payload['formData'] as List?)?.cast<dynamic>();
final formData = formDataListRaw == null
? <FormDataModel>[]
: formDataListRaw
.whereType<Map>()
.map((e) => FormDataModel(
name: (e['name'] as String?) ?? '',
value: (e['value'] as String?) ?? '',
type: (() {
final t = (e['type'] as String?) ?? 'text';
try {
return FormDataType.values
.firstWhere((ft) => ft.name == t);
} catch (_) {
return FormDataType.text;
}
})(),
))
.toList();
ContentType bodyContentType;
if (formFlag || formData.isNotEmpty) {
bodyContentType = ContentType.formdata;
} else if ((body ?? '').trim().isEmpty) {
bodyContentType = ContentType.text;
} else {
try {
jsonDecode(body!);
bodyContentType = ContentType.json;
} catch (_) {
bodyContentType = ContentType.text;
}
}
final withEnvUrl = await urlEnv.maybeSubstituteBaseUrl(
url,
baseUrl,
ensure: ensureBaseUrl,
);
if (target == 'apply_to_selected') {
if (requestId == null) return null;
updateSelected(
id: requestId,
method: method,
url: withEnvUrl,
headers: headers,
isHeaderEnabledList: List<bool>.filled(headers.length, true),
body: body,
bodyContentType: bodyContentType,
formData: formData.isEmpty ? null : formData,
);
return const ApplyResult(
systemMessage: 'Applied cURL to the selected request.',
messageType: ChatMessageType.importCurl,
);
} else if (target == 'apply_to_new') {
final model = HttpRequestModel(
method: method,
url: withEnvUrl,
headers: headers,
isHeaderEnabledList: List<bool>.filled(headers.length, true),
body: body,
bodyContentType: bodyContentType,
formData: formData.isEmpty ? null : formData,
);
addNewRequest(model, name: 'Imported cURL');
return const ApplyResult(
systemMessage: 'Created a new request from the cURL.',
messageType: ChatMessageType.importCurl,
);
}
return null;
}
Future<ApplyResult?> applyOpenApi({
required Map<String, dynamic> payload,
required String?
field, // 'apply_to_selected'|'apply_to_new'|'select_operation'
required String? path,
required String? requestId,
required UpdateSelectedFn updateSelected,
required AddNewRequestFn addNewRequest,
required Future<String> Function(String baseUrl) ensureBaseUrl,
}) async {
String methodStr = (payload['method'] as String?)?.toLowerCase() ?? 'get';
final method = HTTPVerb.values.firstWhere(
(m) => m.name == methodStr,
orElse: () => HTTPVerb.get,
);
final url = payload['url'] as String? ?? '';
final baseUrl = payload['baseUrl'] as String? ?? urlEnv.inferBaseUrl(url);
String routePath;
if (baseUrl.isNotEmpty && url.startsWith(baseUrl)) {
routePath = url.substring(baseUrl.length);
} else {
try {
final u = Uri.parse(url);
routePath = u.path.isEmpty ? '/' : u.path;
} catch (_) {
routePath = url;
}
}
if (!routePath.startsWith('/')) routePath = '/$routePath';
final headersMap =
(payload['headers'] as Map?)?.cast<String, dynamic>() ?? {};
final headers = headersMap.entries
.map((e) => NameValueModel(name: e.key, value: e.value.toString()))
.toList();
final body = payload['body'] as String?;
final formFlag = payload['form'] == true;
final formDataListRaw = (payload['formData'] as List?)?.cast<dynamic>();
final formData = formDataListRaw == null
? <FormDataModel>[]
: formDataListRaw
.whereType<Map>()
.map((e) => FormDataModel(
name: (e['name'] as String?) ?? '',
value: (e['value'] as String?) ?? '',
type: (() {
final t = (e['type'] as String?) ?? 'text';
try {
return FormDataType.values
.firstWhere((ft) => ft.name == t);
} catch (_) {
return FormDataType.text;
}
})(),
))
.toList();
ContentType bodyContentType;
if (formFlag || formData.isNotEmpty) {
bodyContentType = ContentType.formdata;
} else if ((body ?? '').trim().isEmpty) {
bodyContentType = ContentType.text;
} else {
try {
jsonDecode(body!);
bodyContentType = ContentType.json;
} catch (_) {
bodyContentType = ContentType.text;
}
}
final withEnvUrl = await urlEnv.maybeSubstituteBaseUrl(
url,
baseUrl,
ensure: ensureBaseUrl,
);
if (field == 'apply_to_selected') {
if (requestId == null) return null;
updateSelected(
id: requestId,
method: method,
url: withEnvUrl,
headers: headers,
isHeaderEnabledList: List<bool>.filled(headers.length, true),
body: body,
bodyContentType: bodyContentType,
formData: formData.isEmpty ? null : formData,
);
return const ApplyResult(
systemMessage: 'Applied OpenAPI operation to the selected request.',
messageType: ChatMessageType.importOpenApi,
);
} else if (field == 'apply_to_new') {
final model = HttpRequestModel(
method: method,
url: withEnvUrl,
headers: headers,
isHeaderEnabledList: List<bool>.filled(headers.length, true),
body: body,
bodyContentType: bodyContentType,
formData: formData.isEmpty ? null : formData,
);
final displayName = '${method.name.toUpperCase()} $routePath';
addNewRequest(model, name: displayName);
return const ApplyResult(
systemMessage: 'Created a new request from the OpenAPI operation.',
messageType: ChatMessageType.importOpenApi,
);
} else if (field == 'select_operation') {
// UI presents options elsewhere; no system message here.
return const ApplyResult();
}
return null;
}
}

View File

@@ -1,150 +0,0 @@
import 'package:apidash/models/models.dart';
import 'package:apidash/dashbot/core/constants/dashbot_prompts.dart' as dash;
import '../../models/chat_models.dart';
class PromptBuilder {
String buildSystemPrompt(
RequestModel? req,
ChatMessageType type, {
String? overrideLanguage,
List<ChatMessage> history = const [],
}) {
final historyBlock = buildHistoryBlock(history);
final contextBlock = buildContextBlock(req);
final task = buildTaskPrompt(
req,
type,
overrideLanguage: overrideLanguage,
);
return [
if (task != null) task,
if (contextBlock != null) contextBlock,
if (historyBlock.isNotEmpty) historyBlock,
].join('\n\n');
}
String buildHistoryBlock(List<ChatMessage> messages, {int maxTurns = 8}) {
if (messages.isEmpty) return '';
final start = messages.length > maxTurns ? messages.length - maxTurns : 0;
final recent = messages.sublist(start);
final buf = StringBuffer('''<conversation_context>
\tOnly use the following short chat history to maintain continuity. Do not repeat it back.
\t''');
for (final m in recent) {
final role = m.role == MessageRole.user ? 'user' : 'assistant';
buf.writeln('- $role: ${m.content}');
}
buf.writeln('</conversation_context>');
return buf.toString();
}
String? buildContextBlock(RequestModel? req) {
final http = req?.httpRequestModel;
if (req == null || http == null) return null;
final headers = http.headersMap.entries
.map((e) => '"${e.key}": "${e.value}"')
.join(', ');
return '''<request_context>
Request Name: ${req.name}
\tURL: ${http.url}
\tMethod: ${http.method.name.toUpperCase()}
Status: ${req.responseStatus ?? ''}
\tContent-Type: ${http.bodyContentType.name}
\tHeaders: { $headers }
\tBody: ${http.body ?? ''}
Response: ${req.httpResponseModel?.body ?? ''}
\t</request_context>''';
}
String? buildTaskPrompt(
RequestModel? req,
ChatMessageType type, {
String? overrideLanguage,
}) {
if (req == null) return null;
final http = req.httpRequestModel;
final resp = req.httpResponseModel;
final prompts = dash.DashbotPrompts();
switch (type) {
case ChatMessageType.explainResponse:
return prompts.explainApiResponsePrompt(
url: http?.url,
method: http?.method.name.toUpperCase(),
responseStatus: req.responseStatus,
bodyContentType: http?.bodyContentType.name,
message: resp?.body,
headersMap: http?.headersMap,
body: http?.body,
);
case ChatMessageType.debugError:
return prompts.debugApiErrorPrompt(
url: http?.url,
method: http?.method.name.toUpperCase(),
responseStatus: req.responseStatus,
bodyContentType: http?.bodyContentType.name,
message: resp?.body,
headersMap: http?.headersMap,
body: http?.body,
);
case ChatMessageType.generateTest:
return prompts.generateTestCasesPrompt(
url: http?.url,
method: http?.method.name.toUpperCase(),
headersMap: http?.headersMap,
body: http?.body,
);
case ChatMessageType.generateDoc:
return prompts.generateDocumentationPrompt(
url: http?.url,
method: http?.method.name.toUpperCase(),
responseStatus: req.responseStatus,
bodyContentType: http?.bodyContentType.name,
message: resp?.body,
headersMap: http?.headersMap,
body: http?.body,
);
case ChatMessageType.generateCode:
if (overrideLanguage == null || overrideLanguage.isEmpty) {
return prompts.codeGenerationIntroPrompt(
url: http?.url,
method: http?.method.name.toUpperCase(),
headersMap: http?.headersMap,
body: http?.body,
bodyContentType: http?.bodyContentType.name,
paramsMap: http?.paramsMap,
authType: http?.authModel?.type.name,
);
} else {
return prompts.generateCodePrompt(
url: http?.url,
method: http?.method.name.toUpperCase(),
headersMap: http?.headersMap,
body: http?.body,
bodyContentType: http?.bodyContentType.name,
paramsMap: http?.paramsMap,
authType: http?.authModel?.type.name,
language: overrideLanguage,
);
}
case ChatMessageType.importCurl:
return null;
case ChatMessageType.importOpenApi:
return null;
case ChatMessageType.general:
return prompts.generalInteractionPrompt();
}
}
String? detectLanguage(String text) {
final t = text.toLowerCase();
if (t.contains('python')) return 'Python (requests)';
if (t.contains('dart')) return 'Dart (http)';
if (t.contains('golang') || t.contains('go ')) return 'Go (net/http)';
if (t.contains('javascript') || t.contains('js') || t.contains('fetch')) {
return 'JavaScript (fetch)';
}
if (t.contains('curl')) return 'cURL';
return null;
}
}

View File

@@ -1,68 +0,0 @@
import 'package:apidash/consts.dart';
import 'package:apidash_core/apidash_core.dart';
class UrlEnvService {
String inferBaseUrl(String url) {
try {
final u = Uri.parse(url);
if (u.hasScheme && u.host.isNotEmpty) {
final portPart = (u.hasPort && u.port != 0) ? ':${u.port}' : '';
return '${u.scheme}://${u.host}$portPart';
}
} catch (_) {}
final m = RegExp(r'^(https?:\/\/[^\/]+)').firstMatch(url);
return m?.group(1) ?? '';
}
Future<String> ensureBaseUrlEnv(
String baseUrl, {
required Map<String, EnvironmentModel>? Function() readEnvs,
required String? Function() readActiveEnvId,
required void Function(String id, {List<EnvironmentVariableModel>? values})
updateEnv,
}) async {
if (baseUrl.isEmpty) return 'BASE_URL';
String host = 'API';
try {
final u = Uri.parse(baseUrl);
if (u.hasAuthority && u.host.isNotEmpty) host = u.host;
} catch (_) {}
final slug = host
.replaceAll(RegExp(r'[^A-Za-z0-9]+'), '_')
.replaceAll(RegExp(r'_+'), '_')
.replaceAll(RegExp(r'^_|_$'), '')
.toUpperCase();
final key = 'BASE_URL_$slug';
final envs = readEnvs();
String? activeId = readActiveEnvId();
activeId ??= kGlobalEnvironmentId;
final envModel = envs?[activeId];
if (envModel != null) {
final exists = envModel.values.any((v) => v.key == key);
if (!exists) {
final values = [...envModel.values];
values.add(EnvironmentVariableModel(
key: key,
value: baseUrl,
enabled: true,
));
updateEnv(activeId, values: values);
}
}
return key;
}
Future<String> maybeSubstituteBaseUrl(
String url,
String baseUrl, {
required Future<String> Function(String baseUrl) ensure,
}) async {
if (baseUrl.isEmpty || !url.startsWith(baseUrl)) return url;
final key = await ensure(baseUrl);
final path = url.substring(baseUrl.length);
final normalized = path.startsWith('/') ? path : '/$path';
return '{{$key}}$normalized';
}
}