mirror of
https://github.com/foss42/apidash.git
synced 2025-12-06 12:57:08 +08:00
refactor: split chat viewmodel to specific services
This commit is contained in:
67
lib/dashbot/features/chat/providers/service_providers.dart
Normal file
67
lib/dashbot/features/chat/providers/service_providers.dart
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
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 '../../../../providers/providers.dart';
|
||||||
|
import 'package:apidash_core/apidash_core.dart';
|
||||||
|
|
||||||
|
final promptBuilderProvider = Provider<PromptBuilder>((ref) {
|
||||||
|
return PromptBuilder();
|
||||||
|
});
|
||||||
|
|
||||||
|
final urlEnvServiceProvider = Provider<UrlEnvService>((ref) {
|
||||||
|
return UrlEnvService();
|
||||||
|
});
|
||||||
|
|
||||||
|
final requestApplyServiceProvider = Provider<RequestApplyService>((ref) {
|
||||||
|
return RequestApplyService(urlEnv: ref.read(urlEnvServiceProvider));
|
||||||
|
});
|
||||||
|
|
||||||
|
final autoFixServiceProvider = Provider<AutoFixService>((ref) {
|
||||||
|
final collection = ref.read(collectionStateNotifierProvider.notifier);
|
||||||
|
final urlEnv = ref.read(urlEnvServiceProvider);
|
||||||
|
return AutoFixService(
|
||||||
|
requestApply: ref.read(requestApplyServiceProvider),
|
||||||
|
updateSelected: ({
|
||||||
|
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,
|
||||||
|
}) {
|
||||||
|
collection.update(
|
||||||
|
id: id,
|
||||||
|
method: method,
|
||||||
|
url: url,
|
||||||
|
headers: headers,
|
||||||
|
isHeaderEnabledList: isHeaderEnabledList,
|
||||||
|
body: body,
|
||||||
|
bodyContentType: bodyContentType,
|
||||||
|
formData: formData,
|
||||||
|
params: params,
|
||||||
|
isParamEnabledList: isParamEnabledList,
|
||||||
|
postRequestScript: postRequestScript,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
addNewRequest: (model, {name}) =>
|
||||||
|
collection.addRequestModel(model, name: name ?? 'New Request'),
|
||||||
|
readCurrentRequestId: () => ref.read(selectedRequestModelProvider)?.id,
|
||||||
|
ensureBaseUrl: (baseUrl) => urlEnv.ensureBaseUrlEnv(
|
||||||
|
baseUrl,
|
||||||
|
readEnvs: () => ref.read(environmentsStateNotifierProvider),
|
||||||
|
readActiveEnvId: () => ref.read(activeEnvironmentIdStateProvider),
|
||||||
|
updateEnv: (id, {values}) => ref
|
||||||
|
.read(environmentsStateNotifierProvider.notifier)
|
||||||
|
.updateEnvironment(id, values: values),
|
||||||
|
),
|
||||||
|
readCurrentRequest: () => ref.read(selectedRequestModelProvider),
|
||||||
|
);
|
||||||
|
});
|
||||||
183
lib/dashbot/features/chat/services/actions/auto_fix_service.dart
Normal file
183
lib/dashbot/features/chat/services/actions/auto_fix_service.dart
Normal file
@@ -0,0 +1,183 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,251 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
150
lib/dashbot/features/chat/services/agent/prompt_builder.dart
Normal file
150
lib/dashbot/features/chat/services/agent/prompt_builder.dart
Normal file
@@ -0,0 +1,150 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
68
lib/dashbot/features/chat/services/base/url_env_service.dart
Normal file
68
lib/dashbot/features/chat/services/base/url_env_service.dart
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
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';
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,10 +1,8 @@
|
|||||||
import 'dart:async';
|
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'package:apidash_core/apidash_core.dart';
|
import 'package:apidash_core/apidash_core.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:apidash/providers/providers.dart';
|
import 'package:apidash/providers/providers.dart';
|
||||||
import 'package:apidash/consts.dart';
|
|
||||||
import 'package:apidash/models/models.dart';
|
import 'package:apidash/models/models.dart';
|
||||||
import 'package:nanoid/nanoid.dart';
|
import 'package:nanoid/nanoid.dart';
|
||||||
import '../../../core/services/curl_import_service.dart';
|
import '../../../core/services/curl_import_service.dart';
|
||||||
@@ -15,15 +13,14 @@ import '../../../core/constants/dashbot_prompts.dart' as dash;
|
|||||||
import '../models/chat_models.dart';
|
import '../models/chat_models.dart';
|
||||||
import '../repository/chat_remote_repository.dart';
|
import '../repository/chat_remote_repository.dart';
|
||||||
import '../providers/attachments_provider.dart';
|
import '../providers/attachments_provider.dart';
|
||||||
|
import '../providers/service_providers.dart';
|
||||||
|
|
||||||
class ChatViewmodel extends StateNotifier<ChatState> {
|
class ChatViewmodel extends StateNotifier<ChatState> {
|
||||||
ChatViewmodel(this._ref) : super(const ChatState());
|
ChatViewmodel(this._ref) : super(const ChatState());
|
||||||
|
|
||||||
final Ref _ref;
|
final Ref _ref;
|
||||||
StreamSubscription<String>? _sub;
|
|
||||||
|
|
||||||
ChatRemoteRepository get _repo => _ref.read(chatRepositoryProvider);
|
ChatRemoteRepository get _repo => _ref.read(chatRepositoryProvider);
|
||||||
// Currently selected request and AI model are read from app providers
|
|
||||||
RequestModel? get _currentRequest => _ref.read(selectedRequestModelProvider);
|
RequestModel? get _currentRequest => _ref.read(selectedRequestModelProvider);
|
||||||
AIRequestModel? get _selectedAIModel {
|
AIRequestModel? get _selectedAIModel {
|
||||||
final json = _ref.read(settingsProvider).defaultAIModel;
|
final json = _ref.read(settingsProvider).defaultAIModel;
|
||||||
@@ -80,7 +77,6 @@ class ChatViewmodel extends StateNotifier<ChatState> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// If user pasted a cURL in import flow, handle locally without AI
|
|
||||||
final lastSystemImport = existingMessages.lastWhere(
|
final lastSystemImport = existingMessages.lastWhere(
|
||||||
(m) =>
|
(m) =>
|
||||||
m.role == MessageRole.system &&
|
m.role == MessageRole.system &&
|
||||||
@@ -99,13 +95,15 @@ class ChatViewmodel extends StateNotifier<ChatState> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final promptBuilder = _ref.read(promptBuilderProvider);
|
||||||
String systemPrompt;
|
String systemPrompt;
|
||||||
if (type == ChatMessageType.generateCode) {
|
if (type == ChatMessageType.generateCode) {
|
||||||
final detectedLang = _detectLanguageFromText(text);
|
final detectedLang = promptBuilder.detectLanguage(text);
|
||||||
systemPrompt = _composeSystemPrompt(
|
systemPrompt = promptBuilder.buildSystemPrompt(
|
||||||
_currentRequest,
|
_currentRequest,
|
||||||
type,
|
type,
|
||||||
overrideLanguage: detectedLang,
|
overrideLanguage: detectedLang,
|
||||||
|
history: currentMessages,
|
||||||
);
|
);
|
||||||
} else if (type == ChatMessageType.importCurl) {
|
} else if (type == ChatMessageType.importCurl) {
|
||||||
final rqId = _currentRequest?.id ?? 'global';
|
final rqId = _currentRequest?.id ?? 'global';
|
||||||
@@ -156,7 +154,11 @@ class ChatViewmodel extends StateNotifier<ChatState> {
|
|||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
} else {
|
} else {
|
||||||
systemPrompt = _composeSystemPrompt(_currentRequest, type);
|
systemPrompt = promptBuilder.buildSystemPrompt(
|
||||||
|
_currentRequest,
|
||||||
|
type,
|
||||||
|
history: currentMessages,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
final userPrompt = (text.trim().isEmpty && !countAsUser)
|
final userPrompt = (text.trim().isEmpty && !countAsUser)
|
||||||
? 'Please complete the task based on the provided context.'
|
? 'Please complete the task based on the provided context.'
|
||||||
@@ -164,129 +166,61 @@ class ChatViewmodel extends StateNotifier<ChatState> {
|
|||||||
final enriched = ai!.copyWith(
|
final enriched = ai!.copyWith(
|
||||||
systemPrompt: systemPrompt,
|
systemPrompt: systemPrompt,
|
||||||
userPrompt: userPrompt,
|
userPrompt: userPrompt,
|
||||||
stream: true,
|
stream: false,
|
||||||
);
|
);
|
||||||
debugPrint(
|
debugPrint(
|
||||||
'[Chat] prompts prepared: system=${systemPrompt.length} chars, user=${userPrompt.length} chars');
|
'[Chat] prompts prepared: system=${systemPrompt.length} chars, user=${userPrompt.length} chars');
|
||||||
|
|
||||||
// start stream
|
|
||||||
_sub?.cancel();
|
|
||||||
state = state.copyWith(isGenerating: true, currentStreamingResponse: '');
|
state = state.copyWith(isGenerating: true, currentStreamingResponse: '');
|
||||||
bool receivedAnyChunk = false;
|
|
||||||
_sub = _repo.streamChat(request: enriched).listen(
|
|
||||||
(chunk) {
|
|
||||||
receivedAnyChunk = true;
|
|
||||||
if (chunk.isEmpty) return;
|
|
||||||
debugPrint('[Chat] chunk(${chunk.length})');
|
|
||||||
state = state.copyWith(
|
|
||||||
currentStreamingResponse: state.currentStreamingResponse + (chunk),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
onError: (e) {
|
|
||||||
debugPrint('[Chat] stream error: $e');
|
|
||||||
state = state.copyWith(isGenerating: false);
|
|
||||||
_appendSystem('Error: $e', type);
|
|
||||||
},
|
|
||||||
onDone: () async {
|
|
||||||
debugPrint(
|
|
||||||
'[Chat] stream done. total=${state.currentStreamingResponse.length}, anyChunk=$receivedAnyChunk');
|
|
||||||
if (state.currentStreamingResponse.isNotEmpty) {
|
|
||||||
List<ChatAction>? parsedActions;
|
|
||||||
try {
|
try {
|
||||||
debugPrint(
|
final response = await _repo.sendChat(request: enriched);
|
||||||
'[Chat] Attempting to parse response: ${state.currentStreamingResponse}');
|
if (response != null && response.isNotEmpty) {
|
||||||
final Map<String, dynamic> parsed =
|
List<ChatAction>? actions;
|
||||||
MessageJson.safeParse(state.currentStreamingResponse);
|
try {
|
||||||
debugPrint('[Chat] Parsed JSON: $parsed');
|
debugPrint('[Chat] Parsing non-streaming response');
|
||||||
|
final Map<String, dynamic> parsed = MessageJson.safeParse(response);
|
||||||
if (parsed.containsKey('actions') && parsed['actions'] is List) {
|
if (parsed.containsKey('actions') && parsed['actions'] is List) {
|
||||||
parsedActions = (parsed['actions'] as List)
|
actions = (parsed['actions'] as List)
|
||||||
.whereType<Map<String, dynamic>>()
|
.whereType<Map<String, dynamic>>()
|
||||||
.map(ChatAction.fromJson)
|
.map(ChatAction.fromJson)
|
||||||
.toList();
|
.toList();
|
||||||
debugPrint('[Chat] Parsed actions list: ${parsedActions.length}');
|
debugPrint('[Chat] Parsed actions list: ${actions.length}');
|
||||||
}
|
|
||||||
if (parsedActions == null || parsedActions.isEmpty) {
|
|
||||||
debugPrint('[Chat] No actions list found in response');
|
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint('[Chat] Error parsing action: $e');
|
debugPrint('[Chat] Error parsing action: $e');
|
||||||
debugPrint('[Chat] Error details: ${e.toString()}');
|
|
||||||
// If parsing fails, continue without action
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_addMessage(
|
_addMessage(
|
||||||
requestId,
|
requestId,
|
||||||
ChatMessage(
|
ChatMessage(
|
||||||
id: nanoid(),
|
id: nanoid(),
|
||||||
content: state.currentStreamingResponse,
|
content: response,
|
||||||
role: MessageRole.system,
|
role: MessageRole.system,
|
||||||
timestamp: DateTime.now(),
|
timestamp: DateTime.now(),
|
||||||
messageType: type,
|
messageType: type,
|
||||||
actions: parsedActions,
|
actions: actions,
|
||||||
),
|
|
||||||
);
|
|
||||||
} else if (!receivedAnyChunk) {
|
|
||||||
// Fallback to non-streaming request
|
|
||||||
debugPrint(
|
|
||||||
'[Chat] no streamed content; attempting non-streaming fallback');
|
|
||||||
try {
|
|
||||||
final fallback =
|
|
||||||
await _repo.sendChat(request: enriched.copyWith(stream: false));
|
|
||||||
if (fallback != null && fallback.isNotEmpty) {
|
|
||||||
List<ChatAction>? fallbackActions;
|
|
||||||
try {
|
|
||||||
final Map<String, dynamic> parsed =
|
|
||||||
MessageJson.safeParse(fallback);
|
|
||||||
if (parsed.containsKey('actions') &&
|
|
||||||
parsed['actions'] is List) {
|
|
||||||
fallbackActions = (parsed['actions'] as List)
|
|
||||||
.whereType<Map<String, dynamic>>()
|
|
||||||
.map(ChatAction.fromJson)
|
|
||||||
.toList();
|
|
||||||
}
|
|
||||||
if ((fallbackActions == null || fallbackActions.isEmpty)) {
|
|
||||||
// no actions
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
debugPrint('[Chat] Fallback error parsing action: $e');
|
|
||||||
}
|
|
||||||
|
|
||||||
_addMessage(
|
|
||||||
requestId,
|
|
||||||
ChatMessage(
|
|
||||||
id: nanoid(),
|
|
||||||
content: fallback,
|
|
||||||
role: MessageRole.system,
|
|
||||||
timestamp: DateTime.now(),
|
|
||||||
messageType: type,
|
|
||||||
actions: fallbackActions,
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
_appendSystem('No response received from the AI.', type);
|
_appendSystem('No response received from the AI.', type);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (e) {
|
||||||
debugPrint('[Chat] fallback error: $err');
|
debugPrint('[Chat] sendChat error: $e');
|
||||||
_appendSystem('Error: $err', type);
|
_appendSystem('Error: $e', type);
|
||||||
}
|
} finally {
|
||||||
}
|
|
||||||
state = state.copyWith(
|
state = state.copyWith(
|
||||||
isGenerating: false,
|
isGenerating: false,
|
||||||
currentStreamingResponse: '',
|
currentStreamingResponse: '',
|
||||||
);
|
);
|
||||||
},
|
}
|
||||||
cancelOnError: true,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void cancel() {
|
void cancel() {
|
||||||
_sub?.cancel();
|
|
||||||
state = state.copyWith(isGenerating: false);
|
state = state.copyWith(isGenerating: false);
|
||||||
}
|
}
|
||||||
|
|
||||||
void clearCurrentChat() {
|
void clearCurrentChat() {
|
||||||
final id = _currentRequest?.id ?? 'global';
|
final id = _currentRequest?.id ?? 'global';
|
||||||
_sub?.cancel();
|
|
||||||
final newSessions = {...state.chatSessions};
|
final newSessions = {...state.chatSessions};
|
||||||
newSessions[id] = [];
|
newSessions[id] = [];
|
||||||
state = state.copyWith(
|
state = state.copyWith(
|
||||||
@@ -297,49 +231,20 @@ class ChatViewmodel extends StateNotifier<ChatState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> applyAutoFix(ChatAction action) async {
|
Future<void> applyAutoFix(ChatAction action) async {
|
||||||
final requestId = _currentRequest?.id;
|
|
||||||
if (requestId == null) return;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
switch (action.actionType) {
|
final msg = await _ref.read(autoFixServiceProvider).apply(action);
|
||||||
case ChatActionType.updateField:
|
if (msg != null && msg.isNotEmpty) {
|
||||||
await _applyFieldUpdate(action);
|
// Message type depends on action context; choose sensible defaults
|
||||||
break;
|
final t = (action.actionType == ChatActionType.applyCurl)
|
||||||
case ChatActionType.addHeader:
|
? ChatMessageType.importCurl
|
||||||
await _applyHeaderUpdate(action, isAdd: true);
|
: (action.actionType == ChatActionType.applyOpenApi)
|
||||||
break;
|
? ChatMessageType.importOpenApi
|
||||||
case ChatActionType.updateHeader:
|
: ChatMessageType.general;
|
||||||
await _applyHeaderUpdate(action, isAdd: false);
|
_appendSystem(msg, t);
|
||||||
break;
|
}
|
||||||
case ChatActionType.deleteHeader:
|
// Only target-specific 'other' actions remain here
|
||||||
await _applyHeaderDelete(action);
|
if (action.actionType == ChatActionType.other) {
|
||||||
break;
|
|
||||||
case ChatActionType.updateBody:
|
|
||||||
await _applyBodyUpdate(action);
|
|
||||||
break;
|
|
||||||
case ChatActionType.updateUrl:
|
|
||||||
await _applyUrlUpdate(action);
|
|
||||||
break;
|
|
||||||
case ChatActionType.updateMethod:
|
|
||||||
await _applyMethodUpdate(action);
|
|
||||||
break;
|
|
||||||
case ChatActionType.applyCurl:
|
|
||||||
await _applyCurl(action);
|
|
||||||
break;
|
|
||||||
case ChatActionType.applyOpenApi:
|
|
||||||
await _applyOpenApi(action);
|
|
||||||
break;
|
|
||||||
case ChatActionType.other:
|
|
||||||
await _applyOtherAction(action);
|
await _applyOtherAction(action);
|
||||||
break;
|
|
||||||
case ChatActionType.showLanguages:
|
|
||||||
// UI handles selection;
|
|
||||||
break;
|
|
||||||
case ChatActionType.noAction:
|
|
||||||
break;
|
|
||||||
case ChatActionType.uploadAsset:
|
|
||||||
// Handled by UI upload button
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint('[Chat] Error applying auto-fix: $e');
|
debugPrint('[Chat] Error applying auto-fix: $e');
|
||||||
@@ -347,114 +252,11 @@ class ChatViewmodel extends StateNotifier<ChatState> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _applyFieldUpdate(ChatAction action) async {
|
// Field/URL/Method/Body updates are handled by AutoFixService
|
||||||
final requestId = _currentRequest?.id;
|
|
||||||
if (requestId == null) return;
|
|
||||||
|
|
||||||
final collectionNotifier =
|
// Header updates are now handled by AutoFixService
|
||||||
_ref.read(collectionStateNotifierProvider.notifier);
|
|
||||||
|
|
||||||
switch (action.field) {
|
// Body/URL/Method updates handled by AutoFixService
|
||||||
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();
|
|
||||||
final enabled = List<bool>.filled(params.length, true);
|
|
||||||
collectionNotifier.update(
|
|
||||||
params: params,
|
|
||||||
isParamEnabledList: enabled,
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _applyOtherAction(ChatAction action) async {
|
Future<void> _applyOtherAction(ChatAction action) async {
|
||||||
final requestId = _currentRequest?.id;
|
final requestId = _currentRequest?.id;
|
||||||
@@ -864,148 +666,7 @@ class ChatViewmodel extends StateNotifier<ChatState> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
String _composeSystemPrompt(
|
// Prompt helper methods moved to PromptBuilder service.
|
||||||
RequestModel? req,
|
|
||||||
ChatMessageType type, {
|
|
||||||
String? overrideLanguage,
|
|
||||||
}) {
|
|
||||||
final history = _buildHistoryBlock();
|
|
||||||
final contextBlock = _buildContextBlock(req);
|
|
||||||
final task =
|
|
||||||
_buildTaskPrompt(req, type, overrideLanguage: overrideLanguage);
|
|
||||||
return [
|
|
||||||
if (task != null) task,
|
|
||||||
if (contextBlock != null) contextBlock,
|
|
||||||
if (history.isNotEmpty) history,
|
|
||||||
].join('\n\n');
|
|
||||||
}
|
|
||||||
|
|
||||||
String _buildHistoryBlock({int maxTurns = 8}) {
|
|
||||||
final id = _currentRequest?.id ?? 'global';
|
|
||||||
final messages = state.chatSessions[id] ?? const [];
|
|
||||||
if (messages.isEmpty) return '';
|
|
||||||
final start = messages.length > maxTurns ? messages.length - maxTurns : 0;
|
|
||||||
final recent = messages.sublist(start);
|
|
||||||
final buf = StringBuffer('''<conversation_context>
|
|
||||||
Only use the following short chat history to maintain continuity. Do not repeat it back.
|
|
||||||
''');
|
|
||||||
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}
|
|
||||||
URL: ${http.url}
|
|
||||||
Method: ${http.method.name.toUpperCase()}
|
|
||||||
Status: ${req.responseStatus ?? ''}
|
|
||||||
Content-Type: ${http.bodyContentType.name}
|
|
||||||
Headers: { $headers }
|
|
||||||
Body: ${http.body ?? ''}
|
|
||||||
Response: ${req.httpResponseModel?.body ?? ''}
|
|
||||||
</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 a language is provided, go for code generation; else ask for language first
|
|
||||||
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:
|
|
||||||
// No AI prompt needed; handled locally.
|
|
||||||
return null;
|
|
||||||
case ChatMessageType.importOpenApi:
|
|
||||||
// No AI prompt needed; handled locally.
|
|
||||||
return null;
|
|
||||||
case ChatMessageType.general:
|
|
||||||
return prompts.generalInteractionPrompt();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Very light heuristic to detect language keywords in user text
|
|
||||||
String? _detectLanguageFromText(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;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool _looksLikeOpenApi(String text) {
|
bool _looksLikeOpenApi(String text) {
|
||||||
final t = text.trim();
|
final t = text.trim();
|
||||||
@@ -1022,56 +683,28 @@ class ChatViewmodel extends StateNotifier<ChatState> {
|
|||||||
return t.contains('openapi:') || t.contains('swagger:');
|
return t.contains('openapi:') || t.contains('swagger:');
|
||||||
}
|
}
|
||||||
|
|
||||||
String _inferBaseUrl(String url) {
|
String _inferBaseUrl(String url) =>
|
||||||
try {
|
_ref.read(urlEnvServiceProvider).inferBaseUrl(url);
|
||||||
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) async {
|
Future<String> _ensureBaseUrlEnv(String baseUrl) async {
|
||||||
if (baseUrl.isEmpty) return 'BASE_URL';
|
final svc = _ref.read(urlEnvServiceProvider);
|
||||||
String host = 'API';
|
return svc.ensureBaseUrlEnv(
|
||||||
try {
|
baseUrl,
|
||||||
final u = Uri.parse(baseUrl);
|
readEnvs: () => _ref.read(environmentsStateNotifierProvider),
|
||||||
if (u.hasAuthority && u.host.isNotEmpty) host = u.host;
|
readActiveEnvId: () => _ref.read(activeEnvironmentIdStateProvider),
|
||||||
} catch (_) {}
|
updateEnv: (id, {values}) => _ref
|
||||||
final slug = host
|
.read(environmentsStateNotifierProvider.notifier)
|
||||||
.replaceAll(RegExp(r'[^A-Za-z0-9]+'), '_')
|
.updateEnvironment(id, values: values),
|
||||||
.replaceAll(RegExp(r'_+'), '_')
|
);
|
||||||
.replaceAll(RegExp(r'^_|_$'), '')
|
|
||||||
.toUpperCase();
|
|
||||||
final key = 'BASE_URL_$slug';
|
|
||||||
|
|
||||||
final envNotifier = _ref.read(environmentsStateNotifierProvider.notifier);
|
|
||||||
final envs = _ref.read(environmentsStateNotifierProvider);
|
|
||||||
String? activeId = _ref.read(activeEnvironmentIdStateProvider);
|
|
||||||
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));
|
|
||||||
envNotifier.updateEnvironment(activeId, values: values);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return key;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<String> _maybeSubstituteBaseUrl(String url, String baseUrl) async {
|
Future<String> _maybeSubstituteBaseUrl(String url, String baseUrl) async {
|
||||||
if (baseUrl.isEmpty || !url.startsWith(baseUrl)) return url;
|
final svc = _ref.read(urlEnvServiceProvider);
|
||||||
final key = await _ensureBaseUrlEnv(baseUrl);
|
return svc.maybeSubstituteBaseUrl(
|
||||||
final path = url.substring(baseUrl.length);
|
url,
|
||||||
final normalized = path.startsWith('/') ? path : '/$path';
|
baseUrl,
|
||||||
return '{{$key}}$normalized';
|
ensure: (b) => _ensureBaseUrlEnv(b),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user