diff --git a/lib/consts.dart b/lib/consts.dart index 66972b14..71b88ee7 100644 --- a/lib/consts.dart +++ b/lib/consts.dart @@ -159,6 +159,7 @@ enum ResponseBodyView { preview("Preview", Icons.visibility_rounded), code("Preview", Icons.code_rounded), raw("Raw", Icons.text_snippet_rounded), + answer("Answer", Icons.abc), sse("SSE", Icons.stream), none("Preview", Icons.warning); @@ -183,8 +184,9 @@ const kPreviewCodeRawBodyViewOptions = [ ResponseBodyView.code, ResponseBodyView.raw ]; -const kPreviewSSERawBodyViewOptions = [ - ResponseBodyView.sse, +const kSSERawBodyViewOptions = [ResponseBodyView.sse, ResponseBodyView.raw]; +const kAnswerRawBodyViewOptions = [ + ResponseBodyView.answer, ResponseBodyView.raw ]; @@ -200,15 +202,15 @@ const Map>> kSubTypeYaml: kCodeRawBodyViewOptions, kSubTypeXYaml: kCodeRawBodyViewOptions, kSubTypeYml: kCodeRawBodyViewOptions, - kSubTypeXNdjson: kPreviewSSERawBodyViewOptions, - kSubTypeNdjson: kPreviewSSERawBodyViewOptions, - kSubTypeJsonSeq: kPreviewSSERawBodyViewOptions, - kSubTypeXLdjson: kPreviewSSERawBodyViewOptions, - kSubTypeLdjson: kPreviewSSERawBodyViewOptions, - kSubTypeXJsonStream: kPreviewSSERawBodyViewOptions, - kSubTypeJsonStream: kPreviewSSERawBodyViewOptions, - kSubTypeJsonstream: kPreviewSSERawBodyViewOptions, - kSubTypeStreamJson: kPreviewSSERawBodyViewOptions, + kSubTypeXNdjson: kSSERawBodyViewOptions, + kSubTypeNdjson: kSSERawBodyViewOptions, + kSubTypeJsonSeq: kSSERawBodyViewOptions, + kSubTypeXLdjson: kSSERawBodyViewOptions, + kSubTypeLdjson: kSSERawBodyViewOptions, + kSubTypeXJsonStream: kSSERawBodyViewOptions, + kSubTypeJsonStream: kSSERawBodyViewOptions, + kSubTypeJsonstream: kSSERawBodyViewOptions, + kSubTypeStreamJson: kSSERawBodyViewOptions, }, kTypeImage: { kSubTypeDefaultViewOptions: kPreviewBodyViewOptions, @@ -230,7 +232,7 @@ const Map>> kSubTypeTextXml: kCodeRawBodyViewOptions, kSubTypeTextYaml: kCodeRawBodyViewOptions, kSubTypeTextYml: kCodeRawBodyViewOptions, - kSubTypeEventStream: kPreviewSSERawBodyViewOptions, + kSubTypeEventStream: kSSERawBodyViewOptions, }, }; diff --git a/lib/main.dart b/lib/main.dart index 2b43ca94..c4457ad6 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -9,6 +9,7 @@ import 'app.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); + var settingsModel = await getSettingsFromSharedPrefs(); var onboardingStatus = await getOnboardingStatusFromSharedPrefs(); initializeJsRuntime(); @@ -23,6 +24,9 @@ void main() async { settingsModel = settingsModel?.copyWithPath(workspaceFolderPath: null); } + // TODO: Load all models at init + // await ModelManager.loadAvailableLLMs(); + runApp( ProviderScope( overrides: [ diff --git a/lib/models/history_meta_model.g.dart b/lib/models/history_meta_model.g.dart index da184793..ede68a29 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 f46382a7..1169dc7e 100644 --- a/lib/models/history_request_model.dart +++ b/lib/models/history_request_model.dart @@ -14,7 +14,8 @@ class HistoryRequestModel with _$HistoryRequestModel { const factory HistoryRequestModel({ required String historyId, required HistoryMetaModel metaData, - required HttpRequestModel httpRequestModel, + HttpRequestModel? httpRequestModel, + AIRequestModel? aiRequestModel, required HttpResponseModel httpResponseModel, String? preRequestScript, String? postRequestScript, diff --git a/lib/models/history_request_model.freezed.dart b/lib/models/history_request_model.freezed.dart index 020ea13d..dd553171 100644 --- a/lib/models/history_request_model.freezed.dart +++ b/lib/models/history_request_model.freezed.dart @@ -22,7 +22,8 @@ HistoryRequestModel _$HistoryRequestModelFromJson(Map json) { mixin _$HistoryRequestModel { String get historyId => throw _privateConstructorUsedError; HistoryMetaModel get metaData => throw _privateConstructorUsedError; - HttpRequestModel get httpRequestModel => throw _privateConstructorUsedError; + HttpRequestModel? get httpRequestModel => throw _privateConstructorUsedError; + AIRequestModel? get aiRequestModel => throw _privateConstructorUsedError; HttpResponseModel get httpResponseModel => throw _privateConstructorUsedError; String? get preRequestScript => throw _privateConstructorUsedError; String? get postRequestScript => throw _privateConstructorUsedError; @@ -47,14 +48,16 @@ abstract class $HistoryRequestModelCopyWith<$Res> { $Res call( {String historyId, HistoryMetaModel metaData, - HttpRequestModel httpRequestModel, + HttpRequestModel? httpRequestModel, + AIRequestModel? aiRequestModel, HttpResponseModel httpResponseModel, String? preRequestScript, String? postRequestScript, AuthModel? authModel}); $HistoryMetaModelCopyWith<$Res> get metaData; - $HttpRequestModelCopyWith<$Res> get httpRequestModel; + $HttpRequestModelCopyWith<$Res>? get httpRequestModel; + $AIRequestModelCopyWith<$Res>? get aiRequestModel; $HttpResponseModelCopyWith<$Res> get httpResponseModel; $AuthModelCopyWith<$Res>? get authModel; } @@ -76,7 +79,8 @@ class _$HistoryRequestModelCopyWithImpl<$Res, $Val extends HistoryRequestModel> $Res call({ Object? historyId = null, Object? metaData = null, - Object? httpRequestModel = null, + Object? httpRequestModel = freezed, + Object? aiRequestModel = freezed, Object? httpResponseModel = null, Object? preRequestScript = freezed, Object? postRequestScript = freezed, @@ -91,10 +95,14 @@ class _$HistoryRequestModelCopyWithImpl<$Res, $Val extends HistoryRequestModel> ? _value.metaData : metaData // ignore: cast_nullable_to_non_nullable as HistoryMetaModel, - httpRequestModel: null == httpRequestModel + httpRequestModel: freezed == httpRequestModel ? _value.httpRequestModel : httpRequestModel // ignore: cast_nullable_to_non_nullable - as HttpRequestModel, + as HttpRequestModel?, + aiRequestModel: freezed == aiRequestModel + ? _value.aiRequestModel + : aiRequestModel // ignore: cast_nullable_to_non_nullable + as AIRequestModel?, httpResponseModel: null == httpResponseModel ? _value.httpResponseModel : httpResponseModel // ignore: cast_nullable_to_non_nullable @@ -128,12 +136,30 @@ class _$HistoryRequestModelCopyWithImpl<$Res, $Val extends HistoryRequestModel> /// with the given fields replaced by the non-null parameter values. @override @pragma('vm:prefer-inline') - $HttpRequestModelCopyWith<$Res> get httpRequestModel { - return $HttpRequestModelCopyWith<$Res>(_value.httpRequestModel, (value) { + $HttpRequestModelCopyWith<$Res>? get httpRequestModel { + if (_value.httpRequestModel == null) { + return null; + } + + return $HttpRequestModelCopyWith<$Res>(_value.httpRequestModel!, (value) { return _then(_value.copyWith(httpRequestModel: value) as $Val); }); } + /// Create a copy of HistoryRequestModel + /// with the given fields replaced by the non-null parameter values. + @override + @pragma('vm:prefer-inline') + $AIRequestModelCopyWith<$Res>? get aiRequestModel { + if (_value.aiRequestModel == null) { + return null; + } + + return $AIRequestModelCopyWith<$Res>(_value.aiRequestModel!, (value) { + return _then(_value.copyWith(aiRequestModel: value) as $Val); + }); + } + /// Create a copy of HistoryRequestModel /// with the given fields replaced by the non-null parameter values. @override @@ -170,7 +196,8 @@ abstract class _$$HistoryRequestModelImplCopyWith<$Res> $Res call( {String historyId, HistoryMetaModel metaData, - HttpRequestModel httpRequestModel, + HttpRequestModel? httpRequestModel, + AIRequestModel? aiRequestModel, HttpResponseModel httpResponseModel, String? preRequestScript, String? postRequestScript, @@ -179,7 +206,9 @@ abstract class _$$HistoryRequestModelImplCopyWith<$Res> @override $HistoryMetaModelCopyWith<$Res> get metaData; @override - $HttpRequestModelCopyWith<$Res> get httpRequestModel; + $HttpRequestModelCopyWith<$Res>? get httpRequestModel; + @override + $AIRequestModelCopyWith<$Res>? get aiRequestModel; @override $HttpResponseModelCopyWith<$Res> get httpResponseModel; @override @@ -201,7 +230,8 @@ class __$$HistoryRequestModelImplCopyWithImpl<$Res> $Res call({ Object? historyId = null, Object? metaData = null, - Object? httpRequestModel = null, + Object? httpRequestModel = freezed, + Object? aiRequestModel = freezed, Object? httpResponseModel = null, Object? preRequestScript = freezed, Object? postRequestScript = freezed, @@ -216,10 +246,14 @@ class __$$HistoryRequestModelImplCopyWithImpl<$Res> ? _value.metaData : metaData // ignore: cast_nullable_to_non_nullable as HistoryMetaModel, - httpRequestModel: null == httpRequestModel + httpRequestModel: freezed == httpRequestModel ? _value.httpRequestModel : httpRequestModel // ignore: cast_nullable_to_non_nullable - as HttpRequestModel, + as HttpRequestModel?, + aiRequestModel: freezed == aiRequestModel + ? _value.aiRequestModel + : aiRequestModel // ignore: cast_nullable_to_non_nullable + as AIRequestModel?, httpResponseModel: null == httpResponseModel ? _value.httpResponseModel : httpResponseModel // ignore: cast_nullable_to_non_nullable @@ -247,7 +281,8 @@ class _$HistoryRequestModelImpl implements _HistoryRequestModel { const _$HistoryRequestModelImpl( {required this.historyId, required this.metaData, - required this.httpRequestModel, + this.httpRequestModel, + this.aiRequestModel, required this.httpResponseModel, this.preRequestScript, this.postRequestScript, @@ -261,7 +296,9 @@ class _$HistoryRequestModelImpl implements _HistoryRequestModel { @override final HistoryMetaModel metaData; @override - final HttpRequestModel httpRequestModel; + final HttpRequestModel? httpRequestModel; + @override + final AIRequestModel? aiRequestModel; @override final HttpResponseModel httpResponseModel; @override @@ -273,7 +310,7 @@ class _$HistoryRequestModelImpl implements _HistoryRequestModel { @override String toString() { - return 'HistoryRequestModel(historyId: $historyId, metaData: $metaData, httpRequestModel: $httpRequestModel, httpResponseModel: $httpResponseModel, preRequestScript: $preRequestScript, postRequestScript: $postRequestScript, authModel: $authModel)'; + return 'HistoryRequestModel(historyId: $historyId, metaData: $metaData, httpRequestModel: $httpRequestModel, aiRequestModel: $aiRequestModel, httpResponseModel: $httpResponseModel, preRequestScript: $preRequestScript, postRequestScript: $postRequestScript, authModel: $authModel)'; } @override @@ -287,6 +324,8 @@ class _$HistoryRequestModelImpl implements _HistoryRequestModel { other.metaData == metaData) && (identical(other.httpRequestModel, httpRequestModel) || other.httpRequestModel == httpRequestModel) && + (identical(other.aiRequestModel, aiRequestModel) || + other.aiRequestModel == aiRequestModel) && (identical(other.httpResponseModel, httpResponseModel) || other.httpResponseModel == httpResponseModel) && (identical(other.preRequestScript, preRequestScript) || @@ -304,6 +343,7 @@ class _$HistoryRequestModelImpl implements _HistoryRequestModel { historyId, metaData, httpRequestModel, + aiRequestModel, httpResponseModel, preRequestScript, postRequestScript, @@ -330,7 +370,8 @@ abstract class _HistoryRequestModel implements HistoryRequestModel { const factory _HistoryRequestModel( {required final String historyId, required final HistoryMetaModel metaData, - required final HttpRequestModel httpRequestModel, + final HttpRequestModel? httpRequestModel, + final AIRequestModel? aiRequestModel, required final HttpResponseModel httpResponseModel, final String? preRequestScript, final String? postRequestScript, @@ -344,7 +385,9 @@ abstract class _HistoryRequestModel implements HistoryRequestModel { @override HistoryMetaModel get metaData; @override - HttpRequestModel get httpRequestModel; + HttpRequestModel? get httpRequestModel; + @override + AIRequestModel? get aiRequestModel; @override HttpResponseModel get httpResponseModel; @override diff --git a/lib/models/history_request_model.g.dart b/lib/models/history_request_model.g.dart index 66aee83b..2548ce76 100644 --- a/lib/models/history_request_model.g.dart +++ b/lib/models/history_request_model.g.dart @@ -11,8 +11,14 @@ _$HistoryRequestModelImpl _$$HistoryRequestModelImplFromJson(Map json) => historyId: json['historyId'] as String, metaData: HistoryMetaModel.fromJson( Map.from(json['metaData'] as Map)), - httpRequestModel: HttpRequestModel.fromJson( - Map.from(json['httpRequestModel'] as Map)), + httpRequestModel: json['httpRequestModel'] == null + ? null + : HttpRequestModel.fromJson( + Map.from(json['httpRequestModel'] as Map)), + aiRequestModel: json['aiRequestModel'] == null + ? null + : AIRequestModel.fromJson( + Map.from(json['aiRequestModel'] as Map)), httpResponseModel: HttpResponseModel.fromJson( Map.from(json['httpResponseModel'] as Map)), preRequestScript: json['preRequestScript'] as String?, @@ -28,7 +34,8 @@ Map _$$HistoryRequestModelImplToJson( { 'historyId': instance.historyId, 'metaData': instance.metaData.toJson(), - 'httpRequestModel': instance.httpRequestModel.toJson(), + 'httpRequestModel': instance.httpRequestModel?.toJson(), + 'aiRequestModel': instance.aiRequestModel?.toJson(), 'httpResponseModel': instance.httpResponseModel.toJson(), 'preRequestScript': instance.preRequestScript, 'postRequestScript': instance.postRequestScript, diff --git a/lib/models/request_model.dart b/lib/models/request_model.dart index b7052f81..25c36084 100644 --- a/lib/models/request_model.dart +++ b/lib/models/request_model.dart @@ -25,6 +25,7 @@ class RequestModel with _$RequestModel { @JsonKey(includeToJson: false) @Default(false) bool isStreaming, String? preRequestScript, String? postRequestScript, + AIRequestModel? aiRequestModel, }) = _RequestModel; factory RequestModel.fromJson(Map json) => diff --git a/lib/models/request_model.freezed.dart b/lib/models/request_model.freezed.dart index 3ba8979b..d700dba9 100644 --- a/lib/models/request_model.freezed.dart +++ b/lib/models/request_model.freezed.dart @@ -39,6 +39,7 @@ mixin _$RequestModel { bool get isStreaming => throw _privateConstructorUsedError; String? get preRequestScript => throw _privateConstructorUsedError; String? get postRequestScript => throw _privateConstructorUsedError; + AIRequestModel? get aiRequestModel => throw _privateConstructorUsedError; /// Serializes this RequestModel to a JSON map. Map toJson() => throw _privateConstructorUsedError; @@ -70,10 +71,12 @@ abstract class $RequestModelCopyWith<$Res> { @JsonKey(includeToJson: false) DateTime? sendingTime, @JsonKey(includeToJson: false) bool isStreaming, String? preRequestScript, - String? postRequestScript}); + String? postRequestScript, + AIRequestModel? aiRequestModel}); $HttpRequestModelCopyWith<$Res>? get httpRequestModel; $HttpResponseModelCopyWith<$Res>? get httpResponseModel; + $AIRequestModelCopyWith<$Res>? get aiRequestModel; } /// @nodoc @@ -105,6 +108,7 @@ class _$RequestModelCopyWithImpl<$Res, $Val extends RequestModel> Object? isStreaming = null, Object? preRequestScript = freezed, Object? postRequestScript = freezed, + Object? aiRequestModel = freezed, }) { return _then(_value.copyWith( id: null == id @@ -163,6 +167,10 @@ class _$RequestModelCopyWithImpl<$Res, $Val extends RequestModel> ? _value.postRequestScript : postRequestScript // ignore: cast_nullable_to_non_nullable as String?, + aiRequestModel: freezed == aiRequestModel + ? _value.aiRequestModel + : aiRequestModel // ignore: cast_nullable_to_non_nullable + as AIRequestModel?, ) as $Val); } @@ -193,6 +201,20 @@ class _$RequestModelCopyWithImpl<$Res, $Val extends RequestModel> return _then(_value.copyWith(httpResponseModel: value) as $Val); }); } + + /// Create a copy of RequestModel + /// with the given fields replaced by the non-null parameter values. + @override + @pragma('vm:prefer-inline') + $AIRequestModelCopyWith<$Res>? get aiRequestModel { + if (_value.aiRequestModel == null) { + return null; + } + + return $AIRequestModelCopyWith<$Res>(_value.aiRequestModel!, (value) { + return _then(_value.copyWith(aiRequestModel: value) as $Val); + }); + } } /// @nodoc @@ -217,12 +239,15 @@ abstract class _$$RequestModelImplCopyWith<$Res> @JsonKey(includeToJson: false) DateTime? sendingTime, @JsonKey(includeToJson: false) bool isStreaming, String? preRequestScript, - String? postRequestScript}); + String? postRequestScript, + AIRequestModel? aiRequestModel}); @override $HttpRequestModelCopyWith<$Res>? get httpRequestModel; @override $HttpResponseModelCopyWith<$Res>? get httpResponseModel; + @override + $AIRequestModelCopyWith<$Res>? get aiRequestModel; } /// @nodoc @@ -252,6 +277,7 @@ class __$$RequestModelImplCopyWithImpl<$Res> Object? isStreaming = null, Object? preRequestScript = freezed, Object? postRequestScript = freezed, + Object? aiRequestModel = freezed, }) { return _then(_$RequestModelImpl( id: null == id @@ -309,6 +335,10 @@ class __$$RequestModelImplCopyWithImpl<$Res> ? _value.postRequestScript : postRequestScript // ignore: cast_nullable_to_non_nullable as String?, + aiRequestModel: freezed == aiRequestModel + ? _value.aiRequestModel + : aiRequestModel // ignore: cast_nullable_to_non_nullable + as AIRequestModel?, )); } } @@ -331,7 +361,8 @@ class _$RequestModelImpl implements _RequestModel { @JsonKey(includeToJson: false) this.sendingTime, @JsonKey(includeToJson: false) this.isStreaming = false, this.preRequestScript, - this.postRequestScript}); + this.postRequestScript, + this.aiRequestModel}); factory _$RequestModelImpl.fromJson(Map json) => _$$RequestModelImplFromJson(json); @@ -371,10 +402,12 @@ class _$RequestModelImpl implements _RequestModel { final String? preRequestScript; @override final String? postRequestScript; + @override + final AIRequestModel? aiRequestModel; @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, isStreaming: $isStreaming, preRequestScript: $preRequestScript, postRequestScript: $postRequestScript)'; + return 'RequestModel(id: $id, apiType: $apiType, name: $name, description: $description, requestTabIndex: $requestTabIndex, httpRequestModel: $httpRequestModel, responseStatus: $responseStatus, message: $message, httpResponseModel: $httpResponseModel, isWorking: $isWorking, sendingTime: $sendingTime, isStreaming: $isStreaming, preRequestScript: $preRequestScript, postRequestScript: $postRequestScript, aiRequestModel: $aiRequestModel)'; } @override @@ -405,7 +438,9 @@ class _$RequestModelImpl implements _RequestModel { (identical(other.preRequestScript, preRequestScript) || other.preRequestScript == preRequestScript) && (identical(other.postRequestScript, postRequestScript) || - other.postRequestScript == postRequestScript)); + other.postRequestScript == postRequestScript) && + (identical(other.aiRequestModel, aiRequestModel) || + other.aiRequestModel == aiRequestModel)); } @JsonKey(includeFromJson: false, includeToJson: false) @@ -425,7 +460,8 @@ class _$RequestModelImpl implements _RequestModel { sendingTime, isStreaming, preRequestScript, - postRequestScript); + postRequestScript, + aiRequestModel); /// Create a copy of RequestModel /// with the given fields replaced by the non-null parameter values. @@ -458,7 +494,8 @@ abstract class _RequestModel implements RequestModel { @JsonKey(includeToJson: false) final DateTime? sendingTime, @JsonKey(includeToJson: false) final bool isStreaming, final String? preRequestScript, - final String? postRequestScript}) = _$RequestModelImpl; + final String? postRequestScript, + final AIRequestModel? aiRequestModel}) = _$RequestModelImpl; factory _RequestModel.fromJson(Map json) = _$RequestModelImpl.fromJson; @@ -495,6 +532,8 @@ abstract class _RequestModel implements RequestModel { String? get preRequestScript; @override String? get postRequestScript; + @override + AIRequestModel? get aiRequestModel; /// 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 8e0a5a68..272bfca0 100644 --- a/lib/models/request_model.g.dart +++ b/lib/models/request_model.g.dart @@ -30,6 +30,10 @@ _$RequestModelImpl _$$RequestModelImplFromJson(Map json) => _$RequestModelImpl( isStreaming: json['isStreaming'] as bool? ?? false, preRequestScript: json['preRequestScript'] as String?, postRequestScript: json['postRequestScript'] as String?, + aiRequestModel: json['aiRequestModel'] == null + ? null + : AIRequestModel.fromJson( + Map.from(json['aiRequestModel'] as Map)), ); Map _$$RequestModelImplToJson(_$RequestModelImpl instance) => @@ -44,9 +48,11 @@ Map _$$RequestModelImplToJson(_$RequestModelImpl instance) => 'httpResponseModel': instance.httpResponseModel?.toJson(), 'preRequestScript': instance.preRequestScript, 'postRequestScript': instance.postRequestScript, + 'aiRequestModel': instance.aiRequestModel?.toJson(), }; 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 a06b1e59..71a58ebb 100644 --- a/lib/models/settings_model.dart +++ b/lib/models/settings_model.dart @@ -18,6 +18,7 @@ class SettingsModel { this.workspaceFolderPath, this.isSSLDisabled = false, this.isDashBotEnabled = true, + this.defaultAIModel, }); final bool isDark; @@ -33,6 +34,7 @@ class SettingsModel { final String? workspaceFolderPath; final bool isSSLDisabled; final bool isDashBotEnabled; + final Map? defaultAIModel; SettingsModel copyWith({ bool? isDark, @@ -48,6 +50,7 @@ class SettingsModel { String? workspaceFolderPath, bool? isSSLDisabled, bool? isDashBotEnabled, + Map? defaultAIModel, }) { return SettingsModel( isDark: isDark ?? this.isDark, @@ -65,6 +68,7 @@ class SettingsModel { workspaceFolderPath: workspaceFolderPath ?? this.workspaceFolderPath, isSSLDisabled: isSSLDisabled ?? this.isSSLDisabled, isDashBotEnabled: isDashBotEnabled ?? this.isDashBotEnabled, + defaultAIModel: defaultAIModel ?? this.defaultAIModel, ); } @@ -85,6 +89,7 @@ class SettingsModel { workspaceFolderPath: workspaceFolderPath, isSSLDisabled: isSSLDisabled, isDashBotEnabled: isDashBotEnabled, + defaultAIModel: defaultAIModel, ); } @@ -140,7 +145,7 @@ class SettingsModel { final workspaceFolderPath = data["workspaceFolderPath"] as String?; final isSSLDisabled = data["isSSLDisabled"] as bool?; final isDashBotEnabled = data["isDashBotEnabled"] as bool?; - + final defaultAIModel = data["defaultAIModel"] as Map?; const sm = SettingsModel(); return sm.copyWith( @@ -158,6 +163,7 @@ class SettingsModel { workspaceFolderPath: workspaceFolderPath, isSSLDisabled: isSSLDisabled, isDashBotEnabled: isDashBotEnabled, + defaultAIModel: defaultAIModel, ); } @@ -178,6 +184,7 @@ class SettingsModel { "workspaceFolderPath": workspaceFolderPath, "isSSLDisabled": isSSLDisabled, "isDashBotEnabled": isDashBotEnabled, + 'defaultLLMSaveObject': defaultAIModel, }; } @@ -203,7 +210,8 @@ class SettingsModel { other.historyRetentionPeriod == historyRetentionPeriod && other.workspaceFolderPath == workspaceFolderPath && other.isSSLDisabled == isSSLDisabled && - other.isDashBotEnabled == isDashBotEnabled; + other.isDashBotEnabled == isDashBotEnabled && + other.defaultAIModel == defaultAIModel; } @override @@ -223,6 +231,7 @@ class SettingsModel { workspaceFolderPath, isSSLDisabled, isDashBotEnabled, + defaultAIModel, ); } } diff --git a/lib/providers/ai_providers.dart b/lib/providers/ai_providers.dart new file mode 100644 index 00000000..350fea35 --- /dev/null +++ b/lib/providers/ai_providers.dart @@ -0,0 +1,5 @@ +import 'package:apidash_core/apidash_core.dart'; +import 'package:riverpod/riverpod.dart'; + +final aiApiCredentialProvider = + StateProvider>((ref) => {}); diff --git a/lib/providers/collection_providers.dart b/lib/providers/collection_providers.dart index 525185a6..c6e9db55 100644 --- a/lib/providers/collection_providers.dart +++ b/lib/providers/collection_providers.dart @@ -163,6 +163,8 @@ class CollectionStateNotifier requestTabIndex: 0, responseStatus: null, message: null, + httpRequestModel: currentModel.httpRequestModel?.copyWith(), + aiRequestModel: currentModel.aiRequestModel?.copyWith(), httpResponseModel: null, isWorking: false, sendingTime: null, @@ -183,10 +185,14 @@ class CollectionStateNotifier var itemIds = ref.read(requestSequenceProvider); var currentModel = historyRequestModel; + final newModel = RequestModel( + apiType: currentModel.metaData.apiType, id: newId, name: "${currentModel.metaData.name} (history)", - httpRequestModel: currentModel.httpRequestModel, + aiRequestModel: currentModel.aiRequestModel?.copyWith(), + httpRequestModel: + currentModel.httpRequestModel?.copyWith() ?? HttpRequestModel(), responseStatus: currentModel.metaData.responseStatus, message: kResponseCodeReasons[currentModel.metaData.responseStatus], httpResponseModel: currentModel.httpResponseModel, @@ -205,9 +211,9 @@ class CollectionStateNotifier } void update({ + APIType? apiType, String? id, HTTPVerb? method, - APIType? apiType, AuthModel? authModel, String? url, String? name, @@ -226,6 +232,7 @@ class CollectionStateNotifier HttpResponseModel? httpResponseModel, String? preRequestScript, String? postRequestScript, + AIRequestModel? aiRequestModel, }) { final rId = id ?? ref.read(selectedIdStateProvider); if (rId == null) { @@ -234,33 +241,57 @@ class CollectionStateNotifier } var currentModel = state![rId]!; var currentHttpRequestModel = currentModel.httpRequestModel; - final newModel = currentModel.copyWith( - apiType: apiType ?? currentModel.apiType, - name: name ?? currentModel.name, - description: description ?? currentModel.description, - requestTabIndex: requestTabIndex ?? currentModel.requestTabIndex, - httpRequestModel: currentHttpRequestModel?.copyWith( - method: method ?? currentHttpRequestModel.method, - url: url ?? currentHttpRequestModel.url, - headers: headers ?? currentHttpRequestModel.headers, - params: params ?? currentHttpRequestModel.params, - authModel: authModel ?? currentHttpRequestModel.authModel, - isHeaderEnabledList: - isHeaderEnabledList ?? currentHttpRequestModel.isHeaderEnabledList, - isParamEnabledList: - isParamEnabledList ?? currentHttpRequestModel.isParamEnabledList, - bodyContentType: - bodyContentType ?? currentHttpRequestModel.bodyContentType, - body: body ?? currentHttpRequestModel.body, - query: query ?? currentHttpRequestModel.query, - formData: formData ?? currentHttpRequestModel.formData, - ), - responseStatus: responseStatus ?? currentModel.responseStatus, - message: message ?? currentModel.message, - httpResponseModel: httpResponseModel ?? currentModel.httpResponseModel, - preRequestScript: preRequestScript ?? currentModel.preRequestScript, - postRequestScript: postRequestScript ?? currentModel.postRequestScript, - ); + + RequestModel newModel; + + if (apiType != null && currentModel.apiType != apiType) { + final defaultModel = ref.read(settingsProvider).defaultAIModel; + newModel = switch (apiType) { + APIType.rest || APIType.graphql => currentModel.copyWith( + apiType: apiType, + name: name ?? currentModel.name, + description: description ?? currentModel.description, + httpRequestModel: const HttpRequestModel(), + aiRequestModel: null), + APIType.ai => currentModel.copyWith( + apiType: apiType, + name: name ?? currentModel.name, + description: description ?? currentModel.description, + httpRequestModel: null, + aiRequestModel: defaultModel == null + ? const AIRequestModel() + : AIRequestModel.fromJson(defaultModel)), + }; + } else { + newModel = currentModel.copyWith( + apiType: apiType ?? currentModel.apiType, + name: name ?? currentModel.name, + description: description ?? currentModel.description, + requestTabIndex: requestTabIndex ?? currentModel.requestTabIndex, + httpRequestModel: currentHttpRequestModel?.copyWith( + method: method ?? currentHttpRequestModel.method, + url: url ?? currentHttpRequestModel.url, + headers: headers ?? currentHttpRequestModel.headers, + params: params ?? currentHttpRequestModel.params, + authModel: authModel ?? currentHttpRequestModel.authModel, + isHeaderEnabledList: isHeaderEnabledList ?? + currentHttpRequestModel.isHeaderEnabledList, + isParamEnabledList: + isParamEnabledList ?? currentHttpRequestModel.isParamEnabledList, + bodyContentType: + bodyContentType ?? currentHttpRequestModel.bodyContentType, + body: body ?? currentHttpRequestModel.body, + query: query ?? currentHttpRequestModel.query, + formData: formData ?? currentHttpRequestModel.formData, + ), + responseStatus: responseStatus ?? currentModel.responseStatus, + message: message ?? currentModel.message, + httpResponseModel: httpResponseModel ?? currentModel.httpResponseModel, + preRequestScript: preRequestScript ?? currentModel.preRequestScript, + postRequestScript: postRequestScript ?? currentModel.postRequestScript, + aiRequestModel: aiRequestModel ?? currentModel.aiRequestModel, + ); + } var map = {...state!}; map[rId] = newModel; @@ -277,7 +308,8 @@ class CollectionStateNotifier } RequestModel? requestModel = state![requestId]; - if (requestModel?.httpRequestModel == null) { + if (requestModel?.httpRequestModel == null && + requestModel?.aiRequestModel == null) { return; } @@ -304,9 +336,16 @@ class CollectionStateNotifier } APIType apiType = executionRequestModel.apiType; - HttpRequestModel substitutedHttpRequestModel = - getSubstitutedHttpRequestModel(executionRequestModel.httpRequestModel!); bool noSSL = ref.read(settingsProvider).isSSLDisabled; + HttpRequestModel substitutedHttpRequestModel; + + if (apiType == APIType.ai) { + substitutedHttpRequestModel = getSubstitutedHttpRequestModel( + executionRequestModel.aiRequestModel!.httpRequestModel!); + } else { + substitutedHttpRequestModel = getSubstitutedHttpRequestModel( + executionRequestModel.httpRequestModel!); + } // Set model to working and streaming state = { @@ -316,6 +355,7 @@ class CollectionStateNotifier sendingTime: DateTime.now(), ), }; + bool streamingMode = true; //Default: Streaming First final stream = await streamHttpRequest( requestId, @@ -367,6 +407,8 @@ class CollectionStateNotifier .read(historyMetaStateNotifier.notifier) .editHistoryRequest(historyModel!); } + } else { + streamingMode = false; } if (!completer.isCompleted) { @@ -402,6 +444,16 @@ class CollectionStateNotifier isStreamingResponse: isStreamingResponse, ); + //AI-FORMATTING for Non Streaming Varaint + if (!streamingMode && + apiType == APIType.ai && + response.statusCode == 200) { + final fb = executionRequestModel.aiRequestModel?.getFormattedOutput( + kJsonDecoder + .convert(httpResponseModel?.body ?? "Error parsing body")); + httpResponseModel = httpResponseModel?.copyWith(formattedBody: fb); + } + newRequestModel = newRequestModel.copyWith( responseStatus: statusCode, message: kResponseCodeReasons[statusCode], @@ -423,6 +475,7 @@ class CollectionStateNotifier timeStamp: DateTime.now(), ), httpRequestModel: substitutedHttpRequestModel, + aiRequestModel: executionRequestModel.aiRequestModel, httpResponseModel: httpResponseModel!, preRequestScript: requestModel.preRequestScript, postRequestScript: requestModel.postRequestScript, diff --git a/lib/providers/providers.dart b/lib/providers/providers.dart index 29fc6e59..1a906ce5 100644 --- a/lib/providers/providers.dart +++ b/lib/providers/providers.dart @@ -1,3 +1,4 @@ +export 'ai_providers.dart'; export 'collection_providers.dart'; export 'environment_providers.dart'; export 'history_providers.dart'; diff --git a/lib/providers/settings_providers.dart b/lib/providers/settings_providers.dart index d3cb9f2f..be09cc6e 100644 --- a/lib/providers/settings_providers.dart +++ b/lib/providers/settings_providers.dart @@ -34,6 +34,7 @@ class ThemeStateNotifier extends StateNotifier { String? workspaceFolderPath, bool? isSSLDisabled, bool? isDashBotEnabled, + Map? defaultAIModel, }) async { state = state.copyWith( isDark: isDark, @@ -49,6 +50,7 @@ class ThemeStateNotifier extends StateNotifier { workspaceFolderPath: workspaceFolderPath, isSSLDisabled: isSSLDisabled, isDashBotEnabled: isDashBotEnabled, + defaultAIModel: defaultAIModel, ); await setSettingsToSharedPrefs(state); } diff --git a/lib/screens/common_widgets/ai/ai.dart b/lib/screens/common_widgets/ai/ai.dart new file mode 100644 index 00000000..20b1371b --- /dev/null +++ b/lib/screens/common_widgets/ai/ai.dart @@ -0,0 +1,3 @@ +export 'ai_model_selector_button.dart'; +export 'ai_model_selector_dialog.dart'; +export 'ai_model_selector.dart'; diff --git a/lib/screens/common_widgets/ai/ai_model_selector.dart b/lib/screens/common_widgets/ai/ai_model_selector.dart new file mode 100644 index 00000000..d746889a --- /dev/null +++ b/lib/screens/common_widgets/ai/ai_model_selector.dart @@ -0,0 +1,41 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:apidash_core/apidash_core.dart'; +import 'package:apidash/providers/providers.dart'; +import 'ai_model_selector_button.dart'; + +class AIModelSelector extends ConsumerWidget { + final AIRequestModel? readOnlyModel; + + const AIModelSelector({ + super.key, + this.readOnlyModel, + }); + + @override + Widget build(BuildContext context, WidgetRef ref) { + AIRequestModel? aiRequestModel; + if (readOnlyModel == null) { + ref.watch(selectedIdStateProvider); + aiRequestModel = ref.watch(selectedRequestModelProvider + .select((value) => value?.aiRequestModel)); + } else { + aiRequestModel = readOnlyModel; + } + + if (aiRequestModel == null) { + return Container(); + } + + return AIModelSelectorButton( + readonly: (readOnlyModel != null), + key: ValueKey(ref.watch(selectedIdStateProvider)), + aiRequestModel: aiRequestModel, + onModelUpdated: (newAIRequestModel) { + ref + .read(collectionStateNotifierProvider.notifier) + .update(aiRequestModel: newAIRequestModel.copyWith()); + }, + ); + } +} diff --git a/lib/screens/common_widgets/ai/ai_model_selector_button.dart b/lib/screens/common_widgets/ai/ai_model_selector_button.dart new file mode 100644 index 00000000..72aeb60c --- /dev/null +++ b/lib/screens/common_widgets/ai/ai_model_selector_button.dart @@ -0,0 +1,41 @@ +import 'package:apidash_core/apidash_core.dart'; +import 'package:apidash_design_system/apidash_design_system.dart'; +import 'package:flutter/material.dart'; +import 'ai_model_selector_dialog.dart'; + +class AIModelSelectorButton extends StatelessWidget { + final AIRequestModel? aiRequestModel; + final bool readonly; + final Function(AIRequestModel)? onModelUpdated; + const AIModelSelectorButton({ + super.key, + this.aiRequestModel, + this.readonly = false, + this.onModelUpdated, + }); + + @override + Widget build(BuildContext context) { + return ElevatedButton( + onPressed: readonly + ? null + : () async { + final newAIRequestModel = await showDialog( + context: context, + builder: (context) { + return AlertDialog( + scrollable: true, + content: AIModelSelectorDialog( + aiRequestModel: aiRequestModel, + ), + contentPadding: kP10, + ); + }, + ); + if (newAIRequestModel == null) return; + onModelUpdated?.call(newAIRequestModel); + }, + child: Text(aiRequestModel?.model ?? 'Select Model'), + ); + } +} diff --git a/lib/screens/common_widgets/ai/ai_model_selector_dialog.dart b/lib/screens/common_widgets/ai/ai_model_selector_dialog.dart new file mode 100644 index 00000000..be6f7e73 --- /dev/null +++ b/lib/screens/common_widgets/ai/ai_model_selector_dialog.dart @@ -0,0 +1,247 @@ +import 'package:apidash/providers/providers.dart'; +import 'package:apidash/widgets/widgets.dart'; +import 'package:apidash_core/apidash_core.dart'; +import 'package:apidash_design_system/apidash_design_system.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +class AIModelSelectorDialog extends ConsumerStatefulWidget { + final AIRequestModel? aiRequestModel; + const AIModelSelectorDialog({super.key, this.aiRequestModel}); + + @override + ConsumerState createState() => + _AIModelSelectorDialogState(); +} + +class _AIModelSelectorDialogState extends ConsumerState { + late final Future aM; + ModelAPIProvider? selectedProvider; + AIRequestModel? newAIRequestModel; + + @override + void initState() { + super.initState(); + selectedProvider = widget.aiRequestModel?.modelApiProvider; + if (selectedProvider != null && widget.aiRequestModel?.model != null) { + newAIRequestModel = widget.aiRequestModel?.copyWith(); + } + aM = ModelManager.fetchAvailableModels(); + } + + @override + Widget build(BuildContext context) { + ref.watch(aiApiCredentialProvider); + final width = MediaQuery.of(context).size.width * 0.8; + return FutureBuilder( + future: aM, + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.done && + snapshot.hasData && + snapshot.data != null) { + final data = snapshot.data!; + final mappedData = data.map; + if (context.isMediumWindow) { + return Container( + padding: kP20, + width: width, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ElevatedButton( + onPressed: null, + // TODO: Add update model logic + //() async { + // await LLMManager.fetchAvailableLLMs(); + // setState(() {}); + //}, + child: Text('Update Models'), + ), + kVSpacer10, + Row( + children: [ + Text('Select Model Provider'), + kHSpacer20, + Expanded( + child: ADDropdownButton( + onChanged: (x) { + setState(() { + selectedProvider = x; + newAIRequestModel = mappedData[selectedProvider] + ?.toAiRequestModel(); + }); + }, + value: selectedProvider, + values: data.modelProviders + .map((e) => (e.providerId!, e.providerName)), + ), + ), + ], + ), + kVSpacer10, + _buildModelSelector(mappedData[selectedProvider]), + ], + ), + ); + } + + return Container( + padding: kP20, + width: width, + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Flexible( + flex: 1, + child: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ElevatedButton( + onPressed: null, + // TODO: Add update model logic + //() async { + // await LLMManager.fetchAvailableLLMs(); + // setState(() {}); + //}, + child: Text('Update Models'), + ), + SizedBox(height: 20), + ...data.modelProviders.map( + (x) => ListTile( + title: Text(x.providerName ?? ""), + trailing: selectedProvider != x.providerId + ? null + : CircleAvatar( + radius: 5, + backgroundColor: Colors.green, + ), + onTap: () { + setState(() { + selectedProvider = x.providerId; + newAIRequestModel = mappedData[selectedProvider] + ?.toAiRequestModel(); + }); + }, + ), + ), + ], + ), + ), + ), + SizedBox(width: 40), + Flexible( + flex: 3, + child: _buildModelSelector(mappedData[selectedProvider]), + ), + ], + ), + ); + } + return CircularProgressIndicator(); + }, + ); + } + + _buildModelSelector(AIModelProvider? aiModelProvider) { + if (aiModelProvider == null) { + return Center(child: Text("Please select an AI API Provider")); + } + final currentCredential = + ref.watch(aiApiCredentialProvider)[aiModelProvider.providerId!] ?? ""; + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.max, + children: [ + Text( + aiModelProvider.providerName ?? "", + style: TextStyle(fontSize: 28), + ), + SizedBox(height: 20), + if (aiModelProvider.providerId != ModelAPIProvider.ollama) ...[ + Text('API Key / Credential'), + kVSpacer8, + BoundedTextField( + onChanged: (x) { + ref.read(aiApiCredentialProvider.notifier).state = { + ...ref.read(aiApiCredentialProvider), + aiModelProvider.providerId!: x + }; + }, + value: currentCredential, + ), + kVSpacer10, + ], + Text('Endpoint'), + kVSpacer8, + BoundedTextField( + key: ValueKey(aiModelProvider.providerName ?? ""), + onChanged: (x) { + setState(() { + newAIRequestModel = newAIRequestModel?.copyWith(url: x); + }); + }, + value: newAIRequestModel?.url ?? "", + ), + kVSpacer20, + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text('Models'), + // IconButton( + // onPressed: () => addNewModel(context), icon: Icon(Icons.add)) + ], + ), + kVSpacer8, + Container( + height: 300, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(10), + color: const Color.fromARGB(27, 0, 0, 0), + ), + child: Material( + color: Colors.transparent, + child: SingleChildScrollView( + clipBehavior: Clip.hardEdge, + child: Column( + children: [ + ...(aiModelProvider.models ?? []).map( + (x) => ListTile( + title: Text(x.name ?? ""), + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + if (newAIRequestModel?.model == x.id) + CircleAvatar( + radius: 5, + backgroundColor: Colors.green, + ), + ], + ), + onTap: () { + setState(() { + newAIRequestModel = + newAIRequestModel?.copyWith(model: x.id); + }); + }, + ), + ), + ], + ), + ), + ), + ), + kVSpacer10, + Align( + alignment: Alignment.centerRight, + child: ElevatedButton( + onPressed: () { + Navigator.of(context).pop(newAIRequestModel); + }, + child: Text('Save'), + ), + ), + ], + ); + } +} diff --git a/lib/screens/common_widgets/ai/dialog_add_ai_model.dart b/lib/screens/common_widgets/ai/dialog_add_ai_model.dart new file mode 100644 index 00000000..7873b31b --- /dev/null +++ b/lib/screens/common_widgets/ai/dialog_add_ai_model.dart @@ -0,0 +1,44 @@ +import 'package:apidash_design_system/apidash_design_system.dart'; +import 'package:flutter/material.dart'; + +Future addNewModel(BuildContext context) async { + TextEditingController iC = TextEditingController(); + TextEditingController nC = TextEditingController(); + final z = await showDialog( + context: context, + builder: (context) { + return AlertDialog( + title: Text('Add Custom Model'), + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + ADOutlinedTextField( + controller: iC, + hintText: 'Model ID', + ), + kVSpacer10, + ADOutlinedTextField( + controller: nC, + hintText: 'Model Display Name', + ), + kVSpacer10, + SizedBox( + width: double.infinity, + child: ElevatedButton( + onPressed: () { + Navigator.of(context).pop([ + iC.value.text, + nC.value.text, + ]); + }, + child: Text('Add Model'), + ), + ) + ], + ), + ); + }); + if (z == null) return; + // TODO: Add logic to add a new model + // setState(() {}); +} diff --git a/lib/screens/common_widgets/code_pane.dart b/lib/screens/common_widgets/code_pane.dart index 654b9440..5c08cc69 100644 --- a/lib/screens/common_widgets/code_pane.dart +++ b/lib/screens/common_widgets/code_pane.dart @@ -28,6 +28,14 @@ class CodePane extends ConsumerWidget { final selectedRequestModel = isHistoryRequest ? getRequestModelFromHistoryModel(selectedHistoryRequestModel!) : ref.watch(selectedRequestModelProvider); + + // TODO: Add AI Request Codegen + if (selectedRequestModel?.apiType == APIType.ai) { + return const ErrorMessage( + message: "Code generation for AI Requests is currently not available.", + ); + } + final defaultUriScheme = ref.watch(settingsProvider.select((value) => value.defaultUriScheme)); diff --git a/lib/screens/common_widgets/common_widgets.dart b/lib/screens/common_widgets/common_widgets.dart index ff914477..e68b01fb 100644 --- a/lib/screens/common_widgets/common_widgets.dart +++ b/lib/screens/common_widgets/common_widgets.dart @@ -1,3 +1,4 @@ +export 'ai/ai.dart'; export 'auth/auth.dart'; export 'api_type_dropdown.dart'; export 'button_navbar.dart'; diff --git a/lib/screens/history/history_widgets/ai_history_page.dart b/lib/screens/history/history_widgets/ai_history_page.dart new file mode 100644 index 00000000..1a799b39 --- /dev/null +++ b/lib/screens/history/history_widgets/ai_history_page.dart @@ -0,0 +1,165 @@ +import 'package:apidash_core/apidash_core.dart'; +import 'package:apidash_design_system/apidash_design_system.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:apidash/providers/providers.dart'; +import 'package:apidash/widgets/editor.dart'; + +class HisAIRequestPromptSection extends ConsumerWidget { + const HisAIRequestPromptSection({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final selectedHistoryModel = + ref.watch(selectedHistoryRequestModelProvider)!; + final aiReqM = selectedHistoryModel.aiRequestModel; + if (aiReqM == null) { + return kSizedBoxEmpty; + } + + 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', + ), + ), + 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: aiReqM.systemPrompt, + readOnly: true, + ), + ), + ), + SizedBox(height: 10), + Padding( + padding: const EdgeInsets.only(left: 25.0), + child: Text( + 'User Prompt / Input', + ), + ), + 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: aiReqM.userPrompt, + readOnly: true, + ), + ), + ), + ], + ), + ); + } +} + +class HisAIRequestAuthorizationSection extends ConsumerWidget { + const HisAIRequestAuthorizationSection({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final selectedHistoryModel = + ref.watch(selectedHistoryRequestModelProvider)!; + final aiReqM = selectedHistoryModel.aiRequestModel; + if (aiReqM == null) { + return kSizedBoxEmpty; + } + + 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: aiReqM.apiKey, + readOnly: true, + ), + ), + ), + ], + ), + ); + } +} + +class HisAIRequestConfigSection extends ConsumerWidget { + const HisAIRequestConfigSection({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final selectedHistoryModel = + ref.watch(selectedHistoryRequestModelProvider)!; + final aiReqM = selectedHistoryModel.aiRequestModel; + if (aiReqM == null) { + return kSizedBoxEmpty; + } + return SingleChildScrollView( + padding: EdgeInsets.symmetric(vertical: 20), + child: Column( + key: ValueKey(selectedHistoryModel.historyId), + children: [ + ...aiReqM.modelConfigs.map( + (el) => ListTile( + title: Text(el.name), + subtitle: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + el.description, + ), + SizedBox(height: 5), + switch (el.type) { + ConfigType.boolean => AIConfigBool( + readonly: true, + configuration: el, + onConfigUpdated: (x) {}, + ), + ConfigType.numeric => AIConfigField( + readonly: true, + configuration: el, + onConfigUpdated: (x) {}, + numeric: true, + ), + ConfigType.text => AIConfigField( + readonly: true, + configuration: el, + onConfigUpdated: (x) {}, + ), + ConfigType.slider => AIConfigSlider( + readonly: true, + configuration: el, + onSliderUpdated: (x) {}, + ), + }, + SizedBox(height: 10), + ], + ), + ), + ), + ], + ), + ); + } +} diff --git a/lib/screens/history/history_widgets/his_request_pane.dart b/lib/screens/history/history_widgets/his_request_pane.dart index 6766ad1b..0b126600 100644 --- a/lib/screens/history/history_widgets/his_request_pane.dart +++ b/lib/screens/history/history_widgets/his_request_pane.dart @@ -6,6 +6,7 @@ import 'package:apidash/providers/providers.dart'; import 'package:apidash/widgets/widgets.dart'; import 'package:apidash/consts.dart'; import '../../common_widgets/common_widgets.dart'; +import 'ai_history_page.dart'; import 'his_scripts_tab.dart'; class HistoryRequestPane extends ConsumerWidget { @@ -22,23 +23,39 @@ class HistoryRequestPane extends ConsumerWidget { final codePaneVisible = ref.watch(historyCodePaneVisibleStateProvider); final apiType = ref.watch(selectedHistoryRequestModelProvider .select((value) => value?.metaData.apiType)); - final headersMap = ref.watch(selectedHistoryRequestModelProvider - .select((value) => value?.httpRequestModel.headersMap)) ?? + + final headersMap = ref.watch(selectedHistoryRequestModelProvider.select( + (value) { + if (apiType == APIType.ai) return {}; + return value?.httpRequestModel!.headersMap; + }, + )) ?? {}; final headerLength = headersMap.length; - final paramsMap = ref.watch(selectedHistoryRequestModelProvider - .select((value) => value?.httpRequestModel.paramsMap)) ?? + final paramsMap = ref.watch(selectedHistoryRequestModelProvider.select( + (value) { + if (apiType == APIType.ai) return {}; + return value?.httpRequestModel!.paramsMap; + }, + )) ?? {}; final paramLength = paramsMap.length; - final hasBody = ref.watch(selectedHistoryRequestModelProvider - .select((value) => value?.httpRequestModel.hasBody)) ?? + final hasBody = ref.watch(selectedHistoryRequestModelProvider.select( + (value) { + if (apiType == APIType.ai) return false; + return value?.httpRequestModel!.hasBody; + }, + )) ?? false; - final hasQuery = ref.watch(selectedHistoryRequestModelProvider - .select((value) => value?.httpRequestModel.hasQuery)) ?? - false; + final hasQuery = + ref.watch(selectedHistoryRequestModelProvider.select((value) { + if (apiType == APIType.ai) return false; + return value?.httpRequestModel!.hasQuery; + })) ?? + false; final scriptsLength = ref.watch(selectedHistoryRequestModelProvider .select((value) => value?.preRequestScript?.length)) ?? @@ -127,6 +144,27 @@ class HistoryRequestPane extends ConsumerWidget { const HistoryScriptsTab(), ], ), + APIType.ai => RequestPane( + key: const Key("history-request-pane-ai"), + selectedId: selectedId, + codePaneVisible: codePaneVisible, + onPressedCodeButton: () { + ref.read(historyCodePaneVisibleStateProvider.notifier).state = + !codePaneVisible; + }, + showViewCodeButton: !isCompact, + showIndicators: [ + false, + false, + false, + ], + tabLabels: const ["Prompts", "Authorization", "Configuration"], + children: [ + const HisAIRequestPromptSection(), + const HisAIRequestAuthorizationSection(), + const HisAIRequestConfigSection(), + ], + ), _ => kSizedBoxEmpty, }; } diff --git a/lib/screens/history/history_widgets/his_response_pane.dart b/lib/screens/history/history_widgets/his_response_pane.dart index f90390cb..695c3785 100644 --- a/lib/screens/history/history_widgets/his_response_pane.dart +++ b/lib/screens/history/history_widgets/his_response_pane.dart @@ -13,16 +13,20 @@ class HistoryResponsePane extends ConsumerWidget { final selectedId = ref.watch(selectedHistoryIdStateProvider); final selectedHistoryRequest = ref.watch(selectedHistoryRequestModelProvider); + final historyHttpResponseModel = selectedHistoryRequest?.httpResponseModel; if (selectedId != null) { final requestModel = getRequestModelFromHistoryModel(selectedHistoryRequest!); + + final statusCode = historyHttpResponseModel?.statusCode; + return Column( children: [ ResponsePaneHeader( - responseStatus: historyHttpResponseModel?.statusCode, - message: kResponseCodeReasons[historyHttpResponseModel?.statusCode], + responseStatus: statusCode, + message: kResponseCodeReasons[statusCode], time: historyHttpResponseModel?.time, ), Expanded( diff --git a/lib/screens/history/history_widgets/his_url_card.dart b/lib/screens/history/history_widgets/his_url_card.dart index 7a6153b4..5825e31f 100644 --- a/lib/screens/history/history_widgets/his_url_card.dart +++ b/lib/screens/history/history_widgets/his_url_card.dart @@ -1,6 +1,7 @@ import 'package:apidash_core/apidash_core.dart'; import 'package:apidash_design_system/apidash_design_system.dart'; import 'package:flutter/material.dart'; +import 'package:apidash/screens/common_widgets/common_widgets.dart'; import 'package:apidash/widgets/widgets.dart'; import 'package:apidash/models/models.dart'; import 'package:apidash/utils/utils.dart'; @@ -58,6 +59,12 @@ class HistoryURLCard extends StatelessWidget { ), isCompact ? kHSpacer10 : kHSpacer20, ], + if (apiType == APIType.ai) ...[ + AIModelSelector( + readOnlyModel: historyRequestModel?.aiRequestModel, + ), + SizedBox(width: 20), + ], Expanded( child: ReadOnlyTextField( initialValue: url, diff --git a/lib/screens/home_page/collection_pane.dart b/lib/screens/home_page/collection_pane.dart index 4a014971..cc1a2a3b 100644 --- a/lib/screens/home_page/collection_pane.dart +++ b/lib/screens/home_page/collection_pane.dart @@ -193,7 +193,7 @@ class RequestItem extends ConsumerWidget { return SidebarRequestCard( id: id, apiType: requestModel.apiType, - method: requestModel.httpRequestModel!.method, + method: requestModel.httpRequestModel?.method, name: requestModel.name, url: requestModel.httpRequestModel?.url, selectedId: selectedId, diff --git a/lib/screens/home_page/editor_pane/details_card/request_pane/ai_request/aireq_authorization.dart b/lib/screens/home_page/editor_pane/details_card/request_pane/ai_request/aireq_authorization.dart new file mode 100644 index 00000000..51ff7ba8 --- /dev/null +++ b/lib/screens/home_page/editor_pane/details_card/request_pane/ai_request/aireq_authorization.dart @@ -0,0 +1,47 @@ +import 'package:apidash/providers/providers.dart'; +import 'package:apidash/widgets/widgets.dart'; +import 'package:apidash_design_system/apidash_design_system.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 apiKey = ref.watch(selectedRequestModelProvider + .select((value) => value?.aiRequestModel?.apiKey)); + final requestModel = ref + .read(collectionStateNotifierProvider.notifier) + .getRequestModel(selectedId!); + final aiReqM = requestModel?.aiRequestModel; + if (aiReqM == null) { + return kSizedBoxEmpty; + } + + 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: apiKey, + onChanged: (String value) { + ref + .read(collectionStateNotifierProvider.notifier) + .update(aiRequestModel: aiReqM.copyWith(apiKey: value)); + }, + hintText: 'Enter API key or Authorization Credentials', + ), + ), + ), + ], + ), + ); + } +} diff --git a/lib/screens/home_page/editor_pane/details_card/request_pane/ai_request/aireq_configs.dart b/lib/screens/home_page/editor_pane/details_card/request_pane/ai_request/aireq_configs.dart new file mode 100644 index 00000000..94e68307 --- /dev/null +++ b/lib/screens/home_page/editor_pane/details_card/request_pane/ai_request/aireq_configs.dart @@ -0,0 +1,89 @@ +import 'package:apidash/providers/providers.dart'; +import 'package:apidash_core/apidash_core.dart'; +import 'package:apidash_design_system/apidash_design_system.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +class AIRequestConfigSection extends ConsumerWidget { + const AIRequestConfigSection({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final selectedId = ref.watch(selectedIdStateProvider); + final modelConfigs = ref.watch(selectedRequestModelProvider + .select((value) => value?.aiRequestModel?.modelConfigs)); + final requestModel = ref + .read(collectionStateNotifierProvider.notifier) + .getRequestModel(selectedId!); + final aiReqM = requestModel?.aiRequestModel; + if (aiReqM == null || modelConfigs == null) { + return kSizedBoxEmpty; + } + + updateRequestModel(ModelConfig modelConfig) { + final aiRequestModel = ref + .read(collectionStateNotifierProvider.notifier) + .getRequestModel(selectedId) + ?.aiRequestModel; + final idx = aiRequestModel?.getModelConfigIdx(modelConfig.id); + if (idx != null && aiRequestModel != null) { + var l = [...aiRequestModel.modelConfigs]; + l[idx] = modelConfig; + ref.read(collectionStateNotifierProvider.notifier).update( + aiRequestModel: aiRequestModel.copyWith(modelConfigs: l), + ); + } + } + + return SingleChildScrollView( + padding: EdgeInsets.symmetric(vertical: 20), + child: Column( + key: ValueKey(selectedId), + children: [ + ...modelConfigs.map( + (el) => ListTile( + title: Text(el.name), + subtitle: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + el.description, + ), + SizedBox(height: 5), + switch (el.type) { + ConfigType.boolean => AIConfigBool( + configuration: el, + onConfigUpdated: (x) { + updateRequestModel(x); + }, + ), + ConfigType.numeric => AIConfigField( + configuration: el, + onConfigUpdated: (x) { + updateRequestModel(x); + }, + numeric: true, + ), + ConfigType.text => AIConfigField( + configuration: el, + onConfigUpdated: (x) { + updateRequestModel(x); + }, + ), + ConfigType.slider => AIConfigSlider( + configuration: el, + onSliderUpdated: (x) { + updateRequestModel(x); + }, + ), + }, + SizedBox(height: 10), + ], + ), + ), + ), + ], + ), + ); + } +} diff --git a/lib/screens/home_page/editor_pane/details_card/request_pane/ai_request/aireq_prompt.dart b/lib/screens/home_page/editor_pane/details_card/request_pane/ai_request/aireq_prompt.dart new file mode 100644 index 00000000..d63d153e --- /dev/null +++ b/lib/screens/home_page/editor_pane/details_card/request_pane/ai_request/aireq_prompt.dart @@ -0,0 +1,81 @@ +import 'package:apidash/providers/providers.dart'; +import 'package:apidash/widgets/widgets.dart'; +import 'package:apidash_design_system/apidash_design_system.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 systemPrompt = ref.watch(selectedRequestModelProvider + .select((value) => value?.aiRequestModel?.systemPrompt)); + final userPrompt = ref.watch(selectedRequestModelProvider + .select((value) => value?.aiRequestModel?.userPrompt)); + final aiRequestModel = ref + .read(collectionStateNotifierProvider.notifier) + .getRequestModel(selectedId!) + ?.aiRequestModel; + if (aiRequestModel == null) { + return kSizedBoxEmpty; + } + + 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', + ), + ), + 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( + aiRequestModel: + aiRequestModel.copyWith(systemPrompt: value)); + }, + hintText: 'Enter System Prompt', + ), + ), + ), + SizedBox(height: 10), + Padding( + padding: const EdgeInsets.only(left: 25.0), + child: Text( + 'User Prompt / Input', + ), + ), + 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( + aiRequestModel: + aiRequestModel.copyWith(userPrompt: value)); + }, + hintText: 'Enter User Prompt', + ), + ), + ), + ], + ), + ); + } +} diff --git a/lib/screens/home_page/editor_pane/details_card/request_pane/ai_request/request_pane_ai.dart b/lib/screens/home_page/editor_pane/details_card/request_pane/ai_request/request_pane_ai.dart new file mode 100644 index 00000000..29dffd06 --- /dev/null +++ b/lib/screens/home_page/editor_pane/details_card/request_pane/ai_request/request_pane_ai.dart @@ -0,0 +1,49 @@ +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 'aireq_authorization.dart'; +import 'aireq_configs.dart'; +import 'aireq_prompt.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)); + + return RequestPane( + selectedId: selectedId, + codePaneVisible: codePaneVisible, + tabIndex: tabIndex, + onPressedCodeButton: () { + ref.read(codePaneVisibleStateProvider.notifier).state = + !codePaneVisible; + }, + onTapTabBar: (index) { + ref + .read(collectionStateNotifierProvider.notifier) + .update(requestTabIndex: index); + }, + showIndicators: [ + false, + false, + false, + ], + tabLabels: const [ + "Prompt", + "Authorization", + "Configurations", + ], + children: const [ + AIRequestPromptSection(), + AIRequestAuthorizationSection(), + AIRequestConfigSection(), + ], + ); + } +} 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 9c852c71..af82951e 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 @@ -3,6 +3,7 @@ import 'package:apidash_design_system/apidash_design_system.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:apidash/providers/providers.dart'; +import 'ai_request/request_pane_ai.dart'; import 'request_pane_graphql.dart'; import 'request_pane_rest.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/response_pane.dart b/lib/screens/home_page/editor_pane/details_card/response_pane.dart index 367bb412..0e701d5f 100644 --- a/lib/screens/home_page/editor_pane/details_card/response_pane.dart +++ b/lib/screens/home_page/editor_pane/details_card/response_pane.dart @@ -106,12 +106,18 @@ class ResponseHeadersTab extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final requestHeaders = ref.watch(selectedRequestModelProvider - .select((value) => value?.httpResponseModel?.requestHeaders)) ?? - {}; - final responseHeaders = ref.watch(selectedRequestModelProvider - .select((value) => value?.httpResponseModel?.headers)) ?? - {}; + final requestHeaders = + ref.watch(selectedRequestModelProvider.select((value) { + return value?.httpResponseModel!.requestHeaders; + })) ?? + {}; + + final responseHeaders = + ref.watch(selectedRequestModelProvider.select((value) { + return value?.httpResponseModel!.headers; + })) ?? + {}; + return ResponseHeaders( responseHeaders: responseHeaders, requestHeaders: requestHeaders, diff --git a/lib/screens/home_page/editor_pane/url_card.dart b/lib/screens/home_page/editor_pane/url_card.dart index 5aa1ce02..d84f27a7 100644 --- a/lib/screens/home_page/editor_pane/url_card.dart +++ b/lib/screens/home_page/editor_pane/url_card.dart @@ -35,6 +35,7 @@ class EditorPaneRequestURLCard extends ConsumerWidget { switch (apiType) { APIType.rest => const DropdownButtonHTTPMethod(), APIType.graphql => kSizedBoxEmpty, + APIType.ai => const AIModelSelector(), null => kSizedBoxEmpty, }, switch (apiType) { @@ -51,6 +52,7 @@ class EditorPaneRequestURLCard extends ConsumerWidget { switch (apiType) { APIType.rest => const DropdownButtonHTTPMethod(), APIType.graphql => kSizedBoxEmpty, + APIType.ai => const AIModelSelector(), null => kSizedBoxEmpty, }, switch (apiType) { @@ -100,15 +102,27 @@ class URLTextField extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final selectedId = ref.watch(selectedIdStateProvider); + ref.watch(selectedRequestModelProvider + .select((value) => value?.aiRequestModel?.url)); + ref.watch(selectedRequestModelProvider + .select((value) => value?.httpRequestModel?.url)); + final requestModel = ref + .read(collectionStateNotifierProvider.notifier) + .getRequestModel(selectedId!)!; return EnvURLField( - selectedId: selectedId!, - initialValue: ref - .read(collectionStateNotifierProvider.notifier) - .getRequestModel(selectedId) - ?.httpRequestModel - ?.url, + selectedId: selectedId, + initialValue: switch (requestModel.apiType) { + APIType.ai => requestModel.aiRequestModel?.url, + _ => requestModel.httpRequestModel?.url, + }, onChanged: (value) { - ref.read(collectionStateNotifierProvider.notifier).update(url: value); + if (requestModel.apiType == APIType.ai) { + ref.read(collectionStateNotifierProvider.notifier).update( + aiRequestModel: + requestModel.aiRequestModel?.copyWith(url: value)); + } else { + ref.read(collectionStateNotifierProvider.notifier).update(url: value); + } }, onFieldSubmitted: (value) { ref.read(collectionStateNotifierProvider.notifier).sendRequest(); diff --git a/lib/screens/settings_page.dart b/lib/screens/settings_page.dart index 606ef516..4da2a48f 100644 --- a/lib/screens/settings_page.dart +++ b/lib/screens/settings_page.dart @@ -1,3 +1,4 @@ +import 'package:apidash_core/apidash_core.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; @@ -7,6 +8,7 @@ import '../services/services.dart'; import '../utils/utils.dart'; import '../widgets/widgets.dart'; import '../consts.dart'; +import 'common_widgets/common_widgets.dart'; class SettingsPage extends ConsumerWidget { const SettingsPage({super.key}); @@ -114,6 +116,22 @@ class SettingsPage extends ConsumerWidget { }, ), ), + ListTile( + hoverColor: kColorTransparent, + title: const Text('Default Large Language Model (LLM)'), + trailing: AIModelSelectorButton( + aiRequestModel: + AIRequestModel.fromJson(settings.defaultAIModel ?? {}), + onModelUpdated: (d) { + ref.read(settingsProvider.notifier).update( + defaultAIModel: d.copyWith( + modelConfigs: [], + stream: null, + systemPrompt: '', + userPrompt: '').toJson()); + }, + ), + ), CheckboxListTile( title: const Text("Save Responses"), subtitle: diff --git a/lib/utils/history_utils.dart b/lib/utils/history_utils.dart index d1c3dbbc..60ea7ae3 100644 --- a/lib/utils/history_utils.dart +++ b/lib/utils/history_utils.dart @@ -13,6 +13,7 @@ RequestModel getRequestModelFromHistoryModel(HistoryRequestModel model) { name: model.metaData.name, responseStatus: model.httpResponseModel.statusCode, message: kResponseCodeReasons[model.httpResponseModel.statusCode], + aiRequestModel: model.aiRequestModel, httpRequestModel: model.httpRequestModel, httpResponseModel: model.httpResponseModel, ); diff --git a/lib/utils/ui_utils.dart b/lib/utils/ui_utils.dart index 192d816b..52b5fcc9 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 => Colors.amber, }; if (brightness == Brightness.dark) { col = col.toDark; diff --git a/lib/widgets/card_sidebar_request.dart b/lib/widgets/card_sidebar_request.dart index 87dab9e4..372d88b5 100644 --- a/lib/widgets/card_sidebar_request.dart +++ b/lib/widgets/card_sidebar_request.dart @@ -11,7 +11,7 @@ class SidebarRequestCard extends StatelessWidget { super.key, required this.id, required this.apiType, - required this.method, + this.method, this.name, this.url, this.selectedId, @@ -30,7 +30,7 @@ class SidebarRequestCard extends StatelessWidget { final APIType apiType; final String? name; final String? url; - final HTTPVerb method; + final HTTPVerb? method; final String? selectedId; final String? editRequestId; final void Function()? onTap; diff --git a/lib/widgets/field_text_bounded.dart b/lib/widgets/field_text_bounded.dart new file mode 100644 index 00000000..18adeadf --- /dev/null +++ b/lib/widgets/field_text_bounded.dart @@ -0,0 +1,61 @@ +import 'package:apidash_design_system/apidash_design_system.dart'; +import 'package:flutter/material.dart'; + +class BoundedTextField extends StatefulWidget { + const BoundedTextField({ + super.key, + required this.value, + required this.onChanged, + }); + + final String value; + final void Function(String value) onChanged; + + @override + State createState() => _BoundedTextFieldState(); +} + +class _BoundedTextFieldState extends State { + TextEditingController controller = TextEditingController(); + @override + void initState() { + controller.text = widget.value; + super.initState(); + } + + @override + void didUpdateWidget(covariant BoundedTextField 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: double.infinity, + 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/response_body.dart b/lib/widgets/response_body.dart index d0af2baa..f1c866e9 100644 --- a/lib/widgets/response_body.dart +++ b/lib/widgets/response_body.dart @@ -49,7 +49,9 @@ class ResponseBody extends StatelessWidget { // '$kMsgUnknowContentType - ${responseModel.contentType}. $kUnexpectedRaiseIssue'); // } - var responseBodyView = getResponseBodyViewOptions(mediaType); + var responseBodyView = selectedRequestModel?.apiType == APIType.ai + ? (kAnswerRawBodyViewOptions, kSubTypePlain) + : getResponseBodyViewOptions(mediaType); var options = responseBodyView.$1; var highlightLanguage = responseBodyView.$2; @@ -65,8 +67,10 @@ class ResponseBody extends StatelessWidget { bytes: responseModel.bodyBytes!, body: body, formattedBody: formattedBody, - sseOutput: responseModel.sseOutput, highlightLanguage: highlightLanguage, + sseOutput: responseModel.sseOutput, + isAIResponse: selectedRequestModel?.apiType == APIType.ai, + aiRequestModel: selectedRequestModel?.aiRequestModel, ); } } diff --git a/lib/widgets/response_body_success.dart b/lib/widgets/response_body_success.dart index 9b22a4b9..44c9b28a 100644 --- a/lib/widgets/response_body_success.dart +++ b/lib/widgets/response_body_success.dart @@ -8,15 +8,18 @@ import 'package:apidash/consts.dart'; import 'button_share.dart'; class ResponseBodySuccess extends StatefulWidget { - const ResponseBodySuccess( - {super.key, - required this.mediaType, - required this.body, - required this.options, - required this.bytes, - this.formattedBody, - this.sseOutput, - this.highlightLanguage}); + const ResponseBodySuccess({ + super.key, + required this.mediaType, + required this.body, + required this.options, + required this.bytes, + this.formattedBody, + this.highlightLanguage, + this.sseOutput, + this.isAIResponse = false, + this.aiRequestModel, + }); final MediaType mediaType; final List options; final String body; @@ -24,6 +27,8 @@ class ResponseBodySuccess extends StatefulWidget { final String? formattedBody; final List? sseOutput; final String? highlightLanguage; + final bool isAIResponse; + final AIRequestModel? aiRequestModel; @override State createState() => _ResponseBodySuccessState(); @@ -134,7 +139,7 @@ class _ResponseBodySuccessState extends State { ), ), ), - ResponseBodyView.raw => Expanded( + ResponseBodyView.answer => Expanded( child: Container( width: double.maxFinite, padding: kP8, @@ -147,6 +152,21 @@ class _ResponseBodySuccessState extends State { ), ), ), + ResponseBodyView.raw => Expanded( + child: Container( + width: double.maxFinite, + padding: kP8, + decoration: textContainerdecoration, + child: SingleChildScrollView( + child: SelectableText( + widget.isAIResponse + ? widget.body + : (widget.formattedBody ?? widget.body), + style: kCodeStyle, + ), + ), + ), + ), ResponseBodyView.sse => Expanded( child: Container( width: double.maxFinite, @@ -154,6 +174,7 @@ class _ResponseBodySuccessState extends State { decoration: textContainerdecoration, child: SSEDisplay( sseOutput: widget.sseOutput, + aiRequestModel: widget.aiRequestModel, ), ), ), diff --git a/lib/widgets/sse_display.dart b/lib/widgets/sse_display.dart index efa65f43..1fc681ca 100644 --- a/lib/widgets/sse_display.dart +++ b/lib/widgets/sse_display.dart @@ -1,12 +1,15 @@ import 'dart:convert'; +import 'package:apidash_core/apidash_core.dart'; import 'package:apidash_design_system/apidash_design_system.dart'; import 'package:flutter/material.dart'; class SSEDisplay extends StatelessWidget { + final AIRequestModel? aiRequestModel; final List? sseOutput; const SSEDisplay({ super.key, this.sseOutput, + this.aiRequestModel, }); @override @@ -24,9 +27,38 @@ class SSEDisplay extends StatelessWidget { ); } + if (aiRequestModel != null) { + // For RAW Text output (only AI Requests) + String out = ""; + for (String x in sseOutput!) { + x = x.trim(); + if (x.isEmpty || x.contains('[DONE]')) { + continue; + } + + // Start with JSON + final pos = x.indexOf('{'); + if (pos == -1) continue; + x = x.substring(pos); + + Map? dec; + try { + dec = jsonDecode(x); + final z = aiRequestModel?.getFormattedStreamOutput(dec!); + out += z ?? ''; + } catch (e) { + debugPrint("SSEDisplay -> Error in JSONDEC $e"); + } + } + return SingleChildScrollView( + child: Text(out), + ); + } + return ListView( padding: kP1, - children: sseOutput!.reversed.where((e) => e != '').map((chunk) { + children: + sseOutput!.reversed.where((e) => e.trim() != '').map((chunk) { Map? parsedJson; try { parsedJson = jsonDecode(chunk); diff --git a/lib/widgets/texts.dart b/lib/widgets/texts.dart index c64a71b2..9d2e1a84 100644 --- a/lib/widgets/texts.dart +++ b/lib/widgets/texts.dart @@ -7,10 +7,10 @@ class SidebarRequestCardTextBox extends StatelessWidget { const SidebarRequestCardTextBox({ super.key, required this.apiType, - required this.method, + this.method, }); final APIType apiType; - final HTTPVerb method; + final HTTPVerb? method; @override Widget build(BuildContext context) { @@ -18,8 +18,9 @@ class SidebarRequestCardTextBox extends StatelessWidget { width: 24, child: Text( switch (apiType) { - APIType.rest => method.abbr, + APIType.rest => method!.abbr, APIType.graphql => apiType.abbr, + APIType.ai => apiType.abbr, }, textAlign: TextAlign.center, style: TextStyle( diff --git a/lib/widgets/widgets.dart b/lib/widgets/widgets.dart index 8d6af58a..cae6c455 100644 --- a/lib/widgets/widgets.dart +++ b/lib/widgets/widgets.dart @@ -32,6 +32,7 @@ export 'field_cell_obscurable.dart'; export 'field_cell.dart'; export 'field_json_search.dart'; export 'field_read_only.dart'; +export 'field_text_bounded.dart'; export 'field_url.dart'; export 'intro_message.dart'; export 'markdown.dart'; diff --git a/packages/apidash_core/lib/apidash_core.dart b/packages/apidash_core/lib/apidash_core.dart index e68984b3..4c8ca995 100644 --- a/packages/apidash_core/lib/apidash_core.dart +++ b/packages/apidash_core/lib/apidash_core.dart @@ -9,3 +9,4 @@ export 'utils/utils.dart'; // Export 3rd party packages export 'package:freezed_annotation/freezed_annotation.dart'; export 'package:better_networking/better_networking.dart'; +export 'package:genai/genai.dart'; diff --git a/packages/apidash_core/pubspec.yaml b/packages/apidash_core/pubspec.yaml index 2c010a07..0fc8162e 100644 --- a/packages/apidash_core/pubspec.yaml +++ b/packages/apidash_core/pubspec.yaml @@ -16,6 +16,8 @@ dependencies: curl_parser: path: ../curl_parser freezed_annotation: ^2.4.1 + genai: + path: ../genai har: path: ../har insomnia_collection: diff --git a/packages/apidash_core/pubspec_overrides.yaml b/packages/apidash_core/pubspec_overrides.yaml index 87d1d244..1df19ae7 100644 --- a/packages/apidash_core/pubspec_overrides.yaml +++ b/packages/apidash_core/pubspec_overrides.yaml @@ -1,9 +1,11 @@ -# melos_managed_dependency_overrides: better_networking,curl_parser,har,insomnia_collection,postman,seed +# melos_managed_dependency_overrides: better_networking,curl_parser,har,insomnia_collection,postman,seed,genai dependency_overrides: better_networking: path: ../better_networking curl_parser: path: ../curl_parser + genai: + path: ../genai har: path: ../har insomnia_collection: diff --git a/packages/apidash_design_system/lib/tokens/measurements.dart b/packages/apidash_design_system/lib/tokens/measurements.dart index 876c69fb..a1fc7a91 100644 --- a/packages/apidash_design_system/lib/tokens/measurements.dart +++ b/packages/apidash_design_system/lib/tokens/measurements.dart @@ -28,6 +28,7 @@ const kP6 = EdgeInsets.all(6); const kP8 = EdgeInsets.all(8); const kP10 = EdgeInsets.all(10); const kP12 = EdgeInsets.all(12); +const kP20 = EdgeInsets.all(20); const kPs8 = EdgeInsets.only(left: 8); const kPs2 = EdgeInsets.only(left: 2); const kPe4 = EdgeInsets.only(right: 4); diff --git a/packages/better_networking/lib/consts.dart b/packages/better_networking/lib/consts.dart index bbe055ae..3b370596 100644 --- a/packages/better_networking/lib/consts.dart +++ b/packages/better_networking/lib/consts.dart @@ -2,11 +2,19 @@ import 'dart:convert'; enum APIType { rest("HTTP", "HTTP"), + ai("AI", "AI"), graphql("GraphQL", "GQL"); const APIType(this.label, this.abbr); final String label; final String abbr; + + static fromMethod(String method) { + return HTTPVerb.values.firstWhere( + (model) => model.name == method.toLowerCase(), + orElse: () => throw ArgumentError('INVALID HTTP METHOD'), + ); + } } enum APIAuthType { diff --git a/packages/better_networking/lib/services/http_service.dart b/packages/better_networking/lib/services/http_service.dart index e5f3ff63..78ae2349 100644 --- a/packages/better_networking/lib/services/http_service.dart +++ b/packages/better_networking/lib/services/http_service.dart @@ -23,11 +23,11 @@ final httpClientManager = HttpClientManager(); Future<(HttpResponse?, Duration?, String?)> sendHttpRequestV1( String requestId, APIType apiType, - AuthModel? authData, HttpRequestModel requestModel, { SupportedUriSchemes defaultUriScheme = kDefaultUriScheme, bool noSSL = false, }) async { + final authData = requestModel.authModel; if (httpClientManager.wasRequestCancelled(requestId)) { httpClientManager.removeCancelledRequest(requestId); } diff --git a/packages/better_networking/lib/utils/http_request_utils.dart b/packages/better_networking/lib/utils/http_request_utils.dart index c13fab75..6540395a 100644 --- a/packages/better_networking/lib/utils/http_request_utils.dart +++ b/packages/better_networking/lib/utils/http_request_utils.dart @@ -93,6 +93,7 @@ String? getRequestBody(APIType type, HttpRequestModel httpRequestModel) { ? httpRequestModel.body : null, APIType.graphql => getGraphQLBody(httpRequestModel), + APIType.ai => null, //TODO: TAKE A LOOK }; } diff --git a/packages/genai/.gitignore b/packages/genai/.gitignore new file mode 100644 index 00000000..96486fd9 --- /dev/null +++ b/packages/genai/.gitignore @@ -0,0 +1,30 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock. +/pubspec.lock +**/doc/api/ +.dart_tool/ +.packages +build/ diff --git a/packages/genai/CHANGELOG.md b/packages/genai/CHANGELOG.md new file mode 100644 index 00000000..41cc7d81 --- /dev/null +++ b/packages/genai/CHANGELOG.md @@ -0,0 +1,3 @@ +## 0.0.1 + +* TODO: Describe initial release. diff --git a/packages/genai/LICENSE b/packages/genai/LICENSE new file mode 100644 index 00000000..65d4cee9 --- /dev/null +++ b/packages/genai/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + +Copyright 2025 Ashita Prasad, Ankit Mahato + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/packages/genai/README.md b/packages/genai/README.md new file mode 100644 index 00000000..d3b7a091 --- /dev/null +++ b/packages/genai/README.md @@ -0,0 +1,93 @@ +# genai + +`genai` is a lightweight and extensible Dart package designed to simplify AI requests and agentic operations. It provides an easy to use and seamless interface for various AI Providers such as (openai, gemini, antrhopic etc). + +## 🔧 Features + +- **Unified request modeling** via `HttpRequestModel` +- **Consistent response handling** with `HttpResponseModel` +- **Streamed response support** (e.g., SSE) +- **Client management** with cancellation and lifecycle control +- **Built-in utilities** for parsing headers and content types +- **Support for both REST and GraphQL APIs** + +## 📦 Installation + +To install the `genai` package, add it to your `pubspec.yaml`: + +```yaml +dependencies: + genai: ^ +``` + +Then run the following command in your terminal to fetch the package: + +```bash +flutter pub get +``` + +## 🚀 Quick Start + +### Response Mode (Callback Style) + +```dart +final LLMModel geminiModel = LLMProvider.gemini.getLLMByIdentifier('gemini-2.0-flash'); +GenerativeAI.callGenerativeModel( + geminiModel, + onAnswer: (x) { + print(x); + }, + onError: (e){...}, + systemPrompt: 'Give a 100 word summary of the provided word', + userPrompt: 'Pizza', + credential: 'AIza.....', +); +``` + +### Streaming Mode (Callback Style) + +```dart +final LLMModel geminiModel = LLMProvider.gemini.getLLMByIdentifier('gemini-2.0-flash'); +final ModelController controller = model.provider.modelController; +GenerativeAI.callGenerativeModel( + geminiModel, + onAnswer: (x) { + stdout.write(x); //get each word in the stream + }, + onError: (e){}, + systemPrompt: 'Give a 100 word summary of the provided word', + userPrompt: 'Pizza', + credential: 'AIza.....', + stream: true, //pass this to enable streaming +); +``` + +### Procedural(Manual) Request Building + +```dart +final LLMModel model = LLMProvider.gemini.getLLMByIdentifier('gemini-2.0-flash'); +final ModelController controller = model.provider.modelController; + +final payload = controller.inputPayload + ..systemPrompt = 'Say YES or NO' + ..userPrompt = 'The sun sets in the west' + ..credential = 'AIza....'; + +final genAIRequest = controller.createRequest(model, payload); +final answer = await GenerativeAI.executeGenAIRequest(model, genAIRequest); + +print(answer); +``` + +## 🤝 Contributing + +We welcome contributions to the `genai` package! If you'd like to contribute, please fork the repository and submit a pull request. For major changes or new features, it's a good idea to open an issue first to discuss your ideas. + +## Maintainer + +- Ashita Prasad ([GitHub](https://github.com/ashitaprasad), [LinkedIn](https://www.linkedin.com/in/ashitaprasad/), [X](https://x.com/ashitaprasad)) +- Manas Hejmadi (contributor) ([GitHub](https://github.com/synapsecode)) + +## License + +This project is licensed under the [Apache License 2.0](https://github.com/foss42/apidash/blob/main/packages/genai/LICENSE). diff --git a/packages/genai/analysis_options.yaml b/packages/genai/analysis_options.yaml new file mode 100644 index 00000000..9a1eabb4 --- /dev/null +++ b/packages/genai/analysis_options.yaml @@ -0,0 +1,11 @@ +include: package:flutter_lints/flutter.yaml + +analyzer: + errors: + invalid_annotation_target: ignore + exclude: + - "**/*.freezed.dart" + - "**/*.g.dart" + +linter: + rules: diff --git a/packages/genai/genai_example/.gitignore b/packages/genai/genai_example/.gitignore new file mode 100644 index 00000000..79c113f9 --- /dev/null +++ b/packages/genai/genai_example/.gitignore @@ -0,0 +1,45 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.build/ +.buildlog/ +.history +.svn/ +.swiftpm/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +**/doc/api/ +**/ios/Flutter/.last_build_id +.dart_tool/ +.flutter-plugins +.flutter-plugins-dependencies +.pub-cache/ +.pub/ +/build/ + +# Symbolication related +app.*.symbols + +# Obfuscation related +app.*.map.json + +# Android Studio will place build artifacts here +/android/app/debug +/android/app/profile +/android/app/release diff --git a/packages/genai/genai_example/README.md b/packages/genai/genai_example/README.md new file mode 100644 index 00000000..70e37aa4 --- /dev/null +++ b/packages/genai/genai_example/README.md @@ -0,0 +1,59 @@ +# GenAI Example + +This project is a simple demonstration of how to use the GenAI package + +### Fetch all available Remote LLMs +```dart +await LLMManager.fetchAvailableLLMs(); +``` + +### Getting LLM Models for a given Provider +```dart +final List models = LLMProvider.gemini.models; +``` + +### Calling a GenAI Model using the provided helper +```dart +final LLMModel geminiModel = LLMProvider.gemini.getLLMByIdentifier('gemini-2.0-flash'); +final ModelController controller = model.provider.modelController; +GenerativeAI.callGenerativeModel( + geminiModel, + onAnswer: (x) { + print(x); + }, + onError: (e){}, + systemPrompt: 'Give a 100 word summary of the provided word. Only give the answer', + userPrompt: 'Pizza', + credential: 'AIza.....', +); +``` + +### Calling a GenAI model (with Streaming) +```dart +final LLMModel geminiModel = LLMProvider.gemini.getLLMByIdentifier('gemini-2.0-flash'); +final ModelController controller = model.provider.modelController; +GenerativeAI.callGenerativeModel( + geminiModel, + onAnswer: (x) { + stdout.write(x); //each word in the stream + }, + onError: (e){}, + systemPrompt: 'Give a 100 word summary of the provided word. Only give the answer', + userPrompt: 'Pizza', + credential: 'AIza.....', + stream: true, +); +``` + +### Directly Using a Model (eg: Gemini) +```dart +final LLMModel model = LLMProvider.gemini.getLLMByIdentifier('gemini-2.0-flash'); +final ModelController controller = model.provider.modelController; +final payload = controller.inputPayload; +payload.systemPrompt = 'Say YES or NO'; +payload.userPrompt = 'The sun sets in the west'; +payload.credential = 'AIza....'; +final genAIRequest = controller.createRequest(model, payload); +final answer = await GenerativeAI.executeGenAIRequest(model, genAIRequest); +print(answer) +``` \ No newline at end of file diff --git a/packages/genai/genai_example/analysis_options.yaml b/packages/genai/genai_example/analysis_options.yaml new file mode 100644 index 00000000..0d290213 --- /dev/null +++ b/packages/genai/genai_example/analysis_options.yaml @@ -0,0 +1,28 @@ +# This file configures the analyzer, which statically analyzes Dart code to +# check for errors, warnings, and lints. +# +# The issues identified by the analyzer are surfaced in the UI of Dart-enabled +# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be +# invoked from the command line by running `flutter analyze`. + +# The following line activates a set of recommended lints for Flutter apps, +# packages, and plugins designed to encourage good coding practices. +include: package:flutter_lints/flutter.yaml + +linter: + # The lint rules applied to this project can be customized in the + # section below to disable rules from the `package:flutter_lints/flutter.yaml` + # included above or to enable additional rules. A list of all available lints + # and their documentation is published at https://dart.dev/lints. + # + # Instead of disabling a lint rule for the entire project in the + # section below, it can also be suppressed for a single line of code + # or a specific dart file by using the `// ignore: name_of_lint` and + # `// ignore_for_file: name_of_lint` syntax on the line or in the file + # producing the lint. + rules: + # avoid_print: false # Uncomment to disable the `avoid_print` rule + # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule + +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options diff --git a/packages/genai/genai_example/lib/main.dart b/packages/genai/genai_example/lib/main.dart new file mode 100644 index 00000000..ed63985c --- /dev/null +++ b/packages/genai/genai_example/lib/main.dart @@ -0,0 +1,191 @@ +import 'package:flutter/material.dart'; +import 'package:genai/genai.dart'; + +void main() { + runApp(const GenAIExample()); +} + +class GenAIExample extends StatelessWidget { + const GenAIExample({super.key}); + @override + Widget build(BuildContext context) { + return MaterialApp( + title: 'GenAI Example', + theme: ThemeData( + colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple), + ), + home: AIExample(), + ); + } +} + +class AIExample extends StatefulWidget { + const AIExample({super.key}); + + @override + State createState() => _AIExampleState(); +} + +class _AIExampleState extends State { + late final Future aM; + + @override + void initState() { + super.initState(); + aM = ModelManager.fetchAvailableModels(); //fetch latest LLMs + systemPromptController.text = 'Give me a 200 word essay on the given topic'; + inputPromptController.text = 'Apple'; + } + + generateAIResponse({bool stream = false}) { + setState(() { + output = ""; + }); + callGenerativeModel( + kModelProvidersMap[selectedProvider]?.defaultAIRequestModel.copyWith( + model: selectedModel, + apiKey: credentialController.value.text, + systemPrompt: systemPromptController.value.text, + userPrompt: inputPromptController.value.text, + stream: stream, + ), + onAnswer: (x) { + setState(() { + output += "$x "; + }); + }, + onError: (e) { + debugPrint(e); + }, + ); + } + + String output = ""; + ModelAPIProvider selectedProvider = ModelAPIProvider.ollama; + String selectedModel = ""; + + TextEditingController systemPromptController = TextEditingController(); + TextEditingController inputPromptController = TextEditingController(); + TextEditingController credentialController = TextEditingController(); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: Text('GenAI Example')), + body: FutureBuilder( + future: aM, + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.done && + snapshot.hasData && + snapshot.data != null) { + final data = snapshot.data!; + final mappedData = data.map; + return SingleChildScrollView( + padding: EdgeInsets.all(20), + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text('Providers'), + SizedBox(height: 10), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + ...data.modelProviders.map( + (x) => Container( + padding: EdgeInsets.only(right: 10), + child: GestureDetector( + onTap: () { + setState(() { + selectedProvider = x.providerId!; + }); + }, + child: Chip( + label: Text(x.providerName ?? ""), + backgroundColor: selectedProvider == x.providerId + ? Colors.blue[50] + : Colors.transparent, + ), + ), + ), + ), + ], + ), + SizedBox(height: 20), + Text('Models'), + SizedBox(height: 10), + Wrap( + spacing: 5, + runSpacing: 5, + children: [ + ...(mappedData[selectedProvider]?.models ?? []).map( + (x) => Container( + padding: EdgeInsets.only(right: 10), + child: GestureDetector( + onTap: () { + setState(() { + selectedModel = x.id!; + }); + }, + child: Chip( + label: Text(x.name ?? ""), + backgroundColor: selectedModel == x.id + ? Colors.blue[50] + : Colors.transparent, + ), + ), + ), + ), + ], + ), + SizedBox(height: 30), + SizedBox( + width: 400, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text('Input Prompt'), + TextField(controller: inputPromptController), + SizedBox(height: 20), + Text('System Prompt'), + TextField(controller: systemPromptController), + SizedBox(height: 20), + Text('Credential'), + TextField(controller: credentialController), + SizedBox(height: 20), + ], + ), + ), + SizedBox(height: 30), + Row( + mainAxisSize: MainAxisSize.min, + children: [ + ElevatedButton( + onPressed: () { + generateAIResponse(); + }, + child: Text('Generate Response (SINGLE-RESPONSE)'), + ), + SizedBox(width: 20), + ElevatedButton( + onPressed: () { + generateAIResponse(stream: true); + }, + child: Text('Generate Response (STREAM)'), + ), + ], + ), + SizedBox(height: 30), + Divider(), + SizedBox(height: 20), + + Text(output), + ], + ), + ); + } + return CircularProgressIndicator(); + }, + ), + ); + } +} diff --git a/packages/genai/genai_example/pubspec.lock b/packages/genai/genai_example/pubspec.lock new file mode 100644 index 00000000..7fe46cb5 --- /dev/null +++ b/packages/genai/genai_example/pubspec.lock @@ -0,0 +1,370 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + adaptive_number: + dependency: transitive + description: + name: adaptive_number + sha256: "3a567544e9b5c9c803006f51140ad544aedc79604fd4f3f2c1380003f97c1d77" + url: "https://pub.dev" + source: hosted + version: "1.0.0" + async: + dependency: transitive + description: + name: async + sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb" + url: "https://pub.dev" + source: hosted + version: "2.13.0" + better_networking: + dependency: "direct overridden" + description: + path: "../../better_networking" + relative: true + source: path + version: "0.0.1" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + characters: + dependency: transitive + description: + name: characters + sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 + url: "https://pub.dev" + source: hosted + version: "1.4.0" + clock: + dependency: transitive + description: + name: clock + sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b + url: "https://pub.dev" + source: hosted + version: "1.1.2" + collection: + dependency: transitive + description: + name: collection + sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" + url: "https://pub.dev" + source: hosted + version: "1.19.1" + convert: + dependency: transitive + description: + name: convert + sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68 + url: "https://pub.dev" + source: hosted + version: "3.1.2" + crypto: + dependency: transitive + description: + name: crypto + sha256: "1e445881f28f22d6140f181e07737b22f1e099a5e1ff94b0af2f9e4a463f4855" + url: "https://pub.dev" + source: hosted + version: "3.0.6" + cupertino_icons: + dependency: "direct main" + description: + name: cupertino_icons + sha256: ba631d1c7f7bef6b729a622b7b752645a2d076dba9976925b8f25725a30e1ee6 + url: "https://pub.dev" + source: hosted + version: "1.0.8" + dart_jsonwebtoken: + dependency: transitive + description: + name: dart_jsonwebtoken + sha256: "21ce9f8a8712f741e8d6876a9c82c0f8a257fe928c4378a91d8527b92a3fd413" + url: "https://pub.dev" + source: hosted + version: "3.2.0" + ed25519_edwards: + dependency: transitive + description: + name: ed25519_edwards + sha256: "6ce0112d131327ec6d42beede1e5dfd526069b18ad45dcf654f15074ad9276cd" + url: "https://pub.dev" + source: hosted + version: "0.3.1" + fake_async: + dependency: transitive + description: + name: fake_async + sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" + url: "https://pub.dev" + source: hosted + version: "1.3.3" + fixnum: + dependency: transitive + description: + name: fixnum + sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be + url: "https://pub.dev" + source: hosted + version: "1.1.1" + flutter: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_lints: + dependency: "direct dev" + description: + name: flutter_lints + sha256: "5398f14efa795ffb7a33e9b6a08798b26a180edac4ad7db3f231e40f82ce11e1" + url: "https://pub.dev" + source: hosted + version: "5.0.0" + flutter_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" + freezed_annotation: + dependency: transitive + description: + name: freezed_annotation + sha256: c2e2d632dd9b8a2b7751117abcfc2b4888ecfe181bd9fca7170d9ef02e595fe2 + url: "https://pub.dev" + source: hosted + version: "2.4.4" + genai: + dependency: "direct main" + description: + path: ".." + relative: true + source: path + version: "0.0.1" + http: + dependency: transitive + description: + name: http + sha256: "2c11f3f94c687ee9bad77c171151672986360b2b001d109814ee7140b2cf261b" + url: "https://pub.dev" + source: hosted + version: "1.4.0" + http_parser: + dependency: transitive + description: + name: http_parser + sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571" + url: "https://pub.dev" + source: hosted + version: "4.1.2" + json5: + dependency: transitive + description: + name: json5 + sha256: b67d6e06c9e225c8277d3c43f796677af7975a2a2b0669ff12ba38ff466a31f4 + url: "https://pub.dev" + source: hosted + version: "0.8.2" + json_annotation: + dependency: transitive + description: + name: json_annotation + sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1" + url: "https://pub.dev" + source: hosted + version: "4.9.0" + leak_tracker: + dependency: transitive + description: + name: leak_tracker + sha256: "6bb818ecbdffe216e81182c2f0714a2e62b593f4a4f13098713ff1685dfb6ab0" + url: "https://pub.dev" + source: hosted + version: "10.0.9" + leak_tracker_flutter_testing: + dependency: transitive + description: + name: leak_tracker_flutter_testing + sha256: f8b613e7e6a13ec79cfdc0e97638fddb3ab848452eff057653abd3edba760573 + url: "https://pub.dev" + source: hosted + version: "3.0.9" + leak_tracker_testing: + dependency: transitive + description: + name: leak_tracker_testing + sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3" + url: "https://pub.dev" + source: hosted + version: "3.0.1" + lints: + dependency: transitive + description: + name: lints + sha256: c35bb79562d980e9a453fc715854e1ed39e24e7d0297a880ef54e17f9874a9d7 + url: "https://pub.dev" + source: hosted + version: "5.1.1" + matcher: + dependency: transitive + description: + name: matcher + sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 + url: "https://pub.dev" + source: hosted + version: "0.12.17" + material_color_utilities: + dependency: transitive + description: + name: material_color_utilities + sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec + url: "https://pub.dev" + source: hosted + version: "0.11.1" + meta: + dependency: transitive + description: + name: meta + sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c + url: "https://pub.dev" + source: hosted + version: "1.16.0" + nanoid: + dependency: transitive + description: + name: nanoid + sha256: be3f8752d9046c825df2f3914195151eb876f3ad64b9d833dd0b799b77b8759e + url: "https://pub.dev" + source: hosted + version: "1.0.0" + path: + dependency: transitive + description: + name: path + sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" + url: "https://pub.dev" + source: hosted + version: "1.9.1" + petitparser: + dependency: transitive + description: + name: petitparser + sha256: "07c8f0b1913bcde1ff0d26e57ace2f3012ccbf2b204e070290dad3bb22797646" + url: "https://pub.dev" + source: hosted + version: "6.1.0" + pointycastle: + dependency: transitive + description: + name: pointycastle + sha256: "92aa3841d083cc4b0f4709b5c74fd6409a3e6ba833ffc7dc6a8fee096366acf5" + url: "https://pub.dev" + source: hosted + version: "4.0.0" + seed: + dependency: "direct overridden" + description: + path: "../../seed" + relative: true + source: path + version: "0.0.3" + sky_engine: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + source_span: + dependency: transitive + description: + name: source_span + sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c" + url: "https://pub.dev" + source: hosted + version: "1.10.1" + stack_trace: + dependency: transitive + description: + name: stack_trace + sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" + url: "https://pub.dev" + source: hosted + version: "1.12.1" + stream_channel: + dependency: transitive + description: + name: stream_channel + sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + string_scanner: + dependency: transitive + description: + name: string_scanner + sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" + url: "https://pub.dev" + source: hosted + version: "1.4.1" + term_glyph: + dependency: transitive + description: + name: term_glyph + sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" + url: "https://pub.dev" + source: hosted + version: "1.2.2" + test_api: + dependency: transitive + description: + name: test_api + sha256: fb31f383e2ee25fbbfe06b40fe21e1e458d14080e3c67e7ba0acfde4df4e0bbd + url: "https://pub.dev" + source: hosted + version: "0.7.4" + typed_data: + dependency: transitive + description: + name: typed_data + sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 + url: "https://pub.dev" + source: hosted + version: "1.4.0" + vector_math: + dependency: transitive + description: + name: vector_math + sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + vm_service: + dependency: transitive + description: + name: vm_service + sha256: ddfa8d30d89985b96407efce8acbdd124701f96741f2d981ca860662f1c0dc02 + url: "https://pub.dev" + source: hosted + version: "15.0.0" + web: + dependency: transitive + description: + name: web + sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a" + url: "https://pub.dev" + source: hosted + version: "1.1.1" + xml: + dependency: transitive + description: + name: xml + sha256: b015a8ad1c488f66851d762d3090a21c600e479dc75e68328c52774040cf9226 + url: "https://pub.dev" + source: hosted + version: "6.5.0" +sdks: + dart: ">=3.8.0 <4.0.0" + flutter: ">=3.18.0-18.0.pre.54" diff --git a/packages/genai/genai_example/pubspec.yaml b/packages/genai/genai_example/pubspec.yaml new file mode 100644 index 00000000..fbfc5abb --- /dev/null +++ b/packages/genai/genai_example/pubspec.yaml @@ -0,0 +1,90 @@ +name: genai_example +description: "A new Flutter project." +# The following line prevents the package from being accidentally published to +# pub.dev using `flutter pub publish`. This is preferred for private packages. +publish_to: "none" # Remove this line if you wish to publish to pub.dev + +# The following defines the version and build number for your application. +# A version number is three numbers separated by dots, like 1.2.43 +# followed by an optional build number separated by a +. +# Both the version and the builder number may be overridden in flutter +# build by specifying --build-name and --build-number, respectively. +# In Android, build-name is used as versionName while build-number used as versionCode. +# Read more about Android versioning at https://developer.android.com/studio/publish/versioning +# In iOS, build-name is used as CFBundleShortVersionString while build-number is used as CFBundleVersion. +# Read more about iOS versioning at +# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html +# In Windows, build-name is used as the major, minor, and patch parts +# of the product and file versions while build-number is used as the build suffix. +version: 1.0.0+1 + +environment: + sdk: ^3.8.0 + +# Dependencies specify other packages that your package needs in order to work. +# To automatically upgrade your package dependencies to the latest versions +# consider running `flutter pub upgrade --major-versions`. Alternatively, +# dependencies can be manually updated by changing the version numbers below to +# the latest version available on pub.dev. To see which dependencies have newer +# versions available, run `flutter pub outdated`. +dependencies: + flutter: + sdk: flutter + genai: + path: .. + + # The following adds the Cupertino Icons font to your application. + # Use with the CupertinoIcons class for iOS style icons. + cupertino_icons: ^1.0.8 + +dev_dependencies: + flutter_test: + sdk: flutter + + # The "flutter_lints" package below contains a set of recommended lints to + # encourage good coding practices. The lint set provided by the package is + # activated in the `analysis_options.yaml` file located at the root of your + # package. See that file for information about deactivating specific lint + # rules and activating additional ones. + flutter_lints: ^5.0.0 + +# For information on the generic Dart part of this file, see the +# following page: https://dart.dev/tools/pub/pubspec + +# The following section is specific to Flutter packages. +flutter: + # The following line ensures that the Material Icons font is + # included with your application, so that you can use the icons in + # the material Icons class. + uses-material-design: true + + # To add assets to your application, add an assets section, like this: + # assets: + # - images/a_dot_burr.jpeg + # - images/a_dot_ham.jpeg + + # An image asset can refer to one or more resolution-specific "variants", see + # https://flutter.dev/to/resolution-aware-images + + # For details regarding adding assets from package dependencies, see + # https://flutter.dev/to/asset-from-package + + # To add custom fonts to your application, add a fonts section here, + # in this "flutter" section. Each entry in this list should have a + # "family" key with the font family name, and a "fonts" key with a + # list giving the asset and other descriptors for the font. For + # example: + # fonts: + # - family: Schyler + # fonts: + # - asset: fonts/Schyler-Regular.ttf + # - asset: fonts/Schyler-Italic.ttf + # style: italic + # - family: Trajan Pro + # fonts: + # - asset: fonts/TrajanPro.ttf + # - asset: fonts/TrajanPro_Bold.ttf + # weight: 700 + # + # For details regarding fonts from package dependencies, + # see https://flutter.dev/to/font-from-package diff --git a/packages/genai/genai_example/pubspec_overrides.yaml b/packages/genai/genai_example/pubspec_overrides.yaml new file mode 100644 index 00000000..6581ff92 --- /dev/null +++ b/packages/genai/genai_example/pubspec_overrides.yaml @@ -0,0 +1,8 @@ +# melos_managed_dependency_overrides: better_networking,genai,seed +dependency_overrides: + better_networking: + path: ../../better_networking + genai: + path: .. + seed: + path: ../../seed diff --git a/packages/genai/lib/consts.dart b/packages/genai/lib/consts.dart new file mode 100644 index 00000000..6921a8c1 --- /dev/null +++ b/packages/genai/lib/consts.dart @@ -0,0 +1,10 @@ +import 'models/models.dart'; + +const kKeyStream = 'stream'; + +final kAvailableModels = AvailableModels.fromJson(kModelsData); + +const kModelRemoteUrl = + 'https://raw.githubusercontent.com/foss42/apidash/package/genai/packages/genai/models.json'; + +const kBaseOllamaUrl = 'http://localhost:11434'; diff --git a/packages/genai/lib/genai.dart b/packages/genai/lib/genai.dart new file mode 100644 index 00000000..7d58af8d --- /dev/null +++ b/packages/genai/lib/genai.dart @@ -0,0 +1,4 @@ +export 'models/models.dart'; +export 'interface/interface.dart'; +export 'utils/utils.dart'; +export 'widgets/widgets.dart'; diff --git a/packages/genai/lib/interface/consts.dart b/packages/genai/lib/interface/consts.dart new file mode 100644 index 00000000..1b05c9be --- /dev/null +++ b/packages/genai/lib/interface/consts.dart @@ -0,0 +1,71 @@ +import '../consts.dart'; +import '../models/models.dart'; +import 'model_providers/model_providers.dart'; + +enum ModelAPIProvider { openai, anthropic, gemini, azureopenai, ollama } + +final kModelProvidersMap = { + ModelAPIProvider.openai: OpenAIModel.instance, + ModelAPIProvider.anthropic: AnthropicModel.instance, + ModelAPIProvider.gemini: GeminiModel.instance, + ModelAPIProvider.azureopenai: AzureOpenAIModel.instance, + ModelAPIProvider.ollama: OllamaModel.instance, +}; + +const kAnthropicUrl = 'https://api.anthropic.com/v1/messages'; +const kGeminiUrl = 'https://generativelanguage.googleapis.com/v1beta/models'; +const kOpenAIUrl = 'https://api.openai.com/v1/chat/completions'; +const kOllamaUrl = '$kBaseOllamaUrl/v1/chat/completions'; + +final kDefaultAiRequestModel = AIRequestModel( + url: '', + model: '', + apiKey: '', + systemPrompt: '', + userPrompt: '', + modelConfigs: [ + kDefaultModelConfigTemperature, + kDefaultModelConfigTopP, + kDefaultModelConfigMaxTokens, + ], + stream: false, +); + +final kDefaultModelConfigTemperature = ModelConfig( + id: 'temperature', + name: 'Temperature', + description: 'The Temperature of the Model', + type: ConfigType.slider, + value: ConfigSliderValue(value: (0, 0.5, 1)), +); + +final kDefaultModelConfigTopP = ModelConfig( + id: 'top_p', + name: 'Top P', + description: 'The Top P of the Model', + type: ConfigType.slider, + value: ConfigSliderValue(value: (0, 0.95, 1)), +); + +final kDefaultModelConfigMaxTokens = ModelConfig( + id: 'max_tokens', + name: 'Maximum Tokens', + description: 'The maximum number of tokens allowed in the output', + type: ConfigType.numeric, + value: ConfigNumericValue(value: 1024), +); + +final kDefaultModelConfigStream = ModelConfig( + id: 'stream', + name: 'Enable Streaming Mode', + description: 'The LLM output will be sent in a stream instead of all at once', + type: ConfigType.boolean, + value: ConfigBooleanValue(value: false), +); + +final kDefaultGeminiModelConfigTopP = kDefaultModelConfigTopP.copyWith( + id: 'topP', +); + +final kDefaultGeminiModelConfigMaxTokens = kDefaultModelConfigMaxTokens + .copyWith(id: 'maxOutputTokens'); diff --git a/packages/genai/lib/interface/interface.dart b/packages/genai/lib/interface/interface.dart new file mode 100644 index 00000000..4ab63517 --- /dev/null +++ b/packages/genai/lib/interface/interface.dart @@ -0,0 +1,2 @@ +export 'model_providers/model_providers.dart'; +export 'consts.dart'; diff --git a/packages/genai/lib/interface/model_providers/anthropic.dart b/packages/genai/lib/interface/model_providers/anthropic.dart new file mode 100644 index 00000000..c6d1f955 --- /dev/null +++ b/packages/genai/lib/interface/model_providers/anthropic.dart @@ -0,0 +1,52 @@ +import 'package:better_networking/better_networking.dart'; +import '../../models/models.dart'; +import '../consts.dart'; + +class AnthropicModel extends ModelProvider { + static final instance = AnthropicModel(); + + @override + AIRequestModel get defaultAIRequestModel => kDefaultAiRequestModel.copyWith( + modelApiProvider: ModelAPIProvider.anthropic, + url: kAnthropicUrl, + ); + + @override + HttpRequestModel? createRequest(AIRequestModel? aiRequestModel) { + if (aiRequestModel == null) { + return null; + } + return HttpRequestModel( + method: HTTPVerb.post, + url: aiRequestModel.url, + headers: const [ + NameValueModel(name: "anthropic-version", value: "2023-06-01"), + ], + authModel: aiRequestModel.apiKey == null + ? null + : AuthModel( + type: APIAuthType.apiKey, + apikey: AuthApiKeyModel(key: aiRequestModel.apiKey!), + ), + body: kJsonEncoder.convert({ + "model": aiRequestModel.model, + "messages": [ + {"role": "system", "content": aiRequestModel.systemPrompt}, + {"role": "user", "content": aiRequestModel.userPrompt}, + ], + ...aiRequestModel.getModelConfigMap(), + if (aiRequestModel.stream ?? false) ...{'stream': true}, + }), + ); + } + + @override + String? outputFormatter(Map x) { + return x['content']?[0]['text']; + } + + @override + String? streamOutputFormatter(Map x) { + return x['text']; + } +} diff --git a/packages/genai/lib/interface/model_providers/azureopenai.dart b/packages/genai/lib/interface/model_providers/azureopenai.dart new file mode 100644 index 00000000..587c1c49 --- /dev/null +++ b/packages/genai/lib/interface/model_providers/azureopenai.dart @@ -0,0 +1,58 @@ +import 'package:better_networking/better_networking.dart'; +import '../../models/models.dart'; +import '../consts.dart'; + +class AzureOpenAIModel extends ModelProvider { + static final instance = AzureOpenAIModel(); + + @override + AIRequestModel get defaultAIRequestModel => kDefaultAiRequestModel.copyWith( + modelApiProvider: ModelAPIProvider.azureopenai, + ); + + @override + HttpRequestModel? createRequest(AIRequestModel? aiRequestModel) { + if (aiRequestModel == null) { + return null; + } + if (aiRequestModel.url.isEmpty) { + throw Exception('MODEL ENDPOINT IS EMPTY'); + } + return HttpRequestModel( + method: HTTPVerb.post, + url: aiRequestModel.url, + authModel: aiRequestModel.apiKey == null + ? null + : AuthModel( + type: APIAuthType.apiKey, + apikey: AuthApiKeyModel( + key: aiRequestModel.apiKey!, + name: 'api-key', + ), + ), + body: kJsonEncoder.convert({ + "model": aiRequestModel.model, + "messages": [ + {"role": "system", "content": aiRequestModel.systemPrompt}, + if (aiRequestModel.userPrompt.isNotEmpty) ...{ + {"role": "user", "content": aiRequestModel.userPrompt}, + } else ...{ + {"role": "user", "content": "Generate"}, + }, + ], + ...aiRequestModel.getModelConfigMap(), + if (aiRequestModel.stream ?? false) ...{'stream': true}, + }), + ); + } + + @override + String? outputFormatter(Map x) { + return x["choices"]?[0]["message"]?["content"]?.trim(); + } + + @override + String? streamOutputFormatter(Map x) { + return x["choices"]?[0]["delta"]?["content"]; + } +} diff --git a/packages/genai/lib/interface/model_providers/gemini.dart b/packages/genai/lib/interface/model_providers/gemini.dart new file mode 100644 index 00000000..f3c57867 --- /dev/null +++ b/packages/genai/lib/interface/model_providers/gemini.dart @@ -0,0 +1,75 @@ +import 'package:better_networking/better_networking.dart'; +import '../../models/models.dart'; +import '../consts.dart'; + +class GeminiModel extends ModelProvider { + static final instance = GeminiModel(); + + @override + AIRequestModel get defaultAIRequestModel => kDefaultAiRequestModel.copyWith( + modelApiProvider: ModelAPIProvider.gemini, + url: kGeminiUrl, + modelConfigs: [ + kDefaultModelConfigTemperature, + kDefaultGeminiModelConfigTopP, + kDefaultGeminiModelConfigMaxTokens, + ], + ); + + @override + HttpRequestModel? createRequest(AIRequestModel? aiRequestModel) { + if (aiRequestModel == null) { + return null; + } + List params = []; + String endpoint = "${aiRequestModel.url}/${aiRequestModel.model}:"; + if (aiRequestModel.stream ?? false) { + endpoint += 'streamGenerateContent'; + params.add(const NameValueModel(name: "alt", value: "sse")); + } else { + endpoint += 'generateContent'; + } + + return HttpRequestModel( + method: HTTPVerb.post, + url: endpoint, + authModel: aiRequestModel.apiKey == null + ? null + : AuthModel( + type: APIAuthType.apiKey, + apikey: AuthApiKeyModel( + key: aiRequestModel.apiKey!, + location: 'query', + name: 'key', + ), + ), + body: kJsonEncoder.convert({ + "contents": [ + { + "role": "user", + "parts": [ + {"text": aiRequestModel.userPrompt}, + ], + }, + ], + "systemInstruction": { + "role": "system", + "parts": [ + {"text": aiRequestModel.systemPrompt}, + ], + }, + "generationConfig": aiRequestModel.getModelConfigMap(), + }), + ); + } + + @override + String? outputFormatter(Map x) { + return x['candidates']?[0]?['content']?['parts']?[0]?['text']; + } + + @override + String? streamOutputFormatter(Map x) { + return x['candidates']?[0]?['content']?['parts']?[0]?['text']; + } +} diff --git a/packages/genai/lib/interface/model_providers/model_providers.dart b/packages/genai/lib/interface/model_providers/model_providers.dart new file mode 100644 index 00000000..b3fa202f --- /dev/null +++ b/packages/genai/lib/interface/model_providers/model_providers.dart @@ -0,0 +1,5 @@ +export 'anthropic.dart'; +export 'gemini.dart'; +export 'azureopenai.dart'; +export 'openai.dart'; +export 'ollama.dart'; diff --git a/packages/genai/lib/interface/model_providers/ollama.dart b/packages/genai/lib/interface/model_providers/ollama.dart new file mode 100644 index 00000000..6a6fc713 --- /dev/null +++ b/packages/genai/lib/interface/model_providers/ollama.dart @@ -0,0 +1,14 @@ +import '../../models/models.dart'; +import '../consts.dart'; +import 'openai.dart'; + +class OllamaModel extends OpenAIModel { + static final instance = OllamaModel(); + + @override + AIRequestModel get defaultAIRequestModel => kDefaultAiRequestModel.copyWith( + modelApiProvider: ModelAPIProvider.ollama, + url: kOllamaUrl, + modelConfigs: [kDefaultModelConfigTemperature, kDefaultModelConfigTopP], + ); +} diff --git a/packages/genai/lib/interface/model_providers/openai.dart b/packages/genai/lib/interface/model_providers/openai.dart new file mode 100644 index 00000000..a2b08f3d --- /dev/null +++ b/packages/genai/lib/interface/model_providers/openai.dart @@ -0,0 +1,53 @@ +import 'package:better_networking/better_networking.dart'; +import '../../models/models.dart'; +import '../consts.dart'; + +class OpenAIModel extends ModelProvider { + static final instance = OpenAIModel(); + + @override + AIRequestModel get defaultAIRequestModel => kDefaultAiRequestModel.copyWith( + modelApiProvider: ModelAPIProvider.openai, + url: kOpenAIUrl, + ); + + @override + HttpRequestModel? createRequest(AIRequestModel? aiRequestModel) { + if (aiRequestModel == null) { + return null; + } + return HttpRequestModel( + method: HTTPVerb.post, + url: aiRequestModel.url, + authModel: aiRequestModel.apiKey == null + ? null + : AuthModel( + type: APIAuthType.bearer, + bearer: AuthBearerModel(token: aiRequestModel.apiKey!), + ), + body: kJsonEncoder.convert({ + "model": aiRequestModel.model, + "messages": [ + {"role": "system", "content": aiRequestModel.systemPrompt}, + if (aiRequestModel.userPrompt.isNotEmpty) ...{ + {"role": "user", "content": aiRequestModel.userPrompt}, + } else ...{ + {"role": "user", "content": "Generate"}, + }, + ], + ...aiRequestModel.getModelConfigMap(), + if (aiRequestModel.stream ?? false) ...{'stream': true}, + }), + ); + } + + @override + String? outputFormatter(Map x) { + return x["choices"]?[0]["message"]?["content"]?.trim(); + } + + @override + String? streamOutputFormatter(Map x) { + return x["choices"]?[0]["delta"]?["content"]; + } +} diff --git a/packages/genai/lib/models/ai_request_model.dart b/packages/genai/lib/models/ai_request_model.dart new file mode 100644 index 00000000..e9a3a297 --- /dev/null +++ b/packages/genai/lib/models/ai_request_model.dart @@ -0,0 +1,54 @@ +import 'package:better_networking/better_networking.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +import '../interface/interface.dart'; +import 'model_config.dart'; +part 'ai_request_model.freezed.dart'; +part 'ai_request_model.g.dart'; + +@freezed +class AIRequestModel with _$AIRequestModel { + const AIRequestModel._(); + + @JsonSerializable(explicitToJson: true, anyMap: true) + const factory AIRequestModel({ + ModelAPIProvider? modelApiProvider, + @Default("") String url, + @Default(null) String? model, + @Default(null) String? apiKey, + @JsonKey(name: "system_prompt") @Default("") String systemPrompt, + @JsonKey(name: "user_prompt") @Default("") String userPrompt, + @JsonKey(name: "model_configs") + @Default([]) + List modelConfigs, + @Default(null) bool? stream, + }) = _AIRequestModel; + + factory AIRequestModel.fromJson(Map json) => + _$AIRequestModelFromJson(json); + + HttpRequestModel? get httpRequestModel => + kModelProvidersMap[modelApiProvider]?.createRequest(this); + + String? getFormattedOutput(Map x) => + kModelProvidersMap[modelApiProvider]?.outputFormatter(x); + + String? getFormattedStreamOutput(Map x) => + kModelProvidersMap[modelApiProvider]?.streamOutputFormatter(x); + + Map getModelConfigMap() { + Map m = {}; + for (var config in modelConfigs) { + m[config.id] = config.value.getPayloadValue(); + } + return m; + } + + int? getModelConfigIdx(String id) { + for (var idx = 0; idx < modelConfigs.length; idx++) { + if (modelConfigs[idx].id == id) { + return idx; + } + } + return null; + } +} diff --git a/packages/genai/lib/models/ai_request_model.freezed.dart b/packages/genai/lib/models/ai_request_model.freezed.dart new file mode 100644 index 00000000..7cec0388 --- /dev/null +++ b/packages/genai/lib/models/ai_request_model.freezed.dart @@ -0,0 +1,361 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'ai_request_model.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(T value) => value; + +final _privateConstructorUsedError = UnsupportedError( + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models', +); + +AIRequestModel _$AIRequestModelFromJson(Map json) { + return _AIRequestModel.fromJson(json); +} + +/// @nodoc +mixin _$AIRequestModel { + ModelAPIProvider? get modelApiProvider => throw _privateConstructorUsedError; + String get url => throw _privateConstructorUsedError; + String? get model => throw _privateConstructorUsedError; + String? get apiKey => throw _privateConstructorUsedError; + @JsonKey(name: "system_prompt") + String get systemPrompt => throw _privateConstructorUsedError; + @JsonKey(name: "user_prompt") + String get userPrompt => throw _privateConstructorUsedError; + @JsonKey(name: "model_configs") + List get modelConfigs => throw _privateConstructorUsedError; + bool? get stream => throw _privateConstructorUsedError; + + /// Serializes this AIRequestModel to a JSON map. + Map toJson() => throw _privateConstructorUsedError; + + /// Create a copy of AIRequestModel + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $AIRequestModelCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $AIRequestModelCopyWith<$Res> { + factory $AIRequestModelCopyWith( + AIRequestModel value, + $Res Function(AIRequestModel) then, + ) = _$AIRequestModelCopyWithImpl<$Res, AIRequestModel>; + @useResult + $Res call({ + ModelAPIProvider? modelApiProvider, + String url, + String? model, + String? apiKey, + @JsonKey(name: "system_prompt") String systemPrompt, + @JsonKey(name: "user_prompt") String userPrompt, + @JsonKey(name: "model_configs") List modelConfigs, + bool? stream, + }); +} + +/// @nodoc +class _$AIRequestModelCopyWithImpl<$Res, $Val extends AIRequestModel> + implements $AIRequestModelCopyWith<$Res> { + _$AIRequestModelCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of AIRequestModel + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? modelApiProvider = freezed, + Object? url = null, + Object? model = freezed, + Object? apiKey = freezed, + Object? systemPrompt = null, + Object? userPrompt = null, + Object? modelConfigs = null, + Object? stream = freezed, + }) { + return _then( + _value.copyWith( + modelApiProvider: freezed == modelApiProvider + ? _value.modelApiProvider + : modelApiProvider // ignore: cast_nullable_to_non_nullable + as ModelAPIProvider?, + url: null == url + ? _value.url + : url // ignore: cast_nullable_to_non_nullable + as String, + model: freezed == model + ? _value.model + : model // ignore: cast_nullable_to_non_nullable + as String?, + apiKey: freezed == apiKey + ? _value.apiKey + : apiKey // ignore: cast_nullable_to_non_nullable + as String?, + systemPrompt: null == systemPrompt + ? _value.systemPrompt + : systemPrompt // ignore: cast_nullable_to_non_nullable + as String, + userPrompt: null == userPrompt + ? _value.userPrompt + : userPrompt // ignore: cast_nullable_to_non_nullable + as String, + modelConfigs: null == modelConfigs + ? _value.modelConfigs + : modelConfigs // ignore: cast_nullable_to_non_nullable + as List, + stream: freezed == stream + ? _value.stream + : stream // ignore: cast_nullable_to_non_nullable + as bool?, + ) + as $Val, + ); + } +} + +/// @nodoc +abstract class _$$AIRequestModelImplCopyWith<$Res> + implements $AIRequestModelCopyWith<$Res> { + factory _$$AIRequestModelImplCopyWith( + _$AIRequestModelImpl value, + $Res Function(_$AIRequestModelImpl) then, + ) = __$$AIRequestModelImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({ + ModelAPIProvider? modelApiProvider, + String url, + String? model, + String? apiKey, + @JsonKey(name: "system_prompt") String systemPrompt, + @JsonKey(name: "user_prompt") String userPrompt, + @JsonKey(name: "model_configs") List modelConfigs, + bool? stream, + }); +} + +/// @nodoc +class __$$AIRequestModelImplCopyWithImpl<$Res> + extends _$AIRequestModelCopyWithImpl<$Res, _$AIRequestModelImpl> + implements _$$AIRequestModelImplCopyWith<$Res> { + __$$AIRequestModelImplCopyWithImpl( + _$AIRequestModelImpl _value, + $Res Function(_$AIRequestModelImpl) _then, + ) : super(_value, _then); + + /// Create a copy of AIRequestModel + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? modelApiProvider = freezed, + Object? url = null, + Object? model = freezed, + Object? apiKey = freezed, + Object? systemPrompt = null, + Object? userPrompt = null, + Object? modelConfigs = null, + Object? stream = freezed, + }) { + return _then( + _$AIRequestModelImpl( + modelApiProvider: freezed == modelApiProvider + ? _value.modelApiProvider + : modelApiProvider // ignore: cast_nullable_to_non_nullable + as ModelAPIProvider?, + url: null == url + ? _value.url + : url // ignore: cast_nullable_to_non_nullable + as String, + model: freezed == model + ? _value.model + : model // ignore: cast_nullable_to_non_nullable + as String?, + apiKey: freezed == apiKey + ? _value.apiKey + : apiKey // ignore: cast_nullable_to_non_nullable + as String?, + systemPrompt: null == systemPrompt + ? _value.systemPrompt + : systemPrompt // ignore: cast_nullable_to_non_nullable + as String, + userPrompt: null == userPrompt + ? _value.userPrompt + : userPrompt // ignore: cast_nullable_to_non_nullable + as String, + modelConfigs: null == modelConfigs + ? _value._modelConfigs + : modelConfigs // ignore: cast_nullable_to_non_nullable + as List, + stream: freezed == stream + ? _value.stream + : stream // ignore: cast_nullable_to_non_nullable + as bool?, + ), + ); + } +} + +/// @nodoc + +@JsonSerializable(explicitToJson: true, anyMap: true) +class _$AIRequestModelImpl extends _AIRequestModel { + const _$AIRequestModelImpl({ + this.modelApiProvider, + this.url = "", + this.model = null, + this.apiKey = null, + @JsonKey(name: "system_prompt") this.systemPrompt = "", + @JsonKey(name: "user_prompt") this.userPrompt = "", + @JsonKey(name: "model_configs") + final List modelConfigs = const [], + this.stream = null, + }) : _modelConfigs = modelConfigs, + super._(); + + factory _$AIRequestModelImpl.fromJson(Map json) => + _$$AIRequestModelImplFromJson(json); + + @override + final ModelAPIProvider? modelApiProvider; + @override + @JsonKey() + final String url; + @override + @JsonKey() + final String? model; + @override + @JsonKey() + final String? apiKey; + @override + @JsonKey(name: "system_prompt") + final String systemPrompt; + @override + @JsonKey(name: "user_prompt") + final String userPrompt; + final List _modelConfigs; + @override + @JsonKey(name: "model_configs") + List get modelConfigs { + if (_modelConfigs is EqualUnmodifiableListView) return _modelConfigs; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_modelConfigs); + } + + @override + @JsonKey() + final bool? stream; + + @override + String toString() { + return 'AIRequestModel(modelApiProvider: $modelApiProvider, url: $url, model: $model, apiKey: $apiKey, systemPrompt: $systemPrompt, userPrompt: $userPrompt, modelConfigs: $modelConfigs, stream: $stream)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$AIRequestModelImpl && + (identical(other.modelApiProvider, modelApiProvider) || + other.modelApiProvider == modelApiProvider) && + (identical(other.url, url) || other.url == url) && + (identical(other.model, model) || other.model == model) && + (identical(other.apiKey, apiKey) || other.apiKey == apiKey) && + (identical(other.systemPrompt, systemPrompt) || + other.systemPrompt == systemPrompt) && + (identical(other.userPrompt, userPrompt) || + other.userPrompt == userPrompt) && + const DeepCollectionEquality().equals( + other._modelConfigs, + _modelConfigs, + ) && + (identical(other.stream, stream) || other.stream == stream)); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => Object.hash( + runtimeType, + modelApiProvider, + url, + model, + apiKey, + systemPrompt, + userPrompt, + const DeepCollectionEquality().hash(_modelConfigs), + stream, + ); + + /// Create a copy of AIRequestModel + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$AIRequestModelImplCopyWith<_$AIRequestModelImpl> get copyWith => + __$$AIRequestModelImplCopyWithImpl<_$AIRequestModelImpl>( + this, + _$identity, + ); + + @override + Map toJson() { + return _$$AIRequestModelImplToJson(this); + } +} + +abstract class _AIRequestModel extends AIRequestModel { + const factory _AIRequestModel({ + final ModelAPIProvider? modelApiProvider, + final String url, + final String? model, + final String? apiKey, + @JsonKey(name: "system_prompt") final String systemPrompt, + @JsonKey(name: "user_prompt") final String userPrompt, + @JsonKey(name: "model_configs") final List modelConfigs, + final bool? stream, + }) = _$AIRequestModelImpl; + const _AIRequestModel._() : super._(); + + factory _AIRequestModel.fromJson(Map json) = + _$AIRequestModelImpl.fromJson; + + @override + ModelAPIProvider? get modelApiProvider; + @override + String get url; + @override + String? get model; + @override + String? get apiKey; + @override + @JsonKey(name: "system_prompt") + String get systemPrompt; + @override + @JsonKey(name: "user_prompt") + String get userPrompt; + @override + @JsonKey(name: "model_configs") + List get modelConfigs; + @override + bool? get stream; + + /// Create a copy of AIRequestModel + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$AIRequestModelImplCopyWith<_$AIRequestModelImpl> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/packages/genai/lib/models/ai_request_model.g.dart b/packages/genai/lib/models/ai_request_model.g.dart new file mode 100644 index 00000000..66db3eee --- /dev/null +++ b/packages/genai/lib/models/ai_request_model.g.dart @@ -0,0 +1,47 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'ai_request_model.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +_$AIRequestModelImpl _$$AIRequestModelImplFromJson(Map json) => + _$AIRequestModelImpl( + modelApiProvider: $enumDecodeNullable( + _$ModelAPIProviderEnumMap, + json['modelApiProvider'], + ), + url: json['url'] as String? ?? "", + model: json['model'] as String? ?? null, + apiKey: json['apiKey'] as String? ?? null, + systemPrompt: json['system_prompt'] as String? ?? "", + userPrompt: json['user_prompt'] as String? ?? "", + modelConfigs: + (json['model_configs'] as List?) + ?.map((e) => ModelConfig.fromJson(e as Map)) + .toList() ?? + const [], + stream: json['stream'] as bool? ?? null, + ); + +Map _$$AIRequestModelImplToJson( + _$AIRequestModelImpl instance, +) => { + 'modelApiProvider': _$ModelAPIProviderEnumMap[instance.modelApiProvider], + 'url': instance.url, + 'model': instance.model, + 'apiKey': instance.apiKey, + 'system_prompt': instance.systemPrompt, + 'user_prompt': instance.userPrompt, + 'model_configs': instance.modelConfigs.map((e) => e.toJson()).toList(), + 'stream': instance.stream, +}; + +const _$ModelAPIProviderEnumMap = { + ModelAPIProvider.openai: 'openai', + ModelAPIProvider.anthropic: 'anthropic', + ModelAPIProvider.gemini: 'gemini', + ModelAPIProvider.azureopenai: 'azureopenai', + ModelAPIProvider.ollama: 'ollama', +}; diff --git a/packages/genai/lib/models/available_models.dart b/packages/genai/lib/models/available_models.dart new file mode 100644 index 00000000..3b6d4c1d --- /dev/null +++ b/packages/genai/lib/models/available_models.dart @@ -0,0 +1,67 @@ +// To parse this JSON data, do +// +// final availableModels = availableModelsFromJson(jsonString); + +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'dart:convert'; +import '../interface/interface.dart'; +import 'ai_request_model.dart'; +part 'available_models.freezed.dart'; +part 'available_models.g.dart'; + +AvailableModels availableModelsFromJson(String str) => + AvailableModels.fromJson(json.decode(str)); + +String availableModelsToJson(AvailableModels data) => + json.encode(data.toJson()); + +@freezed +class AvailableModels with _$AvailableModels { + const AvailableModels._(); + const factory AvailableModels({ + @JsonKey(name: "version") required double version, + @JsonKey(name: "model_providers") + required List modelProviders, + }) = _AvailableModels; + + factory AvailableModels.fromJson(Map json) => + _$AvailableModelsFromJson(json); + + Map get map => + modelProviders.asMap().map( + (i, d) => MapEntry(d.providerId!, d), + ); +} + +@freezed +class AIModelProvider with _$AIModelProvider { + const AIModelProvider._(); + + const factory AIModelProvider({ + @JsonKey(name: "provider_id") ModelAPIProvider? providerId, + @JsonKey(name: "provider_name") String? providerName, + @JsonKey(name: "source_url") String? sourceUrl, + @JsonKey(name: "models") List? models, + }) = _AIModelProvider; + + factory AIModelProvider.fromJson(Map json) => + _$AIModelProviderFromJson(json); + + AIRequestModel? toAiRequestModel({Model? model}) { + var aiRequest = kModelProvidersMap[providerId]?.defaultAIRequestModel; + if (model != null) { + aiRequest = aiRequest?.copyWith(model: model.id); + } + return aiRequest; + } +} + +@freezed +class Model with _$Model { + const factory Model({ + @JsonKey(name: "id") String? id, + @JsonKey(name: "name") String? name, + }) = _Model; + + factory Model.fromJson(Map json) => _$ModelFromJson(json); +} diff --git a/packages/genai/lib/models/available_models.freezed.dart b/packages/genai/lib/models/available_models.freezed.dart new file mode 100644 index 00000000..32ce0905 --- /dev/null +++ b/packages/genai/lib/models/available_models.freezed.dart @@ -0,0 +1,653 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'available_models.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(T value) => value; + +final _privateConstructorUsedError = UnsupportedError( + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models', +); + +AvailableModels _$AvailableModelsFromJson(Map json) { + return _AvailableModels.fromJson(json); +} + +/// @nodoc +mixin _$AvailableModels { + @JsonKey(name: "version") + double get version => throw _privateConstructorUsedError; + @JsonKey(name: "model_providers") + List get modelProviders => + throw _privateConstructorUsedError; + + /// Serializes this AvailableModels to a JSON map. + Map toJson() => throw _privateConstructorUsedError; + + /// Create a copy of AvailableModels + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $AvailableModelsCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $AvailableModelsCopyWith<$Res> { + factory $AvailableModelsCopyWith( + AvailableModels value, + $Res Function(AvailableModels) then, + ) = _$AvailableModelsCopyWithImpl<$Res, AvailableModels>; + @useResult + $Res call({ + @JsonKey(name: "version") double version, + @JsonKey(name: "model_providers") List modelProviders, + }); +} + +/// @nodoc +class _$AvailableModelsCopyWithImpl<$Res, $Val extends AvailableModels> + implements $AvailableModelsCopyWith<$Res> { + _$AvailableModelsCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of AvailableModels + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({Object? version = null, Object? modelProviders = null}) { + return _then( + _value.copyWith( + version: null == version + ? _value.version + : version // ignore: cast_nullable_to_non_nullable + as double, + modelProviders: null == modelProviders + ? _value.modelProviders + : modelProviders // ignore: cast_nullable_to_non_nullable + as List, + ) + as $Val, + ); + } +} + +/// @nodoc +abstract class _$$AvailableModelsImplCopyWith<$Res> + implements $AvailableModelsCopyWith<$Res> { + factory _$$AvailableModelsImplCopyWith( + _$AvailableModelsImpl value, + $Res Function(_$AvailableModelsImpl) then, + ) = __$$AvailableModelsImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({ + @JsonKey(name: "version") double version, + @JsonKey(name: "model_providers") List modelProviders, + }); +} + +/// @nodoc +class __$$AvailableModelsImplCopyWithImpl<$Res> + extends _$AvailableModelsCopyWithImpl<$Res, _$AvailableModelsImpl> + implements _$$AvailableModelsImplCopyWith<$Res> { + __$$AvailableModelsImplCopyWithImpl( + _$AvailableModelsImpl _value, + $Res Function(_$AvailableModelsImpl) _then, + ) : super(_value, _then); + + /// Create a copy of AvailableModels + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({Object? version = null, Object? modelProviders = null}) { + return _then( + _$AvailableModelsImpl( + version: null == version + ? _value.version + : version // ignore: cast_nullable_to_non_nullable + as double, + modelProviders: null == modelProviders + ? _value._modelProviders + : modelProviders // ignore: cast_nullable_to_non_nullable + as List, + ), + ); + } +} + +/// @nodoc +@JsonSerializable() +class _$AvailableModelsImpl extends _AvailableModels { + const _$AvailableModelsImpl({ + @JsonKey(name: "version") required this.version, + @JsonKey(name: "model_providers") + required final List modelProviders, + }) : _modelProviders = modelProviders, + super._(); + + factory _$AvailableModelsImpl.fromJson(Map json) => + _$$AvailableModelsImplFromJson(json); + + @override + @JsonKey(name: "version") + final double version; + final List _modelProviders; + @override + @JsonKey(name: "model_providers") + List get modelProviders { + if (_modelProviders is EqualUnmodifiableListView) return _modelProviders; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_modelProviders); + } + + @override + String toString() { + return 'AvailableModels(version: $version, modelProviders: $modelProviders)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$AvailableModelsImpl && + (identical(other.version, version) || other.version == version) && + const DeepCollectionEquality().equals( + other._modelProviders, + _modelProviders, + )); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => Object.hash( + runtimeType, + version, + const DeepCollectionEquality().hash(_modelProviders), + ); + + /// Create a copy of AvailableModels + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$AvailableModelsImplCopyWith<_$AvailableModelsImpl> get copyWith => + __$$AvailableModelsImplCopyWithImpl<_$AvailableModelsImpl>( + this, + _$identity, + ); + + @override + Map toJson() { + return _$$AvailableModelsImplToJson(this); + } +} + +abstract class _AvailableModels extends AvailableModels { + const factory _AvailableModels({ + @JsonKey(name: "version") required final double version, + @JsonKey(name: "model_providers") + required final List modelProviders, + }) = _$AvailableModelsImpl; + const _AvailableModels._() : super._(); + + factory _AvailableModels.fromJson(Map json) = + _$AvailableModelsImpl.fromJson; + + @override + @JsonKey(name: "version") + double get version; + @override + @JsonKey(name: "model_providers") + List get modelProviders; + + /// Create a copy of AvailableModels + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$AvailableModelsImplCopyWith<_$AvailableModelsImpl> get copyWith => + throw _privateConstructorUsedError; +} + +AIModelProvider _$AIModelProviderFromJson(Map json) { + return _AIModelProvider.fromJson(json); +} + +/// @nodoc +mixin _$AIModelProvider { + @JsonKey(name: "provider_id") + ModelAPIProvider? get providerId => throw _privateConstructorUsedError; + @JsonKey(name: "provider_name") + String? get providerName => throw _privateConstructorUsedError; + @JsonKey(name: "source_url") + String? get sourceUrl => throw _privateConstructorUsedError; + @JsonKey(name: "models") + List? get models => throw _privateConstructorUsedError; + + /// Serializes this AIModelProvider to a JSON map. + Map toJson() => throw _privateConstructorUsedError; + + /// Create a copy of AIModelProvider + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $AIModelProviderCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $AIModelProviderCopyWith<$Res> { + factory $AIModelProviderCopyWith( + AIModelProvider value, + $Res Function(AIModelProvider) then, + ) = _$AIModelProviderCopyWithImpl<$Res, AIModelProvider>; + @useResult + $Res call({ + @JsonKey(name: "provider_id") ModelAPIProvider? providerId, + @JsonKey(name: "provider_name") String? providerName, + @JsonKey(name: "source_url") String? sourceUrl, + @JsonKey(name: "models") List? models, + }); +} + +/// @nodoc +class _$AIModelProviderCopyWithImpl<$Res, $Val extends AIModelProvider> + implements $AIModelProviderCopyWith<$Res> { + _$AIModelProviderCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of AIModelProvider + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? providerId = freezed, + Object? providerName = freezed, + Object? sourceUrl = freezed, + Object? models = freezed, + }) { + return _then( + _value.copyWith( + providerId: freezed == providerId + ? _value.providerId + : providerId // ignore: cast_nullable_to_non_nullable + as ModelAPIProvider?, + providerName: freezed == providerName + ? _value.providerName + : providerName // ignore: cast_nullable_to_non_nullable + as String?, + sourceUrl: freezed == sourceUrl + ? _value.sourceUrl + : sourceUrl // ignore: cast_nullable_to_non_nullable + as String?, + models: freezed == models + ? _value.models + : models // ignore: cast_nullable_to_non_nullable + as List?, + ) + as $Val, + ); + } +} + +/// @nodoc +abstract class _$$AIModelProviderImplCopyWith<$Res> + implements $AIModelProviderCopyWith<$Res> { + factory _$$AIModelProviderImplCopyWith( + _$AIModelProviderImpl value, + $Res Function(_$AIModelProviderImpl) then, + ) = __$$AIModelProviderImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({ + @JsonKey(name: "provider_id") ModelAPIProvider? providerId, + @JsonKey(name: "provider_name") String? providerName, + @JsonKey(name: "source_url") String? sourceUrl, + @JsonKey(name: "models") List? models, + }); +} + +/// @nodoc +class __$$AIModelProviderImplCopyWithImpl<$Res> + extends _$AIModelProviderCopyWithImpl<$Res, _$AIModelProviderImpl> + implements _$$AIModelProviderImplCopyWith<$Res> { + __$$AIModelProviderImplCopyWithImpl( + _$AIModelProviderImpl _value, + $Res Function(_$AIModelProviderImpl) _then, + ) : super(_value, _then); + + /// Create a copy of AIModelProvider + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? providerId = freezed, + Object? providerName = freezed, + Object? sourceUrl = freezed, + Object? models = freezed, + }) { + return _then( + _$AIModelProviderImpl( + providerId: freezed == providerId + ? _value.providerId + : providerId // ignore: cast_nullable_to_non_nullable + as ModelAPIProvider?, + providerName: freezed == providerName + ? _value.providerName + : providerName // ignore: cast_nullable_to_non_nullable + as String?, + sourceUrl: freezed == sourceUrl + ? _value.sourceUrl + : sourceUrl // ignore: cast_nullable_to_non_nullable + as String?, + models: freezed == models + ? _value._models + : models // ignore: cast_nullable_to_non_nullable + as List?, + ), + ); + } +} + +/// @nodoc +@JsonSerializable() +class _$AIModelProviderImpl extends _AIModelProvider { + const _$AIModelProviderImpl({ + @JsonKey(name: "provider_id") this.providerId, + @JsonKey(name: "provider_name") this.providerName, + @JsonKey(name: "source_url") this.sourceUrl, + @JsonKey(name: "models") final List? models, + }) : _models = models, + super._(); + + factory _$AIModelProviderImpl.fromJson(Map json) => + _$$AIModelProviderImplFromJson(json); + + @override + @JsonKey(name: "provider_id") + final ModelAPIProvider? providerId; + @override + @JsonKey(name: "provider_name") + final String? providerName; + @override + @JsonKey(name: "source_url") + final String? sourceUrl; + final List? _models; + @override + @JsonKey(name: "models") + List? get models { + final value = _models; + if (value == null) return null; + if (_models is EqualUnmodifiableListView) return _models; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(value); + } + + @override + String toString() { + return 'AIModelProvider(providerId: $providerId, providerName: $providerName, sourceUrl: $sourceUrl, models: $models)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$AIModelProviderImpl && + (identical(other.providerId, providerId) || + other.providerId == providerId) && + (identical(other.providerName, providerName) || + other.providerName == providerName) && + (identical(other.sourceUrl, sourceUrl) || + other.sourceUrl == sourceUrl) && + const DeepCollectionEquality().equals(other._models, _models)); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => Object.hash( + runtimeType, + providerId, + providerName, + sourceUrl, + const DeepCollectionEquality().hash(_models), + ); + + /// Create a copy of AIModelProvider + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$AIModelProviderImplCopyWith<_$AIModelProviderImpl> get copyWith => + __$$AIModelProviderImplCopyWithImpl<_$AIModelProviderImpl>( + this, + _$identity, + ); + + @override + Map toJson() { + return _$$AIModelProviderImplToJson(this); + } +} + +abstract class _AIModelProvider extends AIModelProvider { + const factory _AIModelProvider({ + @JsonKey(name: "provider_id") final ModelAPIProvider? providerId, + @JsonKey(name: "provider_name") final String? providerName, + @JsonKey(name: "source_url") final String? sourceUrl, + @JsonKey(name: "models") final List? models, + }) = _$AIModelProviderImpl; + const _AIModelProvider._() : super._(); + + factory _AIModelProvider.fromJson(Map json) = + _$AIModelProviderImpl.fromJson; + + @override + @JsonKey(name: "provider_id") + ModelAPIProvider? get providerId; + @override + @JsonKey(name: "provider_name") + String? get providerName; + @override + @JsonKey(name: "source_url") + String? get sourceUrl; + @override + @JsonKey(name: "models") + List? get models; + + /// Create a copy of AIModelProvider + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$AIModelProviderImplCopyWith<_$AIModelProviderImpl> get copyWith => + throw _privateConstructorUsedError; +} + +Model _$ModelFromJson(Map json) { + return _Model.fromJson(json); +} + +/// @nodoc +mixin _$Model { + @JsonKey(name: "id") + String? get id => throw _privateConstructorUsedError; + @JsonKey(name: "name") + String? get name => throw _privateConstructorUsedError; + + /// Serializes this Model to a JSON map. + Map toJson() => throw _privateConstructorUsedError; + + /// Create a copy of Model + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $ModelCopyWith get copyWith => throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $ModelCopyWith<$Res> { + factory $ModelCopyWith(Model value, $Res Function(Model) then) = + _$ModelCopyWithImpl<$Res, Model>; + @useResult + $Res call({ + @JsonKey(name: "id") String? id, + @JsonKey(name: "name") String? name, + }); +} + +/// @nodoc +class _$ModelCopyWithImpl<$Res, $Val extends Model> + implements $ModelCopyWith<$Res> { + _$ModelCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of Model + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({Object? id = freezed, Object? name = freezed}) { + return _then( + _value.copyWith( + id: freezed == id + ? _value.id + : id // ignore: cast_nullable_to_non_nullable + as String?, + name: freezed == name + ? _value.name + : name // ignore: cast_nullable_to_non_nullable + as String?, + ) + as $Val, + ); + } +} + +/// @nodoc +abstract class _$$ModelImplCopyWith<$Res> implements $ModelCopyWith<$Res> { + factory _$$ModelImplCopyWith( + _$ModelImpl value, + $Res Function(_$ModelImpl) then, + ) = __$$ModelImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({ + @JsonKey(name: "id") String? id, + @JsonKey(name: "name") String? name, + }); +} + +/// @nodoc +class __$$ModelImplCopyWithImpl<$Res> + extends _$ModelCopyWithImpl<$Res, _$ModelImpl> + implements _$$ModelImplCopyWith<$Res> { + __$$ModelImplCopyWithImpl( + _$ModelImpl _value, + $Res Function(_$ModelImpl) _then, + ) : super(_value, _then); + + /// Create a copy of Model + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({Object? id = freezed, Object? name = freezed}) { + return _then( + _$ModelImpl( + id: freezed == id + ? _value.id + : id // ignore: cast_nullable_to_non_nullable + as String?, + name: freezed == name + ? _value.name + : name // ignore: cast_nullable_to_non_nullable + as String?, + ), + ); + } +} + +/// @nodoc +@JsonSerializable() +class _$ModelImpl implements _Model { + const _$ModelImpl({ + @JsonKey(name: "id") this.id, + @JsonKey(name: "name") this.name, + }); + + factory _$ModelImpl.fromJson(Map json) => + _$$ModelImplFromJson(json); + + @override + @JsonKey(name: "id") + final String? id; + @override + @JsonKey(name: "name") + final String? name; + + @override + String toString() { + return 'Model(id: $id, name: $name)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$ModelImpl && + (identical(other.id, id) || other.id == id) && + (identical(other.name, name) || other.name == name)); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => Object.hash(runtimeType, id, name); + + /// Create a copy of Model + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$ModelImplCopyWith<_$ModelImpl> get copyWith => + __$$ModelImplCopyWithImpl<_$ModelImpl>(this, _$identity); + + @override + Map toJson() { + return _$$ModelImplToJson(this); + } +} + +abstract class _Model implements Model { + const factory _Model({ + @JsonKey(name: "id") final String? id, + @JsonKey(name: "name") final String? name, + }) = _$ModelImpl; + + factory _Model.fromJson(Map json) = _$ModelImpl.fromJson; + + @override + @JsonKey(name: "id") + String? get id; + @override + @JsonKey(name: "name") + String? get name; + + /// Create a copy of Model + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$ModelImplCopyWith<_$ModelImpl> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/packages/genai/lib/models/available_models.g.dart b/packages/genai/lib/models/available_models.g.dart new file mode 100644 index 00000000..a8bb7816 --- /dev/null +++ b/packages/genai/lib/models/available_models.g.dart @@ -0,0 +1,60 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'available_models.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +_$AvailableModelsImpl _$$AvailableModelsImplFromJson( + Map json, +) => _$AvailableModelsImpl( + version: (json['version'] as num).toDouble(), + modelProviders: (json['model_providers'] as List) + .map((e) => AIModelProvider.fromJson(e as Map)) + .toList(), +); + +Map _$$AvailableModelsImplToJson( + _$AvailableModelsImpl instance, +) => { + 'version': instance.version, + 'model_providers': instance.modelProviders, +}; + +_$AIModelProviderImpl _$$AIModelProviderImplFromJson( + Map json, +) => _$AIModelProviderImpl( + providerId: $enumDecodeNullable( + _$ModelAPIProviderEnumMap, + json['provider_id'], + ), + providerName: json['provider_name'] as String?, + sourceUrl: json['source_url'] as String?, + models: (json['models'] as List?) + ?.map((e) => Model.fromJson(e as Map)) + .toList(), +); + +Map _$$AIModelProviderImplToJson( + _$AIModelProviderImpl instance, +) => { + 'provider_id': _$ModelAPIProviderEnumMap[instance.providerId], + 'provider_name': instance.providerName, + 'source_url': instance.sourceUrl, + 'models': instance.models, +}; + +const _$ModelAPIProviderEnumMap = { + ModelAPIProvider.openai: 'openai', + ModelAPIProvider.anthropic: 'anthropic', + ModelAPIProvider.gemini: 'gemini', + ModelAPIProvider.azureopenai: 'azureopenai', + ModelAPIProvider.ollama: 'ollama', +}; + +_$ModelImpl _$$ModelImplFromJson(Map json) => + _$ModelImpl(id: json['id'] as String?, name: json['name'] as String?); + +Map _$$ModelImplToJson(_$ModelImpl instance) => + {'id': instance.id, 'name': instance.name}; diff --git a/packages/genai/lib/models/model_config.dart b/packages/genai/lib/models/model_config.dart new file mode 100644 index 00000000..15868d0c --- /dev/null +++ b/packages/genai/lib/models/model_config.dart @@ -0,0 +1,74 @@ +import 'model_config_value.dart'; + +class ModelConfig { + final String id; + final String name; + final String description; + final ConfigType type; + final ConfigValue value; + + ModelConfig({ + required this.id, + required this.name, + required this.description, + required this.type, + required this.value, + }) { + assert(checkTypeValue(type, value)); + } + + ModelConfig updateValue(ConfigValue value) { + return ModelConfig( + id: id, + name: name, + description: description, + type: type, + value: value, + ); + } + + factory ModelConfig.fromJson(Map x) { + final id = x['id'] as String?; + final name = x['name'] as String?; + final description = x['description'] as String?; + final type = x['type'] as String?; + final value = x['value'] as String?; + + final cT = getConfigTypeEnum(type); + final cV = deserilizeValue(cT, value); + + return ModelConfig( + id: id ?? "", + name: name ?? "", + description: description ?? "", + type: cT, + value: cV, + ); + } + + Map toJson() { + return { + 'id': id, + 'name': name, + 'description': description, + 'type': type.name.toString(), + 'value': value.serialize(), + }; + } + + ModelConfig copyWith({ + String? id, + String? name, + String? description, + ConfigType? type, + ConfigValue? value, + }) { + return ModelConfig( + id: id ?? this.id, + name: name ?? this.name, + description: description ?? this.description, + type: type ?? this.type, + value: value ?? this.value, + ); + } +} diff --git a/packages/genai/lib/models/model_config_value.dart b/packages/genai/lib/models/model_config_value.dart new file mode 100644 index 00000000..1aa27552 --- /dev/null +++ b/packages/genai/lib/models/model_config_value.dart @@ -0,0 +1,109 @@ +import 'dart:convert'; +import 'package:flutter/material.dart'; + +enum ConfigType { boolean, slider, numeric, text } + +ConfigType getConfigTypeEnum(String? t) { + try { + final val = ConfigType.values.byName(t ?? ""); + return val; + } catch (e) { + debugPrint("ConfigType <$t> not found."); + return ConfigType.text; + } +} + +bool checkTypeValue(ConfigType t, dynamic v) { + return switch (t) { + ConfigType.boolean => v is ConfigBooleanValue, + ConfigType.slider => v is ConfigSliderValue, + ConfigType.numeric => v is ConfigNumericValue, + ConfigType.text => v is ConfigTextValue, + }; +} + +dynamic deserilizeValue(ConfigType t, String? v) { + return switch (t) { + ConfigType.boolean => ConfigBooleanValue.deserialize(v ?? ""), + ConfigType.slider => ConfigSliderValue.deserialize(v ?? ""), + ConfigType.numeric => ConfigNumericValue.deserialize(v ?? ""), + ConfigType.text => ConfigTextValue.deserialize(v ?? ""), + }; +} + +abstract class ConfigValue { + ConfigValue(this.value); + + dynamic value; + + String serialize(); + + dynamic getPayloadValue() { + return value; + } +} + +class ConfigBooleanValue extends ConfigValue { + ConfigBooleanValue({required bool value}) : super(value); + + @override + String serialize() { + return value.toString(); + } + + static ConfigBooleanValue deserialize(String x) { + return ConfigBooleanValue(value: x == 'true'); + } +} + +class ConfigNumericValue extends ConfigValue { + ConfigNumericValue({required num? value}) : super(value); + + @override + String serialize() { + return value.toString(); + } + + static ConfigNumericValue deserialize(String x) { + return ConfigNumericValue(value: num.tryParse(x)); + } +} + +class ConfigSliderValue extends ConfigValue { + ConfigSliderValue({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]); + } + + @override + dynamic getPayloadValue() { + final v = value as (double, double, double); + return v.$2; + } + + static ConfigSliderValue 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 ConfigSliderValue(value: val); + } +} + +class ConfigTextValue extends ConfigValue { + ConfigTextValue({required String value}) : super(value); + + @override + String serialize() { + return value.toString(); + } + + static ConfigTextValue deserialize(String x) { + return ConfigTextValue(value: x); + } +} diff --git a/packages/genai/lib/models/model_provider.dart b/packages/genai/lib/models/model_provider.dart new file mode 100644 index 00000000..c929645a --- /dev/null +++ b/packages/genai/lib/models/model_provider.dart @@ -0,0 +1,18 @@ +import 'package:better_networking/better_networking.dart'; +import '../models/models.dart'; + +abstract class ModelProvider { + AIRequestModel get defaultAIRequestModel => throw UnimplementedError(); + + HttpRequestModel? createRequest(AIRequestModel? aiRequestModel) { + throw UnimplementedError(); + } + + String? outputFormatter(Map x) { + throw UnimplementedError(); + } + + String? streamOutputFormatter(Map x) { + throw UnimplementedError(); + } +} diff --git a/packages/genai/lib/models/models.dart b/packages/genai/lib/models/models.dart new file mode 100644 index 00000000..5216053a --- /dev/null +++ b/packages/genai/lib/models/models.dart @@ -0,0 +1,6 @@ +export 'ai_request_model.dart'; +export 'available_models.dart'; +export 'model_config_value.dart'; +export 'model_config.dart'; +export 'model_provider.dart'; +export 'models_data.g.dart'; diff --git a/packages/genai/lib/models/models_data.g.dart b/packages/genai/lib/models/models_data.g.dart new file mode 100644 index 00000000..df125b5f --- /dev/null +++ b/packages/genai/lib/models/models_data.g.dart @@ -0,0 +1,2 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND +const kModelsData = {"version":1.0,"model_providers":[{"provider_id":"ollama","provider_name":"Ollama","source_url":null,"models":[{"id":"","name":"Custom"}]},{"provider_id":"openai","provider_name":"OpenAI","source_url":"https://platform.openai.com/docs/models","models":[{"id":"gpt-5","name":"GPT-5"},{"id":"gpt-5-mini","name":"GPT-5 mini"},{"id":"gpt-5-nano","name":"GPT-5 Nano"},{"id":"gpt-4.1","name":"GPT-4.1"},{"id":"gpt-oss-120b","name":"gpt-oss-120b"},{"id":"gpt-oss-20b","name":"gpt-oss-20b"},{"id":"o3-pro","name":"o3-pro"},{"id":"o3","name":"o3"},{"id":"o4-mini","name":"o4-mini"},{"id":"gpt-4o","name":"GPT-4o"},{"id":"gpt-4","name":"GPT-4"},{"id":"gpt-4o-mini","name":"GPT-4o Mini"},{"id":"","name":"Other"}]},{"provider_id":"anthropic","provider_name":"Anthropic","source_url":"https://docs.anthropic.com/en/docs/about-claude/models/overview","models":[{"id":"claude-opus-4-1","name":"Claude Opus 4.1"},{"id":"claude-opus-4-0","name":"Claude Opus 4"},{"id":"claude-sonnet-4-0","name":"Claude Sonnet 4"},{"id":"claude-3-7-sonnet-latest","name":"Claude Sonnet 3.7"},{"id":"claude-3-5-sonnet-latest","name":"Claude Sonnet 3.5"},{"id":"claude-3-5-haiku-latest","name":"Claude Haiku 3.5"},{"id":"","name":"Other"}]},{"provider_id":"gemini","provider_name":"Gemini","source_url":"https://ai.google.dev/gemini-api/docs/models","models":[{"id":"gemini-2.5-pro","name":"Gemini 2.5 Pro"},{"id":"gemini-2.5-flash","name":"Gemini 2.5 Flash"},{"id":"gemini-2.5-flash-lite","name":"Gemini 2.5 Flash-Lite"},{"id":"gemini-2.0-flash","name":"Gemini 2.0 Flash"},{"id":"gemini-2.0-flash-lite","name":"Gemini 2.0 Flash-Lite"},{"id":"","name":"Other"}]},{"provider_id":"azureopenai","provider_name":"Azure OpenAI","source_url":null,"models":[{"id":"","name":"Custom"}]}]}; diff --git a/packages/genai/lib/utils/ai_request_utils.dart b/packages/genai/lib/utils/ai_request_utils.dart new file mode 100644 index 00000000..a4b94481 --- /dev/null +++ b/packages/genai/lib/utils/ai_request_utils.dart @@ -0,0 +1,148 @@ +import 'dart:async'; +import 'dart:convert'; +import 'package:better_networking/better_networking.dart'; +import 'package:flutter/foundation.dart'; +import 'package:nanoid/nanoid.dart'; +import '../models/models.dart'; + +Future executeGenAIRequest(AIRequestModel? aiRequestModel) async { + final httpRequestModel = aiRequestModel?.httpRequestModel; + if (httpRequestModel == null) { + debugPrint("executeGenAIRequest -> httpRequestModel is null"); + return null; + } + final (response, _, _) = await sendHttpRequest( + nanoid(), + APIType.rest, + httpRequestModel, + ); + if (response == null) return null; + if (response.statusCode == 200) { + final data = jsonDecode(response.body); + return aiRequestModel?.getFormattedOutput(data); + } else { + debugPrint('LLM_EXCEPTION: ${response.statusCode}\n${response.body}'); + return null; + } +} + +Future> streamGenAIRequest( + AIRequestModel? aiRequestModel, +) async { + final httpRequestModel = aiRequestModel?.httpRequestModel; + final streamController = StreamController(); + if (httpRequestModel == null) { + debugPrint("streamGenAIRequest -> httpRequestModel is null"); + } else { + final httpStream = await streamHttpRequest( + nanoid(), + APIType.rest, + httpRequestModel, + ); + + final subscription = httpStream.listen( + (dat) { + if (dat == null) { + streamController.addError('STREAMING ERROR: NULL DATA'); + return; + } + + final chunk = dat.$2; + final error = dat.$4; + + if (chunk == null) { + streamController.addError(error ?? 'NULL ERROR'); + return; + } + + final ans = chunk.body; + + final lines = ans.split('\n'); + for (final line in lines) { + if (!line.startsWith('data: ') || line.contains('[DONE]')) continue; + final jsonStr = line.substring(6).trim(); + try { + final jsonData = jsonDecode(jsonStr); + final formattedOutput = aiRequestModel?.getFormattedStreamOutput( + jsonData, + ); + streamController.sink.add(formattedOutput); + } catch (e) { + debugPrint( + '⚠️ JSON decode error in SSE: $e\nSending as Regular Text', + ); + streamController.sink.add(jsonStr); + } + } + }, + onError: (error) { + streamController.addError('STREAM ERROR: $error'); + streamController.close(); + }, + onDone: () { + streamController.close(); + }, + cancelOnError: true, + ); + streamController.onCancel = () async { + await subscription.cancel(); + }; + } + return streamController.stream; +} + +Future callGenerativeModel( + AIRequestModel? aiRequestModel, { + required Function(String?) onAnswer, + required Function(dynamic) onError, +}) async { + if (aiRequestModel != null) { + try { + if (aiRequestModel.stream ?? false) { + final answerStream = await streamGenAIRequest(aiRequestModel); + processGenAIStreamOutput(answerStream, (w) { + onAnswer('$w '); + }, onError); + } else { + final answer = await executeGenAIRequest(aiRequestModel); + onAnswer(answer); + } + } catch (e) { + onError(e); + } + } +} + +void processGenAIStreamOutput( + Stream stream, + Function(String) onWord, + Function(dynamic) onError, +) { + String buffer = ''; + stream.listen( + (chunk) { + if (chunk == null || chunk.isEmpty) return; + buffer += chunk; + // Split on spaces but preserve last partial word + final parts = buffer.split(RegExp(r'\s+')); + if (parts.length > 1) { + // Keep the last part in buffer (it may be incomplete) + buffer = parts.removeLast(); + for (final word in parts) { + if (word.trim().isNotEmpty) { + onWord(word); + } + } + } + }, + onDone: () { + // Print any remaining word when stream is finished + if (buffer.trim().isNotEmpty) { + onWord(buffer); + } + }, + onError: (e) { + onError(e); + }, + ); +} diff --git a/packages/genai/lib/utils/model_manager.dart b/packages/genai/lib/utils/model_manager.dart new file mode 100644 index 00000000..a11518c2 --- /dev/null +++ b/packages/genai/lib/utils/model_manager.dart @@ -0,0 +1,97 @@ +import 'dart:convert'; +import 'package:better_networking/better_networking.dart'; +import 'package:flutter/foundation.dart'; +import '../consts.dart'; +import '../interface/interface.dart'; +import '../models/models.dart'; + +class ModelManager { + static Future fetchModelsFromRemote({ + String? remoteURL, + }) async { + try { + final (resp, _, _) = await sendHttpRequest( + 'FETCH_MODELS', + APIType.rest, + HttpRequestModel( + url: remoteURL ?? kModelRemoteUrl, + method: HTTPVerb.get, + ), + ); + if (resp == null) { + debugPrint('fetchModelsFromRemote -> resp == null'); + } else { + var remoteModels = availableModelsFromJson(resp.body); + return remoteModels; + } + } catch (e) { + debugPrint('fetchModelsFromRemote -> ${e.toString()}'); + } + return null; + } + + static Future fetchAvailableModels({ + String? ollamaUrl, + }) async { + try { + final oM = await fetchInstalledOllamaModels(ollamaUrl: ollamaUrl); + if (oM != null) { + List l = []; + for (var prov in kAvailableModels.modelProviders) { + if (prov.providerId == ModelAPIProvider.ollama) { + l.add( + prov.copyWith( + providerId: prov.providerId, + providerName: prov.providerName, + sourceUrl: prov.sourceUrl, + models: oM, + ), + ); + } else { + l.add(prov); + } + } + return kAvailableModels.copyWith( + version: kAvailableModels.version, + modelProviders: l, + ); + } + } catch (e) { + debugPrint('fetchAvailableModels -> ${e.toString()}'); + } + return kAvailableModels; + } + + static Future?> fetchInstalledOllamaModels({ + String? ollamaUrl, + }) async { + // All available models + // final url = "${ollamaUrl ?? kBaseOllamaUrl}/api/tags"; + // All loaded models + final url = "${ollamaUrl ?? kBaseOllamaUrl}/api/ps"; + + try { + final (resp, _, msg) = await sendHttpRequest( + 'OLLAMA_FETCH', + APIType.rest, + HttpRequestModel(url: url, method: HTTPVerb.get), + noSSL: true, + ); + // debugPrint("fetchInstalledOllamaModels -> $url -> ${resp?.body} -> $msg"); + if (resp == null) { + return null; + } + final output = jsonDecode(resp.body); + final models = output['models']; + if (models == null) return []; + List ollamaModels = []; + for (final m in models) { + ollamaModels.add(Model(id: m['model'], name: m['name'])); + } + return ollamaModels; + } catch (e) { + debugPrint('fetchInstalledOllamaModels -> ${e.toString()}'); + return null; + } + } +} diff --git a/packages/genai/lib/utils/utils.dart b/packages/genai/lib/utils/utils.dart new file mode 100644 index 00000000..e2ea0399 --- /dev/null +++ b/packages/genai/lib/utils/utils.dart @@ -0,0 +1,2 @@ +export 'ai_request_utils.dart'; +export 'model_manager.dart'; diff --git a/packages/genai/lib/widgets/ai_config_bool.dart b/packages/genai/lib/widgets/ai_config_bool.dart new file mode 100644 index 00000000..6d9f8d18 --- /dev/null +++ b/packages/genai/lib/widgets/ai_config_bool.dart @@ -0,0 +1,26 @@ +import 'package:flutter/material.dart'; +import '../models/models.dart'; + +class AIConfigBool extends StatelessWidget { + final ModelConfig configuration; + final Function(ModelConfig) onConfigUpdated; + final bool readonly; + const AIConfigBool({ + super.key, + required this.configuration, + required this.onConfigUpdated, + this.readonly = false, + }); + + @override + Widget build(BuildContext context) { + return Switch( + value: configuration.value.value as bool, + onChanged: (x) { + if (readonly) return; + configuration.value.value = x; + onConfigUpdated(configuration); + }, + ); + } +} diff --git a/packages/genai/lib/widgets/ai_config_field.dart b/packages/genai/lib/widgets/ai_config_field.dart new file mode 100644 index 00000000..ebae9384 --- /dev/null +++ b/packages/genai/lib/widgets/ai_config_field.dart @@ -0,0 +1,34 @@ +import 'package:flutter/material.dart'; +import '../models/models.dart'; + +class AIConfigField extends StatelessWidget { + final bool numeric; + final ModelConfig configuration; + final Function(ModelConfig) onConfigUpdated; + final bool readonly; + const AIConfigField({ + super.key, + this.numeric = false, + required this.configuration, + required this.onConfigUpdated, + this.readonly = false, + }); + + @override + Widget build(BuildContext context) { + return TextFormField( + initialValue: configuration.value.value.toString(), + onChanged: (x) { + if (readonly) return; + if (numeric) { + if (x.isEmpty) x = '0'; + if (num.tryParse(x) == null) return; + configuration.value.value = num.parse(x); + } else { + configuration.value.value = x; + } + onConfigUpdated(configuration); + }, + ); + } +} diff --git a/packages/genai/lib/widgets/ai_config_slider.dart b/packages/genai/lib/widgets/ai_config_slider.dart new file mode 100644 index 00000000..36b48774 --- /dev/null +++ b/packages/genai/lib/widgets/ai_config_slider.dart @@ -0,0 +1,36 @@ +import 'package:flutter/material.dart'; +import '../models/models.dart'; + +class AIConfigSlider extends StatelessWidget { + final ModelConfig configuration; + final Function(ModelConfig) onSliderUpdated; + final bool readonly; + const AIConfigSlider({ + super.key, + required this.configuration, + required this.onSliderUpdated, + this.readonly = false, + }); + + @override + Widget build(BuildContext context) { + final val = configuration.value.value as (double, double, double); + return Row( + children: [ + Expanded( + child: Slider( + min: val.$1, + value: val.$2, + max: val.$3, + onChanged: (x) { + if (readonly) return; + configuration.value.value = (val.$1, x, val.$3); + onSliderUpdated(configuration); + }, + ), + ), + Text(val.$2.toStringAsFixed(2)), + ], + ); + } +} diff --git a/packages/genai/lib/widgets/widgets.dart b/packages/genai/lib/widgets/widgets.dart new file mode 100644 index 00000000..15f9a10c --- /dev/null +++ b/packages/genai/lib/widgets/widgets.dart @@ -0,0 +1,3 @@ +export 'ai_config_bool.dart'; +export 'ai_config_field.dart'; +export 'ai_config_slider.dart'; diff --git a/packages/genai/models.json b/packages/genai/models.json new file mode 100644 index 00000000..5fcb9ad0 --- /dev/null +++ b/packages/genai/models.json @@ -0,0 +1,152 @@ +{ + "version": 1.0, + "model_providers": [ + { + "provider_id": "ollama", + "provider_name": "Ollama", + "source_url": null, + "models": [ + { + "id": "", + "name": "Custom" + } + ] + }, + { + "provider_id": "openai", + "provider_name": "OpenAI", + "source_url": "https://platform.openai.com/docs/models", + "models": [ + { + "id": "gpt-5", + "name": "GPT-5" + }, + { + "id": "gpt-5-mini", + "name": "GPT-5 mini" + }, + { + "id": "gpt-5-nano", + "name": "GPT-5 Nano" + }, + { + "id": "gpt-4.1", + "name": "GPT-4.1" + }, + { + "id": "gpt-oss-120b", + "name": "gpt-oss-120b" + }, + { + "id": "gpt-oss-20b", + "name": "gpt-oss-20b" + }, + { + "id": "o3-pro", + "name": "o3-pro" + }, + { + "id": "o3", + "name": "o3" + }, + { + "id": "o4-mini", + "name": "o4-mini" + }, + { + "id": "gpt-4o", + "name": "GPT-4o" + }, + { + "id": "gpt-4", + "name": "GPT-4" + }, + { + "id": "gpt-4o-mini", + "name": "GPT-4o Mini" + }, + { + "id": "", + "name": "Other" + } + ] + }, + { + "provider_id": "anthropic", + "provider_name": "Anthropic", + "source_url": "https://docs.anthropic.com/en/docs/about-claude/models/overview", + "models": [ + { + "id": "claude-opus-4-1", + "name": "Claude Opus 4.1" + }, + { + "id": "claude-opus-4-0", + "name": "Claude Opus 4" + }, + { + "id": "claude-sonnet-4-0", + "name": "Claude Sonnet 4" + }, + { + "id": "claude-3-7-sonnet-latest", + "name": "Claude Sonnet 3.7" + }, + { + "id": "claude-3-5-sonnet-latest", + "name": "Claude Sonnet 3.5" + }, + { + "id": "claude-3-5-haiku-latest", + "name": "Claude Haiku 3.5" + }, + { + "id": "", + "name": "Other" + } + ] + }, + { + "provider_id": "gemini", + "provider_name": "Gemini", + "source_url": "https://ai.google.dev/gemini-api/docs/models", + "models": [ + { + "id": "gemini-2.5-pro", + "name": "Gemini 2.5 Pro" + }, + { + "id": "gemini-2.5-flash", + "name": "Gemini 2.5 Flash" + }, + { + "id": "gemini-2.5-flash-lite", + "name": "Gemini 2.5 Flash-Lite" + }, + { + "id": "gemini-2.0-flash", + "name": "Gemini 2.0 Flash" + }, + { + "id": "gemini-2.0-flash-lite", + "name": "Gemini 2.0 Flash-Lite" + }, + { + "id": "", + "name": "Other" + } + ] + }, + { + "provider_id": "azureopenai", + "provider_name": "Azure OpenAI", + "source_url": null, + "models": [ + { + "id": "", + "name": "Custom" + } + ] + } + ] +} diff --git a/packages/genai/pubspec.yaml b/packages/genai/pubspec.yaml new file mode 100644 index 00000000..13237382 --- /dev/null +++ b/packages/genai/pubspec.yaml @@ -0,0 +1,27 @@ +name: genai +description: "Generative AI capabilities for flutter applications" +version: 0.0.1 +homepage: +publish_to: none + +environment: + sdk: ^3.8.0 + flutter: ">=1.17.0" + +dependencies: + flutter: + sdk: flutter + better_networking: + path: ../better_networking + freezed_annotation: ^2.4.1 + json_annotation: ^4.9.0 + nanoid: ^1.0.0 + +dev_dependencies: + flutter_test: + sdk: flutter + build_runner: ^2.4.12 + flutter_lints: ^4.0.0 + freezed: ^2.5.7 + json_serializable: ^6.7.1 + test: ^1.25.2 diff --git a/packages/genai/pubspec_overrides.yaml b/packages/genai/pubspec_overrides.yaml new file mode 100644 index 00000000..02df146a --- /dev/null +++ b/packages/genai/pubspec_overrides.yaml @@ -0,0 +1,6 @@ +# melos_managed_dependency_overrides: better_networking,seed +dependency_overrides: + better_networking: + path: ../better_networking + seed: + path: ../seed diff --git a/packages/genai/test/genai_test.dart b/packages/genai/test/genai_test.dart new file mode 100644 index 00000000..e5371074 --- /dev/null +++ b/packages/genai/test/genai_test.dart @@ -0,0 +1,5 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:genai/genai.dart'; + +void main() { +} diff --git a/packages/genai/tool/json_to_dart.dart b/packages/genai/tool/json_to_dart.dart new file mode 100644 index 00000000..796252b4 --- /dev/null +++ b/packages/genai/tool/json_to_dart.dart @@ -0,0 +1,17 @@ +import 'dart:convert'; +import 'dart:io'; + +void main() { + final inputFile = File('models.json'); + final outputFile = File('lib/models/models_data.g.dart'); + + final jsonData = jsonDecode(inputFile.readAsStringSync()); + final dartCode = + ''' +// GENERATED CODE - DO NOT MODIFY BY HAND +const kModelsData = ${jsonEncode(jsonData)}; +'''; + + outputFile.writeAsStringSync(dartCode); + print('✅ Generated data.g.dart from data.json'); +} diff --git a/packages/genai/tool/pre_publish.sh b/packages/genai/tool/pre_publish.sh new file mode 100644 index 00000000..e51fd07a --- /dev/null +++ b/packages/genai/tool/pre_publish.sh @@ -0,0 +1,4 @@ +#!/bin/bash +echo "🔄 Running pre-publish steps..." +dart run tool/json_to_dart.dart +echo "✅ Pre-publish steps completed." diff --git a/pubspec.lock b/pubspec.lock index 0536f2ac..c6d22be0 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -702,6 +702,13 @@ packages: url: "https://pub.dev" source: hosted version: "0.32.1" + genai: + dependency: transitive + description: + path: "packages/genai" + relative: true + source: path + version: "0.0.1" glob: dependency: transitive description: @@ -1110,6 +1117,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.0" + nanoid: + dependency: transitive + description: + name: nanoid + sha256: be3f8752d9046c825df2f3914195151eb876f3ad64b9d833dd0b799b77b8759e + url: "https://pub.dev" + source: hosted + version: "1.0.0" nanoid2: dependency: transitive description: diff --git a/test/models/history_models.dart b/test/models/history_models.dart index 6de41dc1..4256c7d4 100644 --- a/test/models/history_models.dart +++ b/test/models/history_models.dart @@ -57,6 +57,7 @@ final Map historyRequestModelJson1 = { "historyId": "historyId1", "metaData": historyMetaModelJson1, "httpRequestModel": httpRequestModelGet4Json, + 'aiRequestModel': null, "httpResponseModel": responseModelJson, 'preRequestScript': null, 'postRequestScript': null, diff --git a/test/models/request_models.dart b/test/models/request_models.dart index 1762706c..585cbc44 100644 --- a/test/models/request_models.dart +++ b/test/models/request_models.dart @@ -218,7 +218,8 @@ Map requestModelJson = { 'message': null, 'httpResponseModel': responseModelJson, 'preRequestScript': null, - 'postRequestScript': null + 'postRequestScript': null, + 'aiRequestModel': null }; /// Basic GET request model for apidash.dev diff --git a/test/screens/history/history_widgets/his_url_card_test.dart b/test/screens/history/history_widgets/his_url_card_test.dart index 6723fefb..50308a2d 100644 --- a/test/screens/history/history_widgets/his_url_card_test.dart +++ b/test/screens/history/history_widgets/his_url_card_test.dart @@ -31,7 +31,7 @@ void main() { expect( find.text( - historyRequestModel.httpRequestModel.method.name.toUpperCase()), + historyRequestModel.httpRequestModel!.method.name.toUpperCase()), findsOneWidget); }); @@ -45,7 +45,7 @@ void main() { ); expect( - find.text(historyRequestModel.httpRequestModel.url), findsOneWidget); + find.text(historyRequestModel.httpRequestModel!.url), findsOneWidget); }); }); }