diff --git a/lib/consts.dart b/lib/consts.dart index 628598e47..4391d4627 100644 --- a/lib/consts.dart +++ b/lib/consts.dart @@ -156,6 +156,7 @@ enum ResponseBodyView { preview("Preview", Icons.visibility_rounded), code("Preview", Icons.code_rounded), raw("Raw", Icons.text_snippet_rounded), + answer("Answer", Icons.abc), none("Preview", Icons.warning); const ResponseBodyView(this.label, this.icon); diff --git a/lib/models/history_meta_model.g.dart b/lib/models/history_meta_model.g.dart index da184793a..ede68a296 100644 --- a/lib/models/history_meta_model.g.dart +++ b/lib/models/history_meta_model.g.dart @@ -34,6 +34,7 @@ Map _$$HistoryMetaModelImplToJson( const _$APITypeEnumMap = { APIType.rest: 'rest', + APIType.ai: 'ai', APIType.graphql: 'graphql', }; diff --git a/lib/models/history_request_model.dart b/lib/models/history_request_model.dart index a895e945c..61909e5b4 100644 --- a/lib/models/history_request_model.dart +++ b/lib/models/history_request_model.dart @@ -16,6 +16,10 @@ class HistoryRequestModel with _$HistoryRequestModel { required HistoryMetaModel metaData, required HttpRequestModel httpRequestModel, required HttpResponseModel httpResponseModel, +//ExtraDetails for anything else that can be included + @JsonKey(fromJson: customMapFromJson, toJson: customMapToJson) + @Default({}) + Map extraDetails, }) = _HistoryRequestModel; factory HistoryRequestModel.fromJson(Map json) => diff --git a/lib/models/history_request_model.freezed.dart b/lib/models/history_request_model.freezed.dart index bb7e94924..015c072e0 100644 --- a/lib/models/history_request_model.freezed.dart +++ b/lib/models/history_request_model.freezed.dart @@ -23,7 +23,10 @@ mixin _$HistoryRequestModel { String get historyId => throw _privateConstructorUsedError; HistoryMetaModel get metaData => throw _privateConstructorUsedError; HttpRequestModel get httpRequestModel => throw _privateConstructorUsedError; - HttpResponseModel get httpResponseModel => throw _privateConstructorUsedError; + HttpResponseModel get httpResponseModel => + throw _privateConstructorUsedError; //ExtraDetails for anything else that can be included + @JsonKey(fromJson: customMapFromJson, toJson: customMapToJson) + Map get extraDetails => throw _privateConstructorUsedError; /// Serializes this HistoryRequestModel to a JSON map. Map toJson() => throw _privateConstructorUsedError; @@ -45,7 +48,9 @@ abstract class $HistoryRequestModelCopyWith<$Res> { {String historyId, HistoryMetaModel metaData, HttpRequestModel httpRequestModel, - HttpResponseModel httpResponseModel}); + HttpResponseModel httpResponseModel, + @JsonKey(fromJson: customMapFromJson, toJson: customMapToJson) + Map extraDetails}); $HistoryMetaModelCopyWith<$Res> get metaData; $HttpRequestModelCopyWith<$Res> get httpRequestModel; @@ -71,6 +76,7 @@ class _$HistoryRequestModelCopyWithImpl<$Res, $Val extends HistoryRequestModel> Object? metaData = null, Object? httpRequestModel = null, Object? httpResponseModel = null, + Object? extraDetails = null, }) { return _then(_value.copyWith( historyId: null == historyId @@ -89,6 +95,10 @@ class _$HistoryRequestModelCopyWithImpl<$Res, $Val extends HistoryRequestModel> ? _value.httpResponseModel : httpResponseModel // ignore: cast_nullable_to_non_nullable as HttpResponseModel, + extraDetails: null == extraDetails + ? _value.extraDetails + : extraDetails // ignore: cast_nullable_to_non_nullable + as Map, ) as $Val); } @@ -135,7 +145,9 @@ abstract class _$$HistoryRequestModelImplCopyWith<$Res> {String historyId, HistoryMetaModel metaData, HttpRequestModel httpRequestModel, - HttpResponseModel httpResponseModel}); + HttpResponseModel httpResponseModel, + @JsonKey(fromJson: customMapFromJson, toJson: customMapToJson) + Map extraDetails}); @override $HistoryMetaModelCopyWith<$Res> get metaData; @@ -162,6 +174,7 @@ class __$$HistoryRequestModelImplCopyWithImpl<$Res> Object? metaData = null, Object? httpRequestModel = null, Object? httpResponseModel = null, + Object? extraDetails = null, }) { return _then(_$HistoryRequestModelImpl( historyId: null == historyId @@ -180,6 +193,10 @@ class __$$HistoryRequestModelImplCopyWithImpl<$Res> ? _value.httpResponseModel : httpResponseModel // ignore: cast_nullable_to_non_nullable as HttpResponseModel, + extraDetails: null == extraDetails + ? _value._extraDetails + : extraDetails // ignore: cast_nullable_to_non_nullable + as Map, )); } } @@ -192,7 +209,10 @@ class _$HistoryRequestModelImpl implements _HistoryRequestModel { {required this.historyId, required this.metaData, required this.httpRequestModel, - required this.httpResponseModel}); + required this.httpResponseModel, + @JsonKey(fromJson: customMapFromJson, toJson: customMapToJson) + final Map extraDetails = const {}}) + : _extraDetails = extraDetails; factory _$HistoryRequestModelImpl.fromJson(Map json) => _$$HistoryRequestModelImplFromJson(json); @@ -205,10 +225,20 @@ class _$HistoryRequestModelImpl implements _HistoryRequestModel { final HttpRequestModel httpRequestModel; @override final HttpResponseModel httpResponseModel; +//ExtraDetails for anything else that can be included + final Map _extraDetails; +//ExtraDetails for anything else that can be included + @override + @JsonKey(fromJson: customMapFromJson, toJson: customMapToJson) + Map get extraDetails { + if (_extraDetails is EqualUnmodifiableMapView) return _extraDetails; + // ignore: implicit_dynamic_type + return EqualUnmodifiableMapView(_extraDetails); + } @override String toString() { - return 'HistoryRequestModel(historyId: $historyId, metaData: $metaData, httpRequestModel: $httpRequestModel, httpResponseModel: $httpResponseModel)'; + return 'HistoryRequestModel(historyId: $historyId, metaData: $metaData, httpRequestModel: $httpRequestModel, httpResponseModel: $httpResponseModel, extraDetails: $extraDetails)'; } @override @@ -223,13 +253,20 @@ class _$HistoryRequestModelImpl implements _HistoryRequestModel { (identical(other.httpRequestModel, httpRequestModel) || other.httpRequestModel == httpRequestModel) && (identical(other.httpResponseModel, httpResponseModel) || - other.httpResponseModel == httpResponseModel)); + other.httpResponseModel == httpResponseModel) && + const DeepCollectionEquality() + .equals(other._extraDetails, _extraDetails)); } @JsonKey(includeFromJson: false, includeToJson: false) @override int get hashCode => Object.hash( - runtimeType, historyId, metaData, httpRequestModel, httpResponseModel); + runtimeType, + historyId, + metaData, + httpRequestModel, + httpResponseModel, + const DeepCollectionEquality().hash(_extraDetails)); /// Create a copy of HistoryRequestModel /// with the given fields replaced by the non-null parameter values. @@ -250,11 +287,12 @@ class _$HistoryRequestModelImpl implements _HistoryRequestModel { abstract class _HistoryRequestModel implements HistoryRequestModel { const factory _HistoryRequestModel( - {required final String historyId, - required final HistoryMetaModel metaData, - required final HttpRequestModel httpRequestModel, - required final HttpResponseModel httpResponseModel}) = - _$HistoryRequestModelImpl; + {required final String historyId, + required final HistoryMetaModel metaData, + required final HttpRequestModel httpRequestModel, + required final HttpResponseModel httpResponseModel, + @JsonKey(fromJson: customMapFromJson, toJson: customMapToJson) + final Map extraDetails}) = _$HistoryRequestModelImpl; factory _HistoryRequestModel.fromJson(Map json) = _$HistoryRequestModelImpl.fromJson; @@ -266,7 +304,11 @@ abstract class _HistoryRequestModel implements HistoryRequestModel { @override HttpRequestModel get httpRequestModel; @override - HttpResponseModel get httpResponseModel; + HttpResponseModel + get httpResponseModel; //ExtraDetails for anything else that can be included + @override + @JsonKey(fromJson: customMapFromJson, toJson: customMapToJson) + Map get extraDetails; /// Create a copy of HistoryRequestModel /// with the given fields replaced by the non-null parameter values. diff --git a/lib/models/history_request_model.g.dart b/lib/models/history_request_model.g.dart index 830d7a4cf..535c642ae 100644 --- a/lib/models/history_request_model.g.dart +++ b/lib/models/history_request_model.g.dart @@ -15,6 +15,9 @@ _$HistoryRequestModelImpl _$$HistoryRequestModelImplFromJson(Map json) => Map.from(json['httpRequestModel'] as Map)), httpResponseModel: HttpResponseModel.fromJson( Map.from(json['httpResponseModel'] as Map)), + extraDetails: json['extraDetails'] == null + ? const {} + : customMapFromJson(json['extraDetails'] as Map), ); Map _$$HistoryRequestModelImplToJson( @@ -24,4 +27,5 @@ Map _$$HistoryRequestModelImplToJson( 'metaData': instance.metaData.toJson(), 'httpRequestModel': instance.httpRequestModel.toJson(), 'httpResponseModel': instance.httpResponseModel.toJson(), + 'extraDetails': customMapToJson(instance.extraDetails), }; diff --git a/lib/models/llm_models/all_models.dart b/lib/models/llm_models/all_models.dart new file mode 100644 index 000000000..8df16c72a --- /dev/null +++ b/lib/models/llm_models/all_models.dart @@ -0,0 +1,36 @@ +import 'package:apidash/models/llm_models/google/gemini_20_flash.dart'; +import 'package:apidash/models/llm_models/llm_model.dart'; +import 'package:apidash/models/llm_models/ollama/llama3.dart'; +import 'package:apidash/models/llm_models/openai/azure_openai.dart'; + +// Exports +export 'package:apidash/models/llm_models/google/gemini_20_flash.dart'; +export 'package:apidash/models/llm_models/ollama/llama3.dart'; + +Map availableModels = { + Gemini20FlashModel.instance.modelIdentifier: ( + Gemini20FlashModel.instance, + () => Gemini20FlashModel() + ), + LLama3LocalModel.instance.modelIdentifier: ( + LLama3LocalModel.instance, + () => LLama3LocalModel() + ), + AzureOpenAIModel.instance.modelIdentifier: ( + AzureOpenAIModel.instance, + () => AzureOpenAIModel() + ), +}; + +LLMModel? getLLMModelFromID(String modelID, [Map? configMap]) { + for (final entry in availableModels.entries) { + if (entry.key == modelID) { + final m = entry.value.$2(); + if (configMap != null) { + m.loadConfigurations(configMap); + } + return m; + } + } + return null; +} diff --git a/lib/models/llm_models/google/gemini_20_flash.dart b/lib/models/llm_models/google/gemini_20_flash.dart new file mode 100644 index 000000000..e46ed8f79 --- /dev/null +++ b/lib/models/llm_models/google/gemini_20_flash.dart @@ -0,0 +1,146 @@ +import 'dart:convert'; + +import 'package:apidash/models/llm_models/llm_config.dart'; +import 'package:apidash/models/llm_models/llm_model.dart'; +import 'package:apidash_core/apidash_core.dart' as http; + +class Gemini20FlashModel extends LLMModel { + static Gemini20FlashModel instance = Gemini20FlashModel(); + + @override + String provider = 'Google'; + + @override + String modelName = 'Gemini 2.0 Flash'; + + @override + String modelIdentifier = 'gemini_20_flash'; + + @override + LLMModelAuthorizationType authorizationType = + LLMModelAuthorizationType.apiKey; + + @override + Map configurations = { + 'temperature': LLMModelConfiguration( + configId: 'temperature', + configName: 'Temperature', + configDescription: + 'Higher values mean greater variability and lesser values mean more deterministic responses', + configType: LLMModelConfigurationType.slider, + configValue: LLMConfigSliderValue(value: (0.0, 0.5, 1.0)), + ), + 'top_p': LLMModelConfiguration( + configId: 'top_p', + configName: 'Top P', + configDescription: 'Controls the randomness of the LLM Response', + configType: LLMModelConfigurationType.slider, + configValue: LLMConfigSliderValue(value: (0.0, 0.95, 1.0)), + ), + 'max_tokens': LLMModelConfiguration( + configId: 'max_tokens', + configName: 'Max Tokens', + configDescription: + 'The maximum number of tokens to generate. -1 means no limit', + configType: LLMModelConfigurationType.numeric, + configValue: LLMConfigNumericValue(value: -1), + ), + }; + + @override + String providerIcon = 'https://img.icons8.com/color/48/google-logo.png'; + + @override + LLMModelSpecifics specifics = LLMModelSpecifics( + endpoint: + 'https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent', + method: 'POST', + headers: {}, + outputFormatter: (resp) { + if (resp == null) return null; + return resp['candidates']?[0]?['content']?['parts']?[0]?['text']; + }, + ); + + @override + Map getRequestPayload({ + required String systemPrompt, + required String userPrompt, + required String credential, + }) { + final temp = configurations['temperature']!.configValue.serialize(); + final top_p = configurations['top_p']!.configValue.serialize(); + final mT = configurations['max_tokens']!.configValue.serialize(); + final payload = { + "model": "gemini-2.0-flash", + "contents": [ + { + "role": "user", + "parts": [ + {"text": userPrompt} + ] + } + ], + "systemInstruction": { + "role": "system", + "parts": [ + {"text": systemPrompt} + ] + }, + "generationConfig": { + "temperature": (jsonDecode(temp) as List)[1].toString(), + "topP": (jsonDecode(top_p) as List)[1].toString(), + if (mT != '-1') ...{ + "maxOutputTokens": mT, + } + } + }; + final endpoint = specifics.endpoint; + final url = "$endpoint?key=$credential"; + return { + 'url': url, + 'payload': payload, + }; + } + + @override + loadConfigurations(Map configMap) { + print('Loaded Gemini Configurations'); + final double? temperature = configMap['temperature']; + final double? top_p = configMap['top_p']; + final int? max_tokens = configMap['max_tokens']; + + //print('loading configs => $temperature, $top_p, $max_tokens'); + if (temperature != null) { + final config = configurations['temperature']!; + configurations['temperature'] = + configurations['temperature']!.updateValue( + LLMConfigSliderValue(value: ( + config.configValue.value.$1, + temperature, + config.configValue.value.$3 + )), + ); + } + if (top_p != null) { + final config = configurations['top_p']!; + configurations['top_p'] = configurations['top_p']!.updateValue( + LLMConfigSliderValue(value: ( + config.configValue.value.$1, + top_p, + config.configValue.value.$3 + )), + ); + } + if (max_tokens != null) { + configurations['max_tokens'] = configurations['max_tokens']!.updateValue( + LLMConfigNumericValue(value: max_tokens), + ); + } + + // Load Modified Endpoint + if (configMap['modifed_endpoint'] != null) { + specifics.endpoint = configMap['modifed_endpoint']!; + } + } +} diff --git a/lib/models/llm_models/llm_config.dart b/lib/models/llm_models/llm_config.dart new file mode 100644 index 000000000..123beff5c --- /dev/null +++ b/lib/models/llm_models/llm_config.dart @@ -0,0 +1,159 @@ +import 'dart:convert'; + +class LLMModelConfiguration { + final String configId; + final String configName; + final String configDescription; + final LLMModelConfigurationType configType; + final LLMModelConfigValue configValue; + + LLMModelConfiguration updateValue(LLMModelConfigValue value) { + return LLMModelConfiguration( + configId: configId, + configName: configName, + configDescription: configDescription, + configType: configType, + configValue: value, + ); + } + + LLMModelConfiguration({ + required this.configId, + required this.configName, + required this.configDescription, + required this.configType, + required this.configValue, + }) { + // Assert that the configuration type and value matches + switch (configType) { + case LLMModelConfigurationType.boolean: + assert(configValue is LLMConfigBooleanValue); + case LLMModelConfigurationType.slider: + assert(configValue is LLMConfigSliderValue); + case LLMModelConfigurationType.numeric: + assert(configValue is LLMConfigNumericValue); + case LLMModelConfigurationType.text: + assert(configValue is LLMConfigTextValue); + } + } + + factory LLMModelConfiguration.fromJson(Map x) { + LLMModelConfigurationType cT; + LLMModelConfigValue cV; + switch (x['configType']) { + case 'boolean': + cT = LLMModelConfigurationType.boolean; + cV = LLMConfigBooleanValue.deserialize(x['configValue']); + break; + case 'slider': + cT = LLMModelConfigurationType.slider; + cV = LLMConfigSliderValue.deserialize(x['configValue']); + break; + case 'numeric': + cT = LLMModelConfigurationType.numeric; + cV = LLMConfigNumericValue.deserialize(x['configValue']); + break; + case 'text': + cT = LLMModelConfigurationType.text; + cV = LLMConfigTextValue.deserialize(x['configValue']); + break; + default: + cT = LLMModelConfigurationType.text; + cV = LLMConfigTextValue.deserialize(x['configValue']); + } + return LLMModelConfiguration( + configId: x['config_id'], + configName: x['configName'], + configDescription: x['configDescription'], + configType: cT, + configValue: cV, + ); + } + + Map toJson() { + return { + 'configId': configId, + 'configName': configName, + 'configDescription': configDescription, + 'configType': configType.name.toString(), + 'configValue': configValue.serialize(), + }; + } +} + +enum LLMModelConfigurationType { boolean, slider, numeric, text } + +//----------------LLMConfigValues ------------ + +abstract class LLMModelConfigValue { + dynamic _value; + + // ignore: unnecessary_getters_setters + dynamic get value => _value; + + set value(dynamic newValue) => _value = newValue; + + String serialize(); + + LLMModelConfigValue(this._value); +} + +class LLMConfigBooleanValue extends LLMModelConfigValue { + LLMConfigBooleanValue({required bool value}) : super(value); + + @override + String serialize() { + return value.toString(); + } + + static LLMConfigBooleanValue deserialize(String x) { + return LLMConfigBooleanValue(value: x == 'true'); + } +} + +class LLMConfigNumericValue extends LLMModelConfigValue { + LLMConfigNumericValue({required num value}) : super(value); + + @override + String serialize() { + return value.toString(); + } + + static LLMConfigNumericValue deserialize(String x) { + return LLMConfigNumericValue(value: num.parse(x)); + } +} + +class LLMConfigSliderValue extends LLMModelConfigValue { + LLMConfigSliderValue({required (double, double, double) value}) + : super(value); + + @override + String serialize() { + final v = value as (double, double, double); + return jsonEncode([v.$1, v.$2, v.$3]); + } + + static LLMConfigSliderValue deserialize(String x) { + final z = jsonDecode(x) as List; + final val = ( + double.parse(z[0].toString()), + double.parse(z[1].toString()), + double.parse(z[2].toString()) + ); + return LLMConfigSliderValue(value: val); + } +} + +class LLMConfigTextValue extends LLMModelConfigValue { + LLMConfigTextValue({required String value}) : super(value); + + @override + String serialize() { + return value.toString(); + } + + static LLMConfigTextValue deserialize(String x) { + return LLMConfigTextValue(value: x); + } +} diff --git a/lib/models/llm_models/llm_model.dart b/lib/models/llm_models/llm_model.dart new file mode 100644 index 000000000..f2b2bc60f --- /dev/null +++ b/lib/models/llm_models/llm_model.dart @@ -0,0 +1,74 @@ +import 'dart:convert'; + +import 'package:apidash/models/llm_models/llm_config.dart'; +import 'package:apidash_core/apidash_core.dart' as http; + +abstract class LLMModel { + abstract String providerIcon; + abstract String provider; + abstract String modelIdentifier; + abstract String modelName; + abstract LLMModelAuthorizationType authorizationType; + abstract Map configurations; + abstract LLMModelSpecifics specifics; + Map getRequestPayload({ + required String systemPrompt, + required String userPrompt, + required String credential, + }); + loadConfigurations(Map configMap); +} + +extension LLMModelExtensions on LLMModel { + Future call({ + required String systemPrompt, + required String userPrompt, + required String credential, + }) async { + final reqData = getRequestPayload( + systemPrompt: systemPrompt, + userPrompt: userPrompt, + credential: credential, + ); + print('calling => ${reqData['url']}'); + final headers = reqData['headers'] ?? specifics.headers; + final response = await http.post( + Uri.parse(reqData['url']), + headers: {'Content-Type': 'application/json', ...headers}, + body: jsonEncode(reqData['payload']), + ); + if (response.statusCode == 200) { + final data = jsonDecode(response.body); + return specifics.outputFormatter(data); + } else { + print(reqData['url']); + print(response.body); + throw Exception( + 'LLM_EXCEPTION: ${response.statusCode}\n${response.body}', + ); + } + } +} + +enum LLMModelAuthorizationType { + bearerToken('Bearer Token'), + apiKey('API Key'), + none('No Authorization'); + + const LLMModelAuthorizationType(this.label); + final String label; +} + +class LLMModelSpecifics { + String endpoint; + final String method; + final Map headers; + final String? Function(Map?) outputFormatter; + + LLMModelSpecifics({ + required this.endpoint, + required this.method, + required this.headers, + required this.outputFormatter, + }); +} diff --git a/lib/models/llm_models/ollama/llama3.dart b/lib/models/llm_models/ollama/llama3.dart new file mode 100644 index 000000000..a6874c721 --- /dev/null +++ b/lib/models/llm_models/ollama/llama3.dart @@ -0,0 +1,139 @@ +import 'dart:convert'; + +import 'package:apidash/models/llm_models/llm_config.dart'; +import 'package:apidash/models/llm_models/llm_model.dart'; + +class LLama3LocalModel extends LLMModel { + static LLama3LocalModel instance = LLama3LocalModel(); + + @override + String provider = 'Local'; + + @override + String modelName = 'LLaMA 3 (Local)'; + + @override + String modelIdentifier = 'llama3_local'; + + @override + LLMModelAuthorizationType authorizationType = LLMModelAuthorizationType.none; + + @override + Map configurations = { + 'temperature': LLMModelConfiguration( + configId: 'temperature', + configName: 'Temperature', + configDescription: + 'Controls the randomness of the model\'s output. Higher is more creative.', + configType: LLMModelConfigurationType.slider, + configValue: LLMConfigSliderValue(value: (0.0, 0.7, 1.5)), + ), + 'top_p': LLMModelConfiguration( + configId: 'top_p', + configName: 'Top P', + configDescription: + 'Limits token selection to a subset of the most likely options.', + configType: LLMModelConfigurationType.slider, + configValue: LLMConfigSliderValue(value: (0.0, 0.9, 1.0)), + ), + // 'max_tokens': LLMModelConfiguration( + // configId: 'max_tokens', + // configName: 'Max Tokens', + // configDescription: + // 'Maximum tokens to generate. Set low for short answers.', + // configType: LLMModelConfigurationType.numeric, + // configValue: LLMConfigNumericValue(value: 512), + // ), + }; + + @override + String providerIcon = 'https://img.icons8.com/ios-glyphs/30/bot.png'; + + @override + LLMModelSpecifics specifics = LLMModelSpecifics( + endpoint: 'http://localhost:11434/v1/chat/completions', + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + outputFormatter: (resp) { + if (resp == null) return null; + return resp['choices']?[0]?['message']?['content']; + }, + ); + + @override + Map getRequestPayload({ + required String systemPrompt, + required String userPrompt, + required String credential, // not used for local + }) { + final temp = + (jsonDecode(configurations['temperature']!.configValue.serialize()) + as List)[1]; + final topP = (jsonDecode(configurations['top_p']!.configValue.serialize()) + as List)[1]; + // final maxTokens = configurations['max_tokens']!.configValue.value as int; + + final payload = { + "model": "llama3:latest", // or "llama3:instruct" depending on your server + "messages": [ + { + "role": "system", + "content": systemPrompt, + }, + { + "role": "user", + "content": userPrompt, + } + ], + "temperature": temp, + "top_p": topP, + // "max_tokens": maxTokens, + }; + + return { + 'url': specifics.endpoint, + 'payload': payload, + }; + } + + @override + loadConfigurations(Map configMap) { + final double? temperature = configMap['temperature']; + final double? top_p = configMap['top_p']; + final int? max_tokens = configMap['max_tokens']; + + if (temperature != null) { + final config = configurations['temperature']!; + configurations['temperature'] = config.updateValue( + LLMConfigSliderValue(value: ( + config.configValue.value.$1, + temperature, + config.configValue.value.$3 + )), + ); + } + + if (top_p != null) { + final config = configurations['top_p']!; + configurations['top_p'] = config.updateValue( + LLMConfigSliderValue(value: ( + config.configValue.value.$1, + top_p, + config.configValue.value.$3 + )), + ); + } + + // if (max_tokens != null) { + // configurations['max_tokens'] = configurations['max_tokens']! + // .updateValue(LLMConfigNumericValue(value: max_tokens)); + // } + + // Load Modified Endpoint + if (configMap['modifed_endpoint'] != null) { + specifics.endpoint = configMap['modifed_endpoint']!; + } + } +} diff --git a/lib/models/llm_models/openai/azure_openai.dart b/lib/models/llm_models/openai/azure_openai.dart new file mode 100644 index 000000000..a3f776159 --- /dev/null +++ b/lib/models/llm_models/openai/azure_openai.dart @@ -0,0 +1,251 @@ +import 'dart:convert'; + +import 'package:apidash/models/llm_models/llm_config.dart'; +import 'package:apidash/models/llm_models/llm_model.dart'; + +class AzureOpenAIModel extends LLMModel { + static AzureOpenAIModel instance = AzureOpenAIModel(); + + @override + String provider = 'Azure OpenAI'; + + @override + String modelName = 'Azure OpenAI'; + + @override + String modelIdentifier = 'azure_openai'; + + @override + LLMModelAuthorizationType authorizationType = + LLMModelAuthorizationType.apiKey; + @override + Map configurations = { + 'azure_endpoint': LLMModelConfiguration( + configId: 'azure_endpoint', + configName: 'Azure Endpoint', + configDescription: 'The endpoint for Azure OpenAI', + configType: LLMModelConfigurationType.text, + configValue: LLMConfigTextValue( + value: 'https://openai.azure.com', + ), + ), + 'azure_api_version': LLMModelConfiguration( + configId: 'azure_api_version', + configName: 'Azure API Version', + configDescription: + 'The Exact Version of the API used inside Azure OpenAI', + configType: LLMModelConfigurationType.text, + configValue: LLMConfigTextValue( + value: 'YYYY-MM-DD-preview', + ), + ), + 'azure_deployment_name': LLMModelConfiguration( + configId: 'azure_deployment_name', + configName: 'Azure Deployment Name', + configDescription: 'The Model Name within Azure OpenAI', + configType: LLMModelConfigurationType.text, + configValue: LLMConfigTextValue( + value: 'GPT4o', + ), + ), + 'temperature': LLMModelConfiguration( + configId: 'temperature', + configName: 'Temperature', + configDescription: + 'Controls the randomness of the model\'s output. Higher is more creative.', + configType: LLMModelConfigurationType.slider, + configValue: LLMConfigSliderValue(value: (0.0, 0.5, 1.0)), + ), + 'top_p': LLMModelConfiguration( + configId: 'top_p', + configName: 'Top P', + configDescription: + 'Limits token selection to a subset of the most likely options.', + configType: LLMModelConfigurationType.slider, + configValue: LLMConfigSliderValue(value: (0.0, 0.95, 1.0)), + ), + // 'max_tokens': LLMModelConfiguration( + // configId: 'max_tokens', + // configName: 'Max Tokens', + // configDescription: + // 'Maximum tokens to generate. Set low for short answers.', + // configType: LLMModelConfigurationType.numeric, + // configValue: LLMConfigNumericValue(value: 512), + // ), + }; + + @override + String providerIcon = 'https://img.icons8.com/ios-glyphs/30/bot.png'; + + @override + LLMModelSpecifics specifics = LLMModelSpecifics( + endpoint: "", + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + outputFormatter: (resp) { + if (resp == null) return null; + return resp["choices"]?[0]["message"]?["content"]?.trim(); + }, + ); + + @override + Map getRequestPayload({ + required String systemPrompt, + required String userPrompt, + required String credential, + }) { + final temp = + (jsonDecode(configurations['temperature']!.configValue.serialize()) + as List)[1]; + final topP = (jsonDecode(configurations['top_p']!.configValue.serialize()) + as List)[1]; + // final maxTokens = configurations['max_tokens']!.configValue.value as int; + + final payload = { + "messages": [ + { + "role": "system", + "content": systemPrompt, + }, + if (userPrompt.isNotEmpty) ...{ + { + "role": "user", + "content": userPrompt, + } + } else ...{ + {"role": "user", "content": "Generate"} + } + ], + "temperature": temp, + "top_p": topP, + // "max_tokens": maxTokens, + }; + + final azendpoint = configurations['azure_endpoint']!.configValue.value; + final azdep = configurations['azure_deployment_name']!.configValue.value; + final azapiver = configurations['azure_api_version']!.configValue.value; + final url = + "$azendpoint/openai/deployments/$azdep/chat/completions?api-version=$azapiver"; + + return { + 'url': url, + 'payload': payload, + 'headers': {'api-key': credential, ...specifics.headers} + }; + } + + @override + loadConfigurations(Map configMap) { + final double? temperature = configMap['temperature']; + final double? top_p = configMap['top_p']; + // final int? max_tokens = configMap['max_tokens']; + + final String? azure_endpoint = configMap['azure_endpoint']; + final String? azure_api_version = configMap['azure_api_version']; + final String? azure_deployment_name = configMap['azure_deployment_name']; + + if (temperature != null) { + final config = configurations['temperature']!; + configurations['temperature'] = config.updateValue( + LLMConfigSliderValue(value: ( + config.configValue.value.$1, + temperature, + config.configValue.value.$3 + )), + ); + } + + if (top_p != null) { + final config = configurations['top_p']!; + configurations['top_p'] = config.updateValue( + LLMConfigSliderValue(value: ( + config.configValue.value.$1, + top_p, + config.configValue.value.$3 + )), + ); + } + + if (azure_endpoint != null) { + final config = configurations['azure_endpoint']!; + configurations['azure_endpoint'] = config.updateValue( + LLMConfigTextValue(value: azure_endpoint), + ); + } + + if (azure_api_version != null) { + final config = configurations['azure_api_version']!; + configurations['azure_api_version'] = config.updateValue( + LLMConfigTextValue(value: azure_api_version), + ); + } + + if (azure_deployment_name != null) { + final config = configurations['azure_deployment_name']!; + configurations['azure_deployment_name'] = config.updateValue( + LLMConfigTextValue(value: azure_deployment_name), + ); + } + + // if (max_tokens != null) { + // configurations['max_tokens'] = configurations['max_tokens']! + // .updateValue(LLMConfigNumericValue(value: max_tokens)); + // } + } +} + + +/* + + + static Future openai_azure( + String systemPrompt, + String input, + String credential, + ) async { + //KEY_FORMAT: domain|modelname|apiv|key + + final credParts = credential.split('|'); + final domain = credParts[0]; + final modelname = credParts[1]; + final apiversion = credParts[2]; + final apiKey = credParts[3]; + + final String apiUrl = + "https://$domain.openai.azure.com/openai/deployments/$modelname/chat/completions?api-version=$apiversion"; + + try { + final response = await http.post( + Uri.parse(apiUrl), + headers: { + "Content-Type": "application/json", + "api-key": apiKey, + }, + body: jsonEncode({ + "messages": [ + {"role": "system", "content": systemPrompt}, + if (input.isNotEmpty) + {"role": "user", "content": input} + else + {"role": "user", "content": "Generate"} + ], + }), + ); + + if (response.statusCode == 200) { + final Map data = jsonDecode(response.body); + return data["choices"]?[0]["message"]?["content"]?.trim(); + } else { + print("Error: ${response.statusCode} - ${response.body}"); + return null; + } + } catch (e) { + print("Exception: $e"); + return null; + } + } + + +*/ \ No newline at end of file diff --git a/lib/models/request_model.dart b/lib/models/request_model.dart index bb9eefaef..eb0d6b859 100644 --- a/lib/models/request_model.dart +++ b/lib/models/request_model.dart @@ -1,3 +1,6 @@ +import 'package:apidash/models/llm_models/all_models.dart'; +import 'package:apidash/models/llm_models/google/gemini_20_flash.dart'; +import 'package:apidash/models/llm_models/llm_model.dart'; import 'package:apidash_core/apidash_core.dart'; part 'request_model.freezed.dart'; @@ -22,8 +25,40 @@ class RequestModel with _$RequestModel { HttpResponseModel? httpResponseModel, @JsonKey(includeToJson: false) @Default(false) bool isWorking, @JsonKey(includeToJson: false) DateTime? sendingTime, + + //ExtraDetails for anything else that can be included + @JsonKey(fromJson: customMapFromJson, toJson: customMapToJson) + @Default({}) + Map extraDetails, }) = _RequestModel; factory RequestModel.fromJson(Map json) => _$RequestModelFromJson(json); } + +// ----------------- Custom SerDes -------------- + +// Map converter +Map customMapFromJson(Map json) { + Map result = {}; + for (var entry in json.entries) { + if (entry.key == 'model' && entry.value is String) { + result[entry.key] = getLLMModelFromID(entry.value, json); + } else { + result[entry.key] = entry.value; + } + } + return result; +} + +Map customMapToJson(Map map) { + Map? result = {}; + for (var entry in map.entries) { + if (entry.key == 'model' && entry.value is LLMModel) { + result[entry.key] = (entry.value as LLMModel).modelIdentifier; + } else { + result[entry.key] = entry.value; + } + } + return result; +} diff --git a/lib/models/request_model.freezed.dart b/lib/models/request_model.freezed.dart index b237e3726..eb8e6dbb1 100644 --- a/lib/models/request_model.freezed.dart +++ b/lib/models/request_model.freezed.dart @@ -34,7 +34,10 @@ mixin _$RequestModel { @JsonKey(includeToJson: false) bool get isWorking => throw _privateConstructorUsedError; @JsonKey(includeToJson: false) - DateTime? get sendingTime => throw _privateConstructorUsedError; + DateTime? get sendingTime => + throw _privateConstructorUsedError; //ExtraDetails for anything else that can be included + @JsonKey(fromJson: customMapFromJson, toJson: customMapToJson) + Map get extraDetails => throw _privateConstructorUsedError; /// Serializes this RequestModel to a JSON map. Map toJson() => throw _privateConstructorUsedError; @@ -63,7 +66,9 @@ abstract class $RequestModelCopyWith<$Res> { String? message, HttpResponseModel? httpResponseModel, @JsonKey(includeToJson: false) bool isWorking, - @JsonKey(includeToJson: false) DateTime? sendingTime}); + @JsonKey(includeToJson: false) DateTime? sendingTime, + @JsonKey(fromJson: customMapFromJson, toJson: customMapToJson) + Map extraDetails}); $HttpRequestModelCopyWith<$Res>? get httpRequestModel; $HttpResponseModelCopyWith<$Res>? get httpResponseModel; @@ -95,6 +100,7 @@ class _$RequestModelCopyWithImpl<$Res, $Val extends RequestModel> Object? httpResponseModel = freezed, Object? isWorking = null, Object? sendingTime = freezed, + Object? extraDetails = null, }) { return _then(_value.copyWith( id: null == id @@ -141,6 +147,10 @@ class _$RequestModelCopyWithImpl<$Res, $Val extends RequestModel> ? _value.sendingTime : sendingTime // ignore: cast_nullable_to_non_nullable as DateTime?, + extraDetails: null == extraDetails + ? _value.extraDetails + : extraDetails // ignore: cast_nullable_to_non_nullable + as Map, ) as $Val); } @@ -192,7 +202,9 @@ abstract class _$$RequestModelImplCopyWith<$Res> String? message, HttpResponseModel? httpResponseModel, @JsonKey(includeToJson: false) bool isWorking, - @JsonKey(includeToJson: false) DateTime? sendingTime}); + @JsonKey(includeToJson: false) DateTime? sendingTime, + @JsonKey(fromJson: customMapFromJson, toJson: customMapToJson) + Map extraDetails}); @override $HttpRequestModelCopyWith<$Res>? get httpRequestModel; @@ -224,6 +236,7 @@ class __$$RequestModelImplCopyWithImpl<$Res> Object? httpResponseModel = freezed, Object? isWorking = null, Object? sendingTime = freezed, + Object? extraDetails = null, }) { return _then(_$RequestModelImpl( id: null == id @@ -269,6 +282,10 @@ class __$$RequestModelImplCopyWithImpl<$Res> ? _value.sendingTime : sendingTime // ignore: cast_nullable_to_non_nullable as DateTime?, + extraDetails: null == extraDetails + ? _value._extraDetails + : extraDetails // ignore: cast_nullable_to_non_nullable + as Map, )); } } @@ -288,7 +305,10 @@ class _$RequestModelImpl implements _RequestModel { this.message, this.httpResponseModel, @JsonKey(includeToJson: false) this.isWorking = false, - @JsonKey(includeToJson: false) this.sendingTime}); + @JsonKey(includeToJson: false) this.sendingTime, + @JsonKey(fromJson: customMapFromJson, toJson: customMapToJson) + final Map extraDetails = const {}}) + : _extraDetails = extraDetails; factory _$RequestModelImpl.fromJson(Map json) => _$$RequestModelImplFromJson(json); @@ -321,10 +341,20 @@ class _$RequestModelImpl implements _RequestModel { @override @JsonKey(includeToJson: false) final DateTime? sendingTime; +//ExtraDetails for anything else that can be included + final Map _extraDetails; +//ExtraDetails for anything else that can be included + @override + @JsonKey(fromJson: customMapFromJson, toJson: customMapToJson) + Map get extraDetails { + if (_extraDetails is EqualUnmodifiableMapView) return _extraDetails; + // ignore: implicit_dynamic_type + return EqualUnmodifiableMapView(_extraDetails); + } @override String toString() { - return 'RequestModel(id: $id, apiType: $apiType, name: $name, description: $description, requestTabIndex: $requestTabIndex, httpRequestModel: $httpRequestModel, responseStatus: $responseStatus, message: $message, httpResponseModel: $httpResponseModel, isWorking: $isWorking, sendingTime: $sendingTime)'; + return 'RequestModel(id: $id, apiType: $apiType, name: $name, description: $description, requestTabIndex: $requestTabIndex, httpRequestModel: $httpRequestModel, responseStatus: $responseStatus, message: $message, httpResponseModel: $httpResponseModel, isWorking: $isWorking, sendingTime: $sendingTime, extraDetails: $extraDetails)'; } @override @@ -349,7 +379,9 @@ class _$RequestModelImpl implements _RequestModel { (identical(other.isWorking, isWorking) || other.isWorking == isWorking) && (identical(other.sendingTime, sendingTime) || - other.sendingTime == sendingTime)); + other.sendingTime == sendingTime) && + const DeepCollectionEquality() + .equals(other._extraDetails, _extraDetails)); } @JsonKey(includeFromJson: false, includeToJson: false) @@ -366,7 +398,8 @@ class _$RequestModelImpl implements _RequestModel { message, httpResponseModel, isWorking, - sendingTime); + sendingTime, + const DeepCollectionEquality().hash(_extraDetails)); /// Create a copy of RequestModel /// with the given fields replaced by the non-null parameter values. @@ -386,18 +419,19 @@ class _$RequestModelImpl implements _RequestModel { abstract class _RequestModel implements RequestModel { const factory _RequestModel( - {required final String id, - final APIType apiType, - final String name, - final String description, - @JsonKey(includeToJson: false) final dynamic requestTabIndex, - final HttpRequestModel? httpRequestModel, - final int? responseStatus, - final String? message, - final HttpResponseModel? httpResponseModel, - @JsonKey(includeToJson: false) final bool isWorking, - @JsonKey(includeToJson: false) final DateTime? sendingTime}) = - _$RequestModelImpl; + {required final String id, + final APIType apiType, + final String name, + final String description, + @JsonKey(includeToJson: false) final dynamic requestTabIndex, + final HttpRequestModel? httpRequestModel, + final int? responseStatus, + final String? message, + final HttpResponseModel? httpResponseModel, + @JsonKey(includeToJson: false) final bool isWorking, + @JsonKey(includeToJson: false) final DateTime? sendingTime, + @JsonKey(fromJson: customMapFromJson, toJson: customMapToJson) + final Map extraDetails}) = _$RequestModelImpl; factory _RequestModel.fromJson(Map json) = _$RequestModelImpl.fromJson; @@ -426,7 +460,11 @@ abstract class _RequestModel implements RequestModel { bool get isWorking; @override @JsonKey(includeToJson: false) - DateTime? get sendingTime; + DateTime? + get sendingTime; //ExtraDetails for anything else that can be included + @override + @JsonKey(fromJson: customMapFromJson, toJson: customMapToJson) + Map get extraDetails; /// Create a copy of RequestModel /// with the given fields replaced by the non-null parameter values. diff --git a/lib/models/request_model.g.dart b/lib/models/request_model.g.dart index 68b563953..52c91e66c 100644 --- a/lib/models/request_model.g.dart +++ b/lib/models/request_model.g.dart @@ -27,6 +27,9 @@ _$RequestModelImpl _$$RequestModelImplFromJson(Map json) => _$RequestModelImpl( sendingTime: json['sendingTime'] == null ? null : DateTime.parse(json['sendingTime'] as String), + extraDetails: json['extraDetails'] == null + ? const {} + : customMapFromJson(json['extraDetails'] as Map), ); Map _$$RequestModelImplToJson(_$RequestModelImpl instance) => @@ -39,9 +42,11 @@ Map _$$RequestModelImplToJson(_$RequestModelImpl instance) => 'responseStatus': instance.responseStatus, 'message': instance.message, 'httpResponseModel': instance.httpResponseModel?.toJson(), + 'extraDetails': customMapToJson(instance.extraDetails), }; const _$APITypeEnumMap = { APIType.rest: 'rest', + APIType.ai: 'ai', APIType.graphql: 'graphql', }; diff --git a/lib/models/settings_model.dart b/lib/models/settings_model.dart index a06b1e591..0149ba9c8 100644 --- a/lib/models/settings_model.dart +++ b/lib/models/settings_model.dart @@ -18,6 +18,8 @@ class SettingsModel { this.workspaceFolderPath, this.isSSLDisabled = false, this.isDashBotEnabled = true, + this.defaultLLMProvider = 'llama3_local', + this.defaultLLMProviderCredentials = '', }); final bool isDark; @@ -33,6 +35,8 @@ class SettingsModel { final String? workspaceFolderPath; final bool isSSLDisabled; final bool isDashBotEnabled; + final String defaultLLMProvider; + final String defaultLLMProviderCredentials; SettingsModel copyWith({ bool? isDark, @@ -48,6 +52,8 @@ class SettingsModel { String? workspaceFolderPath, bool? isSSLDisabled, bool? isDashBotEnabled, + String? defaultLLMProvider, + String? defaultLLMProviderCredentials, }) { return SettingsModel( isDark: isDark ?? this.isDark, @@ -65,6 +71,9 @@ class SettingsModel { workspaceFolderPath: workspaceFolderPath ?? this.workspaceFolderPath, isSSLDisabled: isSSLDisabled ?? this.isSSLDisabled, isDashBotEnabled: isDashBotEnabled ?? this.isDashBotEnabled, + defaultLLMProvider: defaultLLMProvider ?? this.defaultLLMProvider, + defaultLLMProviderCredentials: + defaultLLMProviderCredentials ?? this.defaultLLMProviderCredentials, ); } @@ -85,6 +94,8 @@ class SettingsModel { workspaceFolderPath: workspaceFolderPath, isSSLDisabled: isSSLDisabled, isDashBotEnabled: isDashBotEnabled, + defaultLLMProvider: defaultLLMProvider, + defaultLLMProviderCredentials: defaultLLMProviderCredentials, ); } @@ -124,6 +135,20 @@ class SettingsModel { // pass } } + + final defaultLLMProvider = data["defaultLLMProvider"] as String?; + + final defaultLLMProviderCredentialsStr = + data['defaultLLMProviderCredentials'] as String?; + String? defaultLLMProviderCredentials; + if (defaultLLMProviderCredentialsStr != null) { + try { + defaultLLMProviderCredentials = defaultLLMProviderCredentialsStr; + } catch (e) { + // pass + } + } + final saveResponses = data["saveResponses"] as bool?; final promptBeforeClosing = data["promptBeforeClosing"] as bool?; final activeEnvironmentId = data["activeEnvironmentId"] as String?; @@ -158,6 +183,8 @@ class SettingsModel { workspaceFolderPath: workspaceFolderPath, isSSLDisabled: isSSLDisabled, isDashBotEnabled: isDashBotEnabled, + defaultLLMProvider: defaultLLMProvider, + defaultLLMProviderCredentials: defaultLLMProviderCredentials, ); } @@ -178,6 +205,8 @@ class SettingsModel { "workspaceFolderPath": workspaceFolderPath, "isSSLDisabled": isSSLDisabled, "isDashBotEnabled": isDashBotEnabled, + "defaultLLMProvider": defaultLLMProvider, + "defaultLLMProviderCredentials": defaultLLMProviderCredentials, }; } @@ -203,7 +232,9 @@ class SettingsModel { other.historyRetentionPeriod == historyRetentionPeriod && other.workspaceFolderPath == workspaceFolderPath && other.isSSLDisabled == isSSLDisabled && - other.isDashBotEnabled == isDashBotEnabled; + other.isDashBotEnabled == isDashBotEnabled && + other.defaultLLMProvider == defaultLLMProvider && + other.defaultLLMProviderCredentials == defaultLLMProviderCredentials; } @override @@ -223,6 +254,8 @@ class SettingsModel { workspaceFolderPath, isSSLDisabled, isDashBotEnabled, + defaultLLMProvider, + defaultLLMProviderCredentials, ); } } diff --git a/lib/providers/ai_request_providers.dart b/lib/providers/ai_request_providers.dart new file mode 100644 index 000000000..047ed534a --- /dev/null +++ b/lib/providers/ai_request_providers.dart @@ -0,0 +1,28 @@ +import 'package:apidash/providers/collection_providers.dart'; +import 'package:apidash/utils/file_utils.dart'; +import 'package:apidash_core/consts.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +final selectedAIRequestTypeProvider = StateProvider((ref) => null); +final aiRequestCollection = + StateProvider>((ref) => {}); + +class AIRequestModel { + final String id; + final String modelIdentifier; + + AIRequestModel({ + required this.id, + required this.modelIdentifier, + }); +} + +final selectedAIRequestModelProvider = StateProvider((ref) { + final selectedId = ref.watch(selectedIdStateProvider); + final collection = ref.watch(aiRequestCollection); + if (selectedId == null || collection.isEmpty) { + return null; + } else { + return collection[selectedId]; + } +}); diff --git a/lib/providers/collection_providers.dart b/lib/providers/collection_providers.dart index 35bc4aa0c..f2e63b1e1 100644 --- a/lib/providers/collection_providers.dart +++ b/lib/providers/collection_providers.dart @@ -1,3 +1,6 @@ +import 'dart:convert'; + +import 'package:apidash/models/llm_models/llm_model.dart'; import 'package:apidash_core/apidash_core.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; @@ -223,6 +226,7 @@ class CollectionStateNotifier int? responseStatus, String? message, HttpResponseModel? httpResponseModel, + Map? extraDetails, }) { final rId = id ?? ref.read(selectedIdStateProvider); if (rId == null) { @@ -251,6 +255,7 @@ class CollectionStateNotifier query: query ?? currentHttpRequestModel.query, formData: formData ?? currentHttpRequestModel.formData, ), + extraDetails: extraDetails ?? currentModel.extraDetails, responseStatus: responseStatus ?? currentModel.responseStatus, message: message ?? currentModel.message, httpResponseModel: httpResponseModel ?? currentModel.httpResponseModel, @@ -280,6 +285,13 @@ class CollectionStateNotifier HttpRequestModel substitutedHttpRequestModel = getSubstitutedHttpRequestModel(requestModel.httpRequestModel!); + if (apiType == APIType.ai) { + substitutedHttpRequestModel = getSubstitutedHttpRequestModelForAIRequest( + requestModel, + substitutedHttpRequestModel, + ); + } + // set current model's isWorking to true and update state var map = {...state!}; map[requestId] = requestModel.copyWith( @@ -305,10 +317,11 @@ class CollectionStateNotifier isWorking: false, ); } else { - final httpResponseModel = baseHttpResponseModel.fromResponse( + HttpResponseModel httpResponseModel = baseHttpResponseModel.fromResponse( response: responseRec.$1!, time: responseRec.$2!, ); + int statusCode = responseRec.$1!.statusCode; newRequestModel = requestModel.copyWith( responseStatus: statusCode, @@ -331,6 +344,7 @@ class CollectionStateNotifier ), httpRequestModel: substitutedHttpRequestModel, httpResponseModel: httpResponseModel, + extraDetails: requestModel!.extraDetails, ); ref.read(historyMetaStateNotifier.notifier).addHistoryRequest(model); } @@ -427,4 +441,33 @@ class CollectionStateNotifier activeEnvId, ); } + + HttpRequestModel getSubstitutedHttpRequestModelForAIRequest( + RequestModel requestModel, + HttpRequestModel httpRequestModel, + ) { + final LLMModel aiModel = requestModel.extraDetails['model']!; + final reqData = aiModel.getRequestPayload( + systemPrompt: requestModel.extraDetails['system_prompt']!, + userPrompt: requestModel.extraDetails['user_prompt']!, + credential: requestModel.extraDetails['authorization_credential'] ?? '', + ); + //Substitute POST + httpRequestModel = httpRequestModel.copyWith(method: HTTPVerb.post); + //Substitute URL + httpRequestModel = httpRequestModel.copyWith(url: reqData['url']!); + //Substitute Payload + httpRequestModel = + httpRequestModel.copyWith(body: jsonEncode(reqData['payload']!)); + //Substitute Headers + final headers = { + ...(reqData['headers'] ?? {}), + ...aiModel.specifics.headers, + }; + httpRequestModel = httpRequestModel.copyWith(headers: [ + ...headers.entries + .map((x) => NameValueModel(name: x.key, value: x.value)), + ]); + return httpRequestModel; + } } diff --git a/lib/providers/settings_providers.dart b/lib/providers/settings_providers.dart index d3cb9f2fc..867b40ae2 100644 --- a/lib/providers/settings_providers.dart +++ b/lib/providers/settings_providers.dart @@ -8,6 +8,12 @@ import '../consts.dart'; final codegenLanguageStateProvider = StateProvider((ref) => ref.watch(settingsProvider.select((value) => value.defaultCodeGenLang))); +final llmProviderStateProvider = StateProvider((ref) => + ref.watch(settingsProvider.select((value) => value.defaultLLMProvider))); + +final llmProviderCredentialsProvider = StateProvider((ref) => ref.watch( + settingsProvider.select((value) => value.defaultLLMProviderCredentials))); + final activeEnvironmentIdStateProvider = StateProvider((ref) => ref.watch(settingsProvider.select((value) => value.activeEnvironmentId))); @@ -34,6 +40,8 @@ class ThemeStateNotifier extends StateNotifier { String? workspaceFolderPath, bool? isSSLDisabled, bool? isDashBotEnabled, + String? defaultLLMProvider, + String? defaultLLMProviderCredentials, }) async { state = state.copyWith( isDark: isDark, @@ -49,6 +57,8 @@ class ThemeStateNotifier extends StateNotifier { workspaceFolderPath: workspaceFolderPath, isSSLDisabled: isSSLDisabled, isDashBotEnabled: isDashBotEnabled, + defaultLLMProvider: defaultLLMProvider, + defaultLLMProviderCredentials: defaultLLMProviderCredentials, ); await setSettingsToSharedPrefs(state); } diff --git a/lib/screens/common_widgets/api_type_dropdown.dart b/lib/screens/common_widgets/api_type_dropdown.dart index c35645d6e..cbcb7e227 100644 --- a/lib/screens/common_widgets/api_type_dropdown.dart +++ b/lib/screens/common_widgets/api_type_dropdown.dart @@ -1,3 +1,7 @@ +import 'package:apidash/models/llm_models/all_models.dart'; +import 'package:apidash/models/llm_models/google/gemini_20_flash.dart'; +import 'package:apidash/models/llm_models/llm_model.dart'; +import 'package:apidash_core/consts.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:apidash/providers/providers.dart'; @@ -17,6 +21,39 @@ class APITypeDropdown extends ConsumerWidget { ref .read(collectionStateNotifierProvider.notifier) .update(apiType: type); + + if (type == APIType.ai) { + //-------------------Setting Default Model-------------------- + final eD = ref + .read(collectionStateNotifierProvider.select( + (value) => value![ref.read(selectedIdStateProvider)!]))! + .extraDetails; + + final cDm = ref.read(settingsProvider).defaultLLMProvider; + LLMModel? defaultModel; + String? authCred; + if (cDm.isEmpty) { + defaultModel = Gemini20FlashModel(); //DEFAULT_MODEL + } else { + authCred = ref.read(settingsProvider).defaultLLMProviderCredentials; + defaultModel = getLLMModelFromID(cDm)!; + } + //-------------------Setting Default Model-------------------- + ref.read(collectionStateNotifierProvider.notifier).update( + extraDetails: { + ...eD, + 'model': defaultModel, + if (authCred != null) ...{ + 'authorization_credential': authCred, + } + }, + ); + // Update the Internal URL to Model URL + ref + .read(collectionStateNotifierProvider.notifier) + .update(url: defaultModel.specifics.endpoint); + // print('setting url -> ${defaultModel.specifics.endpoint}'); + } }, ); } diff --git a/lib/screens/common_widgets/code_pane.dart b/lib/screens/common_widgets/code_pane.dart index 654b94403..54cd0fd0a 100644 --- a/lib/screens/common_widgets/code_pane.dart +++ b/lib/screens/common_widgets/code_pane.dart @@ -47,6 +47,12 @@ class CodePane extends ConsumerWidget { message: "Code generation for GraphQL is currently not available.", ); } + + if (substitutedRequestModel.apiType == APIType.ai) { + return const ErrorMessage( + message: "Code generation for AI Requests is currently not available.", + ); + } if (code == null) { return const ErrorMessage( message: "An error was encountered while generating code. $kRaiseIssue", diff --git a/lib/screens/history/history_widgets/ai_history.dart b/lib/screens/history/history_widgets/ai_history.dart new file mode 100644 index 000000000..c87fa93ef --- /dev/null +++ b/lib/screens/history/history_widgets/ai_history.dart @@ -0,0 +1,208 @@ +import 'package:apidash/models/llm_models/llm_config.dart'; +import 'package:apidash/models/llm_models/llm_model.dart'; +import 'package:apidash/providers/collection_providers.dart'; +import 'package:apidash/providers/history_providers.dart'; +import 'package:apidash/widgets/editor.dart'; +import 'package:apidash_design_system/tokens/measurements.dart'; +import 'package:apidash_design_system/widgets/textfield_outlined.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +class HisAIPromptsSection extends ConsumerWidget { + const HisAIPromptsSection({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final selectedHistoryModel = ref.watch(selectedHistoryRequestModelProvider); + final reqDetails = selectedHistoryModel!.extraDetails; + + final systemPrompt = reqDetails['system_prompt']; + final userPrompt = reqDetails['user_prompt']; + return Container( + padding: EdgeInsets.symmetric(vertical: 20), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.only(left: 25.0), + child: Text( + 'System Prompt', + style: TextStyle(color: Colors.white54), + ), + ), + kVSpacer10, + Expanded( + child: Container( + padding: EdgeInsets.symmetric(horizontal: 20), + child: TextFieldEditor( + key: Key( + "${selectedHistoryModel?.historyId}-aireq-sysprompt-body"), + fieldKey: + "${selectedHistoryModel?.historyId}-aireq-sysprompt-body", + initialValue: systemPrompt, + readOnly: true, + ), + ), + ), + SizedBox(height: 10), + Padding( + padding: const EdgeInsets.only(left: 25.0), + child: Text( + 'User Prompt / Input', + style: TextStyle(color: Colors.white54), + ), + ), + kVSpacer10, + Expanded( + child: Container( + padding: EdgeInsets.symmetric(horizontal: 20), + child: TextFieldEditor( + key: Key( + "${selectedHistoryModel?.historyId}-aireq-userprompt-body"), + fieldKey: + "${selectedHistoryModel?.historyId}-aireq-userprompt-body", + initialValue: userPrompt, + readOnly: true, + ), + ), + ), + ], + ), + ); + } +} + +class HisAIAuthorizationSection extends ConsumerWidget { + const HisAIAuthorizationSection({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final selectedHistoryModel = ref.watch(selectedHistoryRequestModelProvider); + final reqDetails = selectedHistoryModel!.extraDetails; + + final iV = reqDetails['authorization_credential']; + return Container( + padding: EdgeInsets.symmetric(vertical: 20), + child: Column( + children: [ + Expanded( + child: Container( + padding: EdgeInsets.symmetric(horizontal: 20), + child: TextFieldEditor( + key: Key( + "${selectedHistoryModel.historyId}-aireq-authvalue-body"), + fieldKey: + "${selectedHistoryModel.historyId}-aireq-authvalue-body", + initialValue: iV, + readOnly: true, + hintText: 'credential', + ), + ), + ), + ], + ), + ); + } +} + +class HisAIConfigsSection extends ConsumerStatefulWidget { + const HisAIConfigsSection({super.key}); + + @override + ConsumerState createState() => + _HisAIConfigsSectionState(); +} + +class _HisAIConfigsSectionState extends ConsumerState { + @override + Widget build(BuildContext context) { + final selectedHistoryModel = ref.watch(selectedHistoryRequestModelProvider); + final reqDetails = selectedHistoryModel!.extraDetails; + + final LLMModel model = reqDetails['model']!; + + return SingleChildScrollView( + padding: EdgeInsets.symmetric(vertical: 20), + child: Column( + key: ValueKey(selectedHistoryModel.historyId), + children: [ + ...model.configurations.values + .map( + (el) => ListTile( + title: Text(el.configName), + subtitle: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + el.configDescription, + style: TextStyle(color: Colors.white30), + ), + SizedBox(height: 5), + if (el.configType == + LLMModelConfigurationType.boolean) ...[ + Switch( + value: el.configValue.value as bool, + onChanged: (x) {}, + ) + ] else if (el.configType == + LLMModelConfigurationType.numeric) ...[ + ADOutlinedTextField( + initialValue: el.configValue.value.toString(), + readOnly: true, + ) + ] else if (el.configType == + LLMModelConfigurationType.text) ...[ + ADOutlinedTextField( + initialValue: el.configValue.value.toString(), + readOnly: true, + ) + ] else if (el.configType == + LLMModelConfigurationType.slider) ...[ + Row( + children: [ + Expanded( + child: Slider( + min: (el.configValue.value as ( + double, + double, + double + )) + .$1, + value: (el.configValue.value as ( + double, + double, + double + )) + .$2, + max: (el.configValue.value as ( + double, + double, + double + )) + .$3, + onChanged: (x) {}, + ), + ), + Text((el.configValue.value as ( + double, + double, + double + )) + .$2 + .toStringAsFixed(2)), + ], + ) + ], + SizedBox(height: 10), + // Divider(color: Colors.white10), + // SizedBox(height: 10), + ], + ), + ), + ) + .toList(), + ], + ), + ); + } +} diff --git a/lib/screens/history/history_widgets/his_request_pane.dart b/lib/screens/history/history_widgets/his_request_pane.dart index f14350632..917944fd8 100644 --- a/lib/screens/history/history_widgets/his_request_pane.dart +++ b/lib/screens/history/history_widgets/his_request_pane.dart @@ -1,3 +1,4 @@ +import 'package:apidash/screens/history/history_widgets/ai_history.dart'; import 'package:apidash_core/apidash_core.dart'; import 'package:apidash_design_system/apidash_design_system.dart'; import 'package:flutter/material.dart'; @@ -95,6 +96,27 @@ class HistoryRequestPane extends ConsumerWidget { const HisRequestBody(), ], ), + APIType.ai => RequestPane( + key: const Key("history-request-pane-ai"), + selectedId: selectedId, + codePaneVisible: codePaneVisible, + onPressedCodeButton: () { + ref.read(historyCodePaneVisibleStateProvider.notifier).state = + !codePaneVisible; + }, + showViewCodeButton: !isCompact, + showIndicators: [ + true, + false, + false, + ], + tabLabels: const ["Prompts", "Authorization", "Configuration"], + children: [ + const HisAIPromptsSection(), + const HisAIAuthorizationSection(), + const HisAIConfigsSection(), + ], + ), _ => kSizedBoxEmpty, }; } diff --git a/lib/screens/home_page/editor_pane/details_card/request_pane/ai_request_contents/aireq_authorization.dart b/lib/screens/home_page/editor_pane/details_card/request_pane/ai_request_contents/aireq_authorization.dart new file mode 100644 index 000000000..39be09ebb --- /dev/null +++ b/lib/screens/home_page/editor_pane/details_card/request_pane/ai_request_contents/aireq_authorization.dart @@ -0,0 +1,44 @@ +import 'package:apidash/providers/collection_providers.dart'; +import 'package:apidash/widgets/editor.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +class AIRequestAuthorizationSection extends ConsumerWidget { + const AIRequestAuthorizationSection({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final selectedId = ref.watch(selectedIdStateProvider); + final reqDetails = ref + .watch(collectionStateNotifierProvider + .select((value) => value![ref.read(selectedIdStateProvider)!]))! + .extraDetails; + final iV = reqDetails['authorization_credential']; + return Container( + padding: EdgeInsets.symmetric(vertical: 20), + child: Column( + children: [ + Expanded( + child: Container( + padding: EdgeInsets.symmetric(horizontal: 20), + child: TextFieldEditor( + key: Key("$selectedId-aireq-authvalue-body"), + fieldKey: "$selectedId-aireq-authvalue-body", + initialValue: iV, + onChanged: (String value) { + ref.read(collectionStateNotifierProvider.notifier).update( + extraDetails: { + ...reqDetails, + 'authorization_credential': value + }, + ); + }, + hintText: 'Enter API key or Authorization Credentials', + ), + ), + ), + ], + ), + ); + } +} diff --git a/lib/screens/home_page/editor_pane/details_card/request_pane/ai_request_contents/aireq_configs.dart b/lib/screens/home_page/editor_pane/details_card/request_pane/ai_request_contents/aireq_configs.dart new file mode 100644 index 000000000..eda4bbcbe --- /dev/null +++ b/lib/screens/home_page/editor_pane/details_card/request_pane/ai_request_contents/aireq_configs.dart @@ -0,0 +1,170 @@ +import 'package:apidash/models/llm_models/google/gemini_20_flash.dart'; +import 'package:apidash/models/llm_models/llm_config.dart'; +import 'package:apidash/models/llm_models/llm_model.dart'; +import 'package:apidash/providers/collection_providers.dart'; +import 'package:apidash/widgets/editor.dart'; +import 'package:apidash_design_system/widgets/textfield_outlined.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +class AIRequestConfigSection extends ConsumerStatefulWidget { + const AIRequestConfigSection({super.key}); + + @override + ConsumerState createState() => + _AIRequestConfigSectionState(); +} + +class _AIRequestConfigSectionState + extends ConsumerState { + @override + Widget build(BuildContext context) { + final selectedId = ref.watch(selectedIdStateProvider); + final reqDetails = ref + .watch(collectionStateNotifierProvider + .select((value) => value![ref.read(selectedIdStateProvider)!]))! + .extraDetails; + + final LLMModel model = reqDetails['model']!; + + return SingleChildScrollView( + padding: EdgeInsets.symmetric(vertical: 20), + child: Column( + key: ValueKey(selectedId), + children: [ + ...model.configurations.values + .map( + (el) => ListTile( + title: Text(el.configName), + subtitle: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + el.configDescription, + style: TextStyle(color: Colors.white30), + ), + SizedBox(height: 5), + if (el.configType == + LLMModelConfigurationType.boolean) ...[ + Switch( + value: el.configValue.value as bool, + onChanged: (x) { + el.configValue.value = x; + ref + .read(collectionStateNotifierProvider.notifier) + .update( + extraDetails: { + ...reqDetails, + el.configId: x, + 'model': model, + }, + ); + setState(() {}); + }, + ) + ] else if (el.configType == + LLMModelConfigurationType.numeric) ...[ + ADOutlinedTextField( + initialValue: el.configValue.value.toString(), + onChanged: (x) { + if (x.isEmpty) x = '0'; + if (num.tryParse(x) == null) return; + el.configValue.value = num.parse(x); + ref + .read(collectionStateNotifierProvider.notifier) + .update( + extraDetails: { + ...reqDetails, + el.configId: num.parse(x), + 'model': model, + }, + ); + setState(() {}); + }, + ) + ] else if (el.configType == + LLMModelConfigurationType.text) ...[ + ADOutlinedTextField( + initialValue: el.configValue.value.toString(), + onChanged: (x) { + el.configValue.value = x; + ref + .read(collectionStateNotifierProvider.notifier) + .update( + extraDetails: { + ...reqDetails, + el.configId: x, + 'model': model, + }, + ); + setState(() {}); + }, + ) + ] else if (el.configType == + LLMModelConfigurationType.slider) ...[ + Row( + children: [ + Expanded( + child: Slider( + min: (el.configValue.value as ( + double, + double, + double + )) + .$1, + value: (el.configValue.value as ( + double, + double, + double + )) + .$2, + max: (el.configValue.value as ( + double, + double, + double + )) + .$3, + onChanged: (x) { + final z = el.configValue.value as ( + double, + double, + double + ); + el.configValue.value = (z.$1, x, z.$3); + ref + .read(collectionStateNotifierProvider + .notifier) + .update( + extraDetails: { + ...reqDetails, + el.configId: x, + 'model': model, + }, + ); + setState(() {}); + }, + ), + ), + Text((el.configValue.value as ( + double, + double, + double + )) + .$2 + .toStringAsFixed(2)), + ], + ) + ], + SizedBox(height: 10), + // Divider(color: Colors.white10), + // SizedBox(height: 10), + ], + ), + ), + ) + .toList(), + ], + ), + ); + } +} diff --git a/lib/screens/home_page/editor_pane/details_card/request_pane/ai_request_contents/aireq_prompt.dart b/lib/screens/home_page/editor_pane/details_card/request_pane/ai_request_contents/aireq_prompt.dart new file mode 100644 index 000000000..65dfb6316 --- /dev/null +++ b/lib/screens/home_page/editor_pane/details_card/request_pane/ai_request_contents/aireq_prompt.dart @@ -0,0 +1,78 @@ +import 'package:apidash/providers/collection_providers.dart'; +import 'package:apidash/widgets/editor.dart'; +import 'package:apidash_design_system/tokens/measurements.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +class AIRequestPromptSection extends ConsumerWidget { + const AIRequestPromptSection({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final selectedId = ref.watch(selectedIdStateProvider); + final reqDetails = ref + .watch(collectionStateNotifierProvider + .select((value) => value![ref.read(selectedIdStateProvider)!]))! + .extraDetails; + + final systemPrompt = reqDetails['system_prompt']; + final userPrompt = reqDetails['user_prompt']; + return Container( + padding: EdgeInsets.symmetric(vertical: 20), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.only(left: 25.0), + child: Text( + 'System Prompt', + style: TextStyle(color: Colors.white54), + ), + ), + kVSpacer10, + Expanded( + child: Container( + padding: EdgeInsets.symmetric(horizontal: 20), + child: TextFieldEditor( + key: Key("$selectedId-aireq-sysprompt-body"), + fieldKey: "$selectedId-aireq-sysprompt-body", + initialValue: systemPrompt, + onChanged: (String value) { + ref.read(collectionStateNotifierProvider.notifier).update( + extraDetails: {...reqDetails, 'system_prompt': value}, + ); + }, + hintText: 'Enter System Prompt', + ), + ), + ), + SizedBox(height: 10), + Padding( + padding: const EdgeInsets.only(left: 25.0), + child: Text( + 'User Prompt / Input', + style: TextStyle(color: Colors.white54), + ), + ), + kVSpacer10, + Expanded( + child: Container( + padding: EdgeInsets.symmetric(horizontal: 20), + child: TextFieldEditor( + key: Key("$selectedId-aireq-userprompt-body"), + fieldKey: "$selectedId-aireq-userprompt-body", + initialValue: userPrompt, + onChanged: (String value) { + ref.read(collectionStateNotifierProvider.notifier).update( + extraDetails: {...reqDetails, 'user_prompt': value}, + ); + }, + hintText: 'Enter User Prompt', + ), + ), + ), + ], + ), + ); + } +} diff --git a/lib/screens/home_page/editor_pane/details_card/request_pane/request_body.dart b/lib/screens/home_page/editor_pane/details_card/request_pane/request_body.dart index e192f26ee..91d80e419 100644 --- a/lib/screens/home_page/editor_pane/details_card/request_pane/request_body.dart +++ b/lib/screens/home_page/editor_pane/details_card/request_pane/request_body.dart @@ -92,6 +92,7 @@ class EditRequestBody extends ConsumerWidget { ), ), ), + APIType.ai => FlutterLogo(), _ => kSizedBoxEmpty, } ], diff --git a/lib/screens/home_page/editor_pane/details_card/request_pane/request_pane.dart b/lib/screens/home_page/editor_pane/details_card/request_pane/request_pane.dart index 9c852c71b..7bbb2dcc5 100644 --- a/lib/screens/home_page/editor_pane/details_card/request_pane/request_pane.dart +++ b/lib/screens/home_page/editor_pane/details_card/request_pane/request_pane.dart @@ -1,3 +1,4 @@ +import 'package:apidash/screens/home_page/editor_pane/details_card/request_pane/request_pane_ai.dart'; import 'package:apidash_core/apidash_core.dart'; import 'package:apidash_design_system/apidash_design_system.dart'; import 'package:flutter/material.dart'; @@ -17,6 +18,7 @@ class EditRequestPane extends ConsumerWidget { return switch (apiType) { APIType.rest => const EditRestRequestPane(), APIType.graphql => const EditGraphQLRequestPane(), + APIType.ai => const EditAIRequestPane(), _ => kSizedBoxEmpty, }; } diff --git a/lib/screens/home_page/editor_pane/details_card/request_pane/request_pane_ai.dart b/lib/screens/home_page/editor_pane/details_card/request_pane/request_pane_ai.dart new file mode 100644 index 000000000..9af66288f --- /dev/null +++ b/lib/screens/home_page/editor_pane/details_card/request_pane/request_pane_ai.dart @@ -0,0 +1,63 @@ +import 'package:apidash/consts.dart'; +import 'package:apidash/screens/home_page/editor_pane/details_card/request_pane/ai_request_contents/aireq_authorization.dart'; +import 'package:apidash/screens/home_page/editor_pane/details_card/request_pane/ai_request_contents/aireq_configs.dart'; +import 'package:apidash/screens/home_page/editor_pane/details_card/request_pane/ai_request_contents/aireq_prompt.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:apidash/providers/providers.dart'; +import 'package:apidash/widgets/widgets.dart'; +import 'request_headers.dart'; +import 'request_params.dart'; +import 'request_body.dart'; + +class EditAIRequestPane extends ConsumerWidget { + const EditAIRequestPane({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final selectedId = ref.watch(selectedIdStateProvider); + final codePaneVisible = ref.watch(codePaneVisibleStateProvider); + final tabIndex = ref.watch( + selectedRequestModelProvider.select((value) => value?.requestTabIndex)); + + final headerLength = ref.watch(selectedRequestModelProvider + .select((value) => value?.httpRequestModel?.headersMap.length)) ?? + 0; + final paramLength = ref.watch(selectedRequestModelProvider + .select((value) => value?.httpRequestModel?.paramsMap.length)) ?? + 0; + final hasBody = ref.watch(selectedRequestModelProvider + .select((value) => value?.httpRequestModel?.hasBody)) ?? + false; + + return RequestPane( + selectedId: selectedId, + codePaneVisible: false, + tabIndex: tabIndex, + onPressedCodeButton: () { + ref.read(codePaneVisibleStateProvider.notifier).state = + !codePaneVisible; + }, + onTapTabBar: (index) { + ref + .read(collectionStateNotifierProvider.notifier) + .update(requestTabIndex: index); + }, + showIndicators: [ + paramLength > 0, + headerLength > 0, + hasBody, + ], + tabLabels: const [ + "Prompt", + "Authorization", + "Configurations", + ], + children: const [ + AIRequestPromptSection(), + AIRequestAuthorizationSection(), + AIRequestConfigSection(), + ], + ); + } +} diff --git a/lib/screens/home_page/editor_pane/url_card.dart b/lib/screens/home_page/editor_pane/url_card.dart index 829bc5c97..7c9308588 100644 --- a/lib/screens/home_page/editor_pane/url_card.dart +++ b/lib/screens/home_page/editor_pane/url_card.dart @@ -1,3 +1,8 @@ +import 'package:apidash/models/llm_models/all_models.dart'; +import 'package:apidash/models/llm_models/google/gemini_20_flash.dart'; +import 'package:apidash/models/llm_models/llm_model.dart'; +import 'package:apidash/models/llm_models/openai/azure_openai.dart'; +import 'package:apidash/widgets/dropdown_ai_method.dart'; import 'package:apidash_core/apidash_core.dart'; import 'package:apidash_design_system/apidash_design_system.dart'; import 'package:flutter/material.dart'; @@ -14,6 +19,10 @@ class EditorPaneRequestURLCard extends ConsumerWidget { ref.watch(selectedIdStateProvider); final apiType = ref .watch(selectedRequestModelProvider.select((value) => value?.apiType)); + + final aiModel = ref.watch(selectedRequestModelProvider + .select((value) => value?.extraDetails['model'] as LLMModel?)); + return Card( color: kColorTransparent, surfaceTintColor: kColorTransparent, @@ -35,14 +44,19 @@ class EditorPaneRequestURLCard extends ConsumerWidget { switch (apiType) { APIType.rest => const DropdownButtonHTTPMethod(), APIType.graphql => kSizedBoxEmpty, + APIType.ai => const DropdownButtonAIMethod(), null => kSizedBoxEmpty, }, switch (apiType) { APIType.rest => kHSpacer5, _ => kHSpacer8, }, - const Expanded( - child: URLTextField(), + Expanded( + child: URLTextField( + key: aiModel == null + ? null + : ValueKey(aiModel.modelIdentifier), + ), ), ], ) @@ -51,14 +65,20 @@ class EditorPaneRequestURLCard extends ConsumerWidget { switch (apiType) { APIType.rest => const DropdownButtonHTTPMethod(), APIType.graphql => kSizedBoxEmpty, + APIType.ai => const DropdownButtonAIMethod(), null => kSizedBoxEmpty, }, switch (apiType) { APIType.rest => kHSpacer20, + APIType.ai => kHSpacer20, _ => kHSpacer8, }, - const Expanded( - child: URLTextField(), + Expanded( + child: URLTextField( + key: aiModel == null + ? null + : ValueKey(aiModel.modelIdentifier), + ), ), kHSpacer20, const SizedBox( @@ -99,15 +119,28 @@ class URLTextField extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final selectedId = ref.watch(selectedIdStateProvider); + final selectedId = ref.watch(selectedIdStateProvider)!; return EnvURLField( - selectedId: selectedId!, + selectedId: selectedId, initialValue: ref .read(collectionStateNotifierProvider.notifier) .getRequestModel(selectedId) ?.httpRequestModel ?.url, onChanged: (value) { + final obj = ref.read(collectionStateNotifierProvider)![selectedId]!; + final aT = obj.apiType; + if (aT == APIType.ai) { + final model = obj.extraDetails['model']! as LLMModel; + model.specifics.endpoint = value; + ref.read(collectionStateNotifierProvider.notifier).update( + extraDetails: { + ...obj.extraDetails, + 'model': model, + 'modifed_endpoint': value, + }, + ); + } ref.read(collectionStateNotifierProvider.notifier).update(url: value); }, onFieldSubmitted: (value) { @@ -142,3 +175,35 @@ class SendRequestButton extends ConsumerWidget { ); } } + +class DropdownButtonAIMethod extends ConsumerWidget { + const DropdownButtonAIMethod({ + super.key, + }); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final xtraDetails = ref.watch(selectedRequestModelProvider + .select((value) => value?.extraDetails)) ?? + {}; + + final model = (xtraDetails['model'] as LLMModel?); + final String modelId = model?.modelIdentifier ?? 'gemini_20_flash'; + + return DropdownButtonAiMethod( + method: modelId, + onChanged: (String? value) { + final xD = { + ...xtraDetails, + }; + xD.remove('modifed_endpoint'); + final m = getLLMModelFromID(value!, xD); + xD.addAll({'model': m}); + ref + .read(collectionStateNotifierProvider.notifier) + .update(extraDetails: xD, url: m!.specifics.endpoint); + // print('setting url -> ${m.specifics.endpoint}'); + }, + ); + } +} diff --git a/lib/screens/settings_page.dart b/lib/screens/settings_page.dart index 606ef516a..4662a5aa8 100644 --- a/lib/screens/settings_page.dart +++ b/lib/screens/settings_page.dart @@ -1,3 +1,5 @@ +import 'package:apidash/widgets/popup_menu_llmprovider.dart'; +import 'package:apidash/widgets/textfield_llmprovider_credentials.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; @@ -114,6 +116,35 @@ class SettingsPage extends ConsumerWidget { }, ), ), + ListTile( + hoverColor: kColorTransparent, + title: const Text('Default LLM Provider'), + trailing: LLMProviderPopupMenu( + value: settings.defaultLLMProvider, + onChanged: (value) { + ref + .read(settingsProvider.notifier) + .update(defaultLLMProvider: value); + ref + .read(settingsProvider.notifier) + .update(defaultLLMProviderCredentials: ''); + }, + ), + ), + if (settings.defaultLLMProvider != 'llama3_local') ...[ + ListTile( + hoverColor: kColorTransparent, + title: const Text('Default LLM Provider Credentials'), + trailing: LLMProviderCredentialsTextField( + value: settings.defaultLLMProviderCredentials, + onChanged: (value) { + ref + .read(settingsProvider.notifier) + .update(defaultLLMProviderCredentials: value); + }, + ), + ), + ], CheckboxListTile( title: const Text("Save Responses"), subtitle: diff --git a/lib/utils/ui_utils.dart b/lib/utils/ui_utils.dart index 68eaa82c4..b6dbf75bc 100644 --- a/lib/utils/ui_utils.dart +++ b/lib/utils/ui_utils.dart @@ -36,6 +36,7 @@ Color getAPIColor( method, ), APIType.graphql => kColorGQL, + APIType.ai => kColorSchemeSeed, }; if (brightness == Brightness.dark) { col = getDarkModeColor(col); @@ -57,6 +58,10 @@ Color getHTTPMethodColor(HTTPVerb? method) { return col; } +Color getAIMethodColor(String? modelID) { + return kColorHttpMethodGet; +} + Color getDarkModeColor(Color col) { return Color.alphaBlend( col.withValues(alpha: kOpacityDarkModeBlend), diff --git a/lib/widgets/dropdown_ai_method.dart b/lib/widgets/dropdown_ai_method.dart new file mode 100644 index 000000000..fba926bd8 --- /dev/null +++ b/lib/widgets/dropdown_ai_method.dart @@ -0,0 +1,32 @@ +import 'package:apidash/models/llm_models/all_models.dart'; +import 'package:apidash_core/apidash_core.dart'; +import 'package:apidash_design_system/apidash_design_system.dart'; +import 'package:flutter/material.dart'; +import 'package:apidash/utils/utils.dart'; + +class DropdownButtonAiMethod extends StatelessWidget { + const DropdownButtonAiMethod({ + super.key, + this.method, + this.onChanged, + }); + + final String? method; + final void Function(String? value)? onChanged; + + @override + Widget build(BuildContext context) { + return ADDropdownButton( + value: method, + values: availableModels.entries + .map((e) => (e.key, e.value.$1.modelName.toUpperCase())), + onChanged: onChanged, + dropdownMenuItemPadding: + EdgeInsets.only(left: context.isMediumWindow ? 8 : 16, right: 10), + dropdownMenuItemtextStyle: (String v) => kCodeStyle.copyWith( + fontWeight: FontWeight.bold, + color: Colors.white, + ), + ); + } +} diff --git a/lib/widgets/popup_menu_llmprovider.dart b/lib/widgets/popup_menu_llmprovider.dart new file mode 100644 index 000000000..9f1498593 --- /dev/null +++ b/lib/widgets/popup_menu_llmprovider.dart @@ -0,0 +1,29 @@ +import 'package:apidash/models/llm_models/all_models.dart'; +import 'package:apidash_design_system/apidash_design_system.dart'; +import 'package:flutter/material.dart'; +import 'package:apidash/consts.dart'; + +class LLMProviderPopupMenu extends StatelessWidget { + const LLMProviderPopupMenu({ + super.key, + required this.value, + this.onChanged, + }); + + final String value; + final void Function(String? value)? onChanged; + + @override + Widget build(BuildContext context) { + final double width = context.isCompactWindow ? 150 : 220; + return ADPopupMenu( + value: value, + values: availableModels.entries + .map((e) => (e.key, e.value.$1.modelName.toUpperCase())), + width: width, + tooltip: "Select LLM Provider", + onChanged: onChanged, + isOutlined: true, + ); + } +} diff --git a/lib/widgets/response_body_success.dart b/lib/widgets/response_body_success.dart index 3774b606c..c5f055332 100644 --- a/lib/widgets/response_body_success.dart +++ b/lib/widgets/response_body_success.dart @@ -1,3 +1,7 @@ +import 'dart:convert'; + +import 'package:apidash/models/llm_models/llm_model.dart'; +import 'package:apidash/providers/collection_providers.dart'; import 'package:apidash_core/apidash_core.dart'; import 'package:apidash_design_system/apidash_design_system.dart'; import 'package:flutter/foundation.dart'; @@ -5,9 +9,10 @@ import 'package:flutter/material.dart'; import 'package:apidash/utils/utils.dart'; import 'package:apidash/widgets/widgets.dart'; import 'package:apidash/consts.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'button_share.dart'; -class ResponseBodySuccess extends StatefulWidget { +class ResponseBodySuccess extends ConsumerStatefulWidget { const ResponseBodySuccess( {super.key, required this.mediaType, @@ -23,15 +28,37 @@ class ResponseBodySuccess extends StatefulWidget { final String? formattedBody; final String? highlightLanguage; @override - State createState() => _ResponseBodySuccessState(); + ConsumerState createState() => + _ResponseBodySuccessState(); } -class _ResponseBodySuccessState extends State { +class _ResponseBodySuccessState extends ConsumerState { int segmentIdx = 0; @override Widget build(BuildContext context) { - var currentSeg = widget.options[segmentIdx]; + final obj = ref.watch(collectionStateNotifierProvider + .select((value) => value![ref.read(selectedIdStateProvider)!]))!; + + final options = [...widget.options]; + if (obj.apiType == APIType.ai) { + options.remove(ResponseBodyView.preview); + options.add(ResponseBodyView.answer); + } + options.sort((x, y) => x.label.compareTo(y.label)); + + String outputAnswer; + try { + outputAnswer = (obj.extraDetails['model'] as LLMModel?) + ?.specifics + .outputFormatter(jsonDecode(widget.body)) ?? + 'ERROR'; + } catch (e) { + outputAnswer = ''; + options.remove(ResponseBodyView.answer); + } + + var currentSeg = options[segmentIdx]; var codeTheme = Theme.of(context).brightness == Brightness.light ? kLightCodeTheme : kDarkCodeTheme; @@ -46,7 +73,7 @@ class _ResponseBodySuccessState extends State { return LayoutBuilder( builder: (BuildContext context, BoxConstraints constraints) { var showLabel = showButtonLabelsInBodySuccess( - widget.options.length, + options.length, constraints.maxWidth, ); return Padding( @@ -55,14 +82,14 @@ class _ResponseBodySuccessState extends State { children: [ Row( children: [ - (widget.options == kRawBodyViewOptions) + (options == kRawBodyViewOptions) ? const SizedBox() : SegmentedButton( style: SegmentedButton.styleFrom( padding: const EdgeInsets.symmetric(horizontal: 8), ), selectedIcon: Icon(currentSeg.icon), - segments: widget.options + segments: options .map>( (e) => ButtonSegment( value: e, @@ -77,27 +104,35 @@ class _ResponseBodySuccessState extends State { selected: {currentSeg}, onSelectionChanged: (newSelection) { setState(() { - segmentIdx = - widget.options.indexOf(newSelection.first); + segmentIdx = options.indexOf(newSelection.first); }); }, ), const Spacer(), - ((widget.options == kPreviewRawBodyViewOptions) || - kCodeRawBodyViewOptions.contains(currentSeg)) + ((options == kPreviewRawBodyViewOptions) || + kCodeRawBodyViewOptions.contains(currentSeg) || + obj.apiType == APIType.ai) ? CopyButton( - toCopy: widget.formattedBody ?? widget.body, + toCopy: (currentSeg == ResponseBodyView.answer) + ? outputAnswer! + : widget.formattedBody ?? widget.body, showLabel: showLabel, ) : const SizedBox(), kIsMobile ? ShareButton( - toShare: widget.formattedBody ?? widget.body, + toShare: (currentSeg == ResponseBodyView.answer) + ? outputAnswer! + : widget.formattedBody ?? widget.body, showLabel: showLabel, ) : SaveInDownloadsButton( - content: widget.bytes, - mimeType: widget.mediaType.mimeType, + content: (currentSeg == ResponseBodyView.answer) + ? utf8.encode(outputAnswer!) + : widget.bytes, + mimeType: (currentSeg == ResponseBodyView.answer) + ? 'text/plain' + : widget.mediaType.mimeType, showLabel: showLabel, ), ], @@ -114,7 +149,7 @@ class _ResponseBodySuccessState extends State { body: widget.body, type: widget.mediaType.type, subtype: widget.mediaType.subtype, - hasRaw: widget.options.contains(ResponseBodyView.raw), + hasRaw: options.contains(ResponseBodyView.raw), ), ), ), @@ -144,6 +179,19 @@ class _ResponseBodySuccessState extends State { ), ), ), + ResponseBodyView.answer => Expanded( + child: Container( + width: double.maxFinite, + padding: kP8, + decoration: textContainerdecoration, + child: SingleChildScrollView( + child: SelectableText( + outputAnswer!, + style: kCodeStyle, + ), + ), + ), + ), } ], ), diff --git a/lib/widgets/textfield_llmprovider_credentials.dart b/lib/widgets/textfield_llmprovider_credentials.dart new file mode 100644 index 000000000..5af7da22c --- /dev/null +++ b/lib/widgets/textfield_llmprovider_credentials.dart @@ -0,0 +1,64 @@ +import 'package:apidash_design_system/apidash_design_system.dart'; +import 'package:flutter/material.dart'; +import 'package:apidash/consts.dart'; + +class LLMProviderCredentialsTextField extends StatefulWidget { + const LLMProviderCredentialsTextField({ + super.key, + required this.value, + required this.onChanged, + }); + + final String value; + final void Function(String value) onChanged; + + @override + State createState() => + _LLMProviderCredentialsTextFieldState(); +} + +class _LLMProviderCredentialsTextFieldState + extends State { + TextEditingController controller = TextEditingController(); + @override + void initState() { + controller.text = widget.value; + super.initState(); + } + + @override + void didUpdateWidget(covariant LLMProviderCredentialsTextField oldWidget) { + //Assisting in Resetting on Change + if (widget.value == '') { + controller.text = widget.value; + } + super.didUpdateWidget(oldWidget); + } + + @override + Widget build(BuildContext context) { + final double width = context.isCompactWindow ? 150 : 220; + return Container( + height: 40, + decoration: BoxDecoration( + border: Border.all( + color: Theme.of(context).colorScheme.surfaceContainerHighest, + ), + borderRadius: kBorderRadius8, + ), + width: width, + child: Container( + transform: Matrix4.translationValues(0, -5, 0), + child: TextField( + controller: controller, + obscureText: true, + decoration: InputDecoration( + border: InputBorder.none, + contentPadding: EdgeInsets.only(left: 10), + ), + onChanged: widget.onChanged, + ), + ), + ); + } +} diff --git a/lib/widgets/texts.dart b/lib/widgets/texts.dart index c64a71b27..1ec47eb4f 100644 --- a/lib/widgets/texts.dart +++ b/lib/widgets/texts.dart @@ -20,6 +20,7 @@ class SidebarRequestCardTextBox extends StatelessWidget { switch (apiType) { APIType.rest => method.abbr, APIType.graphql => apiType.abbr, + APIType.ai => apiType.abbr, }, textAlign: TextAlign.center, style: TextStyle( diff --git a/packages/apidash_core/lib/consts.dart b/packages/apidash_core/lib/consts.dart index c1d45ad42..26e5350ea 100644 --- a/packages/apidash_core/lib/consts.dart +++ b/packages/apidash_core/lib/consts.dart @@ -2,6 +2,7 @@ import 'dart:convert'; enum APIType { rest("HTTP", "HTTP"), + ai("AI", "AI"), graphql("GraphQL", "GQL"); const APIType(this.label, this.abbr); diff --git a/packages/apidash_core/lib/services/http_service.dart b/packages/apidash_core/lib/services/http_service.dart index 1b05976a0..0baddb6f2 100644 --- a/packages/apidash_core/lib/services/http_service.dart +++ b/packages/apidash_core/lib/services/http_service.dart @@ -125,6 +125,24 @@ Future<(HttpResponse?, Duration?, String?)> sendHttpRequest( body: body, ); } + if (apiType == APIType.ai) { + var requestBody = requestModel.body; + if (requestBody != null) { + var contentLength = utf8.encode(requestBody).length; + if (contentLength > 0) { + body = requestBody; + headers[HttpHeaders.contentLengthHeader] = contentLength.toString(); + if (!requestModel.hasContentTypeHeader) { + headers[HttpHeaders.contentTypeHeader] = ContentType.json.header; + } + } + } + response = await client.post( + requestUrl, + headers: headers, + body: body, + ); + } stopwatch.stop(); return (response, stopwatch.elapsed, null); } catch (e) { diff --git a/packages/apidash_core/lib/utils/http_request_utils.dart b/packages/apidash_core/lib/utils/http_request_utils.dart index b5eab42cc..c62a6f30b 100644 --- a/packages/apidash_core/lib/utils/http_request_utils.dart +++ b/packages/apidash_core/lib/utils/http_request_utils.dart @@ -1,4 +1,5 @@ import 'package:apidash_core/consts.dart'; +import 'package:flutter/widgets.dart'; import 'package:seed/seed.dart'; import '../models/models.dart'; import 'graphql_utils.dart'; @@ -98,5 +99,7 @@ String? getRequestBody(APIType type, HttpRequestModel httpRequestModel) { ? httpRequestModel.body : null, APIType.graphql => getGraphQLBody(httpRequestModel), + APIType.ai => + httpRequestModel.hasTextData ? httpRequestModel.body?.toString() : null, }; } diff --git a/pubspec.lock b/pubspec.lock index c66a144ab..53053acbb 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -325,7 +325,7 @@ packages: path: "packages/curl_parser" relative: true source: path - version: "0.1.2" + version: "0.1.3" dart_style: dependency: "direct main" description: @@ -358,6 +358,22 @@ packages: url: "https://pub.dev" source: hosted version: "0.5.0" + dio: + dependency: transitive + description: + name: dio + sha256: "253a18bbd4851fecba42f7343a1df3a9a4c1d31a2c1b37e221086b4fa8c8dbc9" + url: "https://pub.dev" + source: hosted + version: "5.8.0+1" + dio_web_adapter: + dependency: transitive + description: + name: dio_web_adapter + sha256: "7586e476d70caecaf1686d21eee7247ea43ef5c345eab9e0cc3583ff13378d78" + url: "https://pub.dev" + source: hosted + version: "2.1.1" equatable: dependency: transitive description: @@ -619,7 +635,7 @@ packages: source: hosted version: "2.5.8" freezed_annotation: - dependency: transitive + dependency: "direct overridden" description: name: freezed_annotation sha256: c2e2d632dd9b8a2b7751117abcfc2b4888ecfe181bd9fca7170d9ef02e595fe2 @@ -913,6 +929,14 @@ packages: url: "https://pub.dev" source: hosted version: "5.1.1" + logger: + dependency: transitive + description: + name: logger + sha256: be4b23575aac7ebf01f225a241eb7f6b5641eeaf43c6a8613510fc2f8cf187d1 + url: "https://pub.dev" + source: hosted + version: "2.5.0" logging: dependency: transitive description: @@ -1524,6 +1548,23 @@ packages: url: "https://pub.dev" source: hosted version: "7.0.0" + stac: + dependency: "direct main" + description: + path: "packages/stac" + ref: migr_010 + resolved-ref: "9c62986e2d501dffca8d131c69065ff6ce609fba" + url: "https://github.com/synapsecode/stac" + source: git + version: "0.9.3" + stac_framework: + dependency: transitive + description: + name: stac_framework + sha256: "58fb26982eaf83626eaaefd4db153306f4f198bb2c4a4d288d97705da4eadac0" + url: "https://pub.dev" + source: hosted + version: "0.2.2" stack_trace: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index fef25a170..0bbc2e8b2 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -14,7 +14,6 @@ dependencies: path: packages/apidash_core apidash_design_system: path: packages/apidash_design_system - carousel_slider: ^5.0.0 code_builder: ^4.10.0 csv: ^6.0.0 data_table_2: 2.5.16 @@ -22,7 +21,7 @@ dependencies: desktop_drop: ^0.5.0 extended_text_field: ^16.0.0 file_selector: ^1.0.3 - flex_color_scheme: ^8.2.0 + flex_color_scheme: ^8.1.1 flutter_highlighter: ^0.1.0 flutter_hooks: ^0.21.2 flutter_markdown: ^0.7.6+2 @@ -70,11 +69,19 @@ dependencies: git: url: https://github.com/google/flutter-desktop-embedding.git path: plugins/window_size + carousel_slider: ^5.0.0 + + stac: + git: + url: https://github.com/synapsecode/stac + path: packages/stac + ref: migr_010 dependency_overrides: extended_text_field: ^16.0.0 pdf_widget_wrapper: ^1.0.4 web: ^1.1.1 + freezed_annotation: ^2.4.1 dev_dependencies: flutter_test: @@ -83,7 +90,7 @@ dev_dependencies: flutter_launcher_icons: ^0.14.3 flutter_lints: ^5.0.0 flutter_native_splash: ^2.4.5 - freezed: ^2.5.7 + freezed: ^2.5.8 json_serializable: ^6.9.4 integration_test: sdk: flutter diff --git a/test/models/settings_model_test.dart b/test/models/settings_model_test.dart index 3a7827d5c..9e525f845 100644 --- a/test/models/settings_model_test.dart +++ b/test/models/settings_model_test.dart @@ -12,6 +12,8 @@ void main() { offset: Offset(100, 150), defaultUriScheme: SupportedUriSchemes.http, defaultCodeGenLang: CodegenLanguage.curl, + defaultLLMProvider: 'llama3_local', + defaultLLMProviderCredentials: '', saveResponses: true, promptBeforeClosing: true, activeEnvironmentId: null, @@ -31,6 +33,8 @@ void main() { "dy": 150.0, "defaultUriScheme": "http", "defaultCodeGenLang": "curl", + "defaultLLMProvider": "ollama", + 'defaultLLMProviderCredentials': '', "saveResponses": true, "promptBeforeClosing": true, "activeEnvironmentId": null, @@ -52,6 +56,8 @@ void main() { "dy": 150.0, "defaultUriScheme": "http", "defaultCodeGenLang": "curl", + "defaultLLMProvider": "ollama", + 'defaultLLMProviderCredentials': '', "saveResponses": true, "promptBeforeClosing": true, "activeEnvironmentId": null, @@ -71,6 +77,8 @@ void main() { offset: Offset(100, 150), defaultUriScheme: SupportedUriSchemes.http, defaultCodeGenLang: CodegenLanguage.curl, + defaultLLMProvider: 'llama3_local', + defaultLLMProviderCredentials: '', saveResponses: false, promptBeforeClosing: true, activeEnvironmentId: null, @@ -98,6 +106,8 @@ void main() { "dy": 150.0, "defaultUriScheme": "http", "defaultCodeGenLang": "curl", + "defaultLLMProvider": "ollama", + 'defaultLLMProviderCredentials': '', "saveResponses": true, "promptBeforeClosing": true, "activeEnvironmentId": null,