From effe414268a5f35045159c2bf7c82ebf770e9e93 Mon Sep 17 00:00:00 2001 From: Manas Hejmadi Date: Sun, 22 Jun 2025 23:01:06 +0530 Subject: [PATCH 01/36] genai: Created Package & implemented LLM Config, Manager & Request --- packages/genai/.gitignore | 30 +++++ packages/genai/CHANGELOG.md | 3 + packages/genai/LICENSE | 1 + packages/genai/README.md | 2 + packages/genai/analysis_options.yaml | 4 + packages/genai/lib/genai.dart | 7 + packages/genai/lib/llm_config.dart | 191 +++++++++++++++++++++++++++ packages/genai/lib/llm_manager.dart | 0 packages/genai/lib/llm_request.dart | 13 ++ packages/genai/pubspec.yaml | 26 ++++ packages/genai/test/genai_test.dart | 5 + 11 files changed, 282 insertions(+) create mode 100644 packages/genai/.gitignore create mode 100644 packages/genai/CHANGELOG.md create mode 100644 packages/genai/LICENSE create mode 100644 packages/genai/README.md create mode 100644 packages/genai/analysis_options.yaml create mode 100644 packages/genai/lib/genai.dart create mode 100644 packages/genai/lib/llm_config.dart create mode 100644 packages/genai/lib/llm_manager.dart create mode 100644 packages/genai/lib/llm_request.dart create mode 100644 packages/genai/pubspec.yaml create mode 100644 packages/genai/test/genai_test.dart 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..ba75c69f --- /dev/null +++ b/packages/genai/LICENSE @@ -0,0 +1 @@ +TODO: Add your license here. diff --git a/packages/genai/README.md b/packages/genai/README.md new file mode 100644 index 00000000..ea898451 --- /dev/null +++ b/packages/genai/README.md @@ -0,0 +1,2 @@ +# genai package +This Package contains all the code related to generative AI capabilities and is a foundational package that can be used in various projects \ No newline at end of file diff --git a/packages/genai/analysis_options.yaml b/packages/genai/analysis_options.yaml new file mode 100644 index 00000000..a5744c1c --- /dev/null +++ b/packages/genai/analysis_options.yaml @@ -0,0 +1,4 @@ +include: package:flutter_lints/flutter.yaml + +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options diff --git a/packages/genai/lib/genai.dart b/packages/genai/lib/genai.dart new file mode 100644 index 00000000..83f90435 --- /dev/null +++ b/packages/genai/lib/genai.dart @@ -0,0 +1,7 @@ +library genai; + +/// A Calculator. +class Calculator { + /// Returns [value] plus 1. + int addOne(int value) => value + 1; +} diff --git a/packages/genai/lib/llm_config.dart b/packages/genai/lib/llm_config.dart new file mode 100644 index 00000000..2944bd0e --- /dev/null +++ b/packages/genai/lib/llm_config.dart @@ -0,0 +1,191 @@ +import 'dart:convert'; + +typedef LLMOutputFormatter = String? Function(Map); + +class LLMModelConfiguration { + final String configId; + final String configName; + final String configDescription; + final LLMModelConfigurationType configType; + final LLMModelConfigValue configValue; + + LLMModelConfiguration updateValue(LLMModelConfigValue value) { + return LLMModelConfiguration( + configId: configId, + configName: configName, + configDescription: configDescription, + configType: configType, + configValue: value, + ); + } + + LLMModelConfiguration({ + required this.configId, + required this.configName, + required this.configDescription, + required this.configType, + required this.configValue, + }) { + // Assert that the configuration type and value matches + switch (configType) { + case LLMModelConfigurationType.boolean: + assert(configValue is LLMConfigBooleanValue); + case LLMModelConfigurationType.slider: + assert(configValue is LLMConfigSliderValue); + case LLMModelConfigurationType.numeric: + assert(configValue is LLMConfigNumericValue); + case LLMModelConfigurationType.text: + assert(configValue is LLMConfigTextValue); + } + } + + factory LLMModelConfiguration.fromJson(Map x) { + LLMModelConfigurationType cT; + LLMModelConfigValue cV; + switch (x['configType']) { + case 'boolean': + cT = LLMModelConfigurationType.boolean; + cV = LLMConfigBooleanValue.deserialize(x['configValue']); + break; + case 'slider': + cT = LLMModelConfigurationType.slider; + cV = LLMConfigSliderValue.deserialize(x['configValue']); + break; + case 'numeric': + cT = LLMModelConfigurationType.numeric; + cV = LLMConfigNumericValue.deserialize(x['configValue']); + break; + case 'text': + cT = LLMModelConfigurationType.text; + cV = LLMConfigTextValue.deserialize(x['configValue']); + break; + default: + cT = LLMModelConfigurationType.text; + cV = LLMConfigTextValue.deserialize(x['configValue']); + } + return LLMModelConfiguration( + configId: x['configId'], + configName: x['configName'], + configDescription: x['configDescription'], + configType: cT, + configValue: cV, + ); + } + + Map toJson() { + return { + 'configId': configId, + 'configName': configName, + 'configDescription': configDescription, + 'configType': configType.name.toString(), + 'configValue': configValue.serialize(), + }; + } + + LLMModelConfiguration clone() { + return LLMModelConfiguration.fromJson(toJson()); + } +} + +enum LLMModelConfigurationType { boolean, slider, numeric, text } + +//----------------LLMConfigValues ------------ + +abstract class LLMModelConfigValue { + dynamic _value; + + // ignore: unnecessary_getters_setters + dynamic get value => _value; + + set value(dynamic newValue) => _value = newValue; + + String serialize(); + + LLMModelConfigValue(this._value); +} + +class LLMConfigBooleanValue extends LLMModelConfigValue { + LLMConfigBooleanValue({required bool value}) : super(value); + + @override + String serialize() { + return value.toString(); + } + + static LLMConfigBooleanValue deserialize(String x) { + return LLMConfigBooleanValue(value: x == 'true'); + } +} + +class LLMConfigNumericValue extends LLMModelConfigValue { + LLMConfigNumericValue({required num value}) : super(value); + + @override + String serialize() { + return value.toString(); + } + + static LLMConfigNumericValue deserialize(String x) { + return LLMConfigNumericValue(value: num.parse(x)); + } +} + +class LLMConfigSliderValue extends LLMModelConfigValue { + LLMConfigSliderValue({required (double, double, double) value}) + : super(value); + + @override + String serialize() { + final v = value as (double, double, double); + return jsonEncode([v.$1, v.$2, v.$3]); + } + + static LLMConfigSliderValue deserialize(String x) { + final z = jsonDecode(x) as List; + final val = ( + double.parse(z[0].toString()), + double.parse(z[1].toString()), + double.parse(z[2].toString()), + ); + return LLMConfigSliderValue(value: val); + } +} + +class LLMConfigTextValue extends LLMModelConfigValue { + LLMConfigTextValue({required String value}) : super(value); + + @override + String serialize() { + return value.toString(); + } + + static LLMConfigTextValue deserialize(String x) { + return LLMConfigTextValue(value: x); + } +} + +enum LLMConfigName { temperature, top_p, max_tokens, endpoint } + +Map defaultLLMConfigurations = { + LLMConfigName.temperature: LLMModelConfiguration( + configId: 'temperature', + configName: 'Temperature', + configDescription: 'The Temperature of the Model', + configType: LLMModelConfigurationType.slider, + configValue: LLMConfigSliderValue(value: (0, 0.5, 1)), + ), + LLMConfigName.top_p: LLMModelConfiguration( + configId: 'top_p', + configName: 'Top P', + configDescription: 'The Top P of the Model', + configType: LLMModelConfigurationType.slider, + configValue: LLMConfigSliderValue(value: (0, 0.95, 1)), + ), + LLMConfigName.max_tokens: LLMModelConfiguration( + configId: 'max_tokens', + configName: 'Maximum Tokens', + configDescription: 'The maximum number of tokens allowed in the output', + configType: LLMModelConfigurationType.numeric, + configValue: LLMConfigNumericValue(value: -1), + ), +}; diff --git a/packages/genai/lib/llm_manager.dart b/packages/genai/lib/llm_manager.dart new file mode 100644 index 00000000..e69de29b diff --git a/packages/genai/lib/llm_request.dart b/packages/genai/lib/llm_request.dart new file mode 100644 index 00000000..041fc381 --- /dev/null +++ b/packages/genai/lib/llm_request.dart @@ -0,0 +1,13 @@ +class LLMRequestDetails { + String endpoint; + Map headers; + String method; + Map body; + + LLMRequestDetails({ + required this.endpoint, + required this.headers, + required this.method, + required this.body, + }); +} diff --git a/packages/genai/pubspec.yaml b/packages/genai/pubspec.yaml new file mode 100644 index 00000000..b5b6f696 --- /dev/null +++ b/packages/genai/pubspec.yaml @@ -0,0 +1,26 @@ +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 + shared_preferences: ^2.5.2 + better_networking: + path: ../better_networking + +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 + +flutter: \ No newline at end of file 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() { +} From a6d1c410d690f35889583f9c95716c28021a5345 Mon Sep 17 00:00:00 2001 From: Manas Hejmadi Date: Sun, 22 Jun 2025 23:04:32 +0530 Subject: [PATCH 02/36] Implemented SaveObject, LLMModel class & OllamaProvider --- packages/genai/lib/llm_input_payload.dart | 59 +++++++++++++++++ packages/genai/lib/llm_saveobject.dart | 48 ++++++++++++++ packages/genai/lib/providers/common.dart | 40 ++++++++++++ packages/genai/lib/providers/ollama.dart | 72 +++++++++++++++++++++ packages/genai/lib/providers/providers.dart | 67 +++++++++++++++++++ 5 files changed, 286 insertions(+) create mode 100644 packages/genai/lib/llm_input_payload.dart create mode 100644 packages/genai/lib/llm_saveobject.dart create mode 100644 packages/genai/lib/providers/common.dart create mode 100644 packages/genai/lib/providers/ollama.dart create mode 100644 packages/genai/lib/providers/providers.dart diff --git a/packages/genai/lib/llm_input_payload.dart b/packages/genai/lib/llm_input_payload.dart new file mode 100644 index 00000000..79198911 --- /dev/null +++ b/packages/genai/lib/llm_input_payload.dart @@ -0,0 +1,59 @@ +import 'llm_config.dart'; + +class LLMInputPayload { + String endpoint; + String credential; + String systemPrompt; + String userPrompt; + Map configMap; + + LLMInputPayload({ + required this.endpoint, + required this.credential, + required this.systemPrompt, + required this.userPrompt, + required this.configMap, + }); + + LLMInputPayload clone() { + Map cmap = {}; + for (final k in configMap.keys) { + cmap[k] = configMap[k]!.clone(); + } + return LLMInputPayload( + endpoint: endpoint, + credential: credential, + systemPrompt: systemPrompt, + userPrompt: userPrompt, + configMap: cmap, + ); + } + + static Map toJSON(LLMInputPayload payload) { + Map cmap = {}; + for (final e in payload.configMap.entries) { + cmap[e.key] = e.value.toJson(); + } + return { + 'endpoint': payload.endpoint, + 'credential': payload.credential, + 'system_prompt': payload.systemPrompt, + 'user_prompt': payload.userPrompt, + 'config_map': cmap, + }; + } + + static LLMInputPayload fromJSON(Map json) { + Map cmap = {}; + for (final k in json['config_map'].keys) { + cmap[k] = LLMModelConfiguration.fromJson(json['config_map'][k]); + } + return LLMInputPayload( + endpoint: json['endpoint'], + credential: json['credential'], + systemPrompt: json['system_prompt'], + userPrompt: json['user_prompt'], + configMap: cmap, + ); + } +} diff --git a/packages/genai/lib/llm_saveobject.dart b/packages/genai/lib/llm_saveobject.dart new file mode 100644 index 00000000..cbbafb47 --- /dev/null +++ b/packages/genai/lib/llm_saveobject.dart @@ -0,0 +1,48 @@ +import 'llm_config.dart'; +import 'providers/common.dart'; +import 'providers/providers.dart'; + +class LLMSaveObject { + String endpoint; + String credential; + LLMProvider provider; + LLMModel selectedLLM; + Map configMap; + + LLMSaveObject({ + required this.endpoint, + required this.credential, + required this.configMap, + required this.selectedLLM, + required this.provider, + }); + + Map toJSON() { + Map cmap = {}; + for (final e in configMap.entries) { + cmap[e.key] = e.value.toJson(); + } + return { + 'endpoint': endpoint, + 'credential': credential, + 'config_map': cmap, + 'selected_llm': selectedLLM.identifier, + 'provider': provider.name, + }; + } + + static LLMSaveObject fromJSON(Map json) { + Map cmap = {}; + for (final k in json['config_map'].keys) { + cmap[k] = LLMModelConfiguration.fromJson(json['config_map'][k]); + } + final provider = LLMProvider.fromName(json['provider']); + return LLMSaveObject( + endpoint: json['endpoint'], + credential: json['credential'], + configMap: cmap, + selectedLLM: provider.getLLMByIdentifier(json['selected_llm']), + provider: provider, + ); + } +} diff --git a/packages/genai/lib/providers/common.dart b/packages/genai/lib/providers/common.dart new file mode 100644 index 00000000..e00869b1 --- /dev/null +++ b/packages/genai/lib/providers/common.dart @@ -0,0 +1,40 @@ +import '../llm_input_payload.dart'; +import '../llm_request.dart'; +import 'providers.dart'; + +class LLMModel { + const LLMModel(this.identifier, this.modelName, this.provider); + final String identifier; + final String modelName; + final LLMProvider provider; + + static Map toJson(LLMModel m) { + return {'identifier': m.identifier, 'provider': m.provider.name}; + } + + static LLMModel fromJson(Map json) { + return LLMProvider.fromName( + json['provider'], + ).getLLMByIdentifier(json['identifier']); + } +} + +abstract class ModelController { + LLMInputPayload get inputPayload => throw UnimplementedError(); + + LLMRequestDetails createRequest( + LLMModel model, + LLMInputPayload inputPayload, { + bool stream = false, + }) { + throw UnimplementedError(); + } + + String? outputFormatter(Map x) { + throw UnimplementedError(); + } + + String? streamOutputFormatter(Map x) { + throw UnimplementedError(); + } +} diff --git a/packages/genai/lib/providers/ollama.dart b/packages/genai/lib/providers/ollama.dart new file mode 100644 index 00000000..82c2a0f2 --- /dev/null +++ b/packages/genai/lib/providers/ollama.dart @@ -0,0 +1,72 @@ +import '../llm_config.dart'; +import '../llm_input_payload.dart'; +import '../llm_request.dart'; +import 'common.dart'; + +class OllamaModelController extends ModelController { + static final instance = OllamaModelController(); + + @override + LLMInputPayload get inputPayload => LLMInputPayload( + endpoint: 'http://localhost:11434/v1/chat/completions', + credential: '', + systemPrompt: '', + userPrompt: '', + configMap: { + LLMConfigName.temperature.name: + defaultLLMConfigurations[LLMConfigName.temperature]!, + LLMConfigName.top_p.name: defaultLLMConfigurations[LLMConfigName.top_p]!, + }, + ).clone(); + + @override + LLMRequestDetails createRequest( + LLMModel model, + LLMInputPayload inputPayload, { + bool stream = false, + }) { + return LLMRequestDetails( + endpoint: inputPayload.endpoint, + headers: {}, + method: 'POST', + body: { + "model": model.identifier, + if (stream) ...{'stream': true}, + "messages": [ + {"role": "system", "content": inputPayload.systemPrompt}, + {"role": "user", "content": inputPayload.userPrompt}, + ], + "temperature": + inputPayload + .configMap[LLMConfigName.temperature.name] + ?.configValue + .value + ?.$2 ?? + 0.5, + "top_p": + inputPayload + .configMap[LLMConfigName.top_p.name] + ?.configValue + .value + ?.$2 ?? + 0.95, + if (inputPayload.configMap[LLMConfigName.max_tokens.name] != null) ...{ + "max_tokens": inputPayload + .configMap[LLMConfigName.max_tokens.name]! + .configValue + .value, + }, + }, + ); + } + + @override + String? outputFormatter(Map x) { + return x['choices']?[0]['message']?['content']; + } + + @override + String? streamOutputFormatter(Map x) { + return x['choices']?[0]['delta']?['content']; + } +} diff --git a/packages/genai/lib/providers/providers.dart b/packages/genai/lib/providers/providers.dart new file mode 100644 index 00000000..97ed2378 --- /dev/null +++ b/packages/genai/lib/providers/providers.dart @@ -0,0 +1,67 @@ +import '../llm_manager.dart'; +import 'common.dart'; +import 'ollama.dart'; + +enum LLMProvider { + gemini('Gemini'), + openai('OpenAI'), + anthropic('Anthropic'), + ollama('Ollama'), + azureopenai('Azure OpenAI'); + + const LLMProvider(this.displayName); + + final String displayName; + + List get models { + final avl = LLMManager.models[this.name.toLowerCase()]; + if (avl == null) return []; + List models = []; + for (final x in avl) { + models.add(LLMModel(x[0], x[1], this)); + } + return models; + } + + ModelController get modelController { + switch (this) { + case LLMProvider.ollama: + return OllamaModelController.instance; + case _: + return OllamaModelController.instance; + } + } + + static LLMProvider fromJSON(Map json) { + return LLMProvider.fromName(json['llm_provider']); + } + + static Map toJSON(LLMProvider p) { + return {'llm_provider': p.name}; + } + + static LLMProvider? fromJSONNullable(Map? json) { + if (json == null) return null; + return LLMProvider.fromName(json['llm_provider']); + } + + static Map? toJSONNullable(LLMProvider? p) { + if (p == null) return null; + return {'llm_provider': p.name}; + } + + LLMModel getLLMByIdentifier(String identifier) { + final m = this.models.where((e) => e.identifier == identifier).firstOrNull; + if (m == null) { + throw Exception('MODEL DOES NOT EXIST $identifier'); + } + return m; + } + + static LLMProvider fromName(String name) { + return LLMProvider.values.firstWhere( + (model) => model.name == name, + orElse: () => throw ArgumentError('INVALID LLM PROVIDER: $name'), + ); + } +} From 69e388235779075ff004244032461c150fff5715 Mon Sep 17 00:00:00 2001 From: Manas Hejmadi Date: Sun, 22 Jun 2025 23:07:06 +0530 Subject: [PATCH 03/36] Providers: Implemented anthropic, gemini, openai & azureopenai --- packages/genai/lib/llm_manager.dart | 86 +++++++++++++++++ packages/genai/lib/providers/anthropic.dart | 74 +++++++++++++++ packages/genai/lib/providers/azureopenai.dart | 77 ++++++++++++++++ packages/genai/lib/providers/gemini.dart | 92 +++++++++++++++++++ packages/genai/lib/providers/openai.dart | 76 +++++++++++++++ packages/genai/lib/providers/providers.dart | 14 ++- 6 files changed, 417 insertions(+), 2 deletions(-) create mode 100644 packages/genai/lib/providers/anthropic.dart create mode 100644 packages/genai/lib/providers/azureopenai.dart create mode 100644 packages/genai/lib/providers/gemini.dart create mode 100644 packages/genai/lib/providers/openai.dart diff --git a/packages/genai/lib/llm_manager.dart b/packages/genai/lib/llm_manager.dart index e69de29b..2baf9179 100644 --- a/packages/genai/lib/llm_manager.dart +++ b/packages/genai/lib/llm_manager.dart @@ -0,0 +1,86 @@ +import 'dart:convert'; +import 'package:better_networking/better_networking.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +class LLMManager { + static Map avaiableModels = { + "gemini": [ + ["gemini-2.0-flash", "Gemini 2.0 Flash"], + ], + }; + + static get models => avaiableModels; + + static const String modelRemoteURL = + 'https://raw.githubusercontent.com/synapsecode/apidash/llm_model_rearch/packages/genai/models.json'; + static const String baseOllamaURL = 'http://localhost:11434'; + + static addLLM(String providerID, String modelID, String modelName) async { + avaiableModels[providerID] = [ + ...avaiableModels[providerID], + [modelID, modelName], + ]; + await saveAvailableLLMs(avaiableModels); + } + + static removeLLM(String providerID, String modelID, String modelName) async { + List z = avaiableModels[providerID] as List; + z = z.where((x) => x[0] != modelID && x[1] != modelName).toList(); + avaiableModels[providerID] = z; + await saveAvailableLLMs(avaiableModels); + } + + static fetchAvailableLLMs([String? remoteURL, String? ollamaURL]) async { + //get LLMs from remove + final (resp, _, __) = await sendHttpRequest( + 'FETCH_MODELS', + APIType.rest, + HttpRequestModel(url: remoteURL ?? modelRemoteURL, method: HTTPVerb.get), + ); + if (resp == null) { + throw Exception('UNABLE TO FETCH MODELS'); + } + Map remoteModels = jsonDecode(resp.body); + final oM = await fetchInstalledOllamaModels(ollamaURL); + remoteModels['ollama'] = oM; + saveAvailableLLMs(remoteModels); + loadAvailableLLMs(); + } + + static saveAvailableLLMs(Map updatedLLMs) async { + SharedPreferences prefs = await SharedPreferences.getInstance(); + await prefs.setString('genai_available_llms', jsonEncode(updatedLLMs)); + } + + static loadAvailableLLMs() async { + SharedPreferences prefs = await SharedPreferences.getInstance(); + final avl = prefs.getString('genai_available_llms'); + if (avl != null) { + avaiableModels = (jsonDecode(avl)); + } + } + + static clearAvailableLLMs() async { + SharedPreferences prefs = await SharedPreferences.getInstance(); + prefs.remove('genai_available_llms'); + } + + static Future fetchInstalledOllamaModels([String? ollamaURL]) async { + final url = "${ollamaURL ?? baseOllamaURL}/api/tags"; + final (resp, _, __) = await sendHttpRequest( + 'OLLAMA_FETCH', + APIType.rest, + HttpRequestModel(url: url, method: HTTPVerb.get), + noSSL: true, + ); + if (resp == null) return []; + final output = jsonDecode(resp.body); + final models = output['models']; + if (models == null) return []; + List ollamaModels = []; + for (final m in models) { + ollamaModels.add([m['model'], m['name']]); + } + return ollamaModels; + } +} diff --git a/packages/genai/lib/providers/anthropic.dart b/packages/genai/lib/providers/anthropic.dart new file mode 100644 index 00000000..97deaa2a --- /dev/null +++ b/packages/genai/lib/providers/anthropic.dart @@ -0,0 +1,74 @@ +import '../llm_config.dart'; +import '../llm_input_payload.dart'; +import '../llm_request.dart'; +import 'common.dart'; + +class AnthropicModelController extends ModelController { + static final instance = AnthropicModelController(); + @override + LLMInputPayload get inputPayload => LLMInputPayload( + endpoint: 'https://api.anthropic.com/v1/messages', + credential: '', + systemPrompt: '', + userPrompt: '', + configMap: { + LLMConfigName.temperature.name: + defaultLLMConfigurations[LLMConfigName.temperature]!, + LLMConfigName.top_p.name: defaultLLMConfigurations[LLMConfigName.top_p]!, + }, + ).clone(); + + @override + LLMRequestDetails createRequest( + LLMModel model, + LLMInputPayload inputPayload, { + bool stream = false, + }) { + return LLMRequestDetails( + endpoint: inputPayload.endpoint, + headers: { + 'anthropic-version': '2023-06-01', + 'Authorization': 'Bearer ${inputPayload.credential}', + }, + method: 'POST', + body: { + "model": model.identifier, + if (stream) ...{'stream': true}, + "messages": [ + {"role": "system", "content": inputPayload.systemPrompt}, + {"role": "user", "content": inputPayload.userPrompt}, + ], + "temperature": + inputPayload + .configMap[LLMConfigName.temperature.name] + ?.configValue + .value + ?.$2 ?? + 0.5, + "top_p": + inputPayload + .configMap[LLMConfigName.top_p.name] + ?.configValue + .value + ?.$2 ?? + 0.95, + if (inputPayload.configMap[LLMConfigName.max_tokens.name] != null) ...{ + "max_tokens": inputPayload + .configMap[LLMConfigName.max_tokens.name]! + .configValue + .value, + }, + }, + ); + } + + @override + String? outputFormatter(Map x) { + return x['content']?[0]['text']; + } + + @override + String? streamOutputFormatter(Map x) { + return x['text']; + } +} diff --git a/packages/genai/lib/providers/azureopenai.dart b/packages/genai/lib/providers/azureopenai.dart new file mode 100644 index 00000000..18d45667 --- /dev/null +++ b/packages/genai/lib/providers/azureopenai.dart @@ -0,0 +1,77 @@ +import '../llm_config.dart'; +import '../llm_input_payload.dart'; +import '../llm_request.dart'; +import 'common.dart'; + +class AzureOpenAIModelController extends ModelController { + static final instance = AzureOpenAIModelController(); + @override + LLMInputPayload get inputPayload => LLMInputPayload( + endpoint: '', //TO BE FILLED BY USER + credential: '', + systemPrompt: '', + userPrompt: '', + configMap: { + LLMConfigName.temperature.name: + defaultLLMConfigurations[LLMConfigName.temperature]!, + LLMConfigName.top_p.name: defaultLLMConfigurations[LLMConfigName.top_p]!, + }, + ).clone(); + + @override + LLMRequestDetails createRequest( + LLMModel model, + LLMInputPayload inputPayload, { + bool stream = false, + }) { + if (inputPayload.endpoint.isEmpty) { + throw Exception('MODEL ENDPOINT IS EMPTY'); + } + return LLMRequestDetails( + endpoint: inputPayload.endpoint, + headers: {'api-key': inputPayload.credential}, + method: 'POST', + body: { + if (stream) ...{'stream': true}, + "messages": [ + {"role": "system", "content": inputPayload.systemPrompt}, + if (inputPayload.userPrompt.isNotEmpty) ...{ + {"role": "user", "content": inputPayload.userPrompt}, + } else ...{ + {"role": "user", "content": "Generate"}, + }, + ], + "temperature": + inputPayload + .configMap[LLMConfigName.temperature.name] + ?.configValue + .value + ?.$2 ?? + 0.5, + "top_p": + inputPayload + .configMap[LLMConfigName.top_p.name] + ?.configValue + .value + ?.$2 ?? + 0.95, + if (inputPayload.configMap[LLMConfigName.max_tokens.name] != null) ...{ + "max_tokens": inputPayload + .configMap[LLMConfigName.max_tokens.name]! + .configValue + .value, + }, + }, + ); + } + + @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/providers/gemini.dart b/packages/genai/lib/providers/gemini.dart new file mode 100644 index 00000000..56b85b87 --- /dev/null +++ b/packages/genai/lib/providers/gemini.dart @@ -0,0 +1,92 @@ +import '../llm_config.dart'; +import '../llm_input_payload.dart'; +import '../llm_request.dart'; +import 'common.dart'; + +class GeminiModelController extends ModelController { + static final instance = GeminiModelController(); + @override + LLMInputPayload get inputPayload => LLMInputPayload( + endpoint: 'https://generativelanguage.googleapis.com/v1beta/models', + credential: '', + systemPrompt: '', + userPrompt: '', + configMap: { + LLMConfigName.temperature.name: + defaultLLMConfigurations[LLMConfigName.temperature]!, + LLMConfigName.top_p.name: defaultLLMConfigurations[LLMConfigName.top_p]!, + }, + ).clone(); + + @override + LLMRequestDetails createRequest( + LLMModel model, + LLMInputPayload inputPayload, { + bool stream = false, + }) { + String endpoint = inputPayload.endpoint; + endpoint = + "$endpoint/${model.identifier}:generateContent?key=${inputPayload.credential}"; + if (stream) { + endpoint = endpoint.replaceAll( + 'generateContent?', + 'streamGenerateContent?alt=sse&', + ); + } + return LLMRequestDetails( + endpoint: endpoint, + headers: {}, + method: 'POST', + body: { + "model": model.identifier, + "contents": [ + { + "role": "user", + "parts": [ + {"text": inputPayload.userPrompt}, + ], + }, + ], + "systemInstruction": { + "role": "system", + "parts": [ + {"text": inputPayload.systemPrompt}, + ], + }, + "generationConfig": { + "temperature": + inputPayload + .configMap[LLMConfigName.temperature.name] + ?.configValue + .value + ?.$2 ?? + 0.5, + "topP": + inputPayload + .configMap[LLMConfigName.top_p.name] + ?.configValue + .value + ?.$2 ?? + 0.95, + if (inputPayload.configMap[LLMConfigName.max_tokens.name] != + null) ...{ + "maxOutputTokens": inputPayload + .configMap[LLMConfigName.max_tokens.name]! + .configValue + .value, + }, + }, + }, + ); + } + + @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/providers/openai.dart b/packages/genai/lib/providers/openai.dart new file mode 100644 index 00000000..bd9bdc26 --- /dev/null +++ b/packages/genai/lib/providers/openai.dart @@ -0,0 +1,76 @@ +import '../llm_config.dart'; +import '../llm_input_payload.dart'; +import '../llm_request.dart'; +import 'common.dart'; + +class OpenAIModelController extends ModelController { + static final instance = OpenAIModelController(); + + @override + LLMInputPayload get inputPayload => LLMInputPayload( + endpoint: 'https://api.openai.com/v1/chat/completions', + credential: '', + systemPrompt: '', + userPrompt: '', + configMap: { + LLMConfigName.temperature.name: + defaultLLMConfigurations[LLMConfigName.temperature]!, + LLMConfigName.top_p.name: defaultLLMConfigurations[LLMConfigName.top_p]!, + }, + ).clone(); + + @override + LLMRequestDetails createRequest( + LLMModel model, + LLMInputPayload inputPayload, { + bool stream = false, + }) { + return LLMRequestDetails( + endpoint: inputPayload.endpoint, + headers: {'Authorization': "Bearer ${inputPayload.credential}"}, + method: 'POST', + body: { + 'model': model.identifier, + if (stream) ...{'stream': true}, + "messages": [ + {"role": "system", "content": inputPayload.systemPrompt}, + if (inputPayload.userPrompt.isNotEmpty) ...{ + {"role": "user", "content": inputPayload.userPrompt}, + } else ...{ + {"role": "user", "content": "Generate"}, + }, + ], + "temperature": + inputPayload + .configMap[LLMConfigName.temperature.name] + ?.configValue + .value + ?.$2 ?? + 0.5, + "top_p": + inputPayload + .configMap[LLMConfigName.top_p.name] + ?.configValue + .value + ?.$2 ?? + 0.95, + if (inputPayload.configMap[LLMConfigName.max_tokens.name] != null) ...{ + "max_tokens": inputPayload + .configMap[LLMConfigName.max_tokens.name]! + .configValue + .value, + }, + }, + ); + } + + @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/providers/providers.dart b/packages/genai/lib/providers/providers.dart index 97ed2378..dff914ef 100644 --- a/packages/genai/lib/providers/providers.dart +++ b/packages/genai/lib/providers/providers.dart @@ -1,6 +1,10 @@ import '../llm_manager.dart'; +import 'anthropic.dart'; +import 'azureopenai.dart'; import 'common.dart'; +import 'gemini.dart'; import 'ollama.dart'; +import 'openai.dart'; enum LLMProvider { gemini('Gemini'), @@ -27,8 +31,14 @@ enum LLMProvider { switch (this) { case LLMProvider.ollama: return OllamaModelController.instance; - case _: - return OllamaModelController.instance; + case LLMProvider.gemini: + return GeminiModelController.instance; + case LLMProvider.azureopenai: + return AzureOpenAIModelController.instance; + case LLMProvider.openai: + return OpenAIModelController.instance; + case LLMProvider.anthropic: + return AnthropicModelController.instance; } } From 26d27eba8046a4e22192cc8de289c2fcdcae04e1 Mon Sep 17 00:00:00 2001 From: Manas Hejmadi Date: Sun, 22 Jun 2025 23:12:40 +0530 Subject: [PATCH 04/36] AIRequestModel & AIResponseModel Created --- .../genai/lib/models/ai_request_model.dart | 61 +++ .../lib/models/ai_request_model.freezed.dart | 246 +++++++++++ .../genai/lib/models/ai_request_model.g.dart | 22 + .../genai/lib/models/ai_response_model.dart | 62 +++ .../lib/models/ai_response_model.freezed.dart | 392 ++++++++++++++++++ .../genai/lib/models/ai_response_model.g.dart | 38 ++ 6 files changed, 821 insertions(+) create mode 100644 packages/genai/lib/models/ai_request_model.dart create mode 100644 packages/genai/lib/models/ai_request_model.freezed.dart create mode 100644 packages/genai/lib/models/ai_request_model.g.dart create mode 100644 packages/genai/lib/models/ai_response_model.dart create mode 100644 packages/genai/lib/models/ai_response_model.freezed.dart create mode 100644 packages/genai/lib/models/ai_response_model.g.dart 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..e18ee25a --- /dev/null +++ b/packages/genai/lib/models/ai_request_model.dart @@ -0,0 +1,61 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; +import '../llm_saveobject.dart'; +import '../llm_input_payload.dart'; +import '../llm_request.dart'; +import '../providers/common.dart'; +import '../providers/gemini.dart'; +import '../providers/providers.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) + factory AIRequestModel({ + @JsonKey(fromJson: LLMInputPayload.fromJSON, toJson: LLMInputPayload.toJSON) + required LLMInputPayload payload, + @JsonKey(fromJson: LLMModel.fromJson, toJson: LLMModel.toJson) + required LLMModel model, + @JsonKey(fromJson: LLMProvider.fromJSON, toJson: LLMProvider.toJSON) + required LLMProvider provider, + }) = _AIRequestModel; + + factory AIRequestModel.fromJson(Map json) => + _$AIRequestModelFromJson(json); + + AIRequestModel updatePayload(LLMInputPayload p) { + return AIRequestModel(payload: p, model: model, provider: provider); + } + + LLMRequestDetails createRequest() { + final controller = model.provider.modelController; + return controller.createRequest(model, payload); + } + + factory AIRequestModel.fromDefaultSaveObject(LLMSaveObject? defaultLLMSO) { + final gmC = GeminiModelController.instance; + return AIRequestModel( + model: + defaultLLMSO?.selectedLLM ?? + LLMProvider.gemini.getLLMByIdentifier('gemini-2.0-flash'), + provider: defaultLLMSO?.provider ?? LLMProvider.gemini, + payload: LLMInputPayload( + endpoint: defaultLLMSO?.endpoint ?? gmC.inputPayload.endpoint, + credential: defaultLLMSO?.credential ?? '', + systemPrompt: '', + userPrompt: '', + configMap: defaultLLMSO?.configMap ?? gmC.inputPayload.configMap, + ), + ); + } + + AIRequestModel clone() { + return AIRequestModel( + model: model, + payload: payload.clone(), + provider: provider, + ); + } +} 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..84890f0a --- /dev/null +++ b/packages/genai/lib/models/ai_request_model.freezed.dart @@ -0,0 +1,246 @@ +// 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 { + @JsonKey(fromJson: LLMInputPayload.fromJSON, toJson: LLMInputPayload.toJSON) + LLMInputPayload get payload => throw _privateConstructorUsedError; + @JsonKey(fromJson: LLMModel.fromJson, toJson: LLMModel.toJson) + LLMModel get model => throw _privateConstructorUsedError; + @JsonKey(fromJson: LLMProvider.fromJSON, toJson: LLMProvider.toJSON) + LLMProvider get provider => 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({ + @JsonKey(fromJson: LLMInputPayload.fromJSON, toJson: LLMInputPayload.toJSON) + LLMInputPayload payload, + @JsonKey(fromJson: LLMModel.fromJson, toJson: LLMModel.toJson) + LLMModel model, + @JsonKey(fromJson: LLMProvider.fromJSON, toJson: LLMProvider.toJSON) + LLMProvider provider, + }); +} + +/// @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? payload = null, + Object? model = null, + Object? provider = null, + }) { + return _then( + _value.copyWith( + payload: null == payload + ? _value.payload + : payload // ignore: cast_nullable_to_non_nullable + as LLMInputPayload, + model: null == model + ? _value.model + : model // ignore: cast_nullable_to_non_nullable + as LLMModel, + provider: null == provider + ? _value.provider + : provider // ignore: cast_nullable_to_non_nullable + as LLMProvider, + ) + as $Val, + ); + } +} + +/// @nodoc +abstract class _$$AIRequestModelImplCopyWith<$Res> + implements $AIRequestModelCopyWith<$Res> { + factory _$$AIRequestModelImplCopyWith( + _$AIRequestModelImpl value, + $Res Function(_$AIRequestModelImpl) then, + ) = __$$AIRequestModelImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({ + @JsonKey(fromJson: LLMInputPayload.fromJSON, toJson: LLMInputPayload.toJSON) + LLMInputPayload payload, + @JsonKey(fromJson: LLMModel.fromJson, toJson: LLMModel.toJson) + LLMModel model, + @JsonKey(fromJson: LLMProvider.fromJSON, toJson: LLMProvider.toJSON) + LLMProvider provider, + }); +} + +/// @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? payload = null, + Object? model = null, + Object? provider = null, + }) { + return _then( + _$AIRequestModelImpl( + payload: null == payload + ? _value.payload + : payload // ignore: cast_nullable_to_non_nullable + as LLMInputPayload, + model: null == model + ? _value.model + : model // ignore: cast_nullable_to_non_nullable + as LLMModel, + provider: null == provider + ? _value.provider + : provider // ignore: cast_nullable_to_non_nullable + as LLMProvider, + ), + ); + } +} + +/// @nodoc + +@JsonSerializable(explicitToJson: true, anyMap: true) +class _$AIRequestModelImpl extends _AIRequestModel { + _$AIRequestModelImpl({ + @JsonKey(fromJson: LLMInputPayload.fromJSON, toJson: LLMInputPayload.toJSON) + required this.payload, + @JsonKey(fromJson: LLMModel.fromJson, toJson: LLMModel.toJson) + required this.model, + @JsonKey(fromJson: LLMProvider.fromJSON, toJson: LLMProvider.toJSON) + required this.provider, + }) : super._(); + + factory _$AIRequestModelImpl.fromJson(Map json) => + _$$AIRequestModelImplFromJson(json); + + @override + @JsonKey(fromJson: LLMInputPayload.fromJSON, toJson: LLMInputPayload.toJSON) + final LLMInputPayload payload; + @override + @JsonKey(fromJson: LLMModel.fromJson, toJson: LLMModel.toJson) + final LLMModel model; + @override + @JsonKey(fromJson: LLMProvider.fromJSON, toJson: LLMProvider.toJSON) + final LLMProvider provider; + + @override + String toString() { + return 'AIRequestModel(payload: $payload, model: $model, provider: $provider)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$AIRequestModelImpl && + (identical(other.payload, payload) || other.payload == payload) && + (identical(other.model, model) || other.model == model) && + (identical(other.provider, provider) || + other.provider == provider)); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => Object.hash(runtimeType, payload, model, provider); + + /// 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 { + factory _AIRequestModel({ + @JsonKey(fromJson: LLMInputPayload.fromJSON, toJson: LLMInputPayload.toJSON) + required final LLMInputPayload payload, + @JsonKey(fromJson: LLMModel.fromJson, toJson: LLMModel.toJson) + required final LLMModel model, + @JsonKey(fromJson: LLMProvider.fromJSON, toJson: LLMProvider.toJSON) + required final LLMProvider provider, + }) = _$AIRequestModelImpl; + _AIRequestModel._() : super._(); + + factory _AIRequestModel.fromJson(Map json) = + _$AIRequestModelImpl.fromJson; + + @override + @JsonKey(fromJson: LLMInputPayload.fromJSON, toJson: LLMInputPayload.toJSON) + LLMInputPayload get payload; + @override + @JsonKey(fromJson: LLMModel.fromJson, toJson: LLMModel.toJson) + LLMModel get model; + @override + @JsonKey(fromJson: LLMProvider.fromJSON, toJson: LLMProvider.toJSON) + LLMProvider get provider; + + /// 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..df256f43 --- /dev/null +++ b/packages/genai/lib/models/ai_request_model.g.dart @@ -0,0 +1,22 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'ai_request_model.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +_$AIRequestModelImpl _$$AIRequestModelImplFromJson(Map json) => + _$AIRequestModelImpl( + payload: LLMInputPayload.fromJSON(json['payload'] as Map), + model: LLMModel.fromJson(json['model'] as Map), + provider: LLMProvider.fromJSON(json['provider'] as Map), + ); + +Map _$$AIRequestModelImplToJson( + _$AIRequestModelImpl instance, +) => { + 'payload': LLMInputPayload.toJSON(instance.payload), + 'model': LLMModel.toJson(instance.model), + 'provider': LLMProvider.toJSON(instance.provider), +}; diff --git a/packages/genai/lib/models/ai_response_model.dart b/packages/genai/lib/models/ai_response_model.dart new file mode 100644 index 00000000..dc88936b --- /dev/null +++ b/packages/genai/lib/models/ai_response_model.dart @@ -0,0 +1,62 @@ +import 'dart:convert'; +import 'dart:io'; +import 'dart:typed_data'; +import 'package:better_networking/better_networking.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:collection/collection.dart' show mergeMaps; +import '../providers/providers.dart'; +part 'ai_response_model.freezed.dart'; +part 'ai_response_model.g.dart'; + +@freezed +class AIResponseModel with _$AIResponseModel { + const AIResponseModel._(); + + @JsonSerializable(explicitToJson: true, anyMap: true, createToJson: true) + const factory AIResponseModel({ + int? statusCode, + Map? headers, + Map? requestHeaders, + String? body, + String? formattedBody, + @JsonKey( + fromJson: LLMProvider.fromJSONNullable, + toJson: LLMProvider.toJSONNullable, + ) + LLMProvider? llmProvider, + @Uint8ListConverter() Uint8List? bodyBytes, + @DurationConverter() Duration? time, + }) = _AIResponseModel; + + factory AIResponseModel.fromJson(Map json) => + _$AIResponseModelFromJson(json); + + AIResponseModel fromResponse({ + required Response response, + required LLMProvider provider, + Duration? time, + }) { + final responseHeaders = mergeMaps({ + HttpHeaders.contentLengthHeader: response.contentLength.toString(), + }, response.headers); + MediaType? mediaType = getMediaTypeFromHeaders(responseHeaders); + final body = (mediaType?.subtype == kSubTypeJson) + ? utf8.decode(response.bodyBytes) + : response.body; + return AIResponseModel( + statusCode: response.statusCode, + headers: responseHeaders, + requestHeaders: response.request?.headers, + body: body, + formattedBody: response.statusCode == 200 + ? provider.modelController.outputFormatter(jsonDecode(body)) + : formatBody(body, mediaType), + bodyBytes: response.bodyBytes, + time: time, + llmProvider: provider, + ); + } + + String? get contentType => headers?.getValueContentType(); + MediaType? get mediaType => getMediaTypeFromHeaders(headers); +} diff --git a/packages/genai/lib/models/ai_response_model.freezed.dart b/packages/genai/lib/models/ai_response_model.freezed.dart new file mode 100644 index 00000000..c7322bc9 --- /dev/null +++ b/packages/genai/lib/models/ai_response_model.freezed.dart @@ -0,0 +1,392 @@ +// 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_response_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', +); + +AIResponseModel _$AIResponseModelFromJson(Map json) { + return _AIResponseModel.fromJson(json); +} + +/// @nodoc +mixin _$AIResponseModel { + int? get statusCode => throw _privateConstructorUsedError; + Map? get headers => throw _privateConstructorUsedError; + Map? get requestHeaders => throw _privateConstructorUsedError; + String? get body => throw _privateConstructorUsedError; + String? get formattedBody => throw _privateConstructorUsedError; + @JsonKey( + fromJson: LLMProvider.fromJSONNullable, + toJson: LLMProvider.toJSONNullable, + ) + LLMProvider? get llmProvider => throw _privateConstructorUsedError; + @Uint8ListConverter() + Uint8List? get bodyBytes => throw _privateConstructorUsedError; + @DurationConverter() + Duration? get time => throw _privateConstructorUsedError; + + /// Serializes this AIResponseModel to a JSON map. + Map toJson() => throw _privateConstructorUsedError; + + /// Create a copy of AIResponseModel + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $AIResponseModelCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $AIResponseModelCopyWith<$Res> { + factory $AIResponseModelCopyWith( + AIResponseModel value, + $Res Function(AIResponseModel) then, + ) = _$AIResponseModelCopyWithImpl<$Res, AIResponseModel>; + @useResult + $Res call({ + int? statusCode, + Map? headers, + Map? requestHeaders, + String? body, + String? formattedBody, + @JsonKey( + fromJson: LLMProvider.fromJSONNullable, + toJson: LLMProvider.toJSONNullable, + ) + LLMProvider? llmProvider, + @Uint8ListConverter() Uint8List? bodyBytes, + @DurationConverter() Duration? time, + }); +} + +/// @nodoc +class _$AIResponseModelCopyWithImpl<$Res, $Val extends AIResponseModel> + implements $AIResponseModelCopyWith<$Res> { + _$AIResponseModelCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of AIResponseModel + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? statusCode = freezed, + Object? headers = freezed, + Object? requestHeaders = freezed, + Object? body = freezed, + Object? formattedBody = freezed, + Object? llmProvider = freezed, + Object? bodyBytes = freezed, + Object? time = freezed, + }) { + return _then( + _value.copyWith( + statusCode: freezed == statusCode + ? _value.statusCode + : statusCode // ignore: cast_nullable_to_non_nullable + as int?, + headers: freezed == headers + ? _value.headers + : headers // ignore: cast_nullable_to_non_nullable + as Map?, + requestHeaders: freezed == requestHeaders + ? _value.requestHeaders + : requestHeaders // ignore: cast_nullable_to_non_nullable + as Map?, + body: freezed == body + ? _value.body + : body // ignore: cast_nullable_to_non_nullable + as String?, + formattedBody: freezed == formattedBody + ? _value.formattedBody + : formattedBody // ignore: cast_nullable_to_non_nullable + as String?, + llmProvider: freezed == llmProvider + ? _value.llmProvider + : llmProvider // ignore: cast_nullable_to_non_nullable + as LLMProvider?, + bodyBytes: freezed == bodyBytes + ? _value.bodyBytes + : bodyBytes // ignore: cast_nullable_to_non_nullable + as Uint8List?, + time: freezed == time + ? _value.time + : time // ignore: cast_nullable_to_non_nullable + as Duration?, + ) + as $Val, + ); + } +} + +/// @nodoc +abstract class _$$AIResponseModelImplCopyWith<$Res> + implements $AIResponseModelCopyWith<$Res> { + factory _$$AIResponseModelImplCopyWith( + _$AIResponseModelImpl value, + $Res Function(_$AIResponseModelImpl) then, + ) = __$$AIResponseModelImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({ + int? statusCode, + Map? headers, + Map? requestHeaders, + String? body, + String? formattedBody, + @JsonKey( + fromJson: LLMProvider.fromJSONNullable, + toJson: LLMProvider.toJSONNullable, + ) + LLMProvider? llmProvider, + @Uint8ListConverter() Uint8List? bodyBytes, + @DurationConverter() Duration? time, + }); +} + +/// @nodoc +class __$$AIResponseModelImplCopyWithImpl<$Res> + extends _$AIResponseModelCopyWithImpl<$Res, _$AIResponseModelImpl> + implements _$$AIResponseModelImplCopyWith<$Res> { + __$$AIResponseModelImplCopyWithImpl( + _$AIResponseModelImpl _value, + $Res Function(_$AIResponseModelImpl) _then, + ) : super(_value, _then); + + /// Create a copy of AIResponseModel + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? statusCode = freezed, + Object? headers = freezed, + Object? requestHeaders = freezed, + Object? body = freezed, + Object? formattedBody = freezed, + Object? llmProvider = freezed, + Object? bodyBytes = freezed, + Object? time = freezed, + }) { + return _then( + _$AIResponseModelImpl( + statusCode: freezed == statusCode + ? _value.statusCode + : statusCode // ignore: cast_nullable_to_non_nullable + as int?, + headers: freezed == headers + ? _value._headers + : headers // ignore: cast_nullable_to_non_nullable + as Map?, + requestHeaders: freezed == requestHeaders + ? _value._requestHeaders + : requestHeaders // ignore: cast_nullable_to_non_nullable + as Map?, + body: freezed == body + ? _value.body + : body // ignore: cast_nullable_to_non_nullable + as String?, + formattedBody: freezed == formattedBody + ? _value.formattedBody + : formattedBody // ignore: cast_nullable_to_non_nullable + as String?, + llmProvider: freezed == llmProvider + ? _value.llmProvider + : llmProvider // ignore: cast_nullable_to_non_nullable + as LLMProvider?, + bodyBytes: freezed == bodyBytes + ? _value.bodyBytes + : bodyBytes // ignore: cast_nullable_to_non_nullable + as Uint8List?, + time: freezed == time + ? _value.time + : time // ignore: cast_nullable_to_non_nullable + as Duration?, + ), + ); + } +} + +/// @nodoc + +@JsonSerializable(explicitToJson: true, anyMap: true, createToJson: true) +class _$AIResponseModelImpl extends _AIResponseModel { + const _$AIResponseModelImpl({ + this.statusCode, + final Map? headers, + final Map? requestHeaders, + this.body, + this.formattedBody, + @JsonKey( + fromJson: LLMProvider.fromJSONNullable, + toJson: LLMProvider.toJSONNullable, + ) + this.llmProvider, + @Uint8ListConverter() this.bodyBytes, + @DurationConverter() this.time, + }) : _headers = headers, + _requestHeaders = requestHeaders, + super._(); + + factory _$AIResponseModelImpl.fromJson(Map json) => + _$$AIResponseModelImplFromJson(json); + + @override + final int? statusCode; + final Map? _headers; + @override + Map? get headers { + final value = _headers; + if (value == null) return null; + if (_headers is EqualUnmodifiableMapView) return _headers; + // ignore: implicit_dynamic_type + return EqualUnmodifiableMapView(value); + } + + final Map? _requestHeaders; + @override + Map? get requestHeaders { + final value = _requestHeaders; + if (value == null) return null; + if (_requestHeaders is EqualUnmodifiableMapView) return _requestHeaders; + // ignore: implicit_dynamic_type + return EqualUnmodifiableMapView(value); + } + + @override + final String? body; + @override + final String? formattedBody; + @override + @JsonKey( + fromJson: LLMProvider.fromJSONNullable, + toJson: LLMProvider.toJSONNullable, + ) + final LLMProvider? llmProvider; + @override + @Uint8ListConverter() + final Uint8List? bodyBytes; + @override + @DurationConverter() + final Duration? time; + + @override + String toString() { + return 'AIResponseModel(statusCode: $statusCode, headers: $headers, requestHeaders: $requestHeaders, body: $body, formattedBody: $formattedBody, llmProvider: $llmProvider, bodyBytes: $bodyBytes, time: $time)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$AIResponseModelImpl && + (identical(other.statusCode, statusCode) || + other.statusCode == statusCode) && + const DeepCollectionEquality().equals(other._headers, _headers) && + const DeepCollectionEquality().equals( + other._requestHeaders, + _requestHeaders, + ) && + (identical(other.body, body) || other.body == body) && + (identical(other.formattedBody, formattedBody) || + other.formattedBody == formattedBody) && + (identical(other.llmProvider, llmProvider) || + other.llmProvider == llmProvider) && + const DeepCollectionEquality().equals(other.bodyBytes, bodyBytes) && + (identical(other.time, time) || other.time == time)); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => Object.hash( + runtimeType, + statusCode, + const DeepCollectionEquality().hash(_headers), + const DeepCollectionEquality().hash(_requestHeaders), + body, + formattedBody, + llmProvider, + const DeepCollectionEquality().hash(bodyBytes), + time, + ); + + /// Create a copy of AIResponseModel + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$AIResponseModelImplCopyWith<_$AIResponseModelImpl> get copyWith => + __$$AIResponseModelImplCopyWithImpl<_$AIResponseModelImpl>( + this, + _$identity, + ); + + @override + Map toJson() { + return _$$AIResponseModelImplToJson(this); + } +} + +abstract class _AIResponseModel extends AIResponseModel { + const factory _AIResponseModel({ + final int? statusCode, + final Map? headers, + final Map? requestHeaders, + final String? body, + final String? formattedBody, + @JsonKey( + fromJson: LLMProvider.fromJSONNullable, + toJson: LLMProvider.toJSONNullable, + ) + final LLMProvider? llmProvider, + @Uint8ListConverter() final Uint8List? bodyBytes, + @DurationConverter() final Duration? time, + }) = _$AIResponseModelImpl; + const _AIResponseModel._() : super._(); + + factory _AIResponseModel.fromJson(Map json) = + _$AIResponseModelImpl.fromJson; + + @override + int? get statusCode; + @override + Map? get headers; + @override + Map? get requestHeaders; + @override + String? get body; + @override + String? get formattedBody; + @override + @JsonKey( + fromJson: LLMProvider.fromJSONNullable, + toJson: LLMProvider.toJSONNullable, + ) + LLMProvider? get llmProvider; + @override + @Uint8ListConverter() + Uint8List? get bodyBytes; + @override + @DurationConverter() + Duration? get time; + + /// Create a copy of AIResponseModel + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$AIResponseModelImplCopyWith<_$AIResponseModelImpl> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/packages/genai/lib/models/ai_response_model.g.dart b/packages/genai/lib/models/ai_response_model.g.dart new file mode 100644 index 00000000..f1029f70 --- /dev/null +++ b/packages/genai/lib/models/ai_response_model.g.dart @@ -0,0 +1,38 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'ai_response_model.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +_$AIResponseModelImpl _$$AIResponseModelImplFromJson(Map json) => + _$AIResponseModelImpl( + statusCode: (json['statusCode'] as num?)?.toInt(), + headers: (json['headers'] as Map?)?.map( + (k, e) => MapEntry(k as String, e as String), + ), + requestHeaders: (json['requestHeaders'] as Map?)?.map( + (k, e) => MapEntry(k as String, e as String), + ), + body: json['body'] as String?, + formattedBody: json['formattedBody'] as String?, + llmProvider: LLMProvider.fromJSONNullable(json['llmProvider'] as Map?), + bodyBytes: const Uint8ListConverter().fromJson( + json['bodyBytes'] as List?, + ), + time: const DurationConverter().fromJson((json['time'] as num?)?.toInt()), + ); + +Map _$$AIResponseModelImplToJson( + _$AIResponseModelImpl instance, +) => { + 'statusCode': instance.statusCode, + 'headers': instance.headers, + 'requestHeaders': instance.requestHeaders, + 'body': instance.body, + 'formattedBody': instance.formattedBody, + 'llmProvider': LLMProvider.toJSONNullable(instance.llmProvider), + 'bodyBytes': const Uint8ListConverter().toJson(instance.bodyBytes), + 'time': const DurationConverter().toJson(instance.time), +}; From 7a9afc614b22add618ddc0bccbbb7acb010e1077 Mon Sep 17 00:00:00 2001 From: Manas Hejmadi Date: Sun, 22 Jun 2025 23:39:22 +0530 Subject: [PATCH 05/36] REFACTOR: Improved Package Structure --- packages/genai/lib/genai.dart | 19 +- packages/genai/lib/generative_ai.dart | 195 ++++++++++++++++++ .../{providers/common.dart => llm_model.dart} | 6 +- packages/genai/lib/llm_provider.dart | 73 +++++++ packages/genai/lib/llm_saveobject.dart | 4 +- .../genai/lib/models/ai_request_model.dart | 3 +- .../genai/lib/models/ai_response_model.dart | 2 +- packages/genai/lib/providers/anthropic.dart | 2 +- packages/genai/lib/providers/azureopenai.dart | 2 +- packages/genai/lib/providers/gemini.dart | 2 +- packages/genai/lib/providers/ollama.dart | 2 +- packages/genai/lib/providers/openai.dart | 2 +- packages/genai/lib/providers/providers.dart | 82 +------- 13 files changed, 298 insertions(+), 96 deletions(-) create mode 100644 packages/genai/lib/generative_ai.dart rename packages/genai/lib/{providers/common.dart => llm_model.dart} (90%) create mode 100644 packages/genai/lib/llm_provider.dart diff --git a/packages/genai/lib/genai.dart b/packages/genai/lib/genai.dart index 83f90435..b96c6195 100644 --- a/packages/genai/lib/genai.dart +++ b/packages/genai/lib/genai.dart @@ -1,7 +1,12 @@ -library genai; - -/// A Calculator. -class Calculator { - /// Returns [value] plus 1. - int addOne(int value) => value + 1; -} +// Module Exports +export 'models/ai_request_model.dart'; +export 'models/ai_response_model.dart'; +export 'providers/providers.dart'; +export 'generative_ai.dart'; +export 'llm_config.dart'; +export 'llm_input_payload.dart'; +export 'llm_manager.dart'; +export 'llm_model.dart'; +export 'llm_provider.dart'; +export 'llm_request.dart'; +export 'llm_saveobject.dart'; \ No newline at end of file diff --git a/packages/genai/lib/generative_ai.dart b/packages/genai/lib/generative_ai.dart new file mode 100644 index 00000000..e55d4d60 --- /dev/null +++ b/packages/genai/lib/generative_ai.dart @@ -0,0 +1,195 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:math'; +import 'package:better_networking/better_networking.dart'; +import 'llm_config.dart'; +import 'llm_model.dart'; +import 'llm_request.dart'; + +class GenerativeAI { + static Future executeGenAIRequest( + LLMModel model, + LLMRequestDetails requestDetails, + ) async { + final mC = model.provider.modelController; + final headers = requestDetails.headers; + // print(jsonEncode(requestDetails.body)); + final (response, _, _) = await sendHttpRequest( + (Random().nextDouble() * 9999999 + 1).toString(), + APIType.rest, + HttpRequestModel( + method: HTTPVerb.post, + headers: [ + ...headers.entries.map( + (x) => NameValueModel.fromJson({x.key: x.value}), + ), + ], + url: requestDetails.endpoint, + bodyContentType: ContentType.json, + body: jsonEncode(requestDetails.body), + ), + ); + if (response == null) return null; + if (response.statusCode == 200) { + final data = jsonDecode(response.body); + // print(data); + return mC.outputFormatter(data); + } else { + print(requestDetails.endpoint); + print(response.body); + throw Exception( + 'LLM_EXCEPTION: ${response.statusCode}\n${response.body}', + ); + } + } + + static Future> streamGenAIRequest( + LLMModel model, + LLMRequestDetails requestDetails, + ) async { + final modelController = model.provider.modelController; + + final headers = { + 'Content-Type': 'application/json', + ...requestDetails.headers, + }; + + final httpStream = await streamHttpRequest( + requestDetails.hashCode.toString(), + APIType.rest, + HttpRequestModel( + method: HTTPVerb.post, + headers: headers.entries + .map((entry) => NameValueModel(name: entry.key, value: entry.value)) + .toList(), + url: requestDetails.endpoint, + bodyContentType: ContentType.json, + body: jsonEncode(requestDetails.body), + ), + ); + + final streamController = StreamController(); + + final subscription = httpStream.listen( + (dat) { + if (dat == null) { + streamController.addError('STREAMING ERROR: NULL DATA'); + return; + } + + final chunk = dat.$1; + final error = dat.$3; + + if (chunk == null) { + streamController.addError(error ?? 'NULL ERROR'); + return; + } + + final lines = chunk.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 = modelController.streamOutputFormatter( + jsonData, + ); + streamController.sink.add(formattedOutput); + } catch (e) { + print('⚠️ JSON decode error in SSE: $e\Sending 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; + } + + static callGenerativeModel( + LLMModel model, { + required Function(String?) onAnswer, + required Function(dynamic) onError, + required String systemPrompt, + required String userPrompt, + String? credential, + String? endpoint, + Map? configurations, + bool stream = false, + }) async { + final c = model.provider.modelController; + final payload = c.inputPayload; + payload.systemPrompt = systemPrompt; + payload.userPrompt = userPrompt; + if (credential != null) { + payload.credential = credential; + } + if (configurations != null) { + payload.configMap.addAll(configurations); + } + if (endpoint != null) { + payload.endpoint = endpoint; + } + try { + if (stream) { + final streamRequest = c.createRequest(model, payload, stream: true); + final answerStream = await streamGenAIRequest(model, streamRequest); + processGenAIStreamOutput(answerStream, (w) { + onAnswer('$w '); + }, onError); + } else { + final request = c.createRequest(model, payload); + final answer = await executeGenAIRequest(model, request); + onAnswer(answer); + } + } catch (e) { + onError(e); + } + } + + static 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/providers/common.dart b/packages/genai/lib/llm_model.dart similarity index 90% rename from packages/genai/lib/providers/common.dart rename to packages/genai/lib/llm_model.dart index e00869b1..30458c60 100644 --- a/packages/genai/lib/providers/common.dart +++ b/packages/genai/lib/llm_model.dart @@ -1,6 +1,6 @@ -import '../llm_input_payload.dart'; -import '../llm_request.dart'; -import 'providers.dart'; +import 'llm_input_payload.dart'; +import 'llm_provider.dart'; +import 'llm_request.dart'; class LLMModel { const LLMModel(this.identifier, this.modelName, this.provider); diff --git a/packages/genai/lib/llm_provider.dart b/packages/genai/lib/llm_provider.dart new file mode 100644 index 00000000..1e652910 --- /dev/null +++ b/packages/genai/lib/llm_provider.dart @@ -0,0 +1,73 @@ +import 'providers/providers.dart'; +import '../llm_manager.dart'; +import 'llm_model.dart'; + +enum LLMProvider { + gemini('Gemini'), + openai('OpenAI'), + anthropic('Anthropic'), + ollama('Ollama'), + azureopenai('Azure OpenAI'); + + const LLMProvider(this.displayName); + + final String displayName; + + List get models { + final avl = LLMManager.models[this.name.toLowerCase()]; + if (avl == null) return []; + List models = []; + for (final x in avl) { + models.add(LLMModel(x[0], x[1], this)); + } + return models; + } + + ModelController get modelController { + switch (this) { + case LLMProvider.ollama: + return OllamaModelController.instance; + case LLMProvider.gemini: + return GeminiModelController.instance; + case LLMProvider.azureopenai: + return AzureOpenAIModelController.instance; + case LLMProvider.openai: + return OpenAIModelController.instance; + case LLMProvider.anthropic: + return AnthropicModelController.instance; + } + } + + static LLMProvider fromJSON(Map json) { + return LLMProvider.fromName(json['llm_provider']); + } + + static Map toJSON(LLMProvider p) { + return {'llm_provider': p.name}; + } + + static LLMProvider? fromJSONNullable(Map? json) { + if (json == null) return null; + return LLMProvider.fromName(json['llm_provider']); + } + + static Map? toJSONNullable(LLMProvider? p) { + if (p == null) return null; + return {'llm_provider': p.name}; + } + + LLMModel getLLMByIdentifier(String identifier) { + final m = this.models.where((e) => e.identifier == identifier).firstOrNull; + if (m == null) { + throw Exception('MODEL DOES NOT EXIST $identifier'); + } + return m; + } + + static LLMProvider fromName(String name) { + return LLMProvider.values.firstWhere( + (model) => model.name == name, + orElse: () => throw ArgumentError('INVALID LLM PROVIDER: $name'), + ); + } +} diff --git a/packages/genai/lib/llm_saveobject.dart b/packages/genai/lib/llm_saveobject.dart index cbbafb47..7d315789 100644 --- a/packages/genai/lib/llm_saveobject.dart +++ b/packages/genai/lib/llm_saveobject.dart @@ -1,6 +1,6 @@ import 'llm_config.dart'; -import 'providers/common.dart'; -import 'providers/providers.dart'; +import 'llm_model.dart'; +import 'llm_provider.dart'; class LLMSaveObject { String endpoint; diff --git a/packages/genai/lib/models/ai_request_model.dart b/packages/genai/lib/models/ai_request_model.dart index e18ee25a..f093dfc5 100644 --- a/packages/genai/lib/models/ai_request_model.dart +++ b/packages/genai/lib/models/ai_request_model.dart @@ -1,8 +1,9 @@ import 'package:freezed_annotation/freezed_annotation.dart'; +import '../llm_model.dart'; +import '../llm_provider.dart'; import '../llm_saveobject.dart'; import '../llm_input_payload.dart'; import '../llm_request.dart'; -import '../providers/common.dart'; import '../providers/gemini.dart'; import '../providers/providers.dart'; part 'ai_request_model.freezed.dart'; diff --git a/packages/genai/lib/models/ai_response_model.dart b/packages/genai/lib/models/ai_response_model.dart index dc88936b..a1aa75a4 100644 --- a/packages/genai/lib/models/ai_response_model.dart +++ b/packages/genai/lib/models/ai_response_model.dart @@ -4,7 +4,7 @@ import 'dart:typed_data'; import 'package:better_networking/better_networking.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:collection/collection.dart' show mergeMaps; -import '../providers/providers.dart'; +import '../llm_provider.dart'; part 'ai_response_model.freezed.dart'; part 'ai_response_model.g.dart'; diff --git a/packages/genai/lib/providers/anthropic.dart b/packages/genai/lib/providers/anthropic.dart index 97deaa2a..8fe0ff86 100644 --- a/packages/genai/lib/providers/anthropic.dart +++ b/packages/genai/lib/providers/anthropic.dart @@ -1,7 +1,7 @@ import '../llm_config.dart'; import '../llm_input_payload.dart'; +import '../llm_model.dart'; import '../llm_request.dart'; -import 'common.dart'; class AnthropicModelController extends ModelController { static final instance = AnthropicModelController(); diff --git a/packages/genai/lib/providers/azureopenai.dart b/packages/genai/lib/providers/azureopenai.dart index 18d45667..3c38ac0e 100644 --- a/packages/genai/lib/providers/azureopenai.dart +++ b/packages/genai/lib/providers/azureopenai.dart @@ -1,7 +1,7 @@ import '../llm_config.dart'; import '../llm_input_payload.dart'; +import '../llm_model.dart'; import '../llm_request.dart'; -import 'common.dart'; class AzureOpenAIModelController extends ModelController { static final instance = AzureOpenAIModelController(); diff --git a/packages/genai/lib/providers/gemini.dart b/packages/genai/lib/providers/gemini.dart index 56b85b87..a4508965 100644 --- a/packages/genai/lib/providers/gemini.dart +++ b/packages/genai/lib/providers/gemini.dart @@ -1,7 +1,7 @@ import '../llm_config.dart'; import '../llm_input_payload.dart'; +import '../llm_model.dart'; import '../llm_request.dart'; -import 'common.dart'; class GeminiModelController extends ModelController { static final instance = GeminiModelController(); diff --git a/packages/genai/lib/providers/ollama.dart b/packages/genai/lib/providers/ollama.dart index 82c2a0f2..06750472 100644 --- a/packages/genai/lib/providers/ollama.dart +++ b/packages/genai/lib/providers/ollama.dart @@ -1,7 +1,7 @@ import '../llm_config.dart'; import '../llm_input_payload.dart'; +import '../llm_model.dart'; import '../llm_request.dart'; -import 'common.dart'; class OllamaModelController extends ModelController { static final instance = OllamaModelController(); diff --git a/packages/genai/lib/providers/openai.dart b/packages/genai/lib/providers/openai.dart index bd9bdc26..2c7a2e3b 100644 --- a/packages/genai/lib/providers/openai.dart +++ b/packages/genai/lib/providers/openai.dart @@ -1,7 +1,7 @@ import '../llm_config.dart'; import '../llm_input_payload.dart'; +import '../llm_model.dart'; import '../llm_request.dart'; -import 'common.dart'; class OpenAIModelController extends ModelController { static final instance = OpenAIModelController(); diff --git a/packages/genai/lib/providers/providers.dart b/packages/genai/lib/providers/providers.dart index dff914ef..ae945182 100644 --- a/packages/genai/lib/providers/providers.dart +++ b/packages/genai/lib/providers/providers.dart @@ -1,77 +1,5 @@ -import '../llm_manager.dart'; -import 'anthropic.dart'; -import 'azureopenai.dart'; -import 'common.dart'; -import 'gemini.dart'; -import 'ollama.dart'; -import 'openai.dart'; - -enum LLMProvider { - gemini('Gemini'), - openai('OpenAI'), - anthropic('Anthropic'), - ollama('Ollama'), - azureopenai('Azure OpenAI'); - - const LLMProvider(this.displayName); - - final String displayName; - - List get models { - final avl = LLMManager.models[this.name.toLowerCase()]; - if (avl == null) return []; - List models = []; - for (final x in avl) { - models.add(LLMModel(x[0], x[1], this)); - } - return models; - } - - ModelController get modelController { - switch (this) { - case LLMProvider.ollama: - return OllamaModelController.instance; - case LLMProvider.gemini: - return GeminiModelController.instance; - case LLMProvider.azureopenai: - return AzureOpenAIModelController.instance; - case LLMProvider.openai: - return OpenAIModelController.instance; - case LLMProvider.anthropic: - return AnthropicModelController.instance; - } - } - - static LLMProvider fromJSON(Map json) { - return LLMProvider.fromName(json['llm_provider']); - } - - static Map toJSON(LLMProvider p) { - return {'llm_provider': p.name}; - } - - static LLMProvider? fromJSONNullable(Map? json) { - if (json == null) return null; - return LLMProvider.fromName(json['llm_provider']); - } - - static Map? toJSONNullable(LLMProvider? p) { - if (p == null) return null; - return {'llm_provider': p.name}; - } - - LLMModel getLLMByIdentifier(String identifier) { - final m = this.models.where((e) => e.identifier == identifier).firstOrNull; - if (m == null) { - throw Exception('MODEL DOES NOT EXIST $identifier'); - } - return m; - } - - static LLMProvider fromName(String name) { - return LLMProvider.values.firstWhere( - (model) => model.name == name, - orElse: () => throw ArgumentError('INVALID LLM PROVIDER: $name'), - ); - } -} +export 'anthropic.dart'; +export 'gemini.dart'; +export 'azureopenai.dart'; +export 'openai.dart'; +export 'ollama.dart'; \ No newline at end of file From c05a42be4757e328c6e62b646cb212afe9371f3e Mon Sep 17 00:00:00 2001 From: Manas Hejmadi Date: Mon, 23 Jun 2025 00:06:28 +0530 Subject: [PATCH 06/36] models.json added for RemoteModel Fetch feature --- packages/genai/lib/llm_manager.dart | 2 +- packages/genai/models.json | 98 +++++++++++++++++++++++++++++ 2 files changed, 99 insertions(+), 1 deletion(-) create mode 100644 packages/genai/models.json diff --git a/packages/genai/lib/llm_manager.dart b/packages/genai/lib/llm_manager.dart index 2baf9179..5feb7701 100644 --- a/packages/genai/lib/llm_manager.dart +++ b/packages/genai/lib/llm_manager.dart @@ -12,7 +12,7 @@ class LLMManager { static get models => avaiableModels; static const String modelRemoteURL = - 'https://raw.githubusercontent.com/synapsecode/apidash/llm_model_rearch/packages/genai/models.json'; + 'https://raw.githubusercontent.com/synapsecode/apidash/package/genai/packages/genai/models.json'; static const String baseOllamaURL = 'http://localhost:11434'; static addLLM(String providerID, String modelID, String modelName) async { diff --git a/packages/genai/models.json b/packages/genai/models.json new file mode 100644 index 00000000..e29e6fc8 --- /dev/null +++ b/packages/genai/models.json @@ -0,0 +1,98 @@ +{ + "openai": [ + [ + "gpt-4o", + "GPT-4o" + ], + [ + "gpt-4", + "GPT-4" + ], + [ + "gpt-4o-mini", + "GPT-4o Mini" + ], + [ + "gpt-4-turbo", + "GPT-4 Turbo" + ], + [ + "gpt-4.1", + "GPT-4.1" + ], + [ + "gpt-4.1-mini", + "GPT-4.1 Mini" + ], + [ + "gpt-4.1-nano", + "GPT-4.1 Nano" + ], + [ + "o1", + "o1" + ], + [ + "o3", + "o3" + ], + [ + "o3-mini", + "o3 Mini" + ], + [ + "gpt-3.5-turbo", + "GPT-3.5 Turbo" + ] + ], + "anthropic": [ + [ + "claude-3-opus-latest", + "Claude 3 Opus" + ], + [ + "claude-3-sonnet-latest", + "Claude 3 Sonnet" + ], + [ + "claude-3-haiku-latest", + "Claude 3 Haiku" + ], + [ + "claude-3-5-haiku-latest", + "Claude 3.5 Haiku" + ], + [ + "claude-3-5-sonnet-latest", + "Claude 3.5 Sonnet" + ] + ], + "gemini": [ + [ + "gemini-1.5-pro", + "Gemini 1.5 Pro" + ], + [ + "gemini-1.5-flash-8b", + "Gemini 1.5 Flash 8B" + ], + [ + "gemini-2.0-flash", + "Gemini 2.0 Flash" + ], + [ + "gemini-2.0-flash-lite", + "Gemini 2.0 Flash Lite" + ], + [ + "gemini-2.5-flash-preview_0520", + "Gemini 2.5 Flash Preview 0520" + ] + ], + "azureopenai": [ + [ + "custom", + "Custom" + ] + ] +} \ No newline at end of file From dc5af42cfe5ec8034b02d50c4b51da829cde4617 Mon Sep 17 00:00:00 2001 From: Manas Hejmadi Date: Mon, 23 Jun 2025 01:55:21 +0530 Subject: [PATCH 07/36] genai: Example Project added + README --- packages/genai/README.md | 58 ++- packages/genai/example/.gitignore | 45 ++ packages/genai/example/README.md | 59 +++ packages/genai/example/analysis_options.yaml | 28 ++ packages/genai/example/lib/main.dart | 184 ++++++++ packages/genai/example/pubspec.lock | 432 +++++++++++++++++++ packages/genai/example/pubspec.yaml | 91 ++++ packages/genai/models.json | 2 +- 8 files changed, 897 insertions(+), 2 deletions(-) create mode 100644 packages/genai/example/.gitignore create mode 100644 packages/genai/example/README.md create mode 100644 packages/genai/example/analysis_options.yaml create mode 100644 packages/genai/example/lib/main.dart create mode 100644 packages/genai/example/pubspec.lock create mode 100644 packages/genai/example/pubspec.yaml diff --git a/packages/genai/README.md b/packages/genai/README.md index ea898451..0e12b996 100644 --- a/packages/genai/README.md +++ b/packages/genai/README.md @@ -1,2 +1,58 @@ # genai package -This Package contains all the code related to generative AI capabilities and is a foundational package that can be used in various projects \ No newline at end of file +This Package contains all the code related to generative AI capabilities and is a foundational package that can be used in various projects + +### 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/example/.gitignore b/packages/genai/example/.gitignore new file mode 100644 index 00000000..79c113f9 --- /dev/null +++ b/packages/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/example/README.md b/packages/genai/example/README.md new file mode 100644 index 00000000..70e37aa4 --- /dev/null +++ b/packages/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/example/analysis_options.yaml b/packages/genai/example/analysis_options.yaml new file mode 100644 index 00000000..0d290213 --- /dev/null +++ b/packages/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/example/lib/main.dart b/packages/genai/example/lib/main.dart new file mode 100644 index 00000000..cee60173 --- /dev/null +++ b/packages/genai/example/lib/main.dart @@ -0,0 +1,184 @@ +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 { + @override + void initState() { + super.initState(); + () async { + await LLMManager.fetchAvailableLLMs(); //fetch latest LLMs + await LLMManager.loadAvailableLLMs(); //Load Saved LLMs + setState(() {}); + }(); + LLMManager.fetchAvailableLLMs().then((_) { + LLMManager.loadAvailableLLMs().then((_) {}); + }); + systemPromptController.text = 'Give me a 200 word essay on the given topic'; + inputPromptController.text = 'Apple'; + } + + generateAIResponse({bool stream = false}) { + setState(() { + output = ""; + }); + GenerativeAI.callGenerativeModel( + LLMProvider.fromName( + selectedProvider, + ).getLLMByIdentifier(selectedModel![0]), + onAnswer: (x) { + setState(() { + output += "$x "; + }); + }, + onError: (e) { + print(e); + }, + systemPrompt: systemPromptController.value.text, + userPrompt: inputPromptController.value.text, + credential: credentialController.value.text, + stream: stream, + ); + } + + String output = ""; + String selectedProvider = 'ollama'; + List? selectedModel; + + TextEditingController systemPromptController = TextEditingController(); + TextEditingController inputPromptController = TextEditingController(); + TextEditingController credentialController = TextEditingController(); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: Text('GenAI Example')), + body: SingleChildScrollView( + padding: EdgeInsets.all(20), + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text('Providers'), + SizedBox(height: 10), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + ...LLMManager.avaiableModels.keys.map( + (x) => Container( + child: GestureDetector( + onTap: () { + setState(() { + selectedProvider = x; + }); + }, + child: Chip( + label: Text(x), + backgroundColor: selectedProvider == x + ? Colors.blue[50] + : Colors.transparent, + ), + ), + padding: EdgeInsets.only(right: 10), + ), + ), + ], + ), + SizedBox(height: 20), + Text('Models'), + SizedBox(height: 10), + Wrap( + spacing: 5, + runSpacing: 5, + children: [ + ...(LLMManager.avaiableModels[selectedProvider] ?? []).map( + (x) => Container( + child: GestureDetector( + onTap: () { + setState(() { + selectedModel = x; + }); + }, + child: Chip( + label: Text(x[1].toString()), + backgroundColor: selectedModel == x + ? Colors.blue[50] + : Colors.transparent, + ), + ), + padding: EdgeInsets.only(right: 10), + ), + ), + ], + ), + SizedBox(height: 30), + Container( + 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), + ], + ), + ), + ); + } +} diff --git a/packages/genai/example/pubspec.lock b/packages/genai/example/pubspec.lock new file mode 100644 index 00000000..c6afd837 --- /dev/null +++ b/packages/genai/example/pubspec.lock @@ -0,0 +1,432 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + async: + dependency: transitive + description: + name: async + sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb" + url: "https://pub.dev" + source: hosted + version: "2.13.0" + better_networking: + dependency: transitive + 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" + cupertino_icons: + dependency: "direct main" + description: + name: cupertino_icons + sha256: ba631d1c7f7bef6b729a622b7b752645a2d076dba9976925b8f25725a30e1ee6 + url: "https://pub.dev" + source: hosted + version: "1.0.8" + fake_async: + dependency: transitive + description: + name: fake_async + sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" + url: "https://pub.dev" + source: hosted + version: "1.3.3" + ffi: + dependency: transitive + description: + name: ffi + sha256: "289279317b4b16eb2bb7e271abccd4bf84ec9bdcbe999e278a94b804f5630418" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + file: + dependency: transitive + description: + name: file + sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 + url: "https://pub.dev" + source: hosted + version: "7.0.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" + flutter_web_plugins: + dependency: transitive + 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" + path: + dependency: transitive + description: + name: path + sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" + url: "https://pub.dev" + source: hosted + version: "1.9.1" + path_provider_linux: + dependency: transitive + description: + name: path_provider_linux + sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 + url: "https://pub.dev" + source: hosted + version: "2.2.1" + path_provider_platform_interface: + dependency: transitive + description: + name: path_provider_platform_interface + sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + path_provider_windows: + dependency: transitive + description: + name: path_provider_windows + sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7 + url: "https://pub.dev" + source: hosted + version: "2.3.0" + petitparser: + dependency: transitive + description: + name: petitparser + sha256: "07c8f0b1913bcde1ff0d26e57ace2f3012ccbf2b204e070290dad3bb22797646" + url: "https://pub.dev" + source: hosted + version: "6.1.0" + platform: + dependency: transitive + description: + name: platform + sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984" + url: "https://pub.dev" + source: hosted + version: "3.1.6" + plugin_platform_interface: + dependency: transitive + description: + name: plugin_platform_interface + sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" + url: "https://pub.dev" + source: hosted + version: "2.1.8" + seed: + dependency: transitive + description: + name: seed + sha256: "0d74a46abd169c96a73d9dec4739e6623021915661beadf265e885bb1eafd214" + url: "https://pub.dev" + source: hosted + version: "0.0.3" + shared_preferences: + dependency: transitive + description: + name: shared_preferences + sha256: "6e8bf70b7fef813df4e9a36f658ac46d107db4b4cfe1048b477d4e453a8159f5" + url: "https://pub.dev" + source: hosted + version: "2.5.3" + shared_preferences_android: + dependency: transitive + description: + name: shared_preferences_android + sha256: "20cbd561f743a342c76c151d6ddb93a9ce6005751e7aa458baad3858bfbfb6ac" + url: "https://pub.dev" + source: hosted + version: "2.4.10" + shared_preferences_foundation: + dependency: transitive + description: + name: shared_preferences_foundation + sha256: "6a52cfcdaeac77cad8c97b539ff688ccfc458c007b4db12be584fbe5c0e49e03" + url: "https://pub.dev" + source: hosted + version: "2.5.4" + shared_preferences_linux: + dependency: transitive + description: + name: shared_preferences_linux + sha256: "580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + shared_preferences_platform_interface: + dependency: transitive + description: + name: shared_preferences_platform_interface + sha256: "57cbf196c486bc2cf1f02b85784932c6094376284b3ad5779d1b1c6c6a816b80" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + shared_preferences_web: + dependency: transitive + description: + name: shared_preferences_web + sha256: c49bd060261c9a3f0ff445892695d6212ff603ef3115edbb448509d407600019 + url: "https://pub.dev" + source: hosted + version: "2.4.3" + shared_preferences_windows: + dependency: transitive + description: + name: shared_preferences_windows + sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + 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" + xdg_directories: + dependency: transitive + description: + name: xdg_directories + sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15" + url: "https://pub.dev" + source: hosted + version: "1.1.0" + 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.27.0" diff --git a/packages/genai/example/pubspec.yaml b/packages/genai/example/pubspec.yaml new file mode 100644 index 00000000..8a3c302e --- /dev/null +++ b/packages/genai/example/pubspec.yaml @@ -0,0 +1,91 @@ +name: 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/models.json b/packages/genai/models.json index e29e6fc8..b8156b72 100644 --- a/packages/genai/models.json +++ b/packages/genai/models.json @@ -85,7 +85,7 @@ "Gemini 2.0 Flash Lite" ], [ - "gemini-2.5-flash-preview_0520", + "gemini-2.5-flash-preview-0520", "Gemini 2.5 Flash Preview 0520" ] ], From 058d95ab32e02b869f606bbfd599d2cc98010bf5 Mon Sep 17 00:00:00 2001 From: Manas Hejmadi Date: Wed, 9 Jul 2025 13:07:20 +0530 Subject: [PATCH 08/36] genai modified to use the latest streamHttpRequest --- packages/genai/lib/generative_ai.dart | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/genai/lib/generative_ai.dart b/packages/genai/lib/generative_ai.dart index e55d4d60..6fec8d72 100644 --- a/packages/genai/lib/generative_ai.dart +++ b/packages/genai/lib/generative_ai.dart @@ -77,15 +77,17 @@ class GenerativeAI { return; } - final chunk = dat.$1; - final error = dat.$3; + final chunk = dat.$2; + final error = dat.$4; if (chunk == null) { streamController.addError(error ?? 'NULL ERROR'); return; } - final lines = chunk.split('\n'); + 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(); From 90b4b99135d084ccf8513764adca94745ed0ff4b Mon Sep 17 00:00:00 2001 From: Manas Hejmadi Date: Sat, 7 Jun 2025 13:51:36 +0530 Subject: [PATCH 09/36] Added LLMSelectorWidget & Integrated with APIDash Settings --- lib/main.dart | 7 + lib/models/settings_model.dart | 19 +- lib/providers/settings_providers.dart | 3 + lib/screens/settings_page.dart | 13 + packages/genai/lib/widgets/llm_selector.dart | 268 +++++++++++++++++++ pubspec.lock | 7 + pubspec.yaml | 2 + 7 files changed, 318 insertions(+), 1 deletion(-) create mode 100644 packages/genai/lib/widgets/llm_selector.dart diff --git a/lib/main.dart b/lib/main.dart index 2b43ca94..675b0a54 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,6 +1,7 @@ import 'package:apidash_design_system/apidash_design_system.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:genai/genai.dart'; import 'models/models.dart'; import 'providers/providers.dart'; import 'services/services.dart'; @@ -9,6 +10,7 @@ import 'app.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); + var settingsModel = await getSettingsFromSharedPrefs(); var onboardingStatus = await getOnboardingStatusFromSharedPrefs(); initializeJsRuntime(); @@ -23,6 +25,11 @@ void main() async { settingsModel = settingsModel?.copyWithPath(workspaceFolderPath: null); } + //Load all LLM + LLMManager.fetchAvailableLLMs().then((_) { + LLMManager.loadAvailableLLMs().then((_) {}); + }); + runApp( ProviderScope( overrides: [ diff --git a/lib/models/settings_model.dart b/lib/models/settings_model.dart index a06b1e59..953746cf 100644 --- a/lib/models/settings_model.dart +++ b/lib/models/settings_model.dart @@ -1,6 +1,7 @@ import 'package:apidash_core/apidash_core.dart'; import 'package:flutter/material.dart'; import 'package:apidash/consts.dart'; +import 'package:genai/genai.dart'; @immutable class SettingsModel { @@ -18,6 +19,7 @@ class SettingsModel { this.workspaceFolderPath, this.isSSLDisabled = false, this.isDashBotEnabled = true, + this.defaultLLMSaveObject, }); final bool isDark; @@ -33,6 +35,7 @@ class SettingsModel { final String? workspaceFolderPath; final bool isSSLDisabled; final bool isDashBotEnabled; + final LLMSaveObject? defaultLLMSaveObject; SettingsModel copyWith({ bool? isDark, @@ -48,6 +51,8 @@ class SettingsModel { String? workspaceFolderPath, bool? isSSLDisabled, bool? isDashBotEnabled, + LLMSaveObject? def, + LLMSaveObject? defaultLLMSaveObject, }) { return SettingsModel( isDark: isDark ?? this.isDark, @@ -65,6 +70,7 @@ class SettingsModel { workspaceFolderPath: workspaceFolderPath ?? this.workspaceFolderPath, isSSLDisabled: isSSLDisabled ?? this.isSSLDisabled, isDashBotEnabled: isDashBotEnabled ?? this.isDashBotEnabled, + defaultLLMSaveObject: defaultLLMSaveObject ?? this.defaultLLMSaveObject, ); } @@ -85,6 +91,7 @@ class SettingsModel { workspaceFolderPath: workspaceFolderPath, isSSLDisabled: isSSLDisabled, isDashBotEnabled: isDashBotEnabled, + defaultLLMSaveObject: defaultLLMSaveObject, ); } @@ -141,6 +148,12 @@ class SettingsModel { final isSSLDisabled = data["isSSLDisabled"] as bool?; final isDashBotEnabled = data["isDashBotEnabled"] as bool?; + LLMSaveObject? defaultLLMSaveObject; + if (data["defaultLLMSaveObject"] != null) { + defaultLLMSaveObject = + LLMSaveObject.fromJSON(data["defaultLLMSaveObject"]); + } + const sm = SettingsModel(); return sm.copyWith( @@ -158,6 +171,7 @@ class SettingsModel { workspaceFolderPath: workspaceFolderPath, isSSLDisabled: isSSLDisabled, isDashBotEnabled: isDashBotEnabled, + defaultLLMSaveObject: defaultLLMSaveObject, ); } @@ -178,6 +192,7 @@ class SettingsModel { "workspaceFolderPath": workspaceFolderPath, "isSSLDisabled": isSSLDisabled, "isDashBotEnabled": isDashBotEnabled, + 'defaultLLMSaveObject': defaultLLMSaveObject?.toJSON(), }; } @@ -203,7 +218,8 @@ class SettingsModel { other.historyRetentionPeriod == historyRetentionPeriod && other.workspaceFolderPath == workspaceFolderPath && other.isSSLDisabled == isSSLDisabled && - other.isDashBotEnabled == isDashBotEnabled; + other.isDashBotEnabled == isDashBotEnabled && + other.defaultLLMSaveObject == defaultLLMSaveObject; } @override @@ -223,6 +239,7 @@ class SettingsModel { workspaceFolderPath, isSSLDisabled, isDashBotEnabled, + defaultLLMSaveObject, ); } } diff --git a/lib/providers/settings_providers.dart b/lib/providers/settings_providers.dart index d3cb9f2f..3f9e39a8 100644 --- a/lib/providers/settings_providers.dart +++ b/lib/providers/settings_providers.dart @@ -1,6 +1,7 @@ import 'package:apidash_core/apidash_core.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:genai/genai.dart'; import '../models/models.dart'; import '../services/services.dart'; import '../consts.dart'; @@ -34,6 +35,7 @@ class ThemeStateNotifier extends StateNotifier { String? workspaceFolderPath, bool? isSSLDisabled, bool? isDashBotEnabled, + LLMSaveObject? defaultLLMSaveObject, }) async { state = state.copyWith( isDark: isDark, @@ -49,6 +51,7 @@ class ThemeStateNotifier extends StateNotifier { workspaceFolderPath: workspaceFolderPath, isSSLDisabled: isSSLDisabled, isDashBotEnabled: isDashBotEnabled, + defaultLLMSaveObject: defaultLLMSaveObject, ); await setSettingsToSharedPrefs(state); } diff --git a/lib/screens/settings_page.dart b/lib/screens/settings_page.dart index 606ef516..86d3a68c 100644 --- a/lib/screens/settings_page.dart +++ b/lib/screens/settings_page.dart @@ -2,6 +2,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:apidash_design_system/apidash_design_system.dart'; +import 'package:genai/widgets/llm_selector.dart'; import '../providers/providers.dart'; import '../services/services.dart'; import '../utils/utils.dart'; @@ -114,6 +115,18 @@ class SettingsPage extends ConsumerWidget { }, ), ), + ListTile( + hoverColor: kColorTransparent, + title: const Text('Default Large Language Model (LLM)'), + trailing: DefaultLLMSelectorButton( + defaultLLM: settings.defaultLLMSaveObject, + onDefaultLLMUpdated: (d) { + ref + .read(settingsProvider.notifier) + .update(defaultLLMSaveObject: d); + }, + ), + ), CheckboxListTile( title: const Text("Save Responses"), subtitle: diff --git a/packages/genai/lib/widgets/llm_selector.dart b/packages/genai/lib/widgets/llm_selector.dart new file mode 100644 index 00000000..ddb67c04 --- /dev/null +++ b/packages/genai/lib/widgets/llm_selector.dart @@ -0,0 +1,268 @@ +import 'package:flutter/material.dart'; +import 'package:genai/llm_provider.dart'; +import 'package:genai/llm_saveobject.dart'; +import 'package:genai/providers/ollama.dart'; + +class DefaultLLMSelectorButton extends StatelessWidget { + final LLMSaveObject? defaultLLM; + final Function(LLMSaveObject) onDefaultLLMUpdated; + const DefaultLLMSelectorButton({ + super.key, + this.defaultLLM, + required this.onDefaultLLMUpdated, + }); + + @override + Widget build(BuildContext context) { + return Row( + mainAxisSize: MainAxisSize.min, + children: [ + Chip(label: Text(defaultLLM?.selectedLLM.modelName ?? 'none')), + SizedBox(height: 10), + IconButton( + onPressed: () async { + final saveObject = await showDialog( + context: context, + builder: (context) { + return AlertDialog( + scrollable: true, + content: DefaultLLMSelectorDialog(defaultLLM: defaultLLM), + contentPadding: EdgeInsets.all(10), + ); + }, + ); + if (saveObject == null) return; + onDefaultLLMUpdated(saveObject); + }, + icon: Icon(Icons.edit), + ), + ], + ); + } +} + +class DefaultLLMSelectorDialog extends StatefulWidget { + final LLMSaveObject? defaultLLM; + + const DefaultLLMSelectorDialog({super.key, this.defaultLLM}); + + @override + State createState() => + _DefaultLLMSelectorDialogState(); +} + +class _DefaultLLMSelectorDialogState extends State { + late LLMProvider selectedLLMProvider; + late LLMSaveObject llmSaveObject; + + @override + void initState() { + super.initState(); + + final oC = OllamaModelController().inputPayload; + + llmSaveObject = + widget.defaultLLM ?? + LLMSaveObject( + endpoint: oC.endpoint, + credential: '', + configMap: oC.configMap, + selectedLLM: LLMProvider.gemini.getLLMByIdentifier( + 'gemini-2.0-flash', + ), + provider: LLMProvider.ollama, + ); + + selectedLLMProvider = llmSaveObject.provider; + } + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.all(20), + width: MediaQuery.of(context).size.width * 0.8, + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Left panel - Provider List + Container( + width: 300, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text('Providers'), + const SizedBox(height: 10), + ...LLMProvider.values.map( + (provider) => ListTile( + title: Text(provider.displayName), + trailing: llmSaveObject.provider == provider + ? const CircleAvatar( + radius: 5, + backgroundColor: Colors.green, + ) + : null, + onTap: () { + final input = provider.modelController.inputPayload; + setState(() { + selectedLLMProvider = provider; + llmSaveObject = LLMSaveObject( + endpoint: input.endpoint, + credential: '', + configMap: input.configMap, + selectedLLM: provider.models.first, + provider: provider, + ); + }); + }, + ), + ), + ], + ), + ), + + const SizedBox(width: 40), + + // Right panel - Configuration and Save + Expanded( + flex: 3, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + selectedLLMProvider.displayName, + style: const TextStyle(fontSize: 28), + ), + const SizedBox(height: 20), + + if (selectedLLMProvider != LLMProvider.ollama) ...[ + const Text('API Key / Credential'), + const SizedBox(height: 10), + BoundedTextField( + onChanged: (x) { + llmSaveObject.credential = x; + }, + value: llmSaveObject.credential, + ), + const SizedBox(height: 10), + ], + + const Text('Endpoint'), + const SizedBox(height: 10), + BoundedTextField( + key: ValueKey(llmSaveObject.provider), + onChanged: (x) => llmSaveObject.endpoint = x, + value: llmSaveObject.endpoint, + ), + + const SizedBox(height: 20), + const Text('Models'), + const SizedBox(height: 8), + + Container( + height: 300, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(10), + color: const Color.fromARGB(27, 0, 0, 0), + ), + child: SingleChildScrollView( + child: Column( + children: selectedLLMProvider.models + .map( + (model) => ListTile( + title: Text(model.modelName), + subtitle: Text(model.identifier), + trailing: llmSaveObject.selectedLLM == model + ? const CircleAvatar( + radius: 5, + backgroundColor: Colors.green, + ) + : null, + onTap: () { + setState(() { + llmSaveObject.selectedLLM = model; + }); + }, + ), + ) + .toList(), + ), + ), + ), + const SizedBox(height: 10), + Align( + alignment: Alignment.centerRight, + child: ElevatedButton( + onPressed: () { + llmSaveObject.provider = selectedLLMProvider; + Navigator.of(context).pop(llmSaveObject); + }, + child: const Text('Save Changes'), + ), + ), + ], + ), + ), + ], + ), + ); + } +} + +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: BorderRadius.circular(8), + ), + 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/pubspec.lock b/pubspec.lock index 0536f2ac..81107934 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -702,6 +702,13 @@ packages: url: "https://pub.dev" source: hosted version: "0.32.1" + genai: + dependency: "direct main" + description: + path: "packages/genai" + relative: true + source: path + version: "0.0.1" glob: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 0ef4fae7..4e40b9b5 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -14,6 +14,8 @@ dependencies: path: packages/apidash_core apidash_design_system: path: packages/apidash_design_system + genai: + path: packages/genai carousel_slider: ^5.0.0 code_builder: ^4.10.0 csv: ^6.0.0 From d923996dc83e4922e51f86f7e21b74bff35d9af5 Mon Sep 17 00:00:00 2001 From: Manas Hejmadi Date: Wed, 9 Jul 2025 15:08:06 +0530 Subject: [PATCH 10/36] AIProviderSelector Impl & AI Models added to RequestModel --- lib/models/history_meta_model.g.dart | 1 + lib/models/request_model.dart | 3 + lib/models/request_model.freezed.dart | 92 +++++++++++++++++-- lib/models/request_model.g.dart | 11 +++ lib/providers/collection_providers.dart | 6 ++ .../home_page/editor_pane/url_card.dart | 49 ++++++++++ lib/utils/ui_utils.dart | 1 + lib/widgets/texts.dart | 1 + packages/better_networking/lib/consts.dart | 1 + .../lib/utils/http_request_utils.dart | 1 + 10 files changed, 159 insertions(+), 7 deletions(-) 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/request_model.dart b/lib/models/request_model.dart index b7052f81..952aaa7c 100644 --- a/lib/models/request_model.dart +++ b/lib/models/request_model.dart @@ -1,4 +1,5 @@ import 'package:apidash_core/apidash_core.dart'; +import 'package:genai/genai.dart'; part 'request_model.freezed.dart'; @@ -25,6 +26,8 @@ class RequestModel with _$RequestModel { @JsonKey(includeToJson: false) @Default(false) bool isStreaming, String? preRequestScript, String? postRequestScript, + AIRequestModel? aiRequestModel, + AIResponseModel? aiResponseModel, }) = _RequestModel; factory RequestModel.fromJson(Map json) => diff --git a/lib/models/request_model.freezed.dart b/lib/models/request_model.freezed.dart index 3ba8979b..5998065d 100644 --- a/lib/models/request_model.freezed.dart +++ b/lib/models/request_model.freezed.dart @@ -39,6 +39,8 @@ mixin _$RequestModel { bool get isStreaming => throw _privateConstructorUsedError; String? get preRequestScript => throw _privateConstructorUsedError; String? get postRequestScript => throw _privateConstructorUsedError; + AIRequestModel? get aiRequestModel => throw _privateConstructorUsedError; + AIResponseModel? get aiResponseModel => throw _privateConstructorUsedError; /// Serializes this RequestModel to a JSON map. Map toJson() => throw _privateConstructorUsedError; @@ -70,10 +72,14 @@ abstract class $RequestModelCopyWith<$Res> { @JsonKey(includeToJson: false) DateTime? sendingTime, @JsonKey(includeToJson: false) bool isStreaming, String? preRequestScript, - String? postRequestScript}); + String? postRequestScript, + AIRequestModel? aiRequestModel, + AIResponseModel? aiResponseModel}); $HttpRequestModelCopyWith<$Res>? get httpRequestModel; $HttpResponseModelCopyWith<$Res>? get httpResponseModel; + $AIRequestModelCopyWith<$Res>? get aiRequestModel; + $AIResponseModelCopyWith<$Res>? get aiResponseModel; } /// @nodoc @@ -105,6 +111,8 @@ class _$RequestModelCopyWithImpl<$Res, $Val extends RequestModel> Object? isStreaming = null, Object? preRequestScript = freezed, Object? postRequestScript = freezed, + Object? aiRequestModel = freezed, + Object? aiResponseModel = freezed, }) { return _then(_value.copyWith( id: null == id @@ -163,6 +171,14 @@ 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?, + aiResponseModel: freezed == aiResponseModel + ? _value.aiResponseModel + : aiResponseModel // ignore: cast_nullable_to_non_nullable + as AIResponseModel?, ) as $Val); } @@ -193,6 +209,34 @@ 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); + }); + } + + /// Create a copy of RequestModel + /// with the given fields replaced by the non-null parameter values. + @override + @pragma('vm:prefer-inline') + $AIResponseModelCopyWith<$Res>? get aiResponseModel { + if (_value.aiResponseModel == null) { + return null; + } + + return $AIResponseModelCopyWith<$Res>(_value.aiResponseModel!, (value) { + return _then(_value.copyWith(aiResponseModel: value) as $Val); + }); + } } /// @nodoc @@ -217,12 +261,18 @@ abstract class _$$RequestModelImplCopyWith<$Res> @JsonKey(includeToJson: false) DateTime? sendingTime, @JsonKey(includeToJson: false) bool isStreaming, String? preRequestScript, - String? postRequestScript}); + String? postRequestScript, + AIRequestModel? aiRequestModel, + AIResponseModel? aiResponseModel}); @override $HttpRequestModelCopyWith<$Res>? get httpRequestModel; @override $HttpResponseModelCopyWith<$Res>? get httpResponseModel; + @override + $AIRequestModelCopyWith<$Res>? get aiRequestModel; + @override + $AIResponseModelCopyWith<$Res>? get aiResponseModel; } /// @nodoc @@ -252,6 +302,8 @@ class __$$RequestModelImplCopyWithImpl<$Res> Object? isStreaming = null, Object? preRequestScript = freezed, Object? postRequestScript = freezed, + Object? aiRequestModel = freezed, + Object? aiResponseModel = freezed, }) { return _then(_$RequestModelImpl( id: null == id @@ -309,6 +361,14 @@ 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?, + aiResponseModel: freezed == aiResponseModel + ? _value.aiResponseModel + : aiResponseModel // ignore: cast_nullable_to_non_nullable + as AIResponseModel?, )); } } @@ -331,7 +391,9 @@ class _$RequestModelImpl implements _RequestModel { @JsonKey(includeToJson: false) this.sendingTime, @JsonKey(includeToJson: false) this.isStreaming = false, this.preRequestScript, - this.postRequestScript}); + this.postRequestScript, + this.aiRequestModel, + this.aiResponseModel}); factory _$RequestModelImpl.fromJson(Map json) => _$$RequestModelImplFromJson(json); @@ -371,10 +433,14 @@ class _$RequestModelImpl implements _RequestModel { final String? preRequestScript; @override final String? postRequestScript; + @override + final AIRequestModel? aiRequestModel; + @override + final AIResponseModel? aiResponseModel; @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, aiResponseModel: $aiResponseModel)'; } @override @@ -405,7 +471,11 @@ 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) && + (identical(other.aiResponseModel, aiResponseModel) || + other.aiResponseModel == aiResponseModel)); } @JsonKey(includeFromJson: false, includeToJson: false) @@ -425,7 +495,9 @@ class _$RequestModelImpl implements _RequestModel { sendingTime, isStreaming, preRequestScript, - postRequestScript); + postRequestScript, + aiRequestModel, + aiResponseModel); /// Create a copy of RequestModel /// with the given fields replaced by the non-null parameter values. @@ -458,7 +530,9 @@ 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, + final AIResponseModel? aiResponseModel}) = _$RequestModelImpl; factory _RequestModel.fromJson(Map json) = _$RequestModelImpl.fromJson; @@ -495,6 +569,10 @@ abstract class _RequestModel implements RequestModel { String? get preRequestScript; @override String? get postRequestScript; + @override + AIRequestModel? get aiRequestModel; + @override + AIResponseModel? get aiResponseModel; /// 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..d994e7c4 100644 --- a/lib/models/request_model.g.dart +++ b/lib/models/request_model.g.dart @@ -30,6 +30,14 @@ _$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)), + aiResponseModel: json['aiResponseModel'] == null + ? null + : AIResponseModel.fromJson( + Map.from(json['aiResponseModel'] as Map)), ); Map _$$RequestModelImplToJson(_$RequestModelImpl instance) => @@ -44,9 +52,12 @@ Map _$$RequestModelImplToJson(_$RequestModelImpl instance) => 'httpResponseModel': instance.httpResponseModel?.toJson(), 'preRequestScript': instance.preRequestScript, 'postRequestScript': instance.postRequestScript, + 'aiRequestModel': instance.aiRequestModel?.toJson(), + 'aiResponseModel': instance.aiResponseModel?.toJson(), }; const _$APITypeEnumMap = { APIType.rest: 'rest', + APIType.ai: 'ai', APIType.graphql: 'graphql', }; diff --git a/lib/providers/collection_providers.dart b/lib/providers/collection_providers.dart index 525185a6..1c6e7f73 100644 --- a/lib/providers/collection_providers.dart +++ b/lib/providers/collection_providers.dart @@ -3,6 +3,8 @@ import 'package:apidash_core/apidash_core.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:apidash/consts.dart'; +import 'package:genai/genai.dart'; +import 'package:genai/models/ai_request_model.dart'; import 'providers.dart'; import '../models/models.dart'; import '../services/services.dart'; @@ -226,6 +228,8 @@ class CollectionStateNotifier HttpResponseModel? httpResponseModel, String? preRequestScript, String? postRequestScript, + AIRequestModel? aiRequestModel, + AIResponseModel? aiResponseModel, }) { final rId = id ?? ref.read(selectedIdStateProvider); if (rId == null) { @@ -260,6 +264,8 @@ class CollectionStateNotifier httpResponseModel: httpResponseModel ?? currentModel.httpResponseModel, preRequestScript: preRequestScript ?? currentModel.preRequestScript, postRequestScript: postRequestScript ?? currentModel.postRequestScript, + aiRequestModel: aiRequestModel ?? currentModel.aiRequestModel, + aiResponseModel: aiResponseModel ?? currentModel.aiResponseModel, ); var map = {...state!}; diff --git a/lib/screens/home_page/editor_pane/url_card.dart b/lib/screens/home_page/editor_pane/url_card.dart index 5aa1ce02..bcbfc479 100644 --- a/lib/screens/home_page/editor_pane/url_card.dart +++ b/lib/screens/home_page/editor_pane/url_card.dart @@ -4,6 +4,8 @@ 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 'package:genai/genai.dart'; +import 'package:genai/widgets/llm_selector.dart'; import '../../common_widgets/common_widgets.dart'; class EditorPaneRequestURLCard extends ConsumerWidget { @@ -35,6 +37,7 @@ class EditorPaneRequestURLCard extends ConsumerWidget { switch (apiType) { APIType.rest => const DropdownButtonHTTPMethod(), APIType.graphql => kSizedBoxEmpty, + APIType.ai => const AIProviderSelector(), null => kSizedBoxEmpty, }, switch (apiType) { @@ -51,6 +54,7 @@ class EditorPaneRequestURLCard extends ConsumerWidget { switch (apiType) { APIType.rest => const DropdownButtonHTTPMethod(), APIType.graphql => kSizedBoxEmpty, + APIType.ai => const AIProviderSelector(), null => kSizedBoxEmpty, }, switch (apiType) { @@ -145,3 +149,48 @@ class SendRequestButton extends ConsumerWidget { ); } } + +class AIProviderSelector extends ConsumerWidget { + const AIProviderSelector({ + super.key, + }); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final selectedId = ref.watch(selectedIdStateProvider); + final req = ref.watch(collectionStateNotifierProvider)![selectedId]!; + final aiRequestModel = req.aiRequestModel; + final defaultLLMSO = aiRequestModel == null + ? ref + .read(settingsProvider.notifier) + .settingsModel + ?.defaultLLMSaveObject + : LLMSaveObject( + endpoint: aiRequestModel.payload.endpoint, + credential: aiRequestModel.payload.credential, + configMap: aiRequestModel.payload.configMap, + selectedLLM: aiRequestModel.model, + provider: aiRequestModel.provider, + ); + + return DefaultLLMSelectorButton( + key: ValueKey(ref.watch(selectedIdStateProvider)), + defaultLLM: defaultLLMSO, + onDefaultLLMUpdated: (llmso) { + ref.read(collectionStateNotifierProvider.notifier).update( + aiRequestModel: AIRequestModel( + model: llmso.selectedLLM, + provider: llmso.provider, + payload: LLMInputPayload( + endpoint: llmso.endpoint, + credential: llmso.credential, + systemPrompt: aiRequestModel?.payload.systemPrompt ?? '', + userPrompt: aiRequestModel?.payload.userPrompt ?? '', + configMap: llmso.configMap, + ), + ), + ); + }, + ); + } +} 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/texts.dart b/lib/widgets/texts.dart index c64a71b2..1ec47eb4 100644 --- a/lib/widgets/texts.dart +++ b/lib/widgets/texts.dart @@ -20,6 +20,7 @@ class SidebarRequestCardTextBox extends StatelessWidget { switch (apiType) { APIType.rest => method.abbr, APIType.graphql => apiType.abbr, + APIType.ai => apiType.abbr, }, textAlign: TextAlign.center, style: TextStyle( diff --git a/packages/better_networking/lib/consts.dart b/packages/better_networking/lib/consts.dart index bbe055ae..68c2c0a3 100644 --- a/packages/better_networking/lib/consts.dart +++ b/packages/better_networking/lib/consts.dart @@ -2,6 +2,7 @@ import 'dart:convert'; enum APIType { rest("HTTP", "HTTP"), + ai("AI", "AI"), graphql("GraphQL", "GQL"); const APIType(this.label, this.abbr); diff --git a/packages/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 }; } From 22b8168b73d2aaff75edcf0c3d9335677ecbb13f Mon Sep 17 00:00:00 2001 From: Manas Hejmadi Date: Sat, 7 Jun 2025 21:48:14 +0530 Subject: [PATCH 11/36] AIRequestFeature: Added Prompt, Authorization & Configuration Tabs --- .../history_widgets/his_request_pane.dart | 2 + .../ai_request/aireq_authorization.dart | 43 +++++ .../ai_request/aireq_configs.dart | 153 ++++++++++++++++++ .../request_pane/ai_request/aireq_prompt.dart | 80 +++++++++ .../ai_request/request_pane_ai.dart | 49 ++++++ .../request_pane/request_body.dart | 1 + .../request_pane/request_pane.dart | 2 + 7 files changed, 330 insertions(+) create mode 100644 lib/screens/home_page/editor_pane/details_card/request_pane/ai_request/aireq_authorization.dart create mode 100644 lib/screens/home_page/editor_pane/details_card/request_pane/ai_request/aireq_configs.dart create mode 100644 lib/screens/home_page/editor_pane/details_card/request_pane/ai_request/aireq_prompt.dart create mode 100644 lib/screens/home_page/editor_pane/details_card/request_pane/ai_request/request_pane_ai.dart diff --git a/lib/screens/history/history_widgets/his_request_pane.dart b/lib/screens/history/history_widgets/his_request_pane.dart index 6766ad1b..73f61927 100644 --- a/lib/screens/history/history_widgets/his_request_pane.dart +++ b/lib/screens/history/history_widgets/his_request_pane.dart @@ -127,6 +127,7 @@ class HistoryRequestPane extends ConsumerWidget { const HistoryScriptsTab(), ], ), + APIType.ai => FlutterLogo(), _ => kSizedBoxEmpty, }; } @@ -204,6 +205,7 @@ class HisRequestBody extends ConsumerWidget { readOnly: true, ), ), + APIType.ai => FlutterLogo(), _ => kSizedBoxEmpty, }; } 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..cd5e0221 --- /dev/null +++ b/lib/screens/home_page/editor_pane/details_card/request_pane/ai_request/aireq_authorization.dart @@ -0,0 +1,43 @@ +import 'package:apidash/providers/collection_providers.dart'; +import 'package:apidash/widgets/editor.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +class AIRequestAuthorizationSection extends ConsumerWidget { + const AIRequestAuthorizationSection({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final selectedId = ref.watch(selectedIdStateProvider); + final reqM = ref.read(collectionStateNotifierProvider)![selectedId]!; + final aiReqM = reqM.aiRequestModel!; + final payload = aiReqM.payload; + + final cred = payload.credential; + + return Container( + padding: EdgeInsets.symmetric(vertical: 20), + child: Column( + children: [ + Expanded( + child: Container( + padding: EdgeInsets.symmetric(horizontal: 20), + child: TextFieldEditor( + key: Key("$selectedId-aireq-authvalue-body"), + fieldKey: "$selectedId-aireq-authvalue-body", + initialValue: cred, + onChanged: (String value) { + payload.credential = value; + ref + .read(collectionStateNotifierProvider.notifier) + .update(aiRequestModel: aiReqM.updatePayload(payload)); + }, + 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..b7b3d1ab --- /dev/null +++ b/lib/screens/home_page/editor_pane/details_card/request_pane/ai_request/aireq_configs.dart @@ -0,0 +1,153 @@ +import 'package:apidash/providers/collection_providers.dart'; +import 'package:apidash/widgets/editor.dart'; +import 'package:apidash_design_system/widgets/textfield_outlined.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:genai/genai.dart'; + +class AIRequestConfigSection extends ConsumerStatefulWidget { + const AIRequestConfigSection({super.key}); + + @override + ConsumerState createState() => + _AIRequestConfigSectionState(); +} + +class _AIRequestConfigSectionState + extends ConsumerState { + @override + Widget build(BuildContext context) { + final selectedId = ref.watch(selectedIdStateProvider); + final reqM = ref.read(collectionStateNotifierProvider)![selectedId]!; + final aiReqM = reqM.aiRequestModel!; + final payload = aiReqM.payload; + + return SingleChildScrollView( + padding: EdgeInsets.symmetric(vertical: 20), + child: Column( + key: ValueKey(selectedId), + children: [ + ...payload.configMap.values.map( + (el) => ListTile( + title: Text(el.configName), + subtitle: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + el.configDescription, + style: TextStyle(color: Colors.white30), + ), + SizedBox(height: 5), + if (el.configType == LLMModelConfigurationType.boolean) ...[ + Switch( + value: el.configValue.value as bool, + onChanged: (x) { + el.configValue.value = x; + payload.configMap[el.configId] = el; + ref + .read(collectionStateNotifierProvider.notifier) + .update( + aiRequestModel: aiReqM.updatePayload(payload), + ); + setState(() {}); + }, + ) + ] else if (el.configType == + LLMModelConfigurationType.numeric) ...[ + ADOutlinedTextField( + initialValue: el.configValue.value.toString(), + onChanged: (x) { + if (x.isEmpty) x = '0'; + if (num.tryParse(x) == null) return; + el.configValue.value = num.parse(x); + + payload.configMap[el.configId] = el; + ref + .read(collectionStateNotifierProvider.notifier) + .update( + aiRequestModel: aiReqM.updatePayload(payload), + ); + + setState(() {}); + }, + ) + ] else if (el.configType == + LLMModelConfigurationType.text) ...[ + ADOutlinedTextField( + initialValue: el.configValue.value.toString(), + onChanged: (x) { + el.configValue.value = x; + + payload.configMap[el.configId] = el; + ref + .read(collectionStateNotifierProvider.notifier) + .update( + aiRequestModel: aiReqM.updatePayload(payload), + ); + + setState(() {}); + }, + ) + ] else if (el.configType == + LLMModelConfigurationType.slider) ...[ + Row( + children: [ + Expanded( + child: Slider( + min: (el.configValue.value as ( + double, + double, + double + )) + .$1, + value: (el.configValue.value as ( + double, + double, + double + )) + .$2, + max: (el.configValue.value as ( + double, + double, + double + )) + .$3, + onChanged: (x) { + final z = el.configValue.value as ( + double, + double, + double + ); + el.configValue.value = (z.$1, x, z.$3); + + payload.configMap[el.configId] = el; + ref + .read( + collectionStateNotifierProvider.notifier) + .update( + aiRequestModel: + aiReqM.updatePayload(payload), + ); + + setState(() {}); + }, + ), + ), + Text((el.configValue.value as (double, double, double)) + .$2 + .toStringAsFixed(2)), + ], + ) + ], + SizedBox(height: 10), + // Divider(color: Colors.white10), + // 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..f090ca6e --- /dev/null +++ b/lib/screens/home_page/editor_pane/details_card/request_pane/ai_request/aireq_prompt.dart @@ -0,0 +1,80 @@ +import 'package:apidash/providers/collection_providers.dart'; +import 'package:apidash/widgets/editor.dart'; +import 'package:apidash_design_system/tokens/measurements.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +class AIRequestPromptSection extends ConsumerWidget { + const AIRequestPromptSection({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final selectedId = ref.watch(selectedIdStateProvider); + final reqM = ref.read(collectionStateNotifierProvider)![selectedId]!; + final aiReqM = reqM.aiRequestModel!; + final payload = aiReqM.payload; + + final systemPrompt = payload.systemPrompt; + final userPrompt = payload.userPrompt; + + return Container( + padding: EdgeInsets.symmetric(vertical: 20), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.only(left: 25.0), + child: Text( + 'System Prompt', + style: TextStyle(color: Colors.white54), + ), + ), + kVSpacer10, + Expanded( + child: Container( + padding: EdgeInsets.symmetric(horizontal: 20), + child: TextFieldEditor( + key: Key("$selectedId-aireq-sysprompt-body"), + fieldKey: "$selectedId-aireq-sysprompt-body", + initialValue: systemPrompt, + onChanged: (String value) { + payload.systemPrompt = value; + ref + .read(collectionStateNotifierProvider.notifier) + .update(aiRequestModel: aiReqM.updatePayload(payload)); + }, + hintText: 'Enter System Prompt', + ), + ), + ), + SizedBox(height: 10), + Padding( + padding: const EdgeInsets.only(left: 25.0), + child: Text( + 'User Prompt / Input', + style: TextStyle(color: Colors.white54), + ), + ), + kVSpacer10, + Expanded( + child: Container( + padding: EdgeInsets.symmetric(horizontal: 20), + child: TextFieldEditor( + key: Key("$selectedId-aireq-userprompt-body"), + fieldKey: "$selectedId-aireq-userprompt-body", + initialValue: userPrompt, + onChanged: (String value) { + payload.userPrompt = value; + ref + .read(collectionStateNotifierProvider.notifier) + .update(aiRequestModel: aiReqM.updatePayload(payload)); + }, + 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..5d219717 --- /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:apidash/screens/home_page/editor_pane/details_card/request_pane/ai_request/aireq_authorization.dart'; +import 'package:apidash/screens/home_page/editor_pane/details_card/request_pane/ai_request/aireq_configs.dart'; +import 'package:apidash/screens/home_page/editor_pane/details_card/request_pane/ai_request/aireq_prompt.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:apidash/providers/providers.dart'; +import 'package:apidash/widgets/widgets.dart'; + +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: false, + 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_body.dart b/lib/screens/home_page/editor_pane/details_card/request_pane/request_body.dart index e192f26e..91d80e41 100644 --- a/lib/screens/home_page/editor_pane/details_card/request_pane/request_body.dart +++ b/lib/screens/home_page/editor_pane/details_card/request_pane/request_body.dart @@ -92,6 +92,7 @@ class EditRequestBody extends ConsumerWidget { ), ), ), + APIType.ai => FlutterLogo(), _ => kSizedBoxEmpty, } ], diff --git a/lib/screens/home_page/editor_pane/details_card/request_pane/request_pane.dart b/lib/screens/home_page/editor_pane/details_card/request_pane/request_pane.dart index 9c852c71..40216103 100644 --- a/lib/screens/home_page/editor_pane/details_card/request_pane/request_pane.dart +++ b/lib/screens/home_page/editor_pane/details_card/request_pane/request_pane.dart @@ -1,3 +1,4 @@ +import 'package:apidash/screens/home_page/editor_pane/details_card/request_pane/ai_request/request_pane_ai.dart'; import 'package:apidash_core/apidash_core.dart'; import 'package:apidash_design_system/apidash_design_system.dart'; import 'package:flutter/material.dart'; @@ -17,6 +18,7 @@ class EditRequestPane extends ConsumerWidget { return switch (apiType) { APIType.rest => const EditRestRequestPane(), APIType.graphql => const EditGraphQLRequestPane(), + APIType.ai => const EditAIRequestPane(), _ => kSizedBoxEmpty, }; } From ce568973204c1751ec194761717e223f63bd82cd Mon Sep 17 00:00:00 2001 From: Manas Hejmadi Date: Sat, 7 Jun 2025 22:25:57 +0530 Subject: [PATCH 12/36] initializeAIRequest on AIRequest Creation & ControllerUpdate issues fixed --- .../common_widgets/api_type_dropdown.dart | 35 ++++++++++++++ .../ai_request/aireq_authorization.dart | 7 ++- .../ai_request/aireq_configs.dart | 46 ++++++------------- .../request_pane/ai_request/aireq_prompt.dart | 14 ++++-- .../home_page/editor_pane/url_card.dart | 30 ++++++------ 5 files changed, 79 insertions(+), 53 deletions(-) diff --git a/lib/screens/common_widgets/api_type_dropdown.dart b/lib/screens/common_widgets/api_type_dropdown.dart index c35645d6..22534704 100644 --- a/lib/screens/common_widgets/api_type_dropdown.dart +++ b/lib/screens/common_widgets/api_type_dropdown.dart @@ -1,7 +1,9 @@ +import 'package:apidash_core/apidash_core.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:apidash/providers/providers.dart'; import 'package:apidash/widgets/widgets.dart'; +import 'package:genai/genai.dart'; class APITypeDropdown extends ConsumerWidget { const APITypeDropdown({super.key}); @@ -14,6 +16,9 @@ class APITypeDropdown extends ConsumerWidget { return APITypePopupMenu( apiType: apiType, onChanged: (type) { + if (type == APIType.ai) { + initializeAIRequest(ref); + } ref .read(collectionStateNotifierProvider.notifier) .update(apiType: type); @@ -21,3 +26,33 @@ class APITypeDropdown extends ConsumerWidget { ); } } + +initializeAIRequest(WidgetRef ref) { + final selectedId = ref.watch(selectedIdStateProvider); + final req = ref.watch(collectionStateNotifierProvider)![selectedId]!; + AIRequestModel? aiRequestModel = req.aiRequestModel; + LLMSaveObject? defaultLLMSO = ref + .watch(settingsProvider.notifier) + .settingsModel + ?.defaultLLMSaveObject; //Settings Default + + if (aiRequestModel == null) { + // Creating the AIRequest Model initially + final gmC = GeminiModelController(); + final newAIRM = AIRequestModel( + model: defaultLLMSO?.selectedLLM ?? + LLMProvider.gemini.getLLMByIdentifier('gemini-2.0-flash'), + provider: defaultLLMSO?.provider ?? LLMProvider.gemini, + payload: LLMInputPayload( + endpoint: defaultLLMSO?.endpoint ?? gmC.inputPayload.endpoint, + credential: defaultLLMSO?.credential ?? '', + systemPrompt: '', + userPrompt: '', + configMap: defaultLLMSO?.configMap ?? gmC.inputPayload.configMap, + ), + ); + ref.read(collectionStateNotifierProvider.notifier).update( + aiRequestModel: newAIRM, + ); + } +} 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 index cd5e0221..f17f6101 100644 --- 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 @@ -27,10 +27,13 @@ class AIRequestAuthorizationSection extends ConsumerWidget { fieldKey: "$selectedId-aireq-authvalue-body", initialValue: cred, onChanged: (String value) { - payload.credential = value; + final aim = ref + .read(collectionStateNotifierProvider)![selectedId]! + .aiRequestModel!; + aim.payload.credential = value; ref .read(collectionStateNotifierProvider.notifier) - .update(aiRequestModel: aiReqM.updatePayload(payload)); + .update(aiRequestModel: aim.updatePayload(aim.payload)); }, 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 index b7b3d1ab..eb1fb7cb 100644 --- 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 @@ -22,6 +22,16 @@ class _AIRequestConfigSectionState final aiReqM = reqM.aiRequestModel!; final payload = aiReqM.payload; + updateRequestModel(LLMModelConfiguration el) { + final aim = ref + .read(collectionStateNotifierProvider)![selectedId]! + .aiRequestModel!; + aim.payload.configMap[el.configId] = el; + ref.read(collectionStateNotifierProvider.notifier).update( + aiRequestModel: aim.updatePayload(aim.payload), + ); + } + return SingleChildScrollView( padding: EdgeInsets.symmetric(vertical: 20), child: Column( @@ -43,12 +53,7 @@ class _AIRequestConfigSectionState value: el.configValue.value as bool, onChanged: (x) { el.configValue.value = x; - payload.configMap[el.configId] = el; - ref - .read(collectionStateNotifierProvider.notifier) - .update( - aiRequestModel: aiReqM.updatePayload(payload), - ); + updateRequestModel(el); setState(() {}); }, ) @@ -60,14 +65,7 @@ class _AIRequestConfigSectionState if (x.isEmpty) x = '0'; if (num.tryParse(x) == null) return; el.configValue.value = num.parse(x); - - payload.configMap[el.configId] = el; - ref - .read(collectionStateNotifierProvider.notifier) - .update( - aiRequestModel: aiReqM.updatePayload(payload), - ); - + updateRequestModel(el); setState(() {}); }, ) @@ -77,14 +75,7 @@ class _AIRequestConfigSectionState initialValue: el.configValue.value.toString(), onChanged: (x) { el.configValue.value = x; - - payload.configMap[el.configId] = el; - ref - .read(collectionStateNotifierProvider.notifier) - .update( - aiRequestModel: aiReqM.updatePayload(payload), - ); - + updateRequestModel(el); setState(() {}); }, ) @@ -119,16 +110,7 @@ class _AIRequestConfigSectionState double ); el.configValue.value = (z.$1, x, z.$3); - - payload.configMap[el.configId] = el; - ref - .read( - collectionStateNotifierProvider.notifier) - .update( - aiRequestModel: - aiReqM.updatePayload(payload), - ); - + updateRequestModel(el); setState(() {}); }, ), 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 index f090ca6e..cca6c511 100644 --- 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 @@ -38,10 +38,13 @@ class AIRequestPromptSection extends ConsumerWidget { fieldKey: "$selectedId-aireq-sysprompt-body", initialValue: systemPrompt, onChanged: (String value) { - payload.systemPrompt = value; + final aim = ref + .read(collectionStateNotifierProvider)![selectedId]! + .aiRequestModel!; + aim.payload.systemPrompt = value; ref .read(collectionStateNotifierProvider.notifier) - .update(aiRequestModel: aiReqM.updatePayload(payload)); + .update(aiRequestModel: aim.updatePayload(aim.payload)); }, hintText: 'Enter System Prompt', ), @@ -64,10 +67,13 @@ class AIRequestPromptSection extends ConsumerWidget { fieldKey: "$selectedId-aireq-userprompt-body", initialValue: userPrompt, onChanged: (String value) { - payload.userPrompt = value; + final aim = ref + .read(collectionStateNotifierProvider)![selectedId]! + .aiRequestModel!; + aim.payload.userPrompt = value; ref .read(collectionStateNotifierProvider.notifier) - .update(aiRequestModel: aiReqM.updatePayload(payload)); + .update(aiRequestModel: aim.updatePayload(aim.payload)); }, hintText: 'Enter User Prompt', ), diff --git a/lib/screens/home_page/editor_pane/url_card.dart b/lib/screens/home_page/editor_pane/url_card.dart index bcbfc479..d64288f5 100644 --- a/lib/screens/home_page/editor_pane/url_card.dart +++ b/lib/screens/home_page/editor_pane/url_card.dart @@ -159,19 +159,19 @@ class AIProviderSelector extends ConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final selectedId = ref.watch(selectedIdStateProvider); final req = ref.watch(collectionStateNotifierProvider)![selectedId]!; - final aiRequestModel = req.aiRequestModel; - final defaultLLMSO = aiRequestModel == null - ? ref - .read(settingsProvider.notifier) - .settingsModel - ?.defaultLLMSaveObject - : LLMSaveObject( - endpoint: aiRequestModel.payload.endpoint, - credential: aiRequestModel.payload.credential, - configMap: aiRequestModel.payload.configMap, - selectedLLM: aiRequestModel.model, - provider: aiRequestModel.provider, - ); + AIRequestModel? aiRequestModel = req.aiRequestModel; + + if (aiRequestModel == null) { + return Container(); + } + + LLMSaveObject defaultLLMSO = LLMSaveObject( + endpoint: aiRequestModel.payload.endpoint, + credential: aiRequestModel.payload.credential, + configMap: aiRequestModel.payload.configMap, + selectedLLM: aiRequestModel.model, + provider: aiRequestModel.provider, + ); return DefaultLLMSelectorButton( key: ValueKey(ref.watch(selectedIdStateProvider)), @@ -184,8 +184,8 @@ class AIProviderSelector extends ConsumerWidget { payload: LLMInputPayload( endpoint: llmso.endpoint, credential: llmso.credential, - systemPrompt: aiRequestModel?.payload.systemPrompt ?? '', - userPrompt: aiRequestModel?.payload.userPrompt ?? '', + systemPrompt: aiRequestModel.payload.systemPrompt, + userPrompt: aiRequestModel.payload.userPrompt, configMap: llmso.configMap, ), ), From 3882052cb534a2740e0c3aa19aef5346edb07984 Mon Sep 17 00:00:00 2001 From: Manas Hejmadi Date: Sun, 8 Jun 2025 11:43:08 +0530 Subject: [PATCH 13/36] AIRequests: URL is not modifiable for requests --- .../home_page/editor_pane/url_card.dart | 42 ++++++++++++++----- 1 file changed, 32 insertions(+), 10 deletions(-) diff --git a/lib/screens/home_page/editor_pane/url_card.dart b/lib/screens/home_page/editor_pane/url_card.dart index d64288f5..a60c554e 100644 --- a/lib/screens/home_page/editor_pane/url_card.dart +++ b/lib/screens/home_page/editor_pane/url_card.dart @@ -16,6 +16,8 @@ class EditorPaneRequestURLCard extends ConsumerWidget { ref.watch(selectedIdStateProvider); final apiType = ref .watch(selectedRequestModelProvider.select((value) => value?.apiType)); + final aiHC = ref.watch(selectedRequestModelProvider + .select((v) => v?.aiRequestModel?.hashCode)); return Card( color: kColorTransparent, surfaceTintColor: kColorTransparent, @@ -44,8 +46,10 @@ class EditorPaneRequestURLCard extends ConsumerWidget { APIType.rest => kHSpacer5, _ => kHSpacer8, }, - const Expanded( - child: URLTextField(), + Expanded( + child: URLTextField( + key: aiHC == null ? null : ValueKey(aiHC), + ), ), ], ) @@ -61,8 +65,10 @@ class EditorPaneRequestURLCard extends ConsumerWidget { APIType.rest => kHSpacer20, _ => kHSpacer8, }, - const Expanded( - child: URLTextField(), + Expanded( + child: URLTextField( + key: aiHC == null ? null : ValueKey(aiHC), + ), ), kHSpacer20, const SizedBox( @@ -104,15 +110,31 @@ class URLTextField extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final selectedId = ref.watch(selectedIdStateProvider); + + final reqM = ref.read(collectionStateNotifierProvider)![selectedId]!; + final aiReqM = reqM.aiRequestModel; + final payload = aiReqM?.payload; + return EnvURLField( selectedId: selectedId!, - initialValue: ref - .read(collectionStateNotifierProvider.notifier) - .getRequestModel(selectedId) - ?.httpRequestModel - ?.url, + initialValue: payload?.endpoint ?? + ref + .read(collectionStateNotifierProvider.notifier) + .getRequestModel(selectedId) + ?.httpRequestModel + ?.url, onChanged: (value) { - ref.read(collectionStateNotifierProvider.notifier).update(url: value); + final aim = ref + .read(collectionStateNotifierProvider)![selectedId]! + .aiRequestModel; + if (aim != null) { + aim.payload.endpoint = value; + ref + .read(collectionStateNotifierProvider.notifier) + .update(aiRequestModel: aim.updatePayload(aim.payload)); + } else { + ref.read(collectionStateNotifierProvider.notifier).update(url: value); + } }, onFieldSubmitted: (value) { ref.read(collectionStateNotifierProvider.notifier).sendRequest(); From 8a12ca7c5a71c8ba16bedbddc5d3f707e142985d Mon Sep 17 00:00:00 2001 From: Manas Hejmadi Date: Sun, 8 Jun 2025 12:26:50 +0530 Subject: [PATCH 14/36] initializeAIRequest: ModelController instantiation changed to use .instance --- lib/screens/common_widgets/api_type_dropdown.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/screens/common_widgets/api_type_dropdown.dart b/lib/screens/common_widgets/api_type_dropdown.dart index 22534704..ae9cc726 100644 --- a/lib/screens/common_widgets/api_type_dropdown.dart +++ b/lib/screens/common_widgets/api_type_dropdown.dart @@ -38,7 +38,7 @@ initializeAIRequest(WidgetRef ref) { if (aiRequestModel == null) { // Creating the AIRequest Model initially - final gmC = GeminiModelController(); + final gmC = GeminiModelController.instance; final newAIRM = AIRequestModel( model: defaultLLMSO?.selectedLLM ?? LLMProvider.gemini.getLLMByIdentifier('gemini-2.0-flash'), From b89076985458159a69332e891e8d14f6da543f43 Mon Sep 17 00:00:00 2001 From: Manas Hejmadi Date: Sun, 8 Jun 2025 21:09:47 +0530 Subject: [PATCH 15/36] AI Configuration Widgets simplified & separated --- lib/main.dart | 7 +- .../ai_request/aireq_configs.dart | 80 +++++------------ .../request_pane/ai_request/aireq_prompt.dart | 2 - .../genai/lib/widgets/ai_config_widgets.dart | 90 +++++++++++++++++++ packages/genai/lib/widgets/llm_selector.dart | 35 +++----- 5 files changed, 128 insertions(+), 86 deletions(-) create mode 100644 packages/genai/lib/widgets/ai_config_widgets.dart diff --git a/lib/main.dart b/lib/main.dart index 675b0a54..d3315a14 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -25,10 +25,9 @@ void main() async { settingsModel = settingsModel?.copyWithPath(workspaceFolderPath: null); } - //Load all LLM - LLMManager.fetchAvailableLLMs().then((_) { - LLMManager.loadAvailableLLMs().then((_) {}); - }); + //Load all LLMs + await LLMManager.fetchAvailableLLMs(); + await LLMManager.loadAvailableLLMs(); runApp( ProviderScope( 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 index eb1fb7cb..2fcba89b 100644 --- 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 @@ -4,6 +4,7 @@ import 'package:apidash_design_system/widgets/textfield_outlined.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:genai/genai.dart'; +import 'package:genai/widgets/ai_config_widgets.dart'; class AIRequestConfigSection extends ConsumerStatefulWidget { const AIRequestConfigSection({super.key}); @@ -45,85 +46,46 @@ class _AIRequestConfigSectionState children: [ Text( el.configDescription, - style: TextStyle(color: Colors.white30), ), SizedBox(height: 5), if (el.configType == LLMModelConfigurationType.boolean) ...[ - Switch( - value: el.configValue.value as bool, - onChanged: (x) { - el.configValue.value = x; + BooleanAIConfig( + configuration: el, + onConfigUpdated: (x) { updateRequestModel(el); setState(() {}); }, - ) + ), ] else if (el.configType == LLMModelConfigurationType.numeric) ...[ - ADOutlinedTextField( - initialValue: el.configValue.value.toString(), - onChanged: (x) { - if (x.isEmpty) x = '0'; - if (num.tryParse(x) == null) return; - el.configValue.value = num.parse(x); + WritableAIConfig( + configuration: el, + onConfigUpdated: (x) { updateRequestModel(el); setState(() {}); }, - ) + numeric: true, + ), ] else if (el.configType == LLMModelConfigurationType.text) ...[ - ADOutlinedTextField( - initialValue: el.configValue.value.toString(), - onChanged: (x) { - el.configValue.value = x; + WritableAIConfig( + configuration: el, + onConfigUpdated: (x) { updateRequestModel(el); setState(() {}); }, - ) + ), ] else if (el.configType == LLMModelConfigurationType.slider) ...[ - Row( - children: [ - Expanded( - child: Slider( - min: (el.configValue.value as ( - double, - double, - double - )) - .$1, - value: (el.configValue.value as ( - double, - double, - double - )) - .$2, - max: (el.configValue.value as ( - double, - double, - double - )) - .$3, - onChanged: (x) { - final z = el.configValue.value as ( - double, - double, - double - ); - el.configValue.value = (z.$1, x, z.$3); - updateRequestModel(el); - setState(() {}); - }, - ), - ), - Text((el.configValue.value as (double, double, double)) - .$2 - .toStringAsFixed(2)), - ], - ) + SliderAIConfig( + configuration: el, + onSliderUpdated: (x) { + updateRequestModel(x); + setState(() {}); + }, + ), ], SizedBox(height: 10), - // Divider(color: Colors.white10), - // 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 index cca6c511..ce94f44e 100644 --- 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 @@ -26,7 +26,6 @@ class AIRequestPromptSection extends ConsumerWidget { padding: const EdgeInsets.only(left: 25.0), child: Text( 'System Prompt', - style: TextStyle(color: Colors.white54), ), ), kVSpacer10, @@ -55,7 +54,6 @@ class AIRequestPromptSection extends ConsumerWidget { padding: const EdgeInsets.only(left: 25.0), child: Text( 'User Prompt / Input', - style: TextStyle(color: Colors.white54), ), ), kVSpacer10, diff --git a/packages/genai/lib/widgets/ai_config_widgets.dart b/packages/genai/lib/widgets/ai_config_widgets.dart new file mode 100644 index 00000000..2113b447 --- /dev/null +++ b/packages/genai/lib/widgets/ai_config_widgets.dart @@ -0,0 +1,90 @@ +import 'package:flutter/material.dart'; +import 'package:genai/llm_config.dart'; + +class SliderAIConfig extends StatelessWidget { + final LLMModelConfiguration configuration; + final Function(LLMModelConfiguration) onSliderUpdated; + const SliderAIConfig({ + super.key, + required this.configuration, + required this.onSliderUpdated, + }); + + @override + Widget build(BuildContext context) { + return Row( + children: [ + Expanded( + child: Slider( + min: (configuration.configValue.value as (double, double, double)) + .$1, + value: (configuration.configValue.value as (double, double, double)) + .$2, + max: (configuration.configValue.value as (double, double, double)) + .$3, + onChanged: (x) { + final z = + configuration.configValue.value as (double, double, double); + configuration.configValue.value = (z.$1, x, z.$3); + onSliderUpdated(configuration); + }, + ), + ), + Text( + (configuration.configValue.value as (double, double, double)).$2 + .toStringAsFixed(2), + ), + ], + ); + } +} + +class WritableAIConfig extends StatelessWidget { + final bool numeric; + final LLMModelConfiguration configuration; + final Function(LLMModelConfiguration) onConfigUpdated; + const WritableAIConfig({ + super.key, + this.numeric = false, + required this.configuration, + required this.onConfigUpdated, + }); + + @override + Widget build(BuildContext context) { + return TextFormField( + initialValue: configuration.configValue.value.toString(), + onChanged: (x) { + if (numeric) { + if (x.isEmpty) x = '0'; + if (num.tryParse(x) == null) return; + configuration.configValue.value = num.parse(x); + } else { + configuration.configValue.value = x; + } + onConfigUpdated(configuration); + }, + ); + } +} + +class BooleanAIConfig extends StatelessWidget { + final LLMModelConfiguration configuration; + final Function(LLMModelConfiguration) onConfigUpdated; + const BooleanAIConfig({ + super.key, + required this.configuration, + required this.onConfigUpdated, + }); + + @override + Widget build(BuildContext context) { + return Switch( + value: configuration.configValue.value as bool, + onChanged: (x) { + configuration.configValue.value = x; + onConfigUpdated(configuration); + }, + ); + } +} diff --git a/packages/genai/lib/widgets/llm_selector.dart b/packages/genai/lib/widgets/llm_selector.dart index ddb67c04..f53670d5 100644 --- a/packages/genai/lib/widgets/llm_selector.dart +++ b/packages/genai/lib/widgets/llm_selector.dart @@ -14,29 +14,22 @@ class DefaultLLMSelectorButton extends StatelessWidget { @override Widget build(BuildContext context) { - return Row( - mainAxisSize: MainAxisSize.min, - children: [ - Chip(label: Text(defaultLLM?.selectedLLM.modelName ?? 'none')), - SizedBox(height: 10), - IconButton( - onPressed: () async { - final saveObject = await showDialog( - context: context, - builder: (context) { - return AlertDialog( - scrollable: true, - content: DefaultLLMSelectorDialog(defaultLLM: defaultLLM), - contentPadding: EdgeInsets.all(10), - ); - }, + return ElevatedButton( + onPressed: () async { + final saveObject = await showDialog( + context: context, + builder: (context) { + return AlertDialog( + scrollable: true, + content: DefaultLLMSelectorDialog(defaultLLM: defaultLLM), + contentPadding: EdgeInsets.all(10), ); - if (saveObject == null) return; - onDefaultLLMUpdated(saveObject); }, - icon: Icon(Icons.edit), - ), - ], + ); + if (saveObject == null) return; + onDefaultLLMUpdated(saveObject); + }, + child: Text(defaultLLM?.selectedLLM.modelName ?? 'Select Model'), ); } } From 97d4a7a45b2f1f18d3505341d004b60aaffb1938 Mon Sep 17 00:00:00 2001 From: Manas Hejmadi Date: Sun, 8 Jun 2025 21:53:40 +0530 Subject: [PATCH 16/36] AIRequests: Removed AIResponseModel & sendRequest implemented --- lib/models/request_model.dart | 1 - lib/models/request_model.freezed.dart | 53 +-- lib/models/request_model.g.dart | 5 - lib/providers/collection_providers.dart | 40 +- .../request_pane/request_body.dart | 1 - .../details_card/response_pane.dart | 26 +- lib/widgets/response_body.dart | 38 +- packages/better_networking/lib/consts.dart | 7 + packages/genai/lib/genai.dart | 3 +- .../genai/lib/models/ai_response_model.dart | 62 --- .../lib/models/ai_response_model.freezed.dart | 392 ------------------ .../genai/lib/models/ai_response_model.g.dart | 38 -- 12 files changed, 97 insertions(+), 569 deletions(-) delete mode 100644 packages/genai/lib/models/ai_response_model.dart delete mode 100644 packages/genai/lib/models/ai_response_model.freezed.dart delete mode 100644 packages/genai/lib/models/ai_response_model.g.dart diff --git a/lib/models/request_model.dart b/lib/models/request_model.dart index 952aaa7c..8513e938 100644 --- a/lib/models/request_model.dart +++ b/lib/models/request_model.dart @@ -27,7 +27,6 @@ class RequestModel with _$RequestModel { String? preRequestScript, String? postRequestScript, AIRequestModel? aiRequestModel, - AIResponseModel? aiResponseModel, }) = _RequestModel; factory RequestModel.fromJson(Map json) => diff --git a/lib/models/request_model.freezed.dart b/lib/models/request_model.freezed.dart index 5998065d..d700dba9 100644 --- a/lib/models/request_model.freezed.dart +++ b/lib/models/request_model.freezed.dart @@ -40,7 +40,6 @@ mixin _$RequestModel { String? get preRequestScript => throw _privateConstructorUsedError; String? get postRequestScript => throw _privateConstructorUsedError; AIRequestModel? get aiRequestModel => throw _privateConstructorUsedError; - AIResponseModel? get aiResponseModel => throw _privateConstructorUsedError; /// Serializes this RequestModel to a JSON map. Map toJson() => throw _privateConstructorUsedError; @@ -73,13 +72,11 @@ abstract class $RequestModelCopyWith<$Res> { @JsonKey(includeToJson: false) bool isStreaming, String? preRequestScript, String? postRequestScript, - AIRequestModel? aiRequestModel, - AIResponseModel? aiResponseModel}); + AIRequestModel? aiRequestModel}); $HttpRequestModelCopyWith<$Res>? get httpRequestModel; $HttpResponseModelCopyWith<$Res>? get httpResponseModel; $AIRequestModelCopyWith<$Res>? get aiRequestModel; - $AIResponseModelCopyWith<$Res>? get aiResponseModel; } /// @nodoc @@ -112,7 +109,6 @@ class _$RequestModelCopyWithImpl<$Res, $Val extends RequestModel> Object? preRequestScript = freezed, Object? postRequestScript = freezed, Object? aiRequestModel = freezed, - Object? aiResponseModel = freezed, }) { return _then(_value.copyWith( id: null == id @@ -175,10 +171,6 @@ class _$RequestModelCopyWithImpl<$Res, $Val extends RequestModel> ? _value.aiRequestModel : aiRequestModel // ignore: cast_nullable_to_non_nullable as AIRequestModel?, - aiResponseModel: freezed == aiResponseModel - ? _value.aiResponseModel - : aiResponseModel // ignore: cast_nullable_to_non_nullable - as AIResponseModel?, ) as $Val); } @@ -223,20 +215,6 @@ class _$RequestModelCopyWithImpl<$Res, $Val extends RequestModel> return _then(_value.copyWith(aiRequestModel: value) as $Val); }); } - - /// Create a copy of RequestModel - /// with the given fields replaced by the non-null parameter values. - @override - @pragma('vm:prefer-inline') - $AIResponseModelCopyWith<$Res>? get aiResponseModel { - if (_value.aiResponseModel == null) { - return null; - } - - return $AIResponseModelCopyWith<$Res>(_value.aiResponseModel!, (value) { - return _then(_value.copyWith(aiResponseModel: value) as $Val); - }); - } } /// @nodoc @@ -262,8 +240,7 @@ abstract class _$$RequestModelImplCopyWith<$Res> @JsonKey(includeToJson: false) bool isStreaming, String? preRequestScript, String? postRequestScript, - AIRequestModel? aiRequestModel, - AIResponseModel? aiResponseModel}); + AIRequestModel? aiRequestModel}); @override $HttpRequestModelCopyWith<$Res>? get httpRequestModel; @@ -271,8 +248,6 @@ abstract class _$$RequestModelImplCopyWith<$Res> $HttpResponseModelCopyWith<$Res>? get httpResponseModel; @override $AIRequestModelCopyWith<$Res>? get aiRequestModel; - @override - $AIResponseModelCopyWith<$Res>? get aiResponseModel; } /// @nodoc @@ -303,7 +278,6 @@ class __$$RequestModelImplCopyWithImpl<$Res> Object? preRequestScript = freezed, Object? postRequestScript = freezed, Object? aiRequestModel = freezed, - Object? aiResponseModel = freezed, }) { return _then(_$RequestModelImpl( id: null == id @@ -365,10 +339,6 @@ class __$$RequestModelImplCopyWithImpl<$Res> ? _value.aiRequestModel : aiRequestModel // ignore: cast_nullable_to_non_nullable as AIRequestModel?, - aiResponseModel: freezed == aiResponseModel - ? _value.aiResponseModel - : aiResponseModel // ignore: cast_nullable_to_non_nullable - as AIResponseModel?, )); } } @@ -392,8 +362,7 @@ class _$RequestModelImpl implements _RequestModel { @JsonKey(includeToJson: false) this.isStreaming = false, this.preRequestScript, this.postRequestScript, - this.aiRequestModel, - this.aiResponseModel}); + this.aiRequestModel}); factory _$RequestModelImpl.fromJson(Map json) => _$$RequestModelImplFromJson(json); @@ -435,12 +404,10 @@ class _$RequestModelImpl implements _RequestModel { final String? postRequestScript; @override final AIRequestModel? aiRequestModel; - @override - final AIResponseModel? aiResponseModel; @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, aiRequestModel: $aiRequestModel, aiResponseModel: $aiResponseModel)'; + 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 @@ -473,9 +440,7 @@ class _$RequestModelImpl implements _RequestModel { (identical(other.postRequestScript, postRequestScript) || other.postRequestScript == postRequestScript) && (identical(other.aiRequestModel, aiRequestModel) || - other.aiRequestModel == aiRequestModel) && - (identical(other.aiResponseModel, aiResponseModel) || - other.aiResponseModel == aiResponseModel)); + other.aiRequestModel == aiRequestModel)); } @JsonKey(includeFromJson: false, includeToJson: false) @@ -496,8 +461,7 @@ class _$RequestModelImpl implements _RequestModel { isStreaming, preRequestScript, postRequestScript, - aiRequestModel, - aiResponseModel); + aiRequestModel); /// Create a copy of RequestModel /// with the given fields replaced by the non-null parameter values. @@ -531,8 +495,7 @@ abstract class _RequestModel implements RequestModel { @JsonKey(includeToJson: false) final bool isStreaming, final String? preRequestScript, final String? postRequestScript, - final AIRequestModel? aiRequestModel, - final AIResponseModel? aiResponseModel}) = _$RequestModelImpl; + final AIRequestModel? aiRequestModel}) = _$RequestModelImpl; factory _RequestModel.fromJson(Map json) = _$RequestModelImpl.fromJson; @@ -571,8 +534,6 @@ abstract class _RequestModel implements RequestModel { String? get postRequestScript; @override AIRequestModel? get aiRequestModel; - @override - AIResponseModel? get aiResponseModel; /// 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 d994e7c4..272bfca0 100644 --- a/lib/models/request_model.g.dart +++ b/lib/models/request_model.g.dart @@ -34,10 +34,6 @@ _$RequestModelImpl _$$RequestModelImplFromJson(Map json) => _$RequestModelImpl( ? null : AIRequestModel.fromJson( Map.from(json['aiRequestModel'] as Map)), - aiResponseModel: json['aiResponseModel'] == null - ? null - : AIResponseModel.fromJson( - Map.from(json['aiResponseModel'] as Map)), ); Map _$$RequestModelImplToJson(_$RequestModelImpl instance) => @@ -53,7 +49,6 @@ Map _$$RequestModelImplToJson(_$RequestModelImpl instance) => 'preRequestScript': instance.preRequestScript, 'postRequestScript': instance.postRequestScript, 'aiRequestModel': instance.aiRequestModel?.toJson(), - 'aiResponseModel': instance.aiResponseModel?.toJson(), }; const _$APITypeEnumMap = { diff --git a/lib/providers/collection_providers.dart b/lib/providers/collection_providers.dart index 1c6e7f73..4203f445 100644 --- a/lib/providers/collection_providers.dart +++ b/lib/providers/collection_providers.dart @@ -1,4 +1,6 @@ import 'dart:async'; +import 'dart:convert'; + import 'package:apidash_core/apidash_core.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; @@ -229,7 +231,6 @@ class CollectionStateNotifier String? preRequestScript, String? postRequestScript, AIRequestModel? aiRequestModel, - AIResponseModel? aiResponseModel, }) { final rId = id ?? ref.read(selectedIdStateProvider); if (rId == null) { @@ -265,7 +266,6 @@ class CollectionStateNotifier preRequestScript: preRequestScript ?? currentModel.preRequestScript, postRequestScript: postRequestScript ?? currentModel.postRequestScript, aiRequestModel: aiRequestModel ?? currentModel.aiRequestModel, - aiResponseModel: aiResponseModel ?? currentModel.aiResponseModel, ); var map = {...state!}; @@ -310,10 +310,32 @@ class CollectionStateNotifier } APIType apiType = executionRequestModel.apiType; - HttpRequestModel substitutedHttpRequestModel = - getSubstitutedHttpRequestModel(executionRequestModel.httpRequestModel!); + + late HttpRequestModel substitutedHttpRequestModel; + AIRequestModel? aiRequestModel; bool noSSL = ref.read(settingsProvider).isSSLDisabled; + if (apiType == APIType.ai) { + aiRequestModel = requestModel.aiRequestModel!; + final genAIRequest = aiRequestModel.createRequest(); + substitutedHttpRequestModel = getSubstitutedHttpRequestModel( + HttpRequestModel( + method: HTTPVerb.post, + headers: [ + ...genAIRequest.headers.entries.map( + (x) => NameValueModel.fromJson({x.key: x.value}), + ), + ], + url: genAIRequest.endpoint, + bodyContentType: ContentType.json, + body: jsonEncode(genAIRequest.body), + ), + ); + } else { + substitutedHttpRequestModel = getSubstitutedHttpRequestModel( + executionRequestModel.httpRequestModel!); + } + // Set model to working and streaming state = { ...state!, @@ -339,6 +361,8 @@ class CollectionStateNotifier StreamSubscription? sub; + // bool streaming = true; //DEFAULT to streaming + sub = stream.listen((rec) async { if (rec == null) return; @@ -347,6 +371,14 @@ class CollectionStateNotifier final duration = rec.$3; final errorMessage = rec.$4; + if (isStreamingResponse == false) { + // streaming = false; + if (!completer.isCompleted) { + completer.complete((response, duration, errorMessage)); + } + return; + } + if (isStreamingResponse) { httpResponseModel = httpResponseModel?.copyWith( time: duration, diff --git a/lib/screens/home_page/editor_pane/details_card/request_pane/request_body.dart b/lib/screens/home_page/editor_pane/details_card/request_pane/request_body.dart index 91d80e41..e192f26e 100644 --- a/lib/screens/home_page/editor_pane/details_card/request_pane/request_body.dart +++ b/lib/screens/home_page/editor_pane/details_card/request_pane/request_body.dart @@ -92,7 +92,6 @@ class EditRequestBody extends ConsumerWidget { ), ), ), - APIType.ai => FlutterLogo(), _ => 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..9bb3ab58 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 @@ -52,7 +52,10 @@ class ResponseDetails extends ConsumerWidget { selectedRequestModelProvider.select((value) => value?.responseStatus)); final message = ref .watch(selectedRequestModelProvider.select((value) => value?.message)); - final responseModel = ref.watch(selectedRequestModelProvider + + HttpResponseModel? httpResponseModel; + + httpResponseModel = ref.watch(selectedRequestModelProvider .select((value) => value?.httpResponseModel)); return Column( @@ -60,7 +63,7 @@ class ResponseDetails extends ConsumerWidget { ResponsePaneHeader( responseStatus: responseStatus, message: message, - time: responseModel?.time, + time: httpResponseModel?.time, onClearResponse: () { ref.read(collectionStateNotifierProvider.notifier).clearResponse(); }, @@ -106,15 +109,20 @@ 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, + requestHeaders: requestHeaders as Map? ?? {}, ); } } diff --git a/lib/widgets/response_body.dart b/lib/widgets/response_body.dart index d0af2baa..8b72b44a 100644 --- a/lib/widgets/response_body.dart +++ b/lib/widgets/response_body.dart @@ -1,3 +1,5 @@ +import 'dart:convert'; + import 'package:flutter/material.dart'; import 'package:apidash_core/apidash_core.dart'; import 'package:apidash/models/models.dart'; @@ -16,15 +18,18 @@ class ResponseBody extends StatelessWidget { @override Widget build(BuildContext context) { - final responseModel = selectedRequestModel?.httpResponseModel; - if (responseModel == null) { + HttpResponseModel? httpResponseModel; + httpResponseModel = selectedRequestModel?.httpResponseModel; + + if (httpResponseModel == null) { return const ErrorMessage( message: '$kNullResponseModelError $kUnexpectedRaiseIssue'); } - final isSSE = responseModel.sseOutput?.isNotEmpty ?? false; - var body = responseModel.body; - var formattedBody = responseModel.formattedBody; + final isSSE = httpResponseModel.sseOutput?.isNotEmpty ?? false; + var body = httpResponseModel.body; + var formattedBody = httpResponseModel.formattedBody; + if (body == null) { return const ErrorMessage( message: '$kMsgNullBody $kUnexpectedRaiseIssue'); @@ -37,11 +42,12 @@ class ResponseBody extends StatelessWidget { ); } if (isSSE) { - body = responseModel.sseOutput!.join(); + body = httpResponseModel.sseOutput!.join(); } final mediaType = - responseModel.mediaType ?? MediaType(kTypeText, kSubTypePlain); + httpResponseModel.mediaType ?? MediaType(kTypeText, kSubTypePlain); + // Fix #415: Treat null Content-type as plain text instead of Error message // if (mediaType == null) { // return ErrorMessage( @@ -58,14 +64,28 @@ class ResponseBody extends StatelessWidget { options.remove(ResponseBodyView.code); } + // print('reM -> ${responseModel.sseOutput}'); + + if (httpResponseModel.sseOutput?.isNotEmpty ?? false) { + // final modifiedBody = responseModel.sseOutput!.join('\n\n'); + return ResponseBodySuccess( + key: Key("${selectedRequestModel!.id}-response"), + mediaType: MediaType('text', 'event-stream'), + options: [ResponseBodyView.sse, ResponseBodyView.raw], + bytes: utf8.encode((httpResponseModel.sseOutput!).toString()), + body: jsonEncode(httpResponseModel.sseOutput!), + formattedBody: httpResponseModel.sseOutput!.join('\n'), + ); + } + return ResponseBodySuccess( key: Key("${selectedRequestModel!.id}-response"), mediaType: mediaType, options: options, - bytes: responseModel.bodyBytes!, + bytes: httpResponseModel.bodyBytes!, body: body, formattedBody: formattedBody, - sseOutput: responseModel.sseOutput, + sseOutput: httpResponseModel.sseOutput, highlightLanguage: highlightLanguage, ); } diff --git a/packages/better_networking/lib/consts.dart b/packages/better_networking/lib/consts.dart index 68c2c0a3..3b370596 100644 --- a/packages/better_networking/lib/consts.dart +++ b/packages/better_networking/lib/consts.dart @@ -8,6 +8,13 @@ enum APIType { 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/genai/lib/genai.dart b/packages/genai/lib/genai.dart index b96c6195..2b4be834 100644 --- a/packages/genai/lib/genai.dart +++ b/packages/genai/lib/genai.dart @@ -1,6 +1,5 @@ // Module Exports export 'models/ai_request_model.dart'; -export 'models/ai_response_model.dart'; export 'providers/providers.dart'; export 'generative_ai.dart'; export 'llm_config.dart'; @@ -9,4 +8,4 @@ export 'llm_manager.dart'; export 'llm_model.dart'; export 'llm_provider.dart'; export 'llm_request.dart'; -export 'llm_saveobject.dart'; \ No newline at end of file +export 'llm_saveobject.dart'; diff --git a/packages/genai/lib/models/ai_response_model.dart b/packages/genai/lib/models/ai_response_model.dart deleted file mode 100644 index a1aa75a4..00000000 --- a/packages/genai/lib/models/ai_response_model.dart +++ /dev/null @@ -1,62 +0,0 @@ -import 'dart:convert'; -import 'dart:io'; -import 'dart:typed_data'; -import 'package:better_networking/better_networking.dart'; -import 'package:freezed_annotation/freezed_annotation.dart'; -import 'package:collection/collection.dart' show mergeMaps; -import '../llm_provider.dart'; -part 'ai_response_model.freezed.dart'; -part 'ai_response_model.g.dart'; - -@freezed -class AIResponseModel with _$AIResponseModel { - const AIResponseModel._(); - - @JsonSerializable(explicitToJson: true, anyMap: true, createToJson: true) - const factory AIResponseModel({ - int? statusCode, - Map? headers, - Map? requestHeaders, - String? body, - String? formattedBody, - @JsonKey( - fromJson: LLMProvider.fromJSONNullable, - toJson: LLMProvider.toJSONNullable, - ) - LLMProvider? llmProvider, - @Uint8ListConverter() Uint8List? bodyBytes, - @DurationConverter() Duration? time, - }) = _AIResponseModel; - - factory AIResponseModel.fromJson(Map json) => - _$AIResponseModelFromJson(json); - - AIResponseModel fromResponse({ - required Response response, - required LLMProvider provider, - Duration? time, - }) { - final responseHeaders = mergeMaps({ - HttpHeaders.contentLengthHeader: response.contentLength.toString(), - }, response.headers); - MediaType? mediaType = getMediaTypeFromHeaders(responseHeaders); - final body = (mediaType?.subtype == kSubTypeJson) - ? utf8.decode(response.bodyBytes) - : response.body; - return AIResponseModel( - statusCode: response.statusCode, - headers: responseHeaders, - requestHeaders: response.request?.headers, - body: body, - formattedBody: response.statusCode == 200 - ? provider.modelController.outputFormatter(jsonDecode(body)) - : formatBody(body, mediaType), - bodyBytes: response.bodyBytes, - time: time, - llmProvider: provider, - ); - } - - String? get contentType => headers?.getValueContentType(); - MediaType? get mediaType => getMediaTypeFromHeaders(headers); -} diff --git a/packages/genai/lib/models/ai_response_model.freezed.dart b/packages/genai/lib/models/ai_response_model.freezed.dart deleted file mode 100644 index c7322bc9..00000000 --- a/packages/genai/lib/models/ai_response_model.freezed.dart +++ /dev/null @@ -1,392 +0,0 @@ -// 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_response_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', -); - -AIResponseModel _$AIResponseModelFromJson(Map json) { - return _AIResponseModel.fromJson(json); -} - -/// @nodoc -mixin _$AIResponseModel { - int? get statusCode => throw _privateConstructorUsedError; - Map? get headers => throw _privateConstructorUsedError; - Map? get requestHeaders => throw _privateConstructorUsedError; - String? get body => throw _privateConstructorUsedError; - String? get formattedBody => throw _privateConstructorUsedError; - @JsonKey( - fromJson: LLMProvider.fromJSONNullable, - toJson: LLMProvider.toJSONNullable, - ) - LLMProvider? get llmProvider => throw _privateConstructorUsedError; - @Uint8ListConverter() - Uint8List? get bodyBytes => throw _privateConstructorUsedError; - @DurationConverter() - Duration? get time => throw _privateConstructorUsedError; - - /// Serializes this AIResponseModel to a JSON map. - Map toJson() => throw _privateConstructorUsedError; - - /// Create a copy of AIResponseModel - /// with the given fields replaced by the non-null parameter values. - @JsonKey(includeFromJson: false, includeToJson: false) - $AIResponseModelCopyWith get copyWith => - throw _privateConstructorUsedError; -} - -/// @nodoc -abstract class $AIResponseModelCopyWith<$Res> { - factory $AIResponseModelCopyWith( - AIResponseModel value, - $Res Function(AIResponseModel) then, - ) = _$AIResponseModelCopyWithImpl<$Res, AIResponseModel>; - @useResult - $Res call({ - int? statusCode, - Map? headers, - Map? requestHeaders, - String? body, - String? formattedBody, - @JsonKey( - fromJson: LLMProvider.fromJSONNullable, - toJson: LLMProvider.toJSONNullable, - ) - LLMProvider? llmProvider, - @Uint8ListConverter() Uint8List? bodyBytes, - @DurationConverter() Duration? time, - }); -} - -/// @nodoc -class _$AIResponseModelCopyWithImpl<$Res, $Val extends AIResponseModel> - implements $AIResponseModelCopyWith<$Res> { - _$AIResponseModelCopyWithImpl(this._value, this._then); - - // ignore: unused_field - final $Val _value; - // ignore: unused_field - final $Res Function($Val) _then; - - /// Create a copy of AIResponseModel - /// with the given fields replaced by the non-null parameter values. - @pragma('vm:prefer-inline') - @override - $Res call({ - Object? statusCode = freezed, - Object? headers = freezed, - Object? requestHeaders = freezed, - Object? body = freezed, - Object? formattedBody = freezed, - Object? llmProvider = freezed, - Object? bodyBytes = freezed, - Object? time = freezed, - }) { - return _then( - _value.copyWith( - statusCode: freezed == statusCode - ? _value.statusCode - : statusCode // ignore: cast_nullable_to_non_nullable - as int?, - headers: freezed == headers - ? _value.headers - : headers // ignore: cast_nullable_to_non_nullable - as Map?, - requestHeaders: freezed == requestHeaders - ? _value.requestHeaders - : requestHeaders // ignore: cast_nullable_to_non_nullable - as Map?, - body: freezed == body - ? _value.body - : body // ignore: cast_nullable_to_non_nullable - as String?, - formattedBody: freezed == formattedBody - ? _value.formattedBody - : formattedBody // ignore: cast_nullable_to_non_nullable - as String?, - llmProvider: freezed == llmProvider - ? _value.llmProvider - : llmProvider // ignore: cast_nullable_to_non_nullable - as LLMProvider?, - bodyBytes: freezed == bodyBytes - ? _value.bodyBytes - : bodyBytes // ignore: cast_nullable_to_non_nullable - as Uint8List?, - time: freezed == time - ? _value.time - : time // ignore: cast_nullable_to_non_nullable - as Duration?, - ) - as $Val, - ); - } -} - -/// @nodoc -abstract class _$$AIResponseModelImplCopyWith<$Res> - implements $AIResponseModelCopyWith<$Res> { - factory _$$AIResponseModelImplCopyWith( - _$AIResponseModelImpl value, - $Res Function(_$AIResponseModelImpl) then, - ) = __$$AIResponseModelImplCopyWithImpl<$Res>; - @override - @useResult - $Res call({ - int? statusCode, - Map? headers, - Map? requestHeaders, - String? body, - String? formattedBody, - @JsonKey( - fromJson: LLMProvider.fromJSONNullable, - toJson: LLMProvider.toJSONNullable, - ) - LLMProvider? llmProvider, - @Uint8ListConverter() Uint8List? bodyBytes, - @DurationConverter() Duration? time, - }); -} - -/// @nodoc -class __$$AIResponseModelImplCopyWithImpl<$Res> - extends _$AIResponseModelCopyWithImpl<$Res, _$AIResponseModelImpl> - implements _$$AIResponseModelImplCopyWith<$Res> { - __$$AIResponseModelImplCopyWithImpl( - _$AIResponseModelImpl _value, - $Res Function(_$AIResponseModelImpl) _then, - ) : super(_value, _then); - - /// Create a copy of AIResponseModel - /// with the given fields replaced by the non-null parameter values. - @pragma('vm:prefer-inline') - @override - $Res call({ - Object? statusCode = freezed, - Object? headers = freezed, - Object? requestHeaders = freezed, - Object? body = freezed, - Object? formattedBody = freezed, - Object? llmProvider = freezed, - Object? bodyBytes = freezed, - Object? time = freezed, - }) { - return _then( - _$AIResponseModelImpl( - statusCode: freezed == statusCode - ? _value.statusCode - : statusCode // ignore: cast_nullable_to_non_nullable - as int?, - headers: freezed == headers - ? _value._headers - : headers // ignore: cast_nullable_to_non_nullable - as Map?, - requestHeaders: freezed == requestHeaders - ? _value._requestHeaders - : requestHeaders // ignore: cast_nullable_to_non_nullable - as Map?, - body: freezed == body - ? _value.body - : body // ignore: cast_nullable_to_non_nullable - as String?, - formattedBody: freezed == formattedBody - ? _value.formattedBody - : formattedBody // ignore: cast_nullable_to_non_nullable - as String?, - llmProvider: freezed == llmProvider - ? _value.llmProvider - : llmProvider // ignore: cast_nullable_to_non_nullable - as LLMProvider?, - bodyBytes: freezed == bodyBytes - ? _value.bodyBytes - : bodyBytes // ignore: cast_nullable_to_non_nullable - as Uint8List?, - time: freezed == time - ? _value.time - : time // ignore: cast_nullable_to_non_nullable - as Duration?, - ), - ); - } -} - -/// @nodoc - -@JsonSerializable(explicitToJson: true, anyMap: true, createToJson: true) -class _$AIResponseModelImpl extends _AIResponseModel { - const _$AIResponseModelImpl({ - this.statusCode, - final Map? headers, - final Map? requestHeaders, - this.body, - this.formattedBody, - @JsonKey( - fromJson: LLMProvider.fromJSONNullable, - toJson: LLMProvider.toJSONNullable, - ) - this.llmProvider, - @Uint8ListConverter() this.bodyBytes, - @DurationConverter() this.time, - }) : _headers = headers, - _requestHeaders = requestHeaders, - super._(); - - factory _$AIResponseModelImpl.fromJson(Map json) => - _$$AIResponseModelImplFromJson(json); - - @override - final int? statusCode; - final Map? _headers; - @override - Map? get headers { - final value = _headers; - if (value == null) return null; - if (_headers is EqualUnmodifiableMapView) return _headers; - // ignore: implicit_dynamic_type - return EqualUnmodifiableMapView(value); - } - - final Map? _requestHeaders; - @override - Map? get requestHeaders { - final value = _requestHeaders; - if (value == null) return null; - if (_requestHeaders is EqualUnmodifiableMapView) return _requestHeaders; - // ignore: implicit_dynamic_type - return EqualUnmodifiableMapView(value); - } - - @override - final String? body; - @override - final String? formattedBody; - @override - @JsonKey( - fromJson: LLMProvider.fromJSONNullable, - toJson: LLMProvider.toJSONNullable, - ) - final LLMProvider? llmProvider; - @override - @Uint8ListConverter() - final Uint8List? bodyBytes; - @override - @DurationConverter() - final Duration? time; - - @override - String toString() { - return 'AIResponseModel(statusCode: $statusCode, headers: $headers, requestHeaders: $requestHeaders, body: $body, formattedBody: $formattedBody, llmProvider: $llmProvider, bodyBytes: $bodyBytes, time: $time)'; - } - - @override - bool operator ==(Object other) { - return identical(this, other) || - (other.runtimeType == runtimeType && - other is _$AIResponseModelImpl && - (identical(other.statusCode, statusCode) || - other.statusCode == statusCode) && - const DeepCollectionEquality().equals(other._headers, _headers) && - const DeepCollectionEquality().equals( - other._requestHeaders, - _requestHeaders, - ) && - (identical(other.body, body) || other.body == body) && - (identical(other.formattedBody, formattedBody) || - other.formattedBody == formattedBody) && - (identical(other.llmProvider, llmProvider) || - other.llmProvider == llmProvider) && - const DeepCollectionEquality().equals(other.bodyBytes, bodyBytes) && - (identical(other.time, time) || other.time == time)); - } - - @JsonKey(includeFromJson: false, includeToJson: false) - @override - int get hashCode => Object.hash( - runtimeType, - statusCode, - const DeepCollectionEquality().hash(_headers), - const DeepCollectionEquality().hash(_requestHeaders), - body, - formattedBody, - llmProvider, - const DeepCollectionEquality().hash(bodyBytes), - time, - ); - - /// Create a copy of AIResponseModel - /// with the given fields replaced by the non-null parameter values. - @JsonKey(includeFromJson: false, includeToJson: false) - @override - @pragma('vm:prefer-inline') - _$$AIResponseModelImplCopyWith<_$AIResponseModelImpl> get copyWith => - __$$AIResponseModelImplCopyWithImpl<_$AIResponseModelImpl>( - this, - _$identity, - ); - - @override - Map toJson() { - return _$$AIResponseModelImplToJson(this); - } -} - -abstract class _AIResponseModel extends AIResponseModel { - const factory _AIResponseModel({ - final int? statusCode, - final Map? headers, - final Map? requestHeaders, - final String? body, - final String? formattedBody, - @JsonKey( - fromJson: LLMProvider.fromJSONNullable, - toJson: LLMProvider.toJSONNullable, - ) - final LLMProvider? llmProvider, - @Uint8ListConverter() final Uint8List? bodyBytes, - @DurationConverter() final Duration? time, - }) = _$AIResponseModelImpl; - const _AIResponseModel._() : super._(); - - factory _AIResponseModel.fromJson(Map json) = - _$AIResponseModelImpl.fromJson; - - @override - int? get statusCode; - @override - Map? get headers; - @override - Map? get requestHeaders; - @override - String? get body; - @override - String? get formattedBody; - @override - @JsonKey( - fromJson: LLMProvider.fromJSONNullable, - toJson: LLMProvider.toJSONNullable, - ) - LLMProvider? get llmProvider; - @override - @Uint8ListConverter() - Uint8List? get bodyBytes; - @override - @DurationConverter() - Duration? get time; - - /// Create a copy of AIResponseModel - /// with the given fields replaced by the non-null parameter values. - @override - @JsonKey(includeFromJson: false, includeToJson: false) - _$$AIResponseModelImplCopyWith<_$AIResponseModelImpl> get copyWith => - throw _privateConstructorUsedError; -} diff --git a/packages/genai/lib/models/ai_response_model.g.dart b/packages/genai/lib/models/ai_response_model.g.dart deleted file mode 100644 index f1029f70..00000000 --- a/packages/genai/lib/models/ai_response_model.g.dart +++ /dev/null @@ -1,38 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'ai_response_model.dart'; - -// ************************************************************************** -// JsonSerializableGenerator -// ************************************************************************** - -_$AIResponseModelImpl _$$AIResponseModelImplFromJson(Map json) => - _$AIResponseModelImpl( - statusCode: (json['statusCode'] as num?)?.toInt(), - headers: (json['headers'] as Map?)?.map( - (k, e) => MapEntry(k as String, e as String), - ), - requestHeaders: (json['requestHeaders'] as Map?)?.map( - (k, e) => MapEntry(k as String, e as String), - ), - body: json['body'] as String?, - formattedBody: json['formattedBody'] as String?, - llmProvider: LLMProvider.fromJSONNullable(json['llmProvider'] as Map?), - bodyBytes: const Uint8ListConverter().fromJson( - json['bodyBytes'] as List?, - ), - time: const DurationConverter().fromJson((json['time'] as num?)?.toInt()), - ); - -Map _$$AIResponseModelImplToJson( - _$AIResponseModelImpl instance, -) => { - 'statusCode': instance.statusCode, - 'headers': instance.headers, - 'requestHeaders': instance.requestHeaders, - 'body': instance.body, - 'formattedBody': instance.formattedBody, - 'llmProvider': LLMProvider.toJSONNullable(instance.llmProvider), - 'bodyBytes': const Uint8ListConverter().toJson(instance.bodyBytes), - 'time': const DurationConverter().toJson(instance.time), -}; From 452020f7208d4f2dcc46c1f022ed3f029548b226 Mon Sep 17 00:00:00 2001 From: Manas Hejmadi Date: Sun, 8 Jun 2025 22:34:26 +0530 Subject: [PATCH 17/36] AI Request History & Duplication Feature --- lib/models/history_request_model.dart | 4 +- lib/models/history_request_model.freezed.dart | 79 ++++++-- lib/models/history_request_model.g.dart | 13 +- lib/providers/collection_providers.dart | 8 +- lib/screens/common_widgets/code_pane.dart | 7 + .../history_widgets/ai_history_page.dart | 178 ++++++++++++++++++ .../history_widgets/his_request_pane.dart | 57 +++++- .../history_widgets/his_response_pane.dart | 8 +- lib/utils/history_utils.dart | 1 + .../genai/lib/widgets/ai_config_widgets.dart | 9 + .../history_widgets/his_url_card_test.dart | 4 +- 11 files changed, 331 insertions(+), 37 deletions(-) create mode 100644 lib/screens/history/history_widgets/ai_history_page.dart diff --git a/lib/models/history_request_model.dart b/lib/models/history_request_model.dart index f46382a7..a2b3d84e 100644 --- a/lib/models/history_request_model.dart +++ b/lib/models/history_request_model.dart @@ -1,4 +1,5 @@ import 'package:apidash_core/apidash_core.dart'; +import 'package:genai/genai.dart'; import 'models.dart'; part 'history_request_model.freezed.dart'; @@ -14,7 +15,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/providers/collection_providers.dart b/lib/providers/collection_providers.dart index 4203f445..127fc9a9 100644 --- a/lib/providers/collection_providers.dart +++ b/lib/providers/collection_providers.dart @@ -187,10 +187,15 @@ class CollectionStateNotifier var itemIds = ref.read(requestSequenceProvider); var currentModel = historyRequestModel; + + final aT = currentModel.aiRequestModel != null ? APIType.ai : APIType.rest; + final newModel = RequestModel( + apiType: aT, id: newId, name: "${currentModel.metaData.name} (history)", - httpRequestModel: currentModel.httpRequestModel, + aiRequestModel: currentModel.aiRequestModel, + httpRequestModel: currentModel.httpRequestModel ?? HttpRequestModel(), responseStatus: currentModel.metaData.responseStatus, message: kResponseCodeReasons[currentModel.metaData.responseStatus], httpResponseModel: currentModel.httpResponseModel, @@ -461,6 +466,7 @@ class CollectionStateNotifier timeStamp: DateTime.now(), ), httpRequestModel: substitutedHttpRequestModel, + aiRequestModel: aiRequestModel, httpResponseModel: httpResponseModel!, preRequestScript: requestModel.preRequestScript, postRequestScript: requestModel.postRequestScript, diff --git a/lib/screens/common_widgets/code_pane.dart b/lib/screens/common_widgets/code_pane.dart index 654b9440..c4031fae 100644 --- a/lib/screens/common_widgets/code_pane.dart +++ b/lib/screens/common_widgets/code_pane.dart @@ -28,6 +28,13 @@ class CodePane extends ConsumerWidget { final selectedRequestModel = isHistoryRequest ? getRequestModelFromHistoryModel(selectedHistoryRequestModel!) : ref.watch(selectedRequestModelProvider); + + 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/history/history_widgets/ai_history_page.dart b/lib/screens/history/history_widgets/ai_history_page.dart new file mode 100644 index 00000000..72bb98bd --- /dev/null +++ b/lib/screens/history/history_widgets/ai_history_page.dart @@ -0,0 +1,178 @@ +import 'package:apidash/providers/collection_providers.dart'; +import 'package:apidash/providers/history_providers.dart'; +import 'package:apidash/widgets/editor.dart'; +import 'package:apidash_design_system/tokens/measurements.dart'; +import 'package:apidash_design_system/widgets/textfield_outlined.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:genai/llm_config.dart'; +import 'package:genai/widgets/ai_config_widgets.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!; + final payload = aiReqM.payload; + final systemPrompt = payload.systemPrompt; + final userPrompt = payload.userPrompt; + + 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: 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: 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!; + + final payload = aiReqM.payload; + + final cred = payload.credential; + + return Container( + padding: EdgeInsets.symmetric(vertical: 20), + child: Column( + children: [ + Expanded( + child: Container( + padding: EdgeInsets.symmetric(horizontal: 20), + child: TextFieldEditor( + key: Key( + "${selectedHistoryModel.historyId}-aireq-authvalue-body"), + fieldKey: + "${selectedHistoryModel.historyId}-aireq-authvalue-body", + initialValue: cred, + 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!; + + final payload = aiReqM.payload; + + return SingleChildScrollView( + padding: EdgeInsets.symmetric(vertical: 20), + child: Column( + key: ValueKey(selectedHistoryModel.historyId), + children: [ + ...payload.configMap.values.map( + (el) => ListTile( + title: Text(el.configName), + subtitle: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + el.configDescription, + ), + SizedBox(height: 5), + if (el.configType == LLMModelConfigurationType.boolean) ...[ + BooleanAIConfig( + readonly: true, + configuration: el, + onConfigUpdated: (x) {}, + ), + ] else if (el.configType == + LLMModelConfigurationType.numeric) ...[ + WritableAIConfig( + configuration: el, + onConfigUpdated: (x) {}, + readonly: true, + numeric: true, + ), + ] else if (el.configType == + LLMModelConfigurationType.text) ...[ + WritableAIConfig( + configuration: el, + onConfigUpdated: (x) {}, + readonly: true, + ), + ] else if (el.configType == + LLMModelConfigurationType.slider) ...[ + SliderAIConfig( + configuration: el, + onSliderUpdated: (x) {}, + readonly: true, + ), + ], + 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 73f61927..f6093326 100644 --- a/lib/screens/history/history_widgets/his_request_pane.dart +++ b/lib/screens/history/history_widgets/his_request_pane.dart @@ -1,3 +1,4 @@ +import 'package:apidash/screens/history/history_widgets/ai_history_page.dart'; import 'package:apidash_core/apidash_core.dart'; import 'package:apidash_design_system/apidash_design_system.dart'; import 'package:flutter/material.dart'; @@ -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,7 +144,27 @@ class HistoryRequestPane extends ConsumerWidget { const HistoryScriptsTab(), ], ), - APIType.ai => FlutterLogo(), + 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/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/packages/genai/lib/widgets/ai_config_widgets.dart b/packages/genai/lib/widgets/ai_config_widgets.dart index 2113b447..957c5c1f 100644 --- a/packages/genai/lib/widgets/ai_config_widgets.dart +++ b/packages/genai/lib/widgets/ai_config_widgets.dart @@ -4,10 +4,12 @@ import 'package:genai/llm_config.dart'; class SliderAIConfig extends StatelessWidget { final LLMModelConfiguration configuration; final Function(LLMModelConfiguration) onSliderUpdated; + final bool readonly; const SliderAIConfig({ super.key, required this.configuration, required this.onSliderUpdated, + this.readonly = false, }); @override @@ -23,6 +25,7 @@ class SliderAIConfig extends StatelessWidget { max: (configuration.configValue.value as (double, double, double)) .$3, onChanged: (x) { + if (readonly) return; final z = configuration.configValue.value as (double, double, double); configuration.configValue.value = (z.$1, x, z.$3); @@ -43,11 +46,13 @@ class WritableAIConfig extends StatelessWidget { final bool numeric; final LLMModelConfiguration configuration; final Function(LLMModelConfiguration) onConfigUpdated; + final bool readonly; const WritableAIConfig({ super.key, this.numeric = false, required this.configuration, required this.onConfigUpdated, + this.readonly = false, }); @override @@ -55,6 +60,7 @@ class WritableAIConfig extends StatelessWidget { return TextFormField( initialValue: configuration.configValue.value.toString(), onChanged: (x) { + if (readonly) return; if (numeric) { if (x.isEmpty) x = '0'; if (num.tryParse(x) == null) return; @@ -71,10 +77,12 @@ class WritableAIConfig extends StatelessWidget { class BooleanAIConfig extends StatelessWidget { final LLMModelConfiguration configuration; final Function(LLMModelConfiguration) onConfigUpdated; + final bool readonly; const BooleanAIConfig({ super.key, required this.configuration, required this.onConfigUpdated, + this.readonly = false, }); @override @@ -82,6 +90,7 @@ class BooleanAIConfig extends StatelessWidget { return Switch( value: configuration.configValue.value as bool, onChanged: (x) { + if (readonly) return; configuration.configValue.value = x; onConfigUpdated(configuration); }, 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); }); }); } From 2c756df2c47d63d2365f2e59431a41457d8b379e Mon Sep 17 00:00:00 2001 From: Manas Hejmadi Date: Tue, 10 Jun 2025 01:00:26 +0530 Subject: [PATCH 18/36] Implemented AnswerTab for AI Output --- lib/consts.dart | 1 + lib/providers/collection_providers.dart | 3 +- .../ai_request/request_pane_ai.dart | 2 +- lib/widgets/response_body.dart | 8 ++-- lib/widgets/response_body_success.dart | 43 ++++++++++++++++--- 5 files changed, 47 insertions(+), 10 deletions(-) diff --git a/lib/consts.dart b/lib/consts.dart index 66972b14..681d7698 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); diff --git a/lib/providers/collection_providers.dart b/lib/providers/collection_providers.dart index 127fc9a9..a139844f 100644 --- a/lib/providers/collection_providers.dart +++ b/lib/providers/collection_providers.dart @@ -141,6 +141,7 @@ class CollectionStateNotifier final rId = id ?? ref.read(selectedIdStateProvider); if (rId == null || state?[rId] == null) return; var currentModel = state![rId]!; + final newModel = currentModel.copyWith( responseStatus: null, message: null, @@ -328,7 +329,7 @@ class CollectionStateNotifier method: HTTPVerb.post, headers: [ ...genAIRequest.headers.entries.map( - (x) => NameValueModel.fromJson({x.key: x.value}), + (x) => NameValueModel(name: x.key, value: x.value), ), ], url: genAIRequest.endpoint, 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 index 5d219717..31a490b8 100644 --- 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 @@ -18,7 +18,7 @@ class EditAIRequestPane extends ConsumerWidget { return RequestPane( selectedId: selectedId, - codePaneVisible: false, + codePaneVisible: codePaneVisible, tabIndex: tabIndex, onPressedCodeButton: () { ref.read(codePaneVisibleStateProvider.notifier).state = diff --git a/lib/widgets/response_body.dart b/lib/widgets/response_body.dart index 8b72b44a..c682b756 100644 --- a/lib/widgets/response_body.dart +++ b/lib/widgets/response_body.dart @@ -18,8 +18,8 @@ class ResponseBody extends StatelessWidget { @override Widget build(BuildContext context) { - HttpResponseModel? httpResponseModel; - httpResponseModel = selectedRequestModel?.httpResponseModel; + HttpResponseModel? httpResponseModel = + selectedRequestModel?.httpResponseModel; if (httpResponseModel == null) { return const ErrorMessage( @@ -55,7 +55,9 @@ class ResponseBody extends StatelessWidget { // '$kMsgUnknowContentType - ${responseModel.contentType}. $kUnexpectedRaiseIssue'); // } - var responseBodyView = getResponseBodyViewOptions(mediaType); + var responseBodyView = selectedRequestModel?.apiType == APIType.ai + ? ([ResponseBodyView.answer, ResponseBodyView.raw], 'text') + : getResponseBodyViewOptions(mediaType); var options = responseBodyView.$1; var highlightLanguage = responseBodyView.$2; diff --git a/lib/widgets/response_body_success.dart b/lib/widgets/response_body_success.dart index 9b22a4b9..b4c75dfa 100644 --- a/lib/widgets/response_body_success.dart +++ b/lib/widgets/response_body_success.dart @@ -46,6 +46,8 @@ class _ResponseBodySuccessState extends State { borderRadius: kBorderRadius8, ); + final isAIRequest = widget.options.contains(ResponseBodyView.answer); + return LayoutBuilder( builder: (BuildContext context, BoxConstraints constraints) { var showLabel = showButtonLabelsInBodySuccess( @@ -87,20 +89,33 @@ class _ResponseBodySuccessState extends State { ), const Spacer(), ((widget.options == kPreviewRawBodyViewOptions) || - kCodeRawBodyViewOptions.contains(currentSeg)) + kCodeRawBodyViewOptions.contains(currentSeg) || + isAIRequest) ? CopyButton( - toCopy: widget.formattedBody ?? widget.body, + toCopy: (currentSeg == ResponseBodyView.answer) + ? widget.formattedBody! + : isAIRequest + ? formatBody(widget.body, widget.mediaType)! + : (widget.formattedBody ?? widget.body), showLabel: showLabel, ) : const SizedBox(), kIsMobile ? ShareButton( - toShare: widget.formattedBody ?? widget.body, + toShare: (currentSeg == ResponseBodyView.answer) + ? widget.formattedBody! + : isAIRequest + ? formatBody(widget.body, widget.mediaType)! + : (widget.formattedBody ?? widget.body), showLabel: showLabel, ) : SaveInDownloadsButton( - content: widget.bytes, - mimeType: widget.mediaType.mimeType, + content: (currentSeg == ResponseBodyView.answer) + ? utf8.encode(widget.formattedBody!) + : widget.bytes, + mimeType: (currentSeg == ResponseBodyView.answer) + ? 'text/plain' + : widget.mediaType.mimeType, showLabel: showLabel, ), ], @@ -135,6 +150,24 @@ class _ResponseBodySuccessState extends State { ), ), ResponseBodyView.raw => Expanded( + child: Container( + width: double.maxFinite, + padding: kP8, + decoration: textContainerdecoration, + child: SingleChildScrollView( + child: SelectableText( + widget.options.contains(ResponseBodyView.answer) + ? formatBody( + widget.body, + MediaType(kTypeApplication, kSubTypeJson), + )! + : (widget.formattedBody ?? widget.body), + style: kCodeStyle, + ), + ), + ), + ), + ResponseBodyView.answer => Expanded( child: Container( width: double.maxFinite, padding: kP8, From 49a93cacde21786355689bac4ad2a0ec3af61fe6 Mon Sep 17 00:00:00 2001 From: Manas Hejmadi Date: Tue, 10 Jun 2025 01:24:18 +0530 Subject: [PATCH 19/36] AIRequests: Duplication Bug Fixed --- lib/providers/collection_providers.dart | 2 ++ lib/screens/history/history_widgets/his_request_pane.dart | 1 - 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/providers/collection_providers.dart b/lib/providers/collection_providers.dart index a139844f..38b49c7a 100644 --- a/lib/providers/collection_providers.dart +++ b/lib/providers/collection_providers.dart @@ -168,6 +168,8 @@ class CollectionStateNotifier requestTabIndex: 0, responseStatus: null, message: null, + httpRequestModel: currentModel.httpRequestModel?.copyWith(), + aiRequestModel: currentModel.aiRequestModel?.copyWith(), httpResponseModel: null, isWorking: false, sendingTime: null, diff --git a/lib/screens/history/history_widgets/his_request_pane.dart b/lib/screens/history/history_widgets/his_request_pane.dart index f6093326..859a2378 100644 --- a/lib/screens/history/history_widgets/his_request_pane.dart +++ b/lib/screens/history/history_widgets/his_request_pane.dart @@ -242,7 +242,6 @@ class HisRequestBody extends ConsumerWidget { readOnly: true, ), ), - APIType.ai => FlutterLogo(), _ => kSizedBoxEmpty, }; } From ab2a9d5ca7e35054ff745f691e3388a592cbaa10 Mon Sep 17 00:00:00 2001 From: Manas Hejmadi Date: Sun, 15 Jun 2025 20:07:03 +0530 Subject: [PATCH 20/36] initializeAIRequest moved out of api_type_dropdown & cleaned --- lib/providers/collection_providers.dart | 14 ++++++++ .../common_widgets/api_type_dropdown.dart | 33 ------------------- 2 files changed, 14 insertions(+), 33 deletions(-) diff --git a/lib/providers/collection_providers.dart b/lib/providers/collection_providers.dart index 38b49c7a..c489c1b3 100644 --- a/lib/providers/collection_providers.dart +++ b/lib/providers/collection_providers.dart @@ -245,7 +245,21 @@ class CollectionStateNotifier debugPrint("Unable to update as Request Id is null"); return; } + var currentModel = state![rId]!; + + if (apiType == APIType.ai) { + //Adding default AI Request Modoel + AIRequestModel? aiRM = currentModel.aiRequestModel; + LLMSaveObject? defaultLLMSO = ref + .watch(settingsProvider.notifier) + .settingsModel + ?.defaultLLMSaveObject; //Settings Default + if (aiRM == null) { + aiRequestModel = AIRequestModel.fromDefaultSaveObject(defaultLLMSO); + } + } + var currentHttpRequestModel = currentModel.httpRequestModel; final newModel = currentModel.copyWith( apiType: apiType ?? currentModel.apiType, diff --git a/lib/screens/common_widgets/api_type_dropdown.dart b/lib/screens/common_widgets/api_type_dropdown.dart index ae9cc726..7085049e 100644 --- a/lib/screens/common_widgets/api_type_dropdown.dart +++ b/lib/screens/common_widgets/api_type_dropdown.dart @@ -16,9 +16,6 @@ class APITypeDropdown extends ConsumerWidget { return APITypePopupMenu( apiType: apiType, onChanged: (type) { - if (type == APIType.ai) { - initializeAIRequest(ref); - } ref .read(collectionStateNotifierProvider.notifier) .update(apiType: type); @@ -26,33 +23,3 @@ class APITypeDropdown extends ConsumerWidget { ); } } - -initializeAIRequest(WidgetRef ref) { - final selectedId = ref.watch(selectedIdStateProvider); - final req = ref.watch(collectionStateNotifierProvider)![selectedId]!; - AIRequestModel? aiRequestModel = req.aiRequestModel; - LLMSaveObject? defaultLLMSO = ref - .watch(settingsProvider.notifier) - .settingsModel - ?.defaultLLMSaveObject; //Settings Default - - if (aiRequestModel == null) { - // Creating the AIRequest Model initially - final gmC = GeminiModelController.instance; - final newAIRM = AIRequestModel( - model: defaultLLMSO?.selectedLLM ?? - LLMProvider.gemini.getLLMByIdentifier('gemini-2.0-flash'), - provider: defaultLLMSO?.provider ?? LLMProvider.gemini, - payload: LLMInputPayload( - endpoint: defaultLLMSO?.endpoint ?? gmC.inputPayload.endpoint, - credential: defaultLLMSO?.credential ?? '', - systemPrompt: '', - userPrompt: '', - configMap: defaultLLMSO?.configMap ?? gmC.inputPayload.configMap, - ), - ); - ref.read(collectionStateNotifierProvider.notifier).update( - aiRequestModel: newAIRM, - ); - } -} From 4209569aa5a156ef2d6f792a0688ac5a75645de8 Mon Sep 17 00:00:00 2001 From: Manas Hejmadi Date: Sun, 15 Jun 2025 23:44:59 +0530 Subject: [PATCH 21/36] Improved ShallowCopy of AIRequestModel and substructures --- lib/providers/collection_providers.dart | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/providers/collection_providers.dart b/lib/providers/collection_providers.dart index c489c1b3..37eb9234 100644 --- a/lib/providers/collection_providers.dart +++ b/lib/providers/collection_providers.dart @@ -168,8 +168,8 @@ class CollectionStateNotifier requestTabIndex: 0, responseStatus: null, message: null, - httpRequestModel: currentModel.httpRequestModel?.copyWith(), - aiRequestModel: currentModel.aiRequestModel?.copyWith(), + httpRequestModel: currentModel.httpRequestModel, + aiRequestModel: currentModel.aiRequestModel?.clone(), httpResponseModel: null, isWorking: false, sendingTime: null, @@ -197,7 +197,7 @@ class CollectionStateNotifier apiType: aT, id: newId, name: "${currentModel.metaData.name} (history)", - aiRequestModel: currentModel.aiRequestModel, + aiRequestModel: currentModel.aiRequestModel?.clone(), httpRequestModel: currentModel.httpRequestModel ?? HttpRequestModel(), responseStatus: currentModel.metaData.responseStatus, message: kResponseCodeReasons[currentModel.metaData.responseStatus], From d0dc563ae723444937d5601de08f5ca3d6e25a43 Mon Sep 17 00:00:00 2001 From: Manas Hejmadi Date: Mon, 16 Jun 2025 03:11:15 +0530 Subject: [PATCH 22/36] LLMSelector: Implemented ability to add and remove LLMs dynamically --- lib/main.dart | 2 +- .../ai_request/widgets/llm_selector.dart | 336 ++++++++++++++++++ .../home_page/editor_pane/url_card.dart | 2 +- lib/screens/settings_page.dart | 2 +- 4 files changed, 339 insertions(+), 3 deletions(-) create mode 100644 lib/screens/home_page/editor_pane/details_card/request_pane/ai_request/widgets/llm_selector.dart diff --git a/lib/main.dart b/lib/main.dart index d3315a14..a13230fc 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -26,7 +26,7 @@ void main() async { } //Load all LLMs - await LLMManager.fetchAvailableLLMs(); + // await LLMManager.fetchAvailableLLMs(); await LLMManager.loadAvailableLLMs(); runApp( diff --git a/lib/screens/home_page/editor_pane/details_card/request_pane/ai_request/widgets/llm_selector.dart b/lib/screens/home_page/editor_pane/details_card/request_pane/ai_request/widgets/llm_selector.dart new file mode 100644 index 00000000..386b331a --- /dev/null +++ b/lib/screens/home_page/editor_pane/details_card/request_pane/ai_request/widgets/llm_selector.dart @@ -0,0 +1,336 @@ +import 'package:apidash_design_system/apidash_design_system.dart'; +import 'package:genai/genai.dart'; +import 'package:flutter/material.dart'; + +class DefaultLLMSelectorButton extends StatelessWidget { + final LLMSaveObject? defaultLLM; + final Function(LLMSaveObject) onDefaultLLMUpdated; + const DefaultLLMSelectorButton({ + super.key, + this.defaultLLM, + required this.onDefaultLLMUpdated, + }); + + @override + Widget build(BuildContext context) { + return ElevatedButton( + onPressed: () async { + final saveObject = await showDialog( + context: context, + builder: (context) { + return AlertDialog( + scrollable: true, + content: DefaultLLMSelectorDialog(defaultLLM: defaultLLM), + contentPadding: EdgeInsets.all(10), + ); + }, + ); + if (saveObject == null) return; + onDefaultLLMUpdated(saveObject); + }, + child: Text(defaultLLM?.selectedLLM.modelName ?? 'Select Model'), + ); + } +} + +class DefaultLLMSelectorDialog extends StatefulWidget { + final LLMSaveObject? defaultLLM; + const DefaultLLMSelectorDialog({super.key, this.defaultLLM}); + + @override + State createState() => + _DefaultLLMSelectorDialogState(); +} + +class _DefaultLLMSelectorDialogState extends State { + late LLMProvider selectedLLMProvider; + late LLMSaveObject llmSaveObject; + bool initialized = false; + + initialize() async { + final iP = LLMProvider.gemini.modelController.inputPayload; + llmSaveObject = widget.defaultLLM ?? + LLMSaveObject( + endpoint: iP.endpoint, + credential: '', + configMap: iP.configMap, + selectedLLM: + LLMProvider.gemini.getLLMByIdentifier('gemini-2.0-flash'), + provider: LLMProvider.ollama, + ); + selectedLLMProvider = llmSaveObject.provider; + initialized = true; + setState(() {}); + } + + @override + void initState() { + super.initState(); + initialize(); + } + + @override + Widget build(BuildContext context) { + if (!initialized) return SizedBox(); + return Container( + padding: EdgeInsets.all(20), + width: MediaQuery.of(context).size.width * 0.8, + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Flexible( + flex: 1, + child: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ElevatedButton( + onPressed: () async { + await LLMManager.fetchAvailableLLMs(); + setState(() {}); + }, + child: Text('Fetch Models'), + ), + SizedBox(height: 20), + ...LLMProvider.values.where(((e) => e.models.isNotEmpty)).map( + (x) => ListTile( + title: Text(x.displayName), + trailing: llmSaveObject.provider != x + ? null + : CircleAvatar( + radius: 5, + backgroundColor: Colors.green, + ), + onTap: () { + selectedLLMProvider = x; + final models = x.models; + final mC = x.modelController; + final p = mC.inputPayload; + llmSaveObject = LLMSaveObject( + endpoint: p.endpoint, + credential: '', + configMap: p.configMap, + selectedLLM: models.first, + provider: x, + ); + setState(() {}); + }, + ), + ), + ], + ), + ), + ), + SizedBox(width: 40), + Flexible( + flex: 3, + child: Container( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.max, + children: [ + Text( + selectedLLMProvider.displayName, + style: TextStyle(fontSize: 28), + ), + SizedBox(height: 20), + if (selectedLLMProvider != LLMProvider.ollama) ...[ + Text('API Key / Credential'), + kVSpacer8, + BoundedTextField( + onChanged: (x) { + llmSaveObject.credential = x; + setState(() {}); + }, + value: llmSaveObject.credential, + ), + kVSpacer10, + ], + Text('Endpoint'), + kVSpacer8, + BoundedTextField( + key: ValueKey(llmSaveObject.provider), + onChanged: (x) { + llmSaveObject.endpoint = x; + setState(() {}); + }, + value: llmSaveObject.endpoint, + ), + kVSpacer20, + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text('Models'), + IconButton(onPressed: addNewModel, icon: Icon(Icons.add)) + ], + ), + kVSpacer8, + Container( + height: 300, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(10), + color: const Color.fromARGB(27, 0, 0, 0), + ), + child: SingleChildScrollView( + child: Column( + children: [ + ...selectedLLMProvider.models.map( + (x) => ListTile( + title: Text(x.modelName), + subtitle: Text(x.identifier), + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + if (llmSaveObject.selectedLLM.identifier == + x.identifier) + CircleAvatar( + radius: 5, + backgroundColor: Colors.green, + ), + IconButton( + onPressed: () => removeModel(x), + icon: Icon( + Icons.delete, + size: 20, + )) + ], + ), + onTap: () { + llmSaveObject.selectedLLM = x; + setState(() {}); + }, + ), + ), + ], + ), + ), + ), + kVSpacer10, + Align( + alignment: Alignment.centerRight, + child: ElevatedButton( + onPressed: () { + llmSaveObject.provider = selectedLLMProvider; + Navigator.of(context).pop(llmSaveObject); + }, + child: Text('Save Changes'), + ), + ), + ], + ), + ), + ), + ], + ), + ); + } + + removeModel(LLMModel model) async { + await LLMManager.removeLLM( + selectedLLMProvider.name, model.identifier, model.modelName); + setState(() {}); + } + + addNewModel() async { + TextEditingController iC = TextEditingController(); + TextEditingController nC = TextEditingController(); + final z = await showDialog( + context: context, + builder: (context) { + return AlertDialog( + title: Text('Add Custom Model'), + content: Container( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + ADOutlinedTextField( + controller: iC, + hintText: 'Model ID', + ), + kVSpacer10, + ADOutlinedTextField( + controller: nC, + hintText: 'Model Display Name', + ), + kVSpacer10, + Container( + width: double.infinity, + child: ElevatedButton( + onPressed: () { + Navigator.of(context).pop([ + iC.value.text, + nC.value.text, + ]); + }, + child: Text('Add Model'), + ), + ) + ], + ), + ), + ); + }); + if (z == null) return; + await LLMManager.addLLM(selectedLLMProvider.name, z[0], z[1]); + setState(() {}); + } +} + +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/screens/home_page/editor_pane/url_card.dart b/lib/screens/home_page/editor_pane/url_card.dart index a60c554e..6875cd0d 100644 --- a/lib/screens/home_page/editor_pane/url_card.dart +++ b/lib/screens/home_page/editor_pane/url_card.dart @@ -1,3 +1,4 @@ +import 'package:apidash/screens/home_page/editor_pane/details_card/request_pane/ai_request/widgets/llm_selector.dart'; import 'package:apidash_core/apidash_core.dart'; import 'package:apidash_design_system/apidash_design_system.dart'; import 'package:flutter/material.dart'; @@ -5,7 +6,6 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:apidash/providers/providers.dart'; import 'package:apidash/widgets/widgets.dart'; import 'package:genai/genai.dart'; -import 'package:genai/widgets/llm_selector.dart'; import '../../common_widgets/common_widgets.dart'; class EditorPaneRequestURLCard extends ConsumerWidget { diff --git a/lib/screens/settings_page.dart b/lib/screens/settings_page.dart index 86d3a68c..91641db6 100644 --- a/lib/screens/settings_page.dart +++ b/lib/screens/settings_page.dart @@ -1,8 +1,8 @@ +import 'package:apidash/screens/home_page/editor_pane/details_card/request_pane/ai_request/widgets/llm_selector.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:apidash_design_system/apidash_design_system.dart'; -import 'package:genai/widgets/llm_selector.dart'; import '../providers/providers.dart'; import '../services/services.dart'; import '../utils/utils.dart'; From f60836e4b27b224f7e7396f06d3c8b7c24d4ff9a Mon Sep 17 00:00:00 2001 From: Manas Hejmadi Date: Fri, 25 Jul 2025 19:03:23 +0530 Subject: [PATCH 23/36] Added SSE(Streaming) to Native AI Requests --- lib/widgets/response_body_success.dart | 4 +- lib/widgets/sse_display.dart | 135 +++++---- .../genai/lib/models/ai_request_model.dart | 2 +- packages/genai/lib/widgets/llm_selector.dart | 261 ------------------ 4 files changed, 86 insertions(+), 316 deletions(-) delete mode 100644 packages/genai/lib/widgets/llm_selector.dart diff --git a/lib/widgets/response_body_success.dart b/lib/widgets/response_body_success.dart index b4c75dfa..6836abac 100644 --- a/lib/widgets/response_body_success.dart +++ b/lib/widgets/response_body_success.dart @@ -1,3 +1,5 @@ +import 'dart:convert'; + import 'package:apidash_core/apidash_core.dart'; import 'package:apidash_design_system/apidash_design_system.dart'; import 'package:flutter/foundation.dart'; @@ -186,7 +188,7 @@ class _ResponseBodySuccessState extends State { padding: kP8, decoration: textContainerdecoration, child: SSEDisplay( - sseOutput: widget.sseOutput, + sseOutput: widget.sseOutput ?? [], ), ), ), diff --git a/lib/widgets/sse_display.dart b/lib/widgets/sse_display.dart index efa65f43..b1655a70 100644 --- a/lib/widgets/sse_display.dart +++ b/lib/widgets/sse_display.dart @@ -1,20 +1,31 @@ import 'dart:convert'; +import 'package:apidash/providers/collection_providers.dart'; import 'package:apidash_design_system/apidash_design_system.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; -class SSEDisplay extends StatelessWidget { - final List? sseOutput; +class SSEDisplay extends ConsumerStatefulWidget { + final List sseOutput; const SSEDisplay({ super.key, - this.sseOutput, + required this.sseOutput, }); + @override + ConsumerState createState() => _SSEDisplayState(); +} + +class _SSEDisplayState extends ConsumerState { @override Widget build(BuildContext context) { + final requestModel = ref.read(selectedRequestModelProvider); + final aiRequestModel = requestModel?.aiRequestModel; + final isAIOutput = (aiRequestModel != null); + final theme = Theme.of(context); final fontSizeMedium = theme.textTheme.bodyMedium?.fontSize; final isDark = theme.brightness == Brightness.dark; - if (sseOutput == null || sseOutput!.isEmpty) { + if (widget.sseOutput.isEmpty) { return Text( 'No content', style: kCodeStyle.copyWith( @@ -24,59 +35,77 @@ class SSEDisplay extends StatelessWidget { ); } - return ListView( - padding: kP1, - children: sseOutput!.reversed.where((e) => e != '').map((chunk) { - Map? parsedJson; - try { - parsedJson = jsonDecode(chunk); - } catch (_) {} + if (isAIOutput) { + String out = ""; + for (String x in widget.sseOutput) { + x = x.substring(6); + out += aiRequestModel.model.provider.modelController + .streamOutputFormatter(jsonDecode(x)) ?? + ""; + } + return SingleChildScrollView( + child: Text(out), + ); + } - return Card( - color: theme.colorScheme.surfaceContainerLowest, - shape: RoundedRectangleBorder( - borderRadius: kBorderRadius6, - ), - child: Padding( - padding: kP8, - child: parsedJson != null - ? Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: parsedJson.entries.map((entry) { - return Padding( - padding: const EdgeInsets.symmetric(vertical: 2.0), - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - '${entry.key}: ', - style: kCodeStyle.copyWith( - fontSize: fontSizeMedium, - color: isDark ? kColorGQL.toDark : kColorGQL, - fontWeight: FontWeight.bold, + return SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: (widget.sseOutput) + .reversed + .where((e) => e != '') + .map((chunk) { + Map? parsedJson; + try { + parsedJson = jsonDecode(chunk); + } catch (_) {} + + return Card( + color: theme.colorScheme.surfaceContainerLowest, + shape: RoundedRectangleBorder( + borderRadius: kBorderRadius6, + ), + child: Padding( + padding: kP8, + child: parsedJson != null + ? Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: parsedJson.entries.map((entry) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 2.0), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + '${entry.key}: ', + style: kCodeStyle.copyWith( + fontSize: fontSizeMedium, + color: isDark ? kColorGQL.toDark : kColorGQL, + fontWeight: FontWeight.bold, + ), ), - ), - const SizedBox(width: 4), - Expanded( - child: Text( - entry.value.toString(), - style: kCodeStyle, + const SizedBox(width: 4), + Expanded( + child: Text( + entry.value.toString(), + style: kCodeStyle, + ), ), - ), - ], - ), - ); - }).toList(), - ) - : Text( - chunk.toString().trim(), - style: kCodeStyle.copyWith( - fontSize: fontSizeMedium, + ], + ), + ); + }).toList(), + ) + : Text( + chunk.toString().trim(), + style: kCodeStyle.copyWith( + fontSize: fontSizeMedium, + ), ), - ), - ), - ); - }).toList(), + ), + ); + }).toList(), + ), ); } } diff --git a/packages/genai/lib/models/ai_request_model.dart b/packages/genai/lib/models/ai_request_model.dart index f093dfc5..11c79d3d 100644 --- a/packages/genai/lib/models/ai_request_model.dart +++ b/packages/genai/lib/models/ai_request_model.dart @@ -32,7 +32,7 @@ class AIRequestModel with _$AIRequestModel { LLMRequestDetails createRequest() { final controller = model.provider.modelController; - return controller.createRequest(model, payload); + return controller.createRequest(model, payload, stream: true); } factory AIRequestModel.fromDefaultSaveObject(LLMSaveObject? defaultLLMSO) { diff --git a/packages/genai/lib/widgets/llm_selector.dart b/packages/genai/lib/widgets/llm_selector.dart deleted file mode 100644 index f53670d5..00000000 --- a/packages/genai/lib/widgets/llm_selector.dart +++ /dev/null @@ -1,261 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:genai/llm_provider.dart'; -import 'package:genai/llm_saveobject.dart'; -import 'package:genai/providers/ollama.dart'; - -class DefaultLLMSelectorButton extends StatelessWidget { - final LLMSaveObject? defaultLLM; - final Function(LLMSaveObject) onDefaultLLMUpdated; - const DefaultLLMSelectorButton({ - super.key, - this.defaultLLM, - required this.onDefaultLLMUpdated, - }); - - @override - Widget build(BuildContext context) { - return ElevatedButton( - onPressed: () async { - final saveObject = await showDialog( - context: context, - builder: (context) { - return AlertDialog( - scrollable: true, - content: DefaultLLMSelectorDialog(defaultLLM: defaultLLM), - contentPadding: EdgeInsets.all(10), - ); - }, - ); - if (saveObject == null) return; - onDefaultLLMUpdated(saveObject); - }, - child: Text(defaultLLM?.selectedLLM.modelName ?? 'Select Model'), - ); - } -} - -class DefaultLLMSelectorDialog extends StatefulWidget { - final LLMSaveObject? defaultLLM; - - const DefaultLLMSelectorDialog({super.key, this.defaultLLM}); - - @override - State createState() => - _DefaultLLMSelectorDialogState(); -} - -class _DefaultLLMSelectorDialogState extends State { - late LLMProvider selectedLLMProvider; - late LLMSaveObject llmSaveObject; - - @override - void initState() { - super.initState(); - - final oC = OllamaModelController().inputPayload; - - llmSaveObject = - widget.defaultLLM ?? - LLMSaveObject( - endpoint: oC.endpoint, - credential: '', - configMap: oC.configMap, - selectedLLM: LLMProvider.gemini.getLLMByIdentifier( - 'gemini-2.0-flash', - ), - provider: LLMProvider.ollama, - ); - - selectedLLMProvider = llmSaveObject.provider; - } - - @override - Widget build(BuildContext context) { - return Container( - padding: const EdgeInsets.all(20), - width: MediaQuery.of(context).size.width * 0.8, - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // Left panel - Provider List - Container( - width: 300, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Text('Providers'), - const SizedBox(height: 10), - ...LLMProvider.values.map( - (provider) => ListTile( - title: Text(provider.displayName), - trailing: llmSaveObject.provider == provider - ? const CircleAvatar( - radius: 5, - backgroundColor: Colors.green, - ) - : null, - onTap: () { - final input = provider.modelController.inputPayload; - setState(() { - selectedLLMProvider = provider; - llmSaveObject = LLMSaveObject( - endpoint: input.endpoint, - credential: '', - configMap: input.configMap, - selectedLLM: provider.models.first, - provider: provider, - ); - }); - }, - ), - ), - ], - ), - ), - - const SizedBox(width: 40), - - // Right panel - Configuration and Save - Expanded( - flex: 3, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - selectedLLMProvider.displayName, - style: const TextStyle(fontSize: 28), - ), - const SizedBox(height: 20), - - if (selectedLLMProvider != LLMProvider.ollama) ...[ - const Text('API Key / Credential'), - const SizedBox(height: 10), - BoundedTextField( - onChanged: (x) { - llmSaveObject.credential = x; - }, - value: llmSaveObject.credential, - ), - const SizedBox(height: 10), - ], - - const Text('Endpoint'), - const SizedBox(height: 10), - BoundedTextField( - key: ValueKey(llmSaveObject.provider), - onChanged: (x) => llmSaveObject.endpoint = x, - value: llmSaveObject.endpoint, - ), - - const SizedBox(height: 20), - const Text('Models'), - const SizedBox(height: 8), - - Container( - height: 300, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(10), - color: const Color.fromARGB(27, 0, 0, 0), - ), - child: SingleChildScrollView( - child: Column( - children: selectedLLMProvider.models - .map( - (model) => ListTile( - title: Text(model.modelName), - subtitle: Text(model.identifier), - trailing: llmSaveObject.selectedLLM == model - ? const CircleAvatar( - radius: 5, - backgroundColor: Colors.green, - ) - : null, - onTap: () { - setState(() { - llmSaveObject.selectedLLM = model; - }); - }, - ), - ) - .toList(), - ), - ), - ), - const SizedBox(height: 10), - Align( - alignment: Alignment.centerRight, - child: ElevatedButton( - onPressed: () { - llmSaveObject.provider = selectedLLMProvider; - Navigator.of(context).pop(llmSaveObject); - }, - child: const Text('Save Changes'), - ), - ), - ], - ), - ), - ], - ), - ); - } -} - -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: BorderRadius.circular(8), - ), - 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, - ), - ), - ); - } -} From db599b3d7491bec3a849209fdbe1eb0ed7df35ee Mon Sep 17 00:00:00 2001 From: Manas Hejmadi Date: Sat, 26 Jul 2025 03:37:17 +0530 Subject: [PATCH 24/36] Streaming Option added to AI Request Configurations --- lib/providers/collection_providers.dart | 7 ++++++- packages/genai/lib/llm_config.dart | 10 +++++++++- packages/genai/lib/models/ai_request_model.dart | 4 ++-- packages/genai/lib/providers/gemini.dart | 3 +++ 4 files changed, 20 insertions(+), 4 deletions(-) diff --git a/lib/providers/collection_providers.dart b/lib/providers/collection_providers.dart index 37eb9234..d74dca43 100644 --- a/lib/providers/collection_providers.dart +++ b/lib/providers/collection_providers.dart @@ -339,7 +339,12 @@ class CollectionStateNotifier if (apiType == APIType.ai) { aiRequestModel = requestModel.aiRequestModel!; - final genAIRequest = aiRequestModel.createRequest(); + + final streamingMode = aiRequestModel.payload + .configMap[LLMConfigName.stream.name]?.configValue.value ?? + false; + + final genAIRequest = aiRequestModel.createRequest(stream: streamingMode); substitutedHttpRequestModel = getSubstitutedHttpRequestModel( HttpRequestModel( method: HTTPVerb.post, diff --git a/packages/genai/lib/llm_config.dart b/packages/genai/lib/llm_config.dart index 2944bd0e..19aec010 100644 --- a/packages/genai/lib/llm_config.dart +++ b/packages/genai/lib/llm_config.dart @@ -164,7 +164,7 @@ class LLMConfigTextValue extends LLMModelConfigValue { } } -enum LLMConfigName { temperature, top_p, max_tokens, endpoint } +enum LLMConfigName { temperature, top_p, max_tokens, endpoint, stream } Map defaultLLMConfigurations = { LLMConfigName.temperature: LLMModelConfiguration( @@ -188,4 +188,12 @@ Map defaultLLMConfigurations = { configType: LLMModelConfigurationType.numeric, configValue: LLMConfigNumericValue(value: -1), ), + LLMConfigName.stream: LLMModelConfiguration( + configId: 'stream', + configName: 'Enable Streaming Mode', + configDescription: + 'The LLM output will be sent in a stream instead of all at once', + configType: LLMModelConfigurationType.boolean, + configValue: LLMConfigBooleanValue(value: false), + ), }; diff --git a/packages/genai/lib/models/ai_request_model.dart b/packages/genai/lib/models/ai_request_model.dart index 11c79d3d..724a567c 100644 --- a/packages/genai/lib/models/ai_request_model.dart +++ b/packages/genai/lib/models/ai_request_model.dart @@ -30,9 +30,9 @@ class AIRequestModel with _$AIRequestModel { return AIRequestModel(payload: p, model: model, provider: provider); } - LLMRequestDetails createRequest() { + LLMRequestDetails createRequest({bool stream = false}) { final controller = model.provider.modelController; - return controller.createRequest(model, payload, stream: true); + return controller.createRequest(model, payload, stream: stream); } factory AIRequestModel.fromDefaultSaveObject(LLMSaveObject? defaultLLMSO) { diff --git a/packages/genai/lib/providers/gemini.dart b/packages/genai/lib/providers/gemini.dart index a4508965..65e2843d 100644 --- a/packages/genai/lib/providers/gemini.dart +++ b/packages/genai/lib/providers/gemini.dart @@ -12,9 +12,12 @@ class GeminiModelController extends ModelController { systemPrompt: '', userPrompt: '', configMap: { + //TODO: CHANGES TO THESE DO NOT APPLY TO OLDER REQUESTS!!!!!! LLMConfigName.temperature.name: defaultLLMConfigurations[LLMConfigName.temperature]!, LLMConfigName.top_p.name: defaultLLMConfigurations[LLMConfigName.top_p]!, + LLMConfigName.stream.name: + defaultLLMConfigurations[LLMConfigName.stream]!, }, ).clone(); From 8bc2cbeb7bbf02b3b36ce1bb05c8459d3610f627 Mon Sep 17 00:00:00 2001 From: Manas Hejmadi Date: Mon, 28 Jul 2025 17:35:09 +0530 Subject: [PATCH 25/36] StreamingDone Error Fixed & added Streaming Config to all --- lib/widgets/sse_display.dart | 1 + packages/genai/lib/providers/anthropic.dart | 2 ++ packages/genai/lib/providers/azureopenai.dart | 2 ++ packages/genai/lib/providers/ollama.dart | 2 ++ packages/genai/lib/providers/openai.dart | 2 ++ 5 files changed, 9 insertions(+) diff --git a/lib/widgets/sse_display.dart b/lib/widgets/sse_display.dart index b1655a70..a95eea6e 100644 --- a/lib/widgets/sse_display.dart +++ b/lib/widgets/sse_display.dart @@ -39,6 +39,7 @@ class _SSEDisplayState extends ConsumerState { String out = ""; for (String x in widget.sseOutput) { x = x.substring(6); + if (x.contains('[DONE]')) continue; out += aiRequestModel.model.provider.modelController .streamOutputFormatter(jsonDecode(x)) ?? ""; diff --git a/packages/genai/lib/providers/anthropic.dart b/packages/genai/lib/providers/anthropic.dart index 8fe0ff86..c91fc4cc 100644 --- a/packages/genai/lib/providers/anthropic.dart +++ b/packages/genai/lib/providers/anthropic.dart @@ -15,6 +15,8 @@ class AnthropicModelController extends ModelController { LLMConfigName.temperature.name: defaultLLMConfigurations[LLMConfigName.temperature]!, LLMConfigName.top_p.name: defaultLLMConfigurations[LLMConfigName.top_p]!, + LLMConfigName.stream.name: + defaultLLMConfigurations[LLMConfigName.stream]!, }, ).clone(); diff --git a/packages/genai/lib/providers/azureopenai.dart b/packages/genai/lib/providers/azureopenai.dart index 3c38ac0e..ff2656a4 100644 --- a/packages/genai/lib/providers/azureopenai.dart +++ b/packages/genai/lib/providers/azureopenai.dart @@ -15,6 +15,8 @@ class AzureOpenAIModelController extends ModelController { LLMConfigName.temperature.name: defaultLLMConfigurations[LLMConfigName.temperature]!, LLMConfigName.top_p.name: defaultLLMConfigurations[LLMConfigName.top_p]!, + LLMConfigName.stream.name: + defaultLLMConfigurations[LLMConfigName.stream]!, }, ).clone(); diff --git a/packages/genai/lib/providers/ollama.dart b/packages/genai/lib/providers/ollama.dart index 06750472..44201acd 100644 --- a/packages/genai/lib/providers/ollama.dart +++ b/packages/genai/lib/providers/ollama.dart @@ -16,6 +16,8 @@ class OllamaModelController extends ModelController { LLMConfigName.temperature.name: defaultLLMConfigurations[LLMConfigName.temperature]!, LLMConfigName.top_p.name: defaultLLMConfigurations[LLMConfigName.top_p]!, + LLMConfigName.stream.name: + defaultLLMConfigurations[LLMConfigName.stream]!, }, ).clone(); diff --git a/packages/genai/lib/providers/openai.dart b/packages/genai/lib/providers/openai.dart index 2c7a2e3b..8ad3765e 100644 --- a/packages/genai/lib/providers/openai.dart +++ b/packages/genai/lib/providers/openai.dart @@ -16,6 +16,8 @@ class OpenAIModelController extends ModelController { LLMConfigName.temperature.name: defaultLLMConfigurations[LLMConfigName.temperature]!, LLMConfigName.top_p.name: defaultLLMConfigurations[LLMConfigName.top_p]!, + LLMConfigName.stream.name: + defaultLLMConfigurations[LLMConfigName.stream]!, }, ).clone(); From e06bddca54d1e74ffe0b8785ffa1453a5ec0cdb1 Mon Sep 17 00:00:00 2001 From: Manas Hejmadi Date: Sat, 9 Aug 2025 15:53:22 +0530 Subject: [PATCH 26/36] SSEDislay: SelectedModel provided via parameter & StreamingFixes --- lib/providers/collection_providers.dart | 20 +++- lib/widgets/response_body.dart | 6 +- lib/widgets/response_body_success.dart | 26 +++-- lib/widgets/sse_display.dart | 148 +++++++++++++----------- 4 files changed, 116 insertions(+), 84 deletions(-) diff --git a/lib/providers/collection_providers.dart b/lib/providers/collection_providers.dart index d74dca43..6d427ac3 100644 --- a/lib/providers/collection_providers.dart +++ b/lib/providers/collection_providers.dart @@ -388,7 +388,7 @@ class CollectionStateNotifier StreamSubscription? sub; - // bool streaming = true; //DEFAULT to streaming + bool streaming = true; //DEFAULT to streaming sub = stream.listen((rec) async { if (rec == null) return; @@ -399,7 +399,7 @@ class CollectionStateNotifier final errorMessage = rec.$4; if (isStreamingResponse == false) { - // streaming = false; + streaming = false; if (!completer.isCompleted) { completer.complete((response, duration, errorMessage)); } @@ -467,6 +467,22 @@ class CollectionStateNotifier isStreamingResponse: isStreamingResponse, ); + if (!streaming) { + //AI-FORMATTING for Non Streaming Varaint + if (apiType == APIType.ai) { + final mT = httpResponseModel?.mediaType; + final body = (mT?.subtype == kSubTypeJson) + ? utf8.decode(response.bodyBytes) + : response.body; + + final fb = response.statusCode == 200 + ? aiRequestModel?.model.provider.modelController + .outputFormatter(jsonDecode(body)) + : formatBody(body, mT); + httpResponseModel = httpResponseModel?.copyWith(formattedBody: fb); + } + } + newRequestModel = newRequestModel.copyWith( responseStatus: statusCode, message: kResponseCodeReasons[statusCode], diff --git a/lib/widgets/response_body.dart b/lib/widgets/response_body.dart index c682b756..8d7e12e4 100644 --- a/lib/widgets/response_body.dart +++ b/lib/widgets/response_body.dart @@ -66,10 +66,7 @@ class ResponseBody extends StatelessWidget { options.remove(ResponseBodyView.code); } - // print('reM -> ${responseModel.sseOutput}'); - if (httpResponseModel.sseOutput?.isNotEmpty ?? false) { - // final modifiedBody = responseModel.sseOutput!.join('\n\n'); return ResponseBodySuccess( key: Key("${selectedRequestModel!.id}-response"), mediaType: MediaType('text', 'event-stream'), @@ -77,6 +74,7 @@ class ResponseBody extends StatelessWidget { bytes: utf8.encode((httpResponseModel.sseOutput!).toString()), body: jsonEncode(httpResponseModel.sseOutput!), formattedBody: httpResponseModel.sseOutput!.join('\n'), + selectedModel: selectedRequestModel?.aiRequestModel?.model, ); } @@ -87,8 +85,8 @@ class ResponseBody extends StatelessWidget { bytes: httpResponseModel.bodyBytes!, body: body, formattedBody: formattedBody, - sseOutput: httpResponseModel.sseOutput, highlightLanguage: highlightLanguage, + selectedModel: selectedRequestModel?.aiRequestModel?.model, ); } } diff --git a/lib/widgets/response_body_success.dart b/lib/widgets/response_body_success.dart index 6836abac..7690344b 100644 --- a/lib/widgets/response_body_success.dart +++ b/lib/widgets/response_body_success.dart @@ -7,25 +7,28 @@ import 'package:flutter/material.dart'; import 'package:apidash/utils/utils.dart'; import 'package:apidash/widgets/widgets.dart'; import 'package:apidash/consts.dart'; +import 'package:genai/genai.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.sseOutput, + this.highlightLanguage, + this.selectedModel, + }); final MediaType mediaType; final List options; final String body; final Uint8List bytes; final String? formattedBody; - final List? sseOutput; final String? highlightLanguage; + final LLMModel? selectedModel; //ONLY FOR AI-REQUESTS @override State createState() => _ResponseBodySuccessState(); @@ -188,7 +191,8 @@ class _ResponseBodySuccessState extends State { padding: kP8, decoration: textContainerdecoration, child: SSEDisplay( - sseOutput: widget.sseOutput ?? [], + sseOutput: widget.formattedBody?.split('\n') ?? [], + selectedLLModel: widget.selectedModel, ), ), ), diff --git a/lib/widgets/sse_display.dart b/lib/widgets/sse_display.dart index a95eea6e..1d787525 100644 --- a/lib/widgets/sse_display.dart +++ b/lib/widgets/sse_display.dart @@ -1,27 +1,29 @@ import 'dart:convert'; +import 'package:apidash/models/request_model.dart'; import 'package:apidash/providers/collection_providers.dart'; +import 'package:apidash/providers/history_providers.dart'; import 'package:apidash_design_system/apidash_design_system.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:genai/genai.dart'; +import 'package:genai/models/ai_request_model.dart'; -class SSEDisplay extends ConsumerStatefulWidget { +class SSEDisplay extends StatefulWidget { + final LLMModel? selectedLLModel; final List sseOutput; const SSEDisplay({ super.key, required this.sseOutput, + this.selectedLLModel, }); @override - ConsumerState createState() => _SSEDisplayState(); + State createState() => _SSEDisplayState(); } -class _SSEDisplayState extends ConsumerState { +class _SSEDisplayState extends State { @override Widget build(BuildContext context) { - final requestModel = ref.read(selectedRequestModelProvider); - final aiRequestModel = requestModel?.aiRequestModel; - final isAIOutput = (aiRequestModel != null); - final theme = Theme.of(context); final fontSizeMedium = theme.textTheme.bodyMedium?.fontSize; final isDark = theme.brightness == Brightness.dark; @@ -35,78 +37,90 @@ class _SSEDisplayState extends ConsumerState { ); } - if (isAIOutput) { + if (widget.selectedLLModel != null) { + // For RAW Text output (only AI Requests) String out = ""; + final mc = widget.selectedLLModel!.provider.modelController; for (String x in widget.sseOutput) { - x = x.substring(6); - if (x.contains('[DONE]')) continue; - out += aiRequestModel.model.provider.modelController - .streamOutputFormatter(jsonDecode(x)) ?? - ""; + 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 = mc.streamOutputFormatter(dec!); + out += z ?? ''; + } catch (e) { + print("Error in JSONDEC $e"); + } } return SingleChildScrollView( child: Text(out), ); } - return SingleChildScrollView( - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: (widget.sseOutput) - .reversed - .where((e) => e != '') - .map((chunk) { - Map? parsedJson; - try { - parsedJson = jsonDecode(chunk); - } catch (_) {} + return ListView( + padding: kP1, + children: widget.sseOutput.reversed + .where((e) => e.trim() != '') + .map((chunk) { + Map? parsedJson; + try { + parsedJson = jsonDecode(chunk); + } catch (_) {} - return Card( - color: theme.colorScheme.surfaceContainerLowest, - shape: RoundedRectangleBorder( - borderRadius: kBorderRadius6, - ), - child: Padding( - padding: kP8, - child: parsedJson != null - ? Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: parsedJson.entries.map((entry) { - return Padding( - padding: const EdgeInsets.symmetric(vertical: 2.0), - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - '${entry.key}: ', - style: kCodeStyle.copyWith( - fontSize: fontSizeMedium, - color: isDark ? kColorGQL.toDark : kColorGQL, - fontWeight: FontWeight.bold, - ), + return Card( + color: theme.colorScheme.surfaceContainerLowest, + shape: RoundedRectangleBorder( + borderRadius: kBorderRadius6, + ), + child: Padding( + padding: kP8, + child: parsedJson != null + ? Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: parsedJson.entries.map((entry) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 2.0), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + '${entry.key}: ', + style: kCodeStyle.copyWith( + fontSize: fontSizeMedium, + color: isDark ? kColorGQL.toDark : kColorGQL, + fontWeight: FontWeight.bold, ), - const SizedBox(width: 4), - Expanded( - child: Text( - entry.value.toString(), - style: kCodeStyle, - ), + ), + const SizedBox(width: 4), + Expanded( + child: Text( + entry.value.toString(), + style: kCodeStyle, ), - ], - ), - ); - }).toList(), - ) - : Text( - chunk.toString().trim(), - style: kCodeStyle.copyWith( - fontSize: fontSizeMedium, - ), + ), + ], + ), + ); + }).toList(), + ) + : Text( + chunk.toString().trim(), + style: kCodeStyle.copyWith( + fontSize: fontSizeMedium, ), - ), - ); - }).toList(), - ), + ), + ), + ); + }).toList(), ); } } From d8d5f703797763c5881a3faf0d64ee99f864c5df Mon Sep 17 00:00:00 2001 From: Ankit Mahato Date: Mon, 11 Aug 2025 01:16:07 +0530 Subject: [PATCH 27/36] Move example to genai_example Renamed the example directory to genai_example, updated package name in pubspec.yaml, and added pubspec_overrides.yaml for local dependency overrides. Updated pubspec.lock to reflect new and overridden dependencies. --- .../{example => genai_example}/.gitignore | 0 .../{example => genai_example}/README.md | 0 .../analysis_options.yaml | 0 .../{example => genai_example}/lib/main.dart | 0 .../{example => genai_example}/pubspec.lock | 63 +++++++++++++++++-- .../{example => genai_example}/pubspec.yaml | 5 +- .../genai_example/pubspec_overrides.yaml | 8 +++ 7 files changed, 69 insertions(+), 7 deletions(-) rename packages/genai/{example => genai_example}/.gitignore (100%) rename packages/genai/{example => genai_example}/README.md (100%) rename packages/genai/{example => genai_example}/analysis_options.yaml (100%) rename packages/genai/{example => genai_example}/lib/main.dart (100%) rename packages/genai/{example => genai_example}/pubspec.lock (88%) rename packages/genai/{example => genai_example}/pubspec.yaml (97%) create mode 100644 packages/genai/genai_example/pubspec_overrides.yaml diff --git a/packages/genai/example/.gitignore b/packages/genai/genai_example/.gitignore similarity index 100% rename from packages/genai/example/.gitignore rename to packages/genai/genai_example/.gitignore diff --git a/packages/genai/example/README.md b/packages/genai/genai_example/README.md similarity index 100% rename from packages/genai/example/README.md rename to packages/genai/genai_example/README.md diff --git a/packages/genai/example/analysis_options.yaml b/packages/genai/genai_example/analysis_options.yaml similarity index 100% rename from packages/genai/example/analysis_options.yaml rename to packages/genai/genai_example/analysis_options.yaml diff --git a/packages/genai/example/lib/main.dart b/packages/genai/genai_example/lib/main.dart similarity index 100% rename from packages/genai/example/lib/main.dart rename to packages/genai/genai_example/lib/main.dart diff --git a/packages/genai/example/pubspec.lock b/packages/genai/genai_example/pubspec.lock similarity index 88% rename from packages/genai/example/pubspec.lock rename to packages/genai/genai_example/pubspec.lock index c6afd837..2b38738d 100644 --- a/packages/genai/example/pubspec.lock +++ b/packages/genai/genai_example/pubspec.lock @@ -1,6 +1,14 @@ # 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: @@ -10,7 +18,7 @@ packages: source: hosted version: "2.13.0" better_networking: - dependency: transitive + dependency: "direct overridden" description: path: "../../better_networking" relative: true @@ -48,6 +56,22 @@ packages: 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: @@ -56,6 +80,22 @@ packages: 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: @@ -80,6 +120,14 @@ packages: url: "https://pub.dev" source: hosted version: "7.0.1" + fixnum: + dependency: transitive + description: + name: fixnum + sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be + url: "https://pub.dev" + source: hosted + version: "1.1.1" flutter: dependency: "direct main" description: flutter @@ -262,13 +310,20 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.8" - seed: + pointycastle: dependency: transitive description: - name: seed - sha256: "0d74a46abd169c96a73d9dec4739e6623021915661beadf265e885bb1eafd214" + 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" shared_preferences: dependency: transitive diff --git a/packages/genai/example/pubspec.yaml b/packages/genai/genai_example/pubspec.yaml similarity index 97% rename from packages/genai/example/pubspec.yaml rename to packages/genai/genai_example/pubspec.yaml index 8a3c302e..fbfc5abb 100644 --- a/packages/genai/example/pubspec.yaml +++ b/packages/genai/genai_example/pubspec.yaml @@ -1,8 +1,8 @@ -name: example +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 +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 @@ -53,7 +53,6 @@ dev_dependencies: # 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. 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 From eb852df646fb0706cc7cae4d4482b8b8736faa99 Mon Sep 17 00:00:00 2001 From: Manas Hejmadi Date: Mon, 11 Aug 2025 03:30:43 +0530 Subject: [PATCH 28/36] genai: README & License Updated --- packages/genai/LICENSE | 202 ++++++++++++++++++++++++++++++++++++++- packages/genai/README.md | 88 ++++++++++++----- 2 files changed, 266 insertions(+), 24 deletions(-) diff --git a/packages/genai/LICENSE b/packages/genai/LICENSE index ba75c69f..65d4cee9 100644 --- a/packages/genai/LICENSE +++ b/packages/genai/LICENSE @@ -1 +1,201 @@ -TODO: Add your license here. + 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 index 0e12b996..aebeb239 100644 --- a/packages/genai/README.md +++ b/packages/genai/README.md @@ -1,58 +1,100 @@ -# genai package -This Package contains all the code related to generative AI capabilities and is a foundational package that can be used in various projects +# genai -### Fetch all available Remote LLMs -```dart -await LLMManager.fetchAvailableLLMs(); +`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: ^ ``` -### Getting LLM Models for a given Provider -```dart -final List models = LLMProvider.gemini.models; +Then run the following command in your terminal to fetch the package: + +```bash +flutter pub get ``` -### Calling a GenAI Model using the provided helper +--- + +## 🚀 Quick Start + +### Response 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) { print(x); }, - onError: (e){}, - systemPrompt: 'Give a 100 word summary of the provided word. Only give the answer', + onError: (e){...}, + systemPrompt: 'Give a 100 word summary of the provided word', userPrompt: 'Pizza', credential: 'AIza.....', ); ``` -### Calling a GenAI model (with Streaming) +### 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); //each word in the stream + stdout.write(x); //get each word in the stream }, onError: (e){}, - systemPrompt: 'Give a 100 word summary of the provided word. Only give the answer', + systemPrompt: 'Give a 100 word summary of the provided word', userPrompt: 'Pizza', credential: 'AIza.....', - stream: true, + stream: true, //pass this to enable streaming ); ``` -### Directly Using a Model (eg: Gemini) +### 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; -payload.systemPrompt = 'Say YES or NO'; -payload.userPrompt = 'The sun sets in the west'; -payload.credential = 'AIza....'; + +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) -``` \ No newline at end of file + +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). \ No newline at end of file From 30075a6d95f3114d05f8deb1b0865570ef2b9169 Mon Sep 17 00:00:00 2001 From: Manas Hejmadi Date: Mon, 11 Aug 2025 15:10:13 +0530 Subject: [PATCH 29/36] genai: Minor Refactoring and Corrections --- lib/providers/collection_providers.dart | 53 ++++++------------- .../details_card/response_pane.dart | 14 +++-- .../home_page/editor_pane/url_card.dart | 10 ++-- lib/widgets/response_body.dart | 25 +++++---- lib/widgets/response_body_success.dart | 1 - lib/widgets/sse_display.dart | 5 -- .../genai/lib/models/ai_request_model.dart | 22 ++++++++ 7 files changed, 60 insertions(+), 70 deletions(-) diff --git a/lib/providers/collection_providers.dart b/lib/providers/collection_providers.dart index 6d427ac3..fec5f0e0 100644 --- a/lib/providers/collection_providers.dart +++ b/lib/providers/collection_providers.dart @@ -332,32 +332,14 @@ class CollectionStateNotifier } APIType apiType = executionRequestModel.apiType; - - late HttpRequestModel substitutedHttpRequestModel; AIRequestModel? aiRequestModel; bool noSSL = ref.read(settingsProvider).isSSLDisabled; + HttpRequestModel substitutedHttpRequestModel; if (apiType == APIType.ai) { aiRequestModel = requestModel.aiRequestModel!; - - final streamingMode = aiRequestModel.payload - .configMap[LLMConfigName.stream.name]?.configValue.value ?? - false; - - final genAIRequest = aiRequestModel.createRequest(stream: streamingMode); - substitutedHttpRequestModel = getSubstitutedHttpRequestModel( - HttpRequestModel( - method: HTTPVerb.post, - headers: [ - ...genAIRequest.headers.entries.map( - (x) => NameValueModel(name: x.key, value: x.value), - ), - ], - url: genAIRequest.endpoint, - bodyContentType: ContentType.json, - body: jsonEncode(genAIRequest.body), - ), - ); + substitutedHttpRequestModel = + getSubstitutedHttpRequestModel(aiRequestModel.convertToHTTPRequest()); } else { substitutedHttpRequestModel = getSubstitutedHttpRequestModel( executionRequestModel.httpRequestModel!); @@ -371,6 +353,7 @@ class CollectionStateNotifier sendingTime: DateTime.now(), ), }; + bool streamingMode = true; //Default: Streaming First final stream = await streamHttpRequest( requestId, @@ -388,8 +371,6 @@ class CollectionStateNotifier StreamSubscription? sub; - bool streaming = true; //DEFAULT to streaming - sub = stream.listen((rec) async { if (rec == null) return; @@ -399,7 +380,7 @@ class CollectionStateNotifier final errorMessage = rec.$4; if (isStreamingResponse == false) { - streaming = false; + streamingMode = false; if (!completer.isCompleted) { completer.complete((response, duration, errorMessage)); } @@ -467,20 +448,18 @@ class CollectionStateNotifier isStreamingResponse: isStreamingResponse, ); - if (!streaming) { - //AI-FORMATTING for Non Streaming Varaint - if (apiType == APIType.ai) { - final mT = httpResponseModel?.mediaType; - final body = (mT?.subtype == kSubTypeJson) - ? utf8.decode(response.bodyBytes) - : response.body; + //AI-FORMATTING for Non Streaming Varaint + if (streamingMode == false && apiType == APIType.ai) { + final mT = httpResponseModel?.mediaType; + final body = (mT?.subtype == kSubTypeJson) + ? utf8.decode(response.bodyBytes) + : response.body; - final fb = response.statusCode == 200 - ? aiRequestModel?.model.provider.modelController - .outputFormatter(jsonDecode(body)) - : formatBody(body, mT); - httpResponseModel = httpResponseModel?.copyWith(formattedBody: fb); - } + final fb = response.statusCode == 200 + ? aiRequestModel?.model.provider.modelController + .outputFormatter(jsonDecode(body)) + : formatBody(body, mT); + httpResponseModel = httpResponseModel?.copyWith(formattedBody: fb); } newRequestModel = newRequestModel.copyWith( 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 9bb3ab58..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 @@ -52,10 +52,7 @@ class ResponseDetails extends ConsumerWidget { selectedRequestModelProvider.select((value) => value?.responseStatus)); final message = ref .watch(selectedRequestModelProvider.select((value) => value?.message)); - - HttpResponseModel? httpResponseModel; - - httpResponseModel = ref.watch(selectedRequestModelProvider + final responseModel = ref.watch(selectedRequestModelProvider .select((value) => value?.httpResponseModel)); return Column( @@ -63,7 +60,7 @@ class ResponseDetails extends ConsumerWidget { ResponsePaneHeader( responseStatus: responseStatus, message: message, - time: httpResponseModel?.time, + time: responseModel?.time, onClearResponse: () { ref.read(collectionStateNotifierProvider.notifier).clearResponse(); }, @@ -111,8 +108,9 @@ class ResponseHeadersTab extends ConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final requestHeaders = ref.watch(selectedRequestModelProvider.select((value) { - return value?.httpResponseModel!.requestHeaders; - })); + return value?.httpResponseModel!.requestHeaders; + })) ?? + {}; final responseHeaders = ref.watch(selectedRequestModelProvider.select((value) { @@ -122,7 +120,7 @@ class ResponseHeadersTab extends ConsumerWidget { return ResponseHeaders( responseHeaders: responseHeaders, - requestHeaders: requestHeaders as Map? ?? {}, + 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 6875cd0d..2cbd8870 100644 --- a/lib/screens/home_page/editor_pane/url_card.dart +++ b/lib/screens/home_page/editor_pane/url_card.dart @@ -124,14 +124,12 @@ class URLTextField extends ConsumerWidget { ?.httpRequestModel ?.url, onChanged: (value) { - final aim = ref - .read(collectionStateNotifierProvider)![selectedId]! - .aiRequestModel; - if (aim != null) { - aim.payload.endpoint = value; + if (aiReqM != null) { + // Handle AI Endpoint Changes + aiReqM.payload.endpoint = value; ref .read(collectionStateNotifierProvider.notifier) - .update(aiRequestModel: aim.updatePayload(aim.payload)); + .update(aiRequestModel: aiReqM.updatePayload(aiReqM.payload)); } else { ref.read(collectionStateNotifierProvider.notifier).update(url: value); } diff --git a/lib/widgets/response_body.dart b/lib/widgets/response_body.dart index 8d7e12e4..84774837 100644 --- a/lib/widgets/response_body.dart +++ b/lib/widgets/response_body.dart @@ -18,17 +18,16 @@ class ResponseBody extends StatelessWidget { @override Widget build(BuildContext context) { - HttpResponseModel? httpResponseModel = - selectedRequestModel?.httpResponseModel; + final responseModel = selectedRequestModel?.httpResponseModel; - if (httpResponseModel == null) { + if (responseModel == null) { return const ErrorMessage( message: '$kNullResponseModelError $kUnexpectedRaiseIssue'); } - final isSSE = httpResponseModel.sseOutput?.isNotEmpty ?? false; - var body = httpResponseModel.body; - var formattedBody = httpResponseModel.formattedBody; + final isSSE = responseModel.sseOutput?.isNotEmpty ?? false; + var body = responseModel.body; + var formattedBody = responseModel.formattedBody; if (body == null) { return const ErrorMessage( @@ -42,11 +41,11 @@ class ResponseBody extends StatelessWidget { ); } if (isSSE) { - body = httpResponseModel.sseOutput!.join(); + body = responseModel.sseOutput!.join(); } final mediaType = - httpResponseModel.mediaType ?? MediaType(kTypeText, kSubTypePlain); + responseModel.mediaType ?? MediaType(kTypeText, kSubTypePlain); // Fix #415: Treat null Content-type as plain text instead of Error message // if (mediaType == null) { @@ -66,14 +65,14 @@ class ResponseBody extends StatelessWidget { options.remove(ResponseBodyView.code); } - if (httpResponseModel.sseOutput?.isNotEmpty ?? false) { + if (responseModel.sseOutput?.isNotEmpty ?? false) { return ResponseBodySuccess( key: Key("${selectedRequestModel!.id}-response"), mediaType: MediaType('text', 'event-stream'), options: [ResponseBodyView.sse, ResponseBodyView.raw], - bytes: utf8.encode((httpResponseModel.sseOutput!).toString()), - body: jsonEncode(httpResponseModel.sseOutput!), - formattedBody: httpResponseModel.sseOutput!.join('\n'), + bytes: utf8.encode((responseModel.sseOutput!).toString()), + body: jsonEncode(responseModel.sseOutput!), + formattedBody: responseModel.sseOutput!.join('\n'), selectedModel: selectedRequestModel?.aiRequestModel?.model, ); } @@ -82,7 +81,7 @@ class ResponseBody extends StatelessWidget { key: Key("${selectedRequestModel!.id}-response"), mediaType: mediaType, options: options, - bytes: httpResponseModel.bodyBytes!, + bytes: responseModel.bodyBytes!, body: body, formattedBody: formattedBody, highlightLanguage: highlightLanguage, diff --git a/lib/widgets/response_body_success.dart b/lib/widgets/response_body_success.dart index 7690344b..d7b1329c 100644 --- a/lib/widgets/response_body_success.dart +++ b/lib/widgets/response_body_success.dart @@ -18,7 +18,6 @@ class ResponseBodySuccess extends StatefulWidget { required this.options, required this.bytes, this.formattedBody, - // this.sseOutput, this.highlightLanguage, this.selectedModel, }); diff --git a/lib/widgets/sse_display.dart b/lib/widgets/sse_display.dart index 1d787525..6d0bee1f 100644 --- a/lib/widgets/sse_display.dart +++ b/lib/widgets/sse_display.dart @@ -1,12 +1,7 @@ import 'dart:convert'; -import 'package:apidash/models/request_model.dart'; -import 'package:apidash/providers/collection_providers.dart'; -import 'package:apidash/providers/history_providers.dart'; import 'package:apidash_design_system/apidash_design_system.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:genai/genai.dart'; -import 'package:genai/models/ai_request_model.dart'; class SSEDisplay extends StatefulWidget { final LLMModel? selectedLLModel; diff --git a/packages/genai/lib/models/ai_request_model.dart b/packages/genai/lib/models/ai_request_model.dart index 724a567c..07d66392 100644 --- a/packages/genai/lib/models/ai_request_model.dart +++ b/packages/genai/lib/models/ai_request_model.dart @@ -1,4 +1,8 @@ +import 'dart:convert'; + +import 'package:better_networking/better_networking.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:genai/llm_config.dart'; import '../llm_model.dart'; import '../llm_provider.dart'; import '../llm_saveobject.dart'; @@ -59,4 +63,22 @@ class AIRequestModel with _$AIRequestModel { provider: provider, ); } + + HttpRequestModel convertToHTTPRequest() { + final streamingMode = + payload.configMap[LLMConfigName.stream.name]?.configValue.value ?? + false; + final genAIRequest = createRequest(stream: streamingMode); + return HttpRequestModel( + method: HTTPVerb.post, + headers: [ + ...genAIRequest.headers.entries.map( + (x) => NameValueModel(name: x.key, value: x.value), + ), + ], + url: genAIRequest.endpoint, + bodyContentType: ContentType.json, + body: jsonEncode(genAIRequest.body), + ); + } } From db326b434a38cc890c612ec365dbcb4ebb361c5c Mon Sep 17 00:00:00 2001 From: Manas Hejmadi Date: Sat, 16 Aug 2025 16:02:34 +0530 Subject: [PATCH 30/36] genai: Fixed ModelSelector InkWell Bleed & made it visible in History --- .../history/history_widgets/his_url_card.dart | 7 ++ .../ai_request/widgets/llm_selector.dart | 94 ++++++++++--------- .../home_page/editor_pane/url_card.dart | 6 +- 3 files changed, 63 insertions(+), 44 deletions(-) diff --git a/lib/screens/history/history_widgets/his_url_card.dart b/lib/screens/history/history_widgets/his_url_card.dart index 7a6153b4..f6cb74b5 100644 --- a/lib/screens/history/history_widgets/his_url_card.dart +++ b/lib/screens/history/history_widgets/his_url_card.dart @@ -1,3 +1,4 @@ +import 'package:apidash/screens/home_page/editor_pane/url_card.dart'; import 'package:apidash_core/apidash_core.dart'; import 'package:apidash_design_system/apidash_design_system.dart'; import 'package:flutter/material.dart'; @@ -58,6 +59,12 @@ class HistoryURLCard extends StatelessWidget { ), isCompact ? kHSpacer10 : kHSpacer20, ], + if (apiType == APIType.ai) ...[ + AIProviderSelector( + readOnlyModel: historyRequestModel?.aiRequestModel, + ), + SizedBox(width: 20), + ], Expanded( child: ReadOnlyTextField( initialValue: url, diff --git a/lib/screens/home_page/editor_pane/details_card/request_pane/ai_request/widgets/llm_selector.dart b/lib/screens/home_page/editor_pane/details_card/request_pane/ai_request/widgets/llm_selector.dart index 386b331a..55721719 100644 --- a/lib/screens/home_page/editor_pane/details_card/request_pane/ai_request/widgets/llm_selector.dart +++ b/lib/screens/home_page/editor_pane/details_card/request_pane/ai_request/widgets/llm_selector.dart @@ -4,30 +4,34 @@ import 'package:flutter/material.dart'; class DefaultLLMSelectorButton extends StatelessWidget { final LLMSaveObject? defaultLLM; + final bool readonly; final Function(LLMSaveObject) onDefaultLLMUpdated; const DefaultLLMSelectorButton({ super.key, this.defaultLLM, + this.readonly = false, required this.onDefaultLLMUpdated, }); @override Widget build(BuildContext context) { return ElevatedButton( - onPressed: () async { - final saveObject = await showDialog( - context: context, - builder: (context) { - return AlertDialog( - scrollable: true, - content: DefaultLLMSelectorDialog(defaultLLM: defaultLLM), - contentPadding: EdgeInsets.all(10), - ); - }, - ); - if (saveObject == null) return; - onDefaultLLMUpdated(saveObject); - }, + onPressed: readonly + ? null + : () async { + final saveObject = await showDialog( + context: context, + builder: (context) { + return AlertDialog( + scrollable: true, + content: DefaultLLMSelectorDialog(defaultLLM: defaultLLM), + contentPadding: EdgeInsets.all(10), + ); + }, + ); + if (saveObject == null) return; + onDefaultLLMUpdated(saveObject); + }, child: Text(defaultLLM?.selectedLLM.modelName ?? 'Select Model'), ); } @@ -171,37 +175,41 @@ class _DefaultLLMSelectorDialogState extends State { borderRadius: BorderRadius.circular(10), color: const Color.fromARGB(27, 0, 0, 0), ), - child: SingleChildScrollView( - child: Column( - children: [ - ...selectedLLMProvider.models.map( - (x) => ListTile( - title: Text(x.modelName), - subtitle: Text(x.identifier), - trailing: Row( - mainAxisSize: MainAxisSize.min, - children: [ - if (llmSaveObject.selectedLLM.identifier == - x.identifier) - CircleAvatar( - radius: 5, - backgroundColor: Colors.green, - ), - IconButton( - onPressed: () => removeModel(x), - icon: Icon( - Icons.delete, - size: 20, - )) - ], + child: Material( + color: Colors.transparent, + child: SingleChildScrollView( + clipBehavior: Clip.hardEdge, + child: Column( + children: [ + ...selectedLLMProvider.models.map( + (x) => ListTile( + title: Text(x.modelName), + subtitle: Text(x.identifier), + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + if (llmSaveObject.selectedLLM.identifier == + x.identifier) + CircleAvatar( + radius: 5, + backgroundColor: Colors.green, + ), + IconButton( + onPressed: () => removeModel(x), + icon: Icon( + Icons.delete, + size: 20, + )) + ], + ), + onTap: () { + llmSaveObject.selectedLLM = x; + setState(() {}); + }, ), - onTap: () { - llmSaveObject.selectedLLM = x; - setState(() {}); - }, ), - ), - ], + ], + ), ), ), ), diff --git a/lib/screens/home_page/editor_pane/url_card.dart b/lib/screens/home_page/editor_pane/url_card.dart index 2cbd8870..6428820f 100644 --- a/lib/screens/home_page/editor_pane/url_card.dart +++ b/lib/screens/home_page/editor_pane/url_card.dart @@ -171,15 +171,18 @@ class SendRequestButton extends ConsumerWidget { } class AIProviderSelector extends ConsumerWidget { + final AIRequestModel? readOnlyModel; + const AIProviderSelector({ super.key, + this.readOnlyModel, }); @override Widget build(BuildContext context, WidgetRef ref) { final selectedId = ref.watch(selectedIdStateProvider); final req = ref.watch(collectionStateNotifierProvider)![selectedId]!; - AIRequestModel? aiRequestModel = req.aiRequestModel; + AIRequestModel? aiRequestModel = readOnlyModel ?? req.aiRequestModel; if (aiRequestModel == null) { return Container(); @@ -194,6 +197,7 @@ class AIProviderSelector extends ConsumerWidget { ); return DefaultLLMSelectorButton( + readonly: (readOnlyModel != null), key: ValueKey(ref.watch(selectedIdStateProvider)), defaultLLM: defaultLLMSO, onDefaultLLMUpdated: (llmso) { From 36f58ba26acbcf3996f1963dac236c84bb32f7dd Mon Sep 17 00:00:00 2001 From: Manas Hejmadi Date: Mon, 18 Aug 2025 00:40:13 +0530 Subject: [PATCH 31/36] DefaultLLMSelectorDialog: ResponsiveUI added --- .../ai_request/widgets/llm_selector.dart | 250 ++++++++++-------- 1 file changed, 147 insertions(+), 103 deletions(-) diff --git a/lib/screens/home_page/editor_pane/details_card/request_pane/ai_request/widgets/llm_selector.dart b/lib/screens/home_page/editor_pane/details_card/request_pane/ai_request/widgets/llm_selector.dart index 55721719..a70a4d11 100644 --- a/lib/screens/home_page/editor_pane/details_card/request_pane/ai_request/widgets/llm_selector.dart +++ b/lib/screens/home_page/editor_pane/details_card/request_pane/ai_request/widgets/llm_selector.dart @@ -76,6 +76,58 @@ class _DefaultLLMSelectorDialogState extends State { @override Widget build(BuildContext context) { if (!initialized) return SizedBox(); + + if (context.isMediumWindow) { + return Container( + padding: EdgeInsets.all(20), + width: MediaQuery.of(context).size.width * 0.8, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ElevatedButton( + onPressed: () async { + await LLMManager.fetchAvailableLLMs(); + setState(() {}); + }, + child: Text('Fetch Models'), + ), + kVSpacer10, + Row( + children: [ + Text('Select Provider'), + kHSpacer20, + Expanded( + child: ADDropdownButton( + onChanged: (x) { + if (x == null) return; + selectedLLMProvider = x; + final models = x.models; + final mC = x.modelController; + final p = mC.inputPayload; + llmSaveObject = LLMSaveObject( + endpoint: p.endpoint, + credential: '', + configMap: p.configMap, + selectedLLM: models.first, + provider: x, + ); + setState(() {}); + }, + value: selectedLLMProvider, + values: LLMProvider.values + .where(((e) => e.models.isNotEmpty)) + .map((e) => (e, e.displayName)), + ), + ), + ], + ), + kVSpacer10, + _buildModelSelector(), + ], + ), + ); + } + return Container( padding: EdgeInsets.all(20), width: MediaQuery.of(context).size.width * 0.8, @@ -128,115 +180,107 @@ class _DefaultLLMSelectorDialogState extends State { SizedBox(width: 40), Flexible( flex: 3, - child: Container( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.max, - children: [ - Text( - selectedLLMProvider.displayName, - style: TextStyle(fontSize: 28), - ), - SizedBox(height: 20), - if (selectedLLMProvider != LLMProvider.ollama) ...[ - Text('API Key / Credential'), - kVSpacer8, - BoundedTextField( - onChanged: (x) { - llmSaveObject.credential = x; - setState(() {}); - }, - value: llmSaveObject.credential, - ), - kVSpacer10, - ], - Text('Endpoint'), - kVSpacer8, - BoundedTextField( - key: ValueKey(llmSaveObject.provider), - onChanged: (x) { - llmSaveObject.endpoint = x; - setState(() {}); - }, - value: llmSaveObject.endpoint, - ), - kVSpacer20, - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text('Models'), - IconButton(onPressed: addNewModel, 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: [ - ...selectedLLMProvider.models.map( - (x) => ListTile( - title: Text(x.modelName), - subtitle: Text(x.identifier), - trailing: Row( - mainAxisSize: MainAxisSize.min, - children: [ - if (llmSaveObject.selectedLLM.identifier == - x.identifier) - CircleAvatar( - radius: 5, - backgroundColor: Colors.green, - ), - IconButton( - onPressed: () => removeModel(x), - icon: Icon( - Icons.delete, - size: 20, - )) - ], - ), - onTap: () { - llmSaveObject.selectedLLM = x; - setState(() {}); - }, - ), - ), - ], - ), - ), - ), - ), - kVSpacer10, - Align( - alignment: Alignment.centerRight, - child: ElevatedButton( - onPressed: () { - llmSaveObject.provider = selectedLLMProvider; - Navigator.of(context).pop(llmSaveObject); - }, - child: Text('Save Changes'), - ), - ), - ], - ), - ), + child: _buildModelSelector(), ), ], ), ); } - removeModel(LLMModel model) async { - await LLMManager.removeLLM( - selectedLLMProvider.name, model.identifier, model.modelName); - setState(() {}); + _buildModelSelector() { + return Container( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.max, + children: [ + Text( + selectedLLMProvider.displayName, + style: TextStyle(fontSize: 28), + ), + SizedBox(height: 20), + if (selectedLLMProvider != LLMProvider.ollama) ...[ + Text('API Key / Credential'), + kVSpacer8, + BoundedTextField( + onChanged: (x) { + llmSaveObject.credential = x; + setState(() {}); + }, + value: llmSaveObject.credential, + ), + kVSpacer10, + ], + Text('Endpoint'), + kVSpacer8, + BoundedTextField( + key: ValueKey(llmSaveObject.provider), + onChanged: (x) { + llmSaveObject.endpoint = x; + setState(() {}); + }, + value: llmSaveObject.endpoint, + ), + kVSpacer20, + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text('Models'), + IconButton(onPressed: addNewModel, 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: [ + ...selectedLLMProvider.models.map( + (x) => ListTile( + title: Text(x.modelName), + subtitle: Text(x.identifier), + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + if (llmSaveObject.selectedLLM.identifier == + x.identifier) + CircleAvatar( + radius: 5, + backgroundColor: Colors.green, + ), + ], + ), + onTap: () { + llmSaveObject.selectedLLM = x; + setState(() {}); + }, + ), + ), + ], + ), + ), + ), + ), + kVSpacer10, + Align( + alignment: Alignment.centerRight, + child: ElevatedButton( + onPressed: () { + llmSaveObject.provider = selectedLLMProvider; + Navigator.of(context).pop(llmSaveObject); + }, + child: Text('Save Changes'), + ), + ), + ], + ), + ); } addNewModel() async { From 72fea1ba65dbab940f725f4bf1635a125707e0b1 Mon Sep 17 00:00:00 2001 From: Ankit Mahato Date: Wed, 27 Aug 2025 02:08:36 +0530 Subject: [PATCH 32/36] Refactor genai package to new modular interface Reorganized the genai package by removing legacy LLM-related files and introducing a new modular interface under the 'interface' directory. Added provider-specific model classes, centralized constants, and updated the example to use the new API and data structures. Updated exports in genai.dart and improved dependency management. --- packages/genai/README.md | 13 +- packages/genai/analysis_options.yaml | 11 +- packages/genai/genai_example/lib/main.dart | 240 ++++--- packages/genai/genai_example/pubspec.lock | 135 +--- packages/genai/lib/consts.dart | 10 + packages/genai/lib/genai.dart | 15 +- packages/genai/lib/generative_ai.dart | 197 ------ packages/genai/lib/interface/consts.dart | 71 ++ packages/genai/lib/interface/interface.dart | 2 + .../interface/model_providers/anthropic.dart | 48 ++ .../model_providers/azureopenai.dart | 50 ++ .../lib/interface/model_providers/gemini.dart | 72 ++ .../model_providers/model_providers.dart} | 2 +- .../lib/interface/model_providers/ollama.dart | 13 + .../lib/interface/model_providers/openai.dart | 49 ++ packages/genai/lib/llm_config.dart | 199 ------ packages/genai/lib/llm_input_payload.dart | 59 -- packages/genai/lib/llm_manager.dart | 86 --- packages/genai/lib/llm_model.dart | 40 -- packages/genai/lib/llm_provider.dart | 73 -- packages/genai/lib/llm_request.dart | 13 - packages/genai/lib/llm_saveobject.dart | 48 -- .../genai/lib/models/ai_request_model.dart | 77 +-- .../lib/models/ai_request_model.freezed.dart | 145 ++-- .../genai/lib/models/ai_request_model.g.dart | 25 +- .../genai/lib/models/available_models.dart | 56 ++ .../lib/models/available_models.freezed.dart | 651 ++++++++++++++++++ .../genai/lib/models/available_models.g.dart | 60 ++ packages/genai/lib/models/model_config.dart | 74 ++ .../genai/lib/models/model_config_value.dart | 109 +++ packages/genai/lib/models/model_provider.dart | 18 + .../genai/lib/models/model_request_data.dart | 33 + .../models/model_request_data.freezed.dart | 339 +++++++++ .../lib/models/model_request_data.g.dart | 34 + packages/genai/lib/models/models.dart | 7 + packages/genai/lib/models/models_data.g.dart | 2 + packages/genai/lib/providers/anthropic.dart | 76 -- packages/genai/lib/providers/azureopenai.dart | 79 --- packages/genai/lib/providers/gemini.dart | 95 --- packages/genai/lib/providers/ollama.dart | 74 -- packages/genai/lib/providers/openai.dart | 78 --- .../genai/lib/utils/ai_request_utils.dart | 149 ++++ packages/genai/lib/utils/model_manager.dart | 97 +++ packages/genai/lib/utils/utils.dart | 2 + .../genai/lib/widgets/ai_config_bool.dart | 26 + .../genai/lib/widgets/ai_config_field.dart | 34 + .../genai/lib/widgets/ai_config_slider.dart | 36 + .../genai/lib/widgets/ai_config_widgets.dart | 99 --- packages/genai/lib/widgets/widgets.dart | 3 + packages/genai/models.json | 248 ++++--- packages/genai/pubspec.yaml | 7 +- packages/genai/pubspec_overrides.yaml | 6 + packages/genai/tool/json_to_dart.dart | 17 + packages/genai/tool/pre_publish.sh | 4 + 54 files changed, 2470 insertions(+), 1736 deletions(-) create mode 100644 packages/genai/lib/consts.dart delete mode 100644 packages/genai/lib/generative_ai.dart create mode 100644 packages/genai/lib/interface/consts.dart create mode 100644 packages/genai/lib/interface/interface.dart create mode 100644 packages/genai/lib/interface/model_providers/anthropic.dart create mode 100644 packages/genai/lib/interface/model_providers/azureopenai.dart create mode 100644 packages/genai/lib/interface/model_providers/gemini.dart rename packages/genai/lib/{providers/providers.dart => interface/model_providers/model_providers.dart} (81%) create mode 100644 packages/genai/lib/interface/model_providers/ollama.dart create mode 100644 packages/genai/lib/interface/model_providers/openai.dart delete mode 100644 packages/genai/lib/llm_config.dart delete mode 100644 packages/genai/lib/llm_input_payload.dart delete mode 100644 packages/genai/lib/llm_manager.dart delete mode 100644 packages/genai/lib/llm_model.dart delete mode 100644 packages/genai/lib/llm_provider.dart delete mode 100644 packages/genai/lib/llm_request.dart delete mode 100644 packages/genai/lib/llm_saveobject.dart create mode 100644 packages/genai/lib/models/available_models.dart create mode 100644 packages/genai/lib/models/available_models.freezed.dart create mode 100644 packages/genai/lib/models/available_models.g.dart create mode 100644 packages/genai/lib/models/model_config.dart create mode 100644 packages/genai/lib/models/model_config_value.dart create mode 100644 packages/genai/lib/models/model_provider.dart create mode 100644 packages/genai/lib/models/model_request_data.dart create mode 100644 packages/genai/lib/models/model_request_data.freezed.dart create mode 100644 packages/genai/lib/models/model_request_data.g.dart create mode 100644 packages/genai/lib/models/models.dart create mode 100644 packages/genai/lib/models/models_data.g.dart delete mode 100644 packages/genai/lib/providers/anthropic.dart delete mode 100644 packages/genai/lib/providers/azureopenai.dart delete mode 100644 packages/genai/lib/providers/gemini.dart delete mode 100644 packages/genai/lib/providers/ollama.dart delete mode 100644 packages/genai/lib/providers/openai.dart create mode 100644 packages/genai/lib/utils/ai_request_utils.dart create mode 100644 packages/genai/lib/utils/model_manager.dart create mode 100644 packages/genai/lib/utils/utils.dart create mode 100644 packages/genai/lib/widgets/ai_config_bool.dart create mode 100644 packages/genai/lib/widgets/ai_config_field.dart create mode 100644 packages/genai/lib/widgets/ai_config_slider.dart delete mode 100644 packages/genai/lib/widgets/ai_config_widgets.dart create mode 100644 packages/genai/lib/widgets/widgets.dart create mode 100644 packages/genai/pubspec_overrides.yaml create mode 100644 packages/genai/tool/json_to_dart.dart create mode 100644 packages/genai/tool/pre_publish.sh diff --git a/packages/genai/README.md b/packages/genai/README.md index aebeb239..d3b7a091 100644 --- a/packages/genai/README.md +++ b/packages/genai/README.md @@ -2,8 +2,6 @@ `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` @@ -13,8 +11,6 @@ - **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`: @@ -30,8 +26,6 @@ Then run the following command in your terminal to fetch the package: flutter pub get ``` ---- - ## 🚀 Quick Start ### Response Mode (Callback Style) @@ -51,6 +45,7 @@ GenerativeAI.callGenerativeModel( ``` ### Streaming Mode (Callback Style) + ```dart final LLMModel geminiModel = LLMProvider.gemini.getLLMByIdentifier('gemini-2.0-flash'); final ModelController controller = model.provider.modelController; @@ -68,6 +63,7 @@ GenerativeAI.callGenerativeModel( ``` ### Procedural(Manual) Request Building + ```dart final LLMModel model = LLMProvider.gemini.getLLMByIdentifier('gemini-2.0-flash'); final ModelController controller = model.provider.modelController; @@ -82,9 +78,6 @@ final answer = await GenerativeAI.executeGenAIRequest(model, genAIRequest); print(answer); ``` ---- - - ## 🤝 Contributing @@ -97,4 +90,4 @@ We welcome contributions to the `genai` package! If you'd like to contribute, pl ## License -This project is licensed under the [Apache License 2.0](https://github.com/foss42/apidash/blob/main/packages/genai/LICENSE). \ No newline at end of file +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 index a5744c1c..9a1eabb4 100644 --- a/packages/genai/analysis_options.yaml +++ b/packages/genai/analysis_options.yaml @@ -1,4 +1,11 @@ include: package:flutter_lints/flutter.yaml -# Additional information about this file can be found at -# https://dart.dev/guides/language/analysis-options +analyzer: + errors: + invalid_annotation_target: ignore + exclude: + - "**/*.freezed.dart" + - "**/*.g.dart" + +linter: + rules: diff --git a/packages/genai/genai_example/lib/main.dart b/packages/genai/genai_example/lib/main.dart index cee60173..85c7b12c 100644 --- a/packages/genai/genai_example/lib/main.dart +++ b/packages/genai/genai_example/lib/main.dart @@ -27,17 +27,12 @@ class AIExample extends StatefulWidget { } class _AIExampleState extends State { + late final Future aM; + @override void initState() { super.initState(); - () async { - await LLMManager.fetchAvailableLLMs(); //fetch latest LLMs - await LLMManager.loadAvailableLLMs(); //Load Saved LLMs - setState(() {}); - }(); - LLMManager.fetchAvailableLLMs().then((_) { - LLMManager.loadAvailableLLMs().then((_) {}); - }); + aM = ModelManager.fetchAvailableModels(); //fetch latest LLMs systemPromptController.text = 'Give me a 200 word essay on the given topic'; inputPromptController.text = 'Apple'; } @@ -46,28 +41,33 @@ class _AIExampleState extends State { setState(() { output = ""; }); - GenerativeAI.callGenerativeModel( - LLMProvider.fromName( - selectedProvider, - ).getLLMByIdentifier(selectedModel![0]), + callGenerativeModel( + AIRequestModel( + modelProvider: selectedProvider, + modelRequestData: kModelProvidersMap[selectedProvider] + ?.defaultRequestData + .copyWith( + model: selectedModel, + apiKey: credentialController.value.text, + systemPrompt: systemPromptController.value.text, + userPrompt: inputPromptController.value.text, + stream: stream, + ), + ), onAnswer: (x) { setState(() { output += "$x "; }); }, onError: (e) { - print(e); + debugPrint(e); }, - systemPrompt: systemPromptController.value.text, - userPrompt: inputPromptController.value.text, - credential: credentialController.value.text, - stream: stream, ); } String output = ""; - String selectedProvider = 'ollama'; - List? selectedModel; + ModelAPIProvider selectedProvider = ModelAPIProvider.ollama; + String selectedModel = ""; TextEditingController systemPromptController = TextEditingController(); TextEditingController inputPromptController = TextEditingController(); @@ -77,107 +77,119 @@ class _AIExampleState extends State { Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: Text('GenAI Example')), - body: SingleChildScrollView( - padding: EdgeInsets.all(20), - child: Column( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Text('Providers'), - SizedBox(height: 10), - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - ...LLMManager.avaiableModels.keys.map( - (x) => Container( - child: GestureDetector( - onTap: () { - setState(() { - selectedProvider = x; - }); - }, - child: Chip( - label: Text(x), - backgroundColor: selectedProvider == x - ? Colors.blue[50] - : Colors.transparent, - ), - ), - padding: EdgeInsets.only(right: 10), - ), - ), - ], - ), - SizedBox(height: 20), - Text('Models'), - SizedBox(height: 10), - Wrap( - spacing: 5, - runSpacing: 5, - children: [ - ...(LLMManager.avaiableModels[selectedProvider] ?? []).map( - (x) => Container( - child: GestureDetector( - onTap: () { - setState(() { - selectedModel = x; - }); - }, - child: Chip( - label: Text(x[1].toString()), - backgroundColor: selectedModel == x - ? Colors.blue[50] - : Colors.transparent, - ), - ), - padding: EdgeInsets.only(right: 10), - ), - ), - ], - ), - SizedBox(height: 30), - Container( - width: 400, + 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( - mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, children: [ - Text('Input Prompt'), - TextField(controller: inputPromptController), + 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('System Prompt'), - TextField(controller: systemPromptController), - SizedBox(height: 20), - Text('Credential'), - TextField(controller: credentialController), + 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), + Container( + 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), ], ), - ), - 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 index 2b38738d..7fe46cb5 100644 --- a/packages/genai/genai_example/pubspec.lock +++ b/packages/genai/genai_example/pubspec.lock @@ -104,22 +104,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.3" - ffi: - dependency: transitive - description: - name: ffi - sha256: "289279317b4b16eb2bb7e271abccd4bf84ec9bdcbe999e278a94b804f5630418" - url: "https://pub.dev" - source: hosted - version: "2.1.4" - file: - dependency: transitive - description: - name: file - sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 - url: "https://pub.dev" - source: hosted - version: "7.0.1" fixnum: dependency: transitive description: @@ -146,11 +130,6 @@ packages: description: flutter source: sdk version: "0.0.0" - flutter_web_plugins: - dependency: transitive - description: flutter - source: sdk - version: "0.0.0" freezed_annotation: dependency: transitive description: @@ -254,6 +233,14 @@ packages: 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: @@ -262,30 +249,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.9.1" - path_provider_linux: - dependency: transitive - description: - name: path_provider_linux - sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 - url: "https://pub.dev" - source: hosted - version: "2.2.1" - path_provider_platform_interface: - dependency: transitive - description: - name: path_provider_platform_interface - sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334" - url: "https://pub.dev" - source: hosted - version: "2.1.2" - path_provider_windows: - dependency: transitive - description: - name: path_provider_windows - sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7 - url: "https://pub.dev" - source: hosted - version: "2.3.0" petitparser: dependency: transitive description: @@ -294,22 +257,6 @@ packages: url: "https://pub.dev" source: hosted version: "6.1.0" - platform: - dependency: transitive - description: - name: platform - sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984" - url: "https://pub.dev" - source: hosted - version: "3.1.6" - plugin_platform_interface: - dependency: transitive - description: - name: plugin_platform_interface - sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" - url: "https://pub.dev" - source: hosted - version: "2.1.8" pointycastle: dependency: transitive description: @@ -325,62 +272,6 @@ packages: relative: true source: path version: "0.0.3" - shared_preferences: - dependency: transitive - description: - name: shared_preferences - sha256: "6e8bf70b7fef813df4e9a36f658ac46d107db4b4cfe1048b477d4e453a8159f5" - url: "https://pub.dev" - source: hosted - version: "2.5.3" - shared_preferences_android: - dependency: transitive - description: - name: shared_preferences_android - sha256: "20cbd561f743a342c76c151d6ddb93a9ce6005751e7aa458baad3858bfbfb6ac" - url: "https://pub.dev" - source: hosted - version: "2.4.10" - shared_preferences_foundation: - dependency: transitive - description: - name: shared_preferences_foundation - sha256: "6a52cfcdaeac77cad8c97b539ff688ccfc458c007b4db12be584fbe5c0e49e03" - url: "https://pub.dev" - source: hosted - version: "2.5.4" - shared_preferences_linux: - dependency: transitive - description: - name: shared_preferences_linux - sha256: "580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f" - url: "https://pub.dev" - source: hosted - version: "2.4.1" - shared_preferences_platform_interface: - dependency: transitive - description: - name: shared_preferences_platform_interface - sha256: "57cbf196c486bc2cf1f02b85784932c6094376284b3ad5779d1b1c6c6a816b80" - url: "https://pub.dev" - source: hosted - version: "2.4.1" - shared_preferences_web: - dependency: transitive - description: - name: shared_preferences_web - sha256: c49bd060261c9a3f0ff445892695d6212ff603ef3115edbb448509d407600019 - url: "https://pub.dev" - source: hosted - version: "2.4.3" - shared_preferences_windows: - dependency: transitive - description: - name: shared_preferences_windows - sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1" - url: "https://pub.dev" - source: hosted - version: "2.4.1" sky_engine: dependency: transitive description: flutter @@ -466,14 +357,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.1" - xdg_directories: - dependency: transitive - description: - name: xdg_directories - sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15" - url: "https://pub.dev" - source: hosted - version: "1.1.0" xml: dependency: transitive description: @@ -484,4 +367,4 @@ packages: version: "6.5.0" sdks: dart: ">=3.8.0 <4.0.0" - flutter: ">=3.27.0" + flutter: ">=3.18.0-18.0.pre.54" 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 index 2b4be834..7d58af8d 100644 --- a/packages/genai/lib/genai.dart +++ b/packages/genai/lib/genai.dart @@ -1,11 +1,4 @@ -// Module Exports -export 'models/ai_request_model.dart'; -export 'providers/providers.dart'; -export 'generative_ai.dart'; -export 'llm_config.dart'; -export 'llm_input_payload.dart'; -export 'llm_manager.dart'; -export 'llm_model.dart'; -export 'llm_provider.dart'; -export 'llm_request.dart'; -export 'llm_saveobject.dart'; +export 'models/models.dart'; +export 'interface/interface.dart'; +export 'utils/utils.dart'; +export 'widgets/widgets.dart'; diff --git a/packages/genai/lib/generative_ai.dart b/packages/genai/lib/generative_ai.dart deleted file mode 100644 index 6fec8d72..00000000 --- a/packages/genai/lib/generative_ai.dart +++ /dev/null @@ -1,197 +0,0 @@ -import 'dart:async'; -import 'dart:convert'; -import 'dart:math'; -import 'package:better_networking/better_networking.dart'; -import 'llm_config.dart'; -import 'llm_model.dart'; -import 'llm_request.dart'; - -class GenerativeAI { - static Future executeGenAIRequest( - LLMModel model, - LLMRequestDetails requestDetails, - ) async { - final mC = model.provider.modelController; - final headers = requestDetails.headers; - // print(jsonEncode(requestDetails.body)); - final (response, _, _) = await sendHttpRequest( - (Random().nextDouble() * 9999999 + 1).toString(), - APIType.rest, - HttpRequestModel( - method: HTTPVerb.post, - headers: [ - ...headers.entries.map( - (x) => NameValueModel.fromJson({x.key: x.value}), - ), - ], - url: requestDetails.endpoint, - bodyContentType: ContentType.json, - body: jsonEncode(requestDetails.body), - ), - ); - if (response == null) return null; - if (response.statusCode == 200) { - final data = jsonDecode(response.body); - // print(data); - return mC.outputFormatter(data); - } else { - print(requestDetails.endpoint); - print(response.body); - throw Exception( - 'LLM_EXCEPTION: ${response.statusCode}\n${response.body}', - ); - } - } - - static Future> streamGenAIRequest( - LLMModel model, - LLMRequestDetails requestDetails, - ) async { - final modelController = model.provider.modelController; - - final headers = { - 'Content-Type': 'application/json', - ...requestDetails.headers, - }; - - final httpStream = await streamHttpRequest( - requestDetails.hashCode.toString(), - APIType.rest, - HttpRequestModel( - method: HTTPVerb.post, - headers: headers.entries - .map((entry) => NameValueModel(name: entry.key, value: entry.value)) - .toList(), - url: requestDetails.endpoint, - bodyContentType: ContentType.json, - body: jsonEncode(requestDetails.body), - ), - ); - - final streamController = StreamController(); - - 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 = modelController.streamOutputFormatter( - jsonData, - ); - streamController.sink.add(formattedOutput); - } catch (e) { - print('⚠️ JSON decode error in SSE: $e\Sending 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; - } - - static callGenerativeModel( - LLMModel model, { - required Function(String?) onAnswer, - required Function(dynamic) onError, - required String systemPrompt, - required String userPrompt, - String? credential, - String? endpoint, - Map? configurations, - bool stream = false, - }) async { - final c = model.provider.modelController; - final payload = c.inputPayload; - payload.systemPrompt = systemPrompt; - payload.userPrompt = userPrompt; - if (credential != null) { - payload.credential = credential; - } - if (configurations != null) { - payload.configMap.addAll(configurations); - } - if (endpoint != null) { - payload.endpoint = endpoint; - } - try { - if (stream) { - final streamRequest = c.createRequest(model, payload, stream: true); - final answerStream = await streamGenAIRequest(model, streamRequest); - processGenAIStreamOutput(answerStream, (w) { - onAnswer('$w '); - }, onError); - } else { - final request = c.createRequest(model, payload); - final answer = await executeGenAIRequest(model, request); - onAnswer(answer); - } - } catch (e) { - onError(e); - } - } - - static 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/interface/consts.dart b/packages/genai/lib/interface/consts.dart new file mode 100644 index 00000000..cd09eec5 --- /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 kDefaultModelRequestData = ModelRequestData( + 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..88c18009 --- /dev/null +++ b/packages/genai/lib/interface/model_providers/anthropic.dart @@ -0,0 +1,48 @@ +import 'package:better_networking/better_networking.dart'; +import '../../models/models.dart'; +import '../consts.dart'; + +class AnthropicModel extends ModelProvider { + static final instance = AnthropicModel(); + + @override + ModelRequestData get defaultRequestData => + kDefaultModelRequestData.copyWith(url: kAnthropicUrl); + + @override + HttpRequestModel? createRequest(ModelRequestData? requestData) { + if (requestData == null) { + return null; + } + return HttpRequestModel( + method: HTTPVerb.post, + url: requestData.url, + headers: const [ + NameValueModel(name: "anthropic-version", value: "2023-06-01"), + ], + authModel: AuthModel( + type: APIAuthType.apiKey, + apikey: AuthApiKeyModel(key: requestData.apiKey), + ), + body: kJsonEncoder.convert({ + "model": requestData.model, + "messages": [ + {"role": "system", "content": requestData.systemPrompt}, + {"role": "user", "content": requestData.userPrompt}, + ], + ...requestData.getModelConfigMap(), + if (requestData.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..5f4da780 --- /dev/null +++ b/packages/genai/lib/interface/model_providers/azureopenai.dart @@ -0,0 +1,50 @@ +import 'package:better_networking/better_networking.dart'; +import '../../models/models.dart'; +import '../consts.dart'; + +class AzureOpenAIModel extends ModelProvider { + static final instance = AzureOpenAIModel(); + @override + ModelRequestData get defaultRequestData => kDefaultModelRequestData; + + @override + HttpRequestModel? createRequest(ModelRequestData? requestData) { + if (requestData == null) { + return null; + } + if (requestData.url.isEmpty) { + throw Exception('MODEL ENDPOINT IS EMPTY'); + } + return HttpRequestModel( + method: HTTPVerb.post, + url: requestData.url, + authModel: AuthModel( + type: APIAuthType.apiKey, + apikey: AuthApiKeyModel(key: requestData.apiKey, name: 'api-key'), + ), + body: kJsonEncoder.convert({ + "model": requestData.model, + "messages": [ + {"role": "system", "content": requestData.systemPrompt}, + if (requestData.userPrompt.isNotEmpty) ...{ + {"role": "user", "content": requestData.userPrompt}, + } else ...{ + {"role": "user", "content": "Generate"}, + }, + ], + ...requestData.getModelConfigMap(), + if (requestData.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..4ca3c313 --- /dev/null +++ b/packages/genai/lib/interface/model_providers/gemini.dart @@ -0,0 +1,72 @@ +import 'package:better_networking/better_networking.dart'; +import '../../models/models.dart'; +import '../consts.dart'; + +class GeminiModel extends ModelProvider { + static final instance = GeminiModel(); + + @override + ModelRequestData get defaultRequestData => kDefaultModelRequestData.copyWith( + url: kGeminiUrl, + modelConfigs: [ + kDefaultModelConfigTemperature, + kDefaultGeminiModelConfigTopP, + kDefaultGeminiModelConfigMaxTokens, + ], + ); + + @override + HttpRequestModel? createRequest(ModelRequestData? requestData) { + if (requestData == null) { + return null; + } + List params = []; + String endpoint = "${requestData.url}/${requestData.model}:"; + if (requestData.stream ?? false) { + endpoint += 'streamGenerateContent'; + params.add(const NameValueModel(name: "alt", value: "sse")); + } else { + endpoint += 'generateContent'; + } + + return HttpRequestModel( + method: HTTPVerb.post, + url: endpoint, + authModel: AuthModel( + type: APIAuthType.apiKey, + apikey: AuthApiKeyModel( + key: requestData.apiKey, + location: 'query', + name: 'key', + ), + ), + body: kJsonEncoder.convert({ + "contents": [ + { + "role": "user", + "parts": [ + {"text": requestData.userPrompt}, + ], + }, + ], + "systemInstruction": { + "role": "system", + "parts": [ + {"text": requestData.systemPrompt}, + ], + }, + "generationConfig": requestData.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/providers/providers.dart b/packages/genai/lib/interface/model_providers/model_providers.dart similarity index 81% rename from packages/genai/lib/providers/providers.dart rename to packages/genai/lib/interface/model_providers/model_providers.dart index ae945182..b3fa202f 100644 --- a/packages/genai/lib/providers/providers.dart +++ b/packages/genai/lib/interface/model_providers/model_providers.dart @@ -2,4 +2,4 @@ export 'anthropic.dart'; export 'gemini.dart'; export 'azureopenai.dart'; export 'openai.dart'; -export 'ollama.dart'; \ No newline at end of file +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..78c87cf3 --- /dev/null +++ b/packages/genai/lib/interface/model_providers/ollama.dart @@ -0,0 +1,13 @@ +import '../../models/models.dart'; +import '../consts.dart'; +import 'openai.dart'; + +class OllamaModel extends OpenAIModel { + static final instance = OllamaModel(); + + @override + ModelRequestData get defaultRequestData => kDefaultModelRequestData.copyWith( + 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..f42b6c31 --- /dev/null +++ b/packages/genai/lib/interface/model_providers/openai.dart @@ -0,0 +1,49 @@ +import 'package:better_networking/better_networking.dart'; +import '../../models/models.dart'; +import '../consts.dart'; + +class OpenAIModel extends ModelProvider { + static final instance = OpenAIModel(); + + @override + ModelRequestData get defaultRequestData => + kDefaultModelRequestData.copyWith(url: kOpenAIUrl); + + @override + HttpRequestModel? createRequest(ModelRequestData? requestData) { + if (requestData == null) { + return null; + } + return HttpRequestModel( + method: HTTPVerb.post, + url: requestData.url, + authModel: AuthModel( + type: APIAuthType.bearer, + bearer: AuthBearerModel(token: requestData.apiKey), + ), + body: kJsonEncoder.convert({ + "model": requestData.model, + "messages": [ + {"role": "system", "content": requestData.systemPrompt}, + if (requestData.userPrompt.isNotEmpty) ...{ + {"role": "user", "content": requestData.userPrompt}, + } else ...{ + {"role": "user", "content": "Generate"}, + }, + ], + ...requestData.getModelConfigMap(), + if (requestData.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/llm_config.dart b/packages/genai/lib/llm_config.dart deleted file mode 100644 index 19aec010..00000000 --- a/packages/genai/lib/llm_config.dart +++ /dev/null @@ -1,199 +0,0 @@ -import 'dart:convert'; - -typedef LLMOutputFormatter = String? Function(Map); - -class LLMModelConfiguration { - final String configId; - final String configName; - final String configDescription; - final LLMModelConfigurationType configType; - final LLMModelConfigValue configValue; - - LLMModelConfiguration updateValue(LLMModelConfigValue value) { - return LLMModelConfiguration( - configId: configId, - configName: configName, - configDescription: configDescription, - configType: configType, - configValue: value, - ); - } - - LLMModelConfiguration({ - required this.configId, - required this.configName, - required this.configDescription, - required this.configType, - required this.configValue, - }) { - // Assert that the configuration type and value matches - switch (configType) { - case LLMModelConfigurationType.boolean: - assert(configValue is LLMConfigBooleanValue); - case LLMModelConfigurationType.slider: - assert(configValue is LLMConfigSliderValue); - case LLMModelConfigurationType.numeric: - assert(configValue is LLMConfigNumericValue); - case LLMModelConfigurationType.text: - assert(configValue is LLMConfigTextValue); - } - } - - factory LLMModelConfiguration.fromJson(Map x) { - LLMModelConfigurationType cT; - LLMModelConfigValue cV; - switch (x['configType']) { - case 'boolean': - cT = LLMModelConfigurationType.boolean; - cV = LLMConfigBooleanValue.deserialize(x['configValue']); - break; - case 'slider': - cT = LLMModelConfigurationType.slider; - cV = LLMConfigSliderValue.deserialize(x['configValue']); - break; - case 'numeric': - cT = LLMModelConfigurationType.numeric; - cV = LLMConfigNumericValue.deserialize(x['configValue']); - break; - case 'text': - cT = LLMModelConfigurationType.text; - cV = LLMConfigTextValue.deserialize(x['configValue']); - break; - default: - cT = LLMModelConfigurationType.text; - cV = LLMConfigTextValue.deserialize(x['configValue']); - } - return LLMModelConfiguration( - configId: x['configId'], - configName: x['configName'], - configDescription: x['configDescription'], - configType: cT, - configValue: cV, - ); - } - - Map toJson() { - return { - 'configId': configId, - 'configName': configName, - 'configDescription': configDescription, - 'configType': configType.name.toString(), - 'configValue': configValue.serialize(), - }; - } - - LLMModelConfiguration clone() { - return LLMModelConfiguration.fromJson(toJson()); - } -} - -enum LLMModelConfigurationType { boolean, slider, numeric, text } - -//----------------LLMConfigValues ------------ - -abstract class LLMModelConfigValue { - dynamic _value; - - // ignore: unnecessary_getters_setters - dynamic get value => _value; - - set value(dynamic newValue) => _value = newValue; - - String serialize(); - - LLMModelConfigValue(this._value); -} - -class LLMConfigBooleanValue extends LLMModelConfigValue { - LLMConfigBooleanValue({required bool value}) : super(value); - - @override - String serialize() { - return value.toString(); - } - - static LLMConfigBooleanValue deserialize(String x) { - return LLMConfigBooleanValue(value: x == 'true'); - } -} - -class LLMConfigNumericValue extends LLMModelConfigValue { - LLMConfigNumericValue({required num value}) : super(value); - - @override - String serialize() { - return value.toString(); - } - - static LLMConfigNumericValue deserialize(String x) { - return LLMConfigNumericValue(value: num.parse(x)); - } -} - -class LLMConfigSliderValue extends LLMModelConfigValue { - LLMConfigSliderValue({required (double, double, double) value}) - : super(value); - - @override - String serialize() { - final v = value as (double, double, double); - return jsonEncode([v.$1, v.$2, v.$3]); - } - - static LLMConfigSliderValue deserialize(String x) { - final z = jsonDecode(x) as List; - final val = ( - double.parse(z[0].toString()), - double.parse(z[1].toString()), - double.parse(z[2].toString()), - ); - return LLMConfigSliderValue(value: val); - } -} - -class LLMConfigTextValue extends LLMModelConfigValue { - LLMConfigTextValue({required String value}) : super(value); - - @override - String serialize() { - return value.toString(); - } - - static LLMConfigTextValue deserialize(String x) { - return LLMConfigTextValue(value: x); - } -} - -enum LLMConfigName { temperature, top_p, max_tokens, endpoint, stream } - -Map defaultLLMConfigurations = { - LLMConfigName.temperature: LLMModelConfiguration( - configId: 'temperature', - configName: 'Temperature', - configDescription: 'The Temperature of the Model', - configType: LLMModelConfigurationType.slider, - configValue: LLMConfigSliderValue(value: (0, 0.5, 1)), - ), - LLMConfigName.top_p: LLMModelConfiguration( - configId: 'top_p', - configName: 'Top P', - configDescription: 'The Top P of the Model', - configType: LLMModelConfigurationType.slider, - configValue: LLMConfigSliderValue(value: (0, 0.95, 1)), - ), - LLMConfigName.max_tokens: LLMModelConfiguration( - configId: 'max_tokens', - configName: 'Maximum Tokens', - configDescription: 'The maximum number of tokens allowed in the output', - configType: LLMModelConfigurationType.numeric, - configValue: LLMConfigNumericValue(value: -1), - ), - LLMConfigName.stream: LLMModelConfiguration( - configId: 'stream', - configName: 'Enable Streaming Mode', - configDescription: - 'The LLM output will be sent in a stream instead of all at once', - configType: LLMModelConfigurationType.boolean, - configValue: LLMConfigBooleanValue(value: false), - ), -}; diff --git a/packages/genai/lib/llm_input_payload.dart b/packages/genai/lib/llm_input_payload.dart deleted file mode 100644 index 79198911..00000000 --- a/packages/genai/lib/llm_input_payload.dart +++ /dev/null @@ -1,59 +0,0 @@ -import 'llm_config.dart'; - -class LLMInputPayload { - String endpoint; - String credential; - String systemPrompt; - String userPrompt; - Map configMap; - - LLMInputPayload({ - required this.endpoint, - required this.credential, - required this.systemPrompt, - required this.userPrompt, - required this.configMap, - }); - - LLMInputPayload clone() { - Map cmap = {}; - for (final k in configMap.keys) { - cmap[k] = configMap[k]!.clone(); - } - return LLMInputPayload( - endpoint: endpoint, - credential: credential, - systemPrompt: systemPrompt, - userPrompt: userPrompt, - configMap: cmap, - ); - } - - static Map toJSON(LLMInputPayload payload) { - Map cmap = {}; - for (final e in payload.configMap.entries) { - cmap[e.key] = e.value.toJson(); - } - return { - 'endpoint': payload.endpoint, - 'credential': payload.credential, - 'system_prompt': payload.systemPrompt, - 'user_prompt': payload.userPrompt, - 'config_map': cmap, - }; - } - - static LLMInputPayload fromJSON(Map json) { - Map cmap = {}; - for (final k in json['config_map'].keys) { - cmap[k] = LLMModelConfiguration.fromJson(json['config_map'][k]); - } - return LLMInputPayload( - endpoint: json['endpoint'], - credential: json['credential'], - systemPrompt: json['system_prompt'], - userPrompt: json['user_prompt'], - configMap: cmap, - ); - } -} diff --git a/packages/genai/lib/llm_manager.dart b/packages/genai/lib/llm_manager.dart deleted file mode 100644 index 5feb7701..00000000 --- a/packages/genai/lib/llm_manager.dart +++ /dev/null @@ -1,86 +0,0 @@ -import 'dart:convert'; -import 'package:better_networking/better_networking.dart'; -import 'package:shared_preferences/shared_preferences.dart'; - -class LLMManager { - static Map avaiableModels = { - "gemini": [ - ["gemini-2.0-flash", "Gemini 2.0 Flash"], - ], - }; - - static get models => avaiableModels; - - static const String modelRemoteURL = - 'https://raw.githubusercontent.com/synapsecode/apidash/package/genai/packages/genai/models.json'; - static const String baseOllamaURL = 'http://localhost:11434'; - - static addLLM(String providerID, String modelID, String modelName) async { - avaiableModels[providerID] = [ - ...avaiableModels[providerID], - [modelID, modelName], - ]; - await saveAvailableLLMs(avaiableModels); - } - - static removeLLM(String providerID, String modelID, String modelName) async { - List z = avaiableModels[providerID] as List; - z = z.where((x) => x[0] != modelID && x[1] != modelName).toList(); - avaiableModels[providerID] = z; - await saveAvailableLLMs(avaiableModels); - } - - static fetchAvailableLLMs([String? remoteURL, String? ollamaURL]) async { - //get LLMs from remove - final (resp, _, __) = await sendHttpRequest( - 'FETCH_MODELS', - APIType.rest, - HttpRequestModel(url: remoteURL ?? modelRemoteURL, method: HTTPVerb.get), - ); - if (resp == null) { - throw Exception('UNABLE TO FETCH MODELS'); - } - Map remoteModels = jsonDecode(resp.body); - final oM = await fetchInstalledOllamaModels(ollamaURL); - remoteModels['ollama'] = oM; - saveAvailableLLMs(remoteModels); - loadAvailableLLMs(); - } - - static saveAvailableLLMs(Map updatedLLMs) async { - SharedPreferences prefs = await SharedPreferences.getInstance(); - await prefs.setString('genai_available_llms', jsonEncode(updatedLLMs)); - } - - static loadAvailableLLMs() async { - SharedPreferences prefs = await SharedPreferences.getInstance(); - final avl = prefs.getString('genai_available_llms'); - if (avl != null) { - avaiableModels = (jsonDecode(avl)); - } - } - - static clearAvailableLLMs() async { - SharedPreferences prefs = await SharedPreferences.getInstance(); - prefs.remove('genai_available_llms'); - } - - static Future fetchInstalledOllamaModels([String? ollamaURL]) async { - final url = "${ollamaURL ?? baseOllamaURL}/api/tags"; - final (resp, _, __) = await sendHttpRequest( - 'OLLAMA_FETCH', - APIType.rest, - HttpRequestModel(url: url, method: HTTPVerb.get), - noSSL: true, - ); - if (resp == null) return []; - final output = jsonDecode(resp.body); - final models = output['models']; - if (models == null) return []; - List ollamaModels = []; - for (final m in models) { - ollamaModels.add([m['model'], m['name']]); - } - return ollamaModels; - } -} diff --git a/packages/genai/lib/llm_model.dart b/packages/genai/lib/llm_model.dart deleted file mode 100644 index 30458c60..00000000 --- a/packages/genai/lib/llm_model.dart +++ /dev/null @@ -1,40 +0,0 @@ -import 'llm_input_payload.dart'; -import 'llm_provider.dart'; -import 'llm_request.dart'; - -class LLMModel { - const LLMModel(this.identifier, this.modelName, this.provider); - final String identifier; - final String modelName; - final LLMProvider provider; - - static Map toJson(LLMModel m) { - return {'identifier': m.identifier, 'provider': m.provider.name}; - } - - static LLMModel fromJson(Map json) { - return LLMProvider.fromName( - json['provider'], - ).getLLMByIdentifier(json['identifier']); - } -} - -abstract class ModelController { - LLMInputPayload get inputPayload => throw UnimplementedError(); - - LLMRequestDetails createRequest( - LLMModel model, - LLMInputPayload inputPayload, { - bool stream = false, - }) { - throw UnimplementedError(); - } - - String? outputFormatter(Map x) { - throw UnimplementedError(); - } - - String? streamOutputFormatter(Map x) { - throw UnimplementedError(); - } -} diff --git a/packages/genai/lib/llm_provider.dart b/packages/genai/lib/llm_provider.dart deleted file mode 100644 index 1e652910..00000000 --- a/packages/genai/lib/llm_provider.dart +++ /dev/null @@ -1,73 +0,0 @@ -import 'providers/providers.dart'; -import '../llm_manager.dart'; -import 'llm_model.dart'; - -enum LLMProvider { - gemini('Gemini'), - openai('OpenAI'), - anthropic('Anthropic'), - ollama('Ollama'), - azureopenai('Azure OpenAI'); - - const LLMProvider(this.displayName); - - final String displayName; - - List get models { - final avl = LLMManager.models[this.name.toLowerCase()]; - if (avl == null) return []; - List models = []; - for (final x in avl) { - models.add(LLMModel(x[0], x[1], this)); - } - return models; - } - - ModelController get modelController { - switch (this) { - case LLMProvider.ollama: - return OllamaModelController.instance; - case LLMProvider.gemini: - return GeminiModelController.instance; - case LLMProvider.azureopenai: - return AzureOpenAIModelController.instance; - case LLMProvider.openai: - return OpenAIModelController.instance; - case LLMProvider.anthropic: - return AnthropicModelController.instance; - } - } - - static LLMProvider fromJSON(Map json) { - return LLMProvider.fromName(json['llm_provider']); - } - - static Map toJSON(LLMProvider p) { - return {'llm_provider': p.name}; - } - - static LLMProvider? fromJSONNullable(Map? json) { - if (json == null) return null; - return LLMProvider.fromName(json['llm_provider']); - } - - static Map? toJSONNullable(LLMProvider? p) { - if (p == null) return null; - return {'llm_provider': p.name}; - } - - LLMModel getLLMByIdentifier(String identifier) { - final m = this.models.where((e) => e.identifier == identifier).firstOrNull; - if (m == null) { - throw Exception('MODEL DOES NOT EXIST $identifier'); - } - return m; - } - - static LLMProvider fromName(String name) { - return LLMProvider.values.firstWhere( - (model) => model.name == name, - orElse: () => throw ArgumentError('INVALID LLM PROVIDER: $name'), - ); - } -} diff --git a/packages/genai/lib/llm_request.dart b/packages/genai/lib/llm_request.dart deleted file mode 100644 index 041fc381..00000000 --- a/packages/genai/lib/llm_request.dart +++ /dev/null @@ -1,13 +0,0 @@ -class LLMRequestDetails { - String endpoint; - Map headers; - String method; - Map body; - - LLMRequestDetails({ - required this.endpoint, - required this.headers, - required this.method, - required this.body, - }); -} diff --git a/packages/genai/lib/llm_saveobject.dart b/packages/genai/lib/llm_saveobject.dart deleted file mode 100644 index 7d315789..00000000 --- a/packages/genai/lib/llm_saveobject.dart +++ /dev/null @@ -1,48 +0,0 @@ -import 'llm_config.dart'; -import 'llm_model.dart'; -import 'llm_provider.dart'; - -class LLMSaveObject { - String endpoint; - String credential; - LLMProvider provider; - LLMModel selectedLLM; - Map configMap; - - LLMSaveObject({ - required this.endpoint, - required this.credential, - required this.configMap, - required this.selectedLLM, - required this.provider, - }); - - Map toJSON() { - Map cmap = {}; - for (final e in configMap.entries) { - cmap[e.key] = e.value.toJson(); - } - return { - 'endpoint': endpoint, - 'credential': credential, - 'config_map': cmap, - 'selected_llm': selectedLLM.identifier, - 'provider': provider.name, - }; - } - - static LLMSaveObject fromJSON(Map json) { - Map cmap = {}; - for (final k in json['config_map'].keys) { - cmap[k] = LLMModelConfiguration.fromJson(json['config_map'][k]); - } - final provider = LLMProvider.fromName(json['provider']); - return LLMSaveObject( - endpoint: json['endpoint'], - credential: json['credential'], - configMap: cmap, - selectedLLM: provider.getLLMByIdentifier(json['selected_llm']), - provider: provider, - ); - } -} diff --git a/packages/genai/lib/models/ai_request_model.dart b/packages/genai/lib/models/ai_request_model.dart index 07d66392..7ed9e4fc 100644 --- a/packages/genai/lib/models/ai_request_model.dart +++ b/packages/genai/lib/models/ai_request_model.dart @@ -1,15 +1,7 @@ -import 'dart:convert'; - import 'package:better_networking/better_networking.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; -import 'package:genai/llm_config.dart'; -import '../llm_model.dart'; -import '../llm_provider.dart'; -import '../llm_saveobject.dart'; -import '../llm_input_payload.dart'; -import '../llm_request.dart'; -import '../providers/gemini.dart'; -import '../providers/providers.dart'; +import '../interface/interface.dart'; +import 'model_request_data.dart'; part 'ai_request_model.freezed.dart'; part 'ai_request_model.g.dart'; @@ -18,67 +10,20 @@ class AIRequestModel with _$AIRequestModel { const AIRequestModel._(); @JsonSerializable(explicitToJson: true, anyMap: true) - factory AIRequestModel({ - @JsonKey(fromJson: LLMInputPayload.fromJSON, toJson: LLMInputPayload.toJSON) - required LLMInputPayload payload, - @JsonKey(fromJson: LLMModel.fromJson, toJson: LLMModel.toJson) - required LLMModel model, - @JsonKey(fromJson: LLMProvider.fromJSON, toJson: LLMProvider.toJSON) - required LLMProvider provider, + const factory AIRequestModel({ + ModelAPIProvider? modelProvider, + ModelRequestData? modelRequestData, }) = _AIRequestModel; factory AIRequestModel.fromJson(Map json) => _$AIRequestModelFromJson(json); - AIRequestModel updatePayload(LLMInputPayload p) { - return AIRequestModel(payload: p, model: model, provider: provider); - } + HttpRequestModel? get httpRequestModel => + kModelProvidersMap[modelProvider]?.createRequest(modelRequestData); - LLMRequestDetails createRequest({bool stream = false}) { - final controller = model.provider.modelController; - return controller.createRequest(model, payload, stream: stream); - } + String? getFormattedOutput(Map x) => + kModelProvidersMap[modelProvider]?.outputFormatter(x); - factory AIRequestModel.fromDefaultSaveObject(LLMSaveObject? defaultLLMSO) { - final gmC = GeminiModelController.instance; - return AIRequestModel( - model: - defaultLLMSO?.selectedLLM ?? - LLMProvider.gemini.getLLMByIdentifier('gemini-2.0-flash'), - provider: defaultLLMSO?.provider ?? LLMProvider.gemini, - payload: LLMInputPayload( - endpoint: defaultLLMSO?.endpoint ?? gmC.inputPayload.endpoint, - credential: defaultLLMSO?.credential ?? '', - systemPrompt: '', - userPrompt: '', - configMap: defaultLLMSO?.configMap ?? gmC.inputPayload.configMap, - ), - ); - } - - AIRequestModel clone() { - return AIRequestModel( - model: model, - payload: payload.clone(), - provider: provider, - ); - } - - HttpRequestModel convertToHTTPRequest() { - final streamingMode = - payload.configMap[LLMConfigName.stream.name]?.configValue.value ?? - false; - final genAIRequest = createRequest(stream: streamingMode); - return HttpRequestModel( - method: HTTPVerb.post, - headers: [ - ...genAIRequest.headers.entries.map( - (x) => NameValueModel(name: x.key, value: x.value), - ), - ], - url: genAIRequest.endpoint, - bodyContentType: ContentType.json, - body: jsonEncode(genAIRequest.body), - ); - } + String? getFormattedStreamOutput(Map x) => + kModelProvidersMap[modelProvider]?.streamOutputFormatter(x); } diff --git a/packages/genai/lib/models/ai_request_model.freezed.dart b/packages/genai/lib/models/ai_request_model.freezed.dart index 84890f0a..7d9b3220 100644 --- a/packages/genai/lib/models/ai_request_model.freezed.dart +++ b/packages/genai/lib/models/ai_request_model.freezed.dart @@ -21,12 +21,8 @@ AIRequestModel _$AIRequestModelFromJson(Map json) { /// @nodoc mixin _$AIRequestModel { - @JsonKey(fromJson: LLMInputPayload.fromJSON, toJson: LLMInputPayload.toJSON) - LLMInputPayload get payload => throw _privateConstructorUsedError; - @JsonKey(fromJson: LLMModel.fromJson, toJson: LLMModel.toJson) - LLMModel get model => throw _privateConstructorUsedError; - @JsonKey(fromJson: LLMProvider.fromJSON, toJson: LLMProvider.toJSON) - LLMProvider get provider => throw _privateConstructorUsedError; + ModelAPIProvider? get modelProvider => throw _privateConstructorUsedError; + ModelRequestData? get modelRequestData => throw _privateConstructorUsedError; /// Serializes this AIRequestModel to a JSON map. Map toJson() => throw _privateConstructorUsedError; @@ -46,13 +42,11 @@ abstract class $AIRequestModelCopyWith<$Res> { ) = _$AIRequestModelCopyWithImpl<$Res, AIRequestModel>; @useResult $Res call({ - @JsonKey(fromJson: LLMInputPayload.fromJSON, toJson: LLMInputPayload.toJSON) - LLMInputPayload payload, - @JsonKey(fromJson: LLMModel.fromJson, toJson: LLMModel.toJson) - LLMModel model, - @JsonKey(fromJson: LLMProvider.fromJSON, toJson: LLMProvider.toJSON) - LLMProvider provider, + ModelAPIProvider? modelProvider, + ModelRequestData? modelRequestData, }); + + $ModelRequestDataCopyWith<$Res>? get modelRequestData; } /// @nodoc @@ -70,28 +64,37 @@ class _$AIRequestModelCopyWithImpl<$Res, $Val extends AIRequestModel> @pragma('vm:prefer-inline') @override $Res call({ - Object? payload = null, - Object? model = null, - Object? provider = null, + Object? modelProvider = freezed, + Object? modelRequestData = freezed, }) { return _then( _value.copyWith( - payload: null == payload - ? _value.payload - : payload // ignore: cast_nullable_to_non_nullable - as LLMInputPayload, - model: null == model - ? _value.model - : model // ignore: cast_nullable_to_non_nullable - as LLMModel, - provider: null == provider - ? _value.provider - : provider // ignore: cast_nullable_to_non_nullable - as LLMProvider, + modelProvider: freezed == modelProvider + ? _value.modelProvider + : modelProvider // ignore: cast_nullable_to_non_nullable + as ModelAPIProvider?, + modelRequestData: freezed == modelRequestData + ? _value.modelRequestData + : modelRequestData // ignore: cast_nullable_to_non_nullable + as ModelRequestData?, ) as $Val, ); } + + /// Create a copy of AIRequestModel + /// with the given fields replaced by the non-null parameter values. + @override + @pragma('vm:prefer-inline') + $ModelRequestDataCopyWith<$Res>? get modelRequestData { + if (_value.modelRequestData == null) { + return null; + } + + return $ModelRequestDataCopyWith<$Res>(_value.modelRequestData!, (value) { + return _then(_value.copyWith(modelRequestData: value) as $Val); + }); + } } /// @nodoc @@ -104,13 +107,12 @@ abstract class _$$AIRequestModelImplCopyWith<$Res> @override @useResult $Res call({ - @JsonKey(fromJson: LLMInputPayload.fromJSON, toJson: LLMInputPayload.toJSON) - LLMInputPayload payload, - @JsonKey(fromJson: LLMModel.fromJson, toJson: LLMModel.toJson) - LLMModel model, - @JsonKey(fromJson: LLMProvider.fromJSON, toJson: LLMProvider.toJSON) - LLMProvider provider, + ModelAPIProvider? modelProvider, + ModelRequestData? modelRequestData, }); + + @override + $ModelRequestDataCopyWith<$Res>? get modelRequestData; } /// @nodoc @@ -127,24 +129,19 @@ class __$$AIRequestModelImplCopyWithImpl<$Res> @pragma('vm:prefer-inline') @override $Res call({ - Object? payload = null, - Object? model = null, - Object? provider = null, + Object? modelProvider = freezed, + Object? modelRequestData = freezed, }) { return _then( _$AIRequestModelImpl( - payload: null == payload - ? _value.payload - : payload // ignore: cast_nullable_to_non_nullable - as LLMInputPayload, - model: null == model - ? _value.model - : model // ignore: cast_nullable_to_non_nullable - as LLMModel, - provider: null == provider - ? _value.provider - : provider // ignore: cast_nullable_to_non_nullable - as LLMProvider, + modelProvider: freezed == modelProvider + ? _value.modelProvider + : modelProvider // ignore: cast_nullable_to_non_nullable + as ModelAPIProvider?, + modelRequestData: freezed == modelRequestData + ? _value.modelRequestData + : modelRequestData // ignore: cast_nullable_to_non_nullable + as ModelRequestData?, ), ); } @@ -154,31 +151,20 @@ class __$$AIRequestModelImplCopyWithImpl<$Res> @JsonSerializable(explicitToJson: true, anyMap: true) class _$AIRequestModelImpl extends _AIRequestModel { - _$AIRequestModelImpl({ - @JsonKey(fromJson: LLMInputPayload.fromJSON, toJson: LLMInputPayload.toJSON) - required this.payload, - @JsonKey(fromJson: LLMModel.fromJson, toJson: LLMModel.toJson) - required this.model, - @JsonKey(fromJson: LLMProvider.fromJSON, toJson: LLMProvider.toJSON) - required this.provider, - }) : super._(); + const _$AIRequestModelImpl({this.modelProvider, this.modelRequestData}) + : super._(); factory _$AIRequestModelImpl.fromJson(Map json) => _$$AIRequestModelImplFromJson(json); @override - @JsonKey(fromJson: LLMInputPayload.fromJSON, toJson: LLMInputPayload.toJSON) - final LLMInputPayload payload; + final ModelAPIProvider? modelProvider; @override - @JsonKey(fromJson: LLMModel.fromJson, toJson: LLMModel.toJson) - final LLMModel model; - @override - @JsonKey(fromJson: LLMProvider.fromJSON, toJson: LLMProvider.toJSON) - final LLMProvider provider; + final ModelRequestData? modelRequestData; @override String toString() { - return 'AIRequestModel(payload: $payload, model: $model, provider: $provider)'; + return 'AIRequestModel(modelProvider: $modelProvider, modelRequestData: $modelRequestData)'; } @override @@ -186,15 +172,15 @@ class _$AIRequestModelImpl extends _AIRequestModel { return identical(this, other) || (other.runtimeType == runtimeType && other is _$AIRequestModelImpl && - (identical(other.payload, payload) || other.payload == payload) && - (identical(other.model, model) || other.model == model) && - (identical(other.provider, provider) || - other.provider == provider)); + (identical(other.modelProvider, modelProvider) || + other.modelProvider == modelProvider) && + (identical(other.modelRequestData, modelRequestData) || + other.modelRequestData == modelRequestData)); } @JsonKey(includeFromJson: false, includeToJson: false) @override - int get hashCode => Object.hash(runtimeType, payload, model, provider); + int get hashCode => Object.hash(runtimeType, modelProvider, modelRequestData); /// Create a copy of AIRequestModel /// with the given fields replaced by the non-null parameter values. @@ -214,28 +200,19 @@ class _$AIRequestModelImpl extends _AIRequestModel { } abstract class _AIRequestModel extends AIRequestModel { - factory _AIRequestModel({ - @JsonKey(fromJson: LLMInputPayload.fromJSON, toJson: LLMInputPayload.toJSON) - required final LLMInputPayload payload, - @JsonKey(fromJson: LLMModel.fromJson, toJson: LLMModel.toJson) - required final LLMModel model, - @JsonKey(fromJson: LLMProvider.fromJSON, toJson: LLMProvider.toJSON) - required final LLMProvider provider, + const factory _AIRequestModel({ + final ModelAPIProvider? modelProvider, + final ModelRequestData? modelRequestData, }) = _$AIRequestModelImpl; - _AIRequestModel._() : super._(); + const _AIRequestModel._() : super._(); factory _AIRequestModel.fromJson(Map json) = _$AIRequestModelImpl.fromJson; @override - @JsonKey(fromJson: LLMInputPayload.fromJSON, toJson: LLMInputPayload.toJSON) - LLMInputPayload get payload; + ModelAPIProvider? get modelProvider; @override - @JsonKey(fromJson: LLMModel.fromJson, toJson: LLMModel.toJson) - LLMModel get model; - @override - @JsonKey(fromJson: LLMProvider.fromJSON, toJson: LLMProvider.toJSON) - LLMProvider get provider; + ModelRequestData? get modelRequestData; /// Create a copy of AIRequestModel /// with the given fields replaced by the non-null parameter values. diff --git a/packages/genai/lib/models/ai_request_model.g.dart b/packages/genai/lib/models/ai_request_model.g.dart index df256f43..182fe1cb 100644 --- a/packages/genai/lib/models/ai_request_model.g.dart +++ b/packages/genai/lib/models/ai_request_model.g.dart @@ -8,15 +8,28 @@ part of 'ai_request_model.dart'; _$AIRequestModelImpl _$$AIRequestModelImplFromJson(Map json) => _$AIRequestModelImpl( - payload: LLMInputPayload.fromJSON(json['payload'] as Map), - model: LLMModel.fromJson(json['model'] as Map), - provider: LLMProvider.fromJSON(json['provider'] as Map), + modelProvider: $enumDecodeNullable( + _$ModelAPIProviderEnumMap, + json['modelProvider'], + ), + modelRequestData: json['modelRequestData'] == null + ? null + : ModelRequestData.fromJson( + Map.from(json['modelRequestData'] as Map), + ), ); Map _$$AIRequestModelImplToJson( _$AIRequestModelImpl instance, ) => { - 'payload': LLMInputPayload.toJSON(instance.payload), - 'model': LLMModel.toJson(instance.model), - 'provider': LLMProvider.toJSON(instance.provider), + 'modelProvider': _$ModelAPIProviderEnumMap[instance.modelProvider], + 'modelRequestData': instance.modelRequestData?.toJson(), +}; + +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..ef9fc306 --- /dev/null +++ b/packages/genai/lib/models/available_models.dart @@ -0,0 +1,56 @@ +// To parse this JSON data, do +// +// final availableModels = availableModelsFromJson(jsonString); + +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'dart:convert'; +import '../interface/interface.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 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); +} + +@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..725ef027 --- /dev/null +++ b/packages/genai/lib/models/available_models.freezed.dart @@ -0,0 +1,651 @@ +// 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 implements _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; + + 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 implements 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; + + 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..70b019fc --- /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 { + ModelRequestData get defaultRequestData => throw UnimplementedError(); + + HttpRequestModel? createRequest(ModelRequestData? requestData) { + throw UnimplementedError(); + } + + String? outputFormatter(Map x) { + throw UnimplementedError(); + } + + String? streamOutputFormatter(Map x) { + throw UnimplementedError(); + } +} diff --git a/packages/genai/lib/models/model_request_data.dart b/packages/genai/lib/models/model_request_data.dart new file mode 100644 index 00000000..7fa04e33 --- /dev/null +++ b/packages/genai/lib/models/model_request_data.dart @@ -0,0 +1,33 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'model_config.dart'; +part 'model_request_data.freezed.dart'; +part 'model_request_data.g.dart'; + +@freezed +class ModelRequestData with _$ModelRequestData { + const ModelRequestData._(); + + @JsonSerializable(explicitToJson: true, anyMap: true) + const factory ModelRequestData({ + @Default("") String url, + @Default("") String model, + @Default("") 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, + }) = _ModelRequestData; + + factory ModelRequestData.fromJson(Map json) => + _$ModelRequestDataFromJson(json); + + Map getModelConfigMap() { + Map m = {}; + for (var config in modelConfigs) { + m[config.id] = config.value.getPayloadValue(); + } + return m; + } +} diff --git a/packages/genai/lib/models/model_request_data.freezed.dart b/packages/genai/lib/models/model_request_data.freezed.dart new file mode 100644 index 00000000..35c0a7a5 --- /dev/null +++ b/packages/genai/lib/models/model_request_data.freezed.dart @@ -0,0 +1,339 @@ +// 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 'model_request_data.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', +); + +ModelRequestData _$ModelRequestDataFromJson(Map json) { + return _ModelRequestData.fromJson(json); +} + +/// @nodoc +mixin _$ModelRequestData { + 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 ModelRequestData to a JSON map. + Map toJson() => throw _privateConstructorUsedError; + + /// Create a copy of ModelRequestData + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $ModelRequestDataCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $ModelRequestDataCopyWith<$Res> { + factory $ModelRequestDataCopyWith( + ModelRequestData value, + $Res Function(ModelRequestData) then, + ) = _$ModelRequestDataCopyWithImpl<$Res, ModelRequestData>; + @useResult + $Res call({ + 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 _$ModelRequestDataCopyWithImpl<$Res, $Val extends ModelRequestData> + implements $ModelRequestDataCopyWith<$Res> { + _$ModelRequestDataCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of ModelRequestData + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? url = null, + Object? model = null, + Object? apiKey = null, + Object? systemPrompt = null, + Object? userPrompt = null, + Object? modelConfigs = null, + Object? stream = freezed, + }) { + return _then( + _value.copyWith( + url: null == url + ? _value.url + : url // ignore: cast_nullable_to_non_nullable + as String, + model: null == model + ? _value.model + : model // ignore: cast_nullable_to_non_nullable + as String, + apiKey: null == 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 _$$ModelRequestDataImplCopyWith<$Res> + implements $ModelRequestDataCopyWith<$Res> { + factory _$$ModelRequestDataImplCopyWith( + _$ModelRequestDataImpl value, + $Res Function(_$ModelRequestDataImpl) then, + ) = __$$ModelRequestDataImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({ + 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 __$$ModelRequestDataImplCopyWithImpl<$Res> + extends _$ModelRequestDataCopyWithImpl<$Res, _$ModelRequestDataImpl> + implements _$$ModelRequestDataImplCopyWith<$Res> { + __$$ModelRequestDataImplCopyWithImpl( + _$ModelRequestDataImpl _value, + $Res Function(_$ModelRequestDataImpl) _then, + ) : super(_value, _then); + + /// Create a copy of ModelRequestData + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? url = null, + Object? model = null, + Object? apiKey = null, + Object? systemPrompt = null, + Object? userPrompt = null, + Object? modelConfigs = null, + Object? stream = freezed, + }) { + return _then( + _$ModelRequestDataImpl( + url: null == url + ? _value.url + : url // ignore: cast_nullable_to_non_nullable + as String, + model: null == model + ? _value.model + : model // ignore: cast_nullable_to_non_nullable + as String, + apiKey: null == 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 _$ModelRequestDataImpl extends _ModelRequestData { + const _$ModelRequestDataImpl({ + this.url = "", + this.model = "", + this.apiKey = "", + @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 _$ModelRequestDataImpl.fromJson(Map json) => + _$$ModelRequestDataImplFromJson(json); + + @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 'ModelRequestData(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 _$ModelRequestDataImpl && + (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, + url, + model, + apiKey, + systemPrompt, + userPrompt, + const DeepCollectionEquality().hash(_modelConfigs), + stream, + ); + + /// Create a copy of ModelRequestData + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$ModelRequestDataImplCopyWith<_$ModelRequestDataImpl> get copyWith => + __$$ModelRequestDataImplCopyWithImpl<_$ModelRequestDataImpl>( + this, + _$identity, + ); + + @override + Map toJson() { + return _$$ModelRequestDataImplToJson(this); + } +} + +abstract class _ModelRequestData extends ModelRequestData { + const factory _ModelRequestData({ + 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, + }) = _$ModelRequestDataImpl; + const _ModelRequestData._() : super._(); + + factory _ModelRequestData.fromJson(Map json) = + _$ModelRequestDataImpl.fromJson; + + @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 ModelRequestData + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$ModelRequestDataImplCopyWith<_$ModelRequestDataImpl> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/packages/genai/lib/models/model_request_data.g.dart b/packages/genai/lib/models/model_request_data.g.dart new file mode 100644 index 00000000..8522874a --- /dev/null +++ b/packages/genai/lib/models/model_request_data.g.dart @@ -0,0 +1,34 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'model_request_data.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +_$ModelRequestDataImpl _$$ModelRequestDataImplFromJson(Map json) => + _$ModelRequestDataImpl( + url: json['url'] as String? ?? "", + model: json['model'] as String? ?? "", + apiKey: json['apiKey'] as String? ?? "", + 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 _$$ModelRequestDataImplToJson( + _$ModelRequestDataImpl instance, +) => { + '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, +}; diff --git a/packages/genai/lib/models/models.dart b/packages/genai/lib/models/models.dart new file mode 100644 index 00000000..69ecfe07 --- /dev/null +++ b/packages/genai/lib/models/models.dart @@ -0,0 +1,7 @@ +export 'ai_request_model.dart'; +export 'available_models.dart'; +export 'model_config_value.dart'; +export 'model_config.dart'; +export 'model_provider.dart'; +export 'model_request_data.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/providers/anthropic.dart b/packages/genai/lib/providers/anthropic.dart deleted file mode 100644 index c91fc4cc..00000000 --- a/packages/genai/lib/providers/anthropic.dart +++ /dev/null @@ -1,76 +0,0 @@ -import '../llm_config.dart'; -import '../llm_input_payload.dart'; -import '../llm_model.dart'; -import '../llm_request.dart'; - -class AnthropicModelController extends ModelController { - static final instance = AnthropicModelController(); - @override - LLMInputPayload get inputPayload => LLMInputPayload( - endpoint: 'https://api.anthropic.com/v1/messages', - credential: '', - systemPrompt: '', - userPrompt: '', - configMap: { - LLMConfigName.temperature.name: - defaultLLMConfigurations[LLMConfigName.temperature]!, - LLMConfigName.top_p.name: defaultLLMConfigurations[LLMConfigName.top_p]!, - LLMConfigName.stream.name: - defaultLLMConfigurations[LLMConfigName.stream]!, - }, - ).clone(); - - @override - LLMRequestDetails createRequest( - LLMModel model, - LLMInputPayload inputPayload, { - bool stream = false, - }) { - return LLMRequestDetails( - endpoint: inputPayload.endpoint, - headers: { - 'anthropic-version': '2023-06-01', - 'Authorization': 'Bearer ${inputPayload.credential}', - }, - method: 'POST', - body: { - "model": model.identifier, - if (stream) ...{'stream': true}, - "messages": [ - {"role": "system", "content": inputPayload.systemPrompt}, - {"role": "user", "content": inputPayload.userPrompt}, - ], - "temperature": - inputPayload - .configMap[LLMConfigName.temperature.name] - ?.configValue - .value - ?.$2 ?? - 0.5, - "top_p": - inputPayload - .configMap[LLMConfigName.top_p.name] - ?.configValue - .value - ?.$2 ?? - 0.95, - if (inputPayload.configMap[LLMConfigName.max_tokens.name] != null) ...{ - "max_tokens": inputPayload - .configMap[LLMConfigName.max_tokens.name]! - .configValue - .value, - }, - }, - ); - } - - @override - String? outputFormatter(Map x) { - return x['content']?[0]['text']; - } - - @override - String? streamOutputFormatter(Map x) { - return x['text']; - } -} diff --git a/packages/genai/lib/providers/azureopenai.dart b/packages/genai/lib/providers/azureopenai.dart deleted file mode 100644 index ff2656a4..00000000 --- a/packages/genai/lib/providers/azureopenai.dart +++ /dev/null @@ -1,79 +0,0 @@ -import '../llm_config.dart'; -import '../llm_input_payload.dart'; -import '../llm_model.dart'; -import '../llm_request.dart'; - -class AzureOpenAIModelController extends ModelController { - static final instance = AzureOpenAIModelController(); - @override - LLMInputPayload get inputPayload => LLMInputPayload( - endpoint: '', //TO BE FILLED BY USER - credential: '', - systemPrompt: '', - userPrompt: '', - configMap: { - LLMConfigName.temperature.name: - defaultLLMConfigurations[LLMConfigName.temperature]!, - LLMConfigName.top_p.name: defaultLLMConfigurations[LLMConfigName.top_p]!, - LLMConfigName.stream.name: - defaultLLMConfigurations[LLMConfigName.stream]!, - }, - ).clone(); - - @override - LLMRequestDetails createRequest( - LLMModel model, - LLMInputPayload inputPayload, { - bool stream = false, - }) { - if (inputPayload.endpoint.isEmpty) { - throw Exception('MODEL ENDPOINT IS EMPTY'); - } - return LLMRequestDetails( - endpoint: inputPayload.endpoint, - headers: {'api-key': inputPayload.credential}, - method: 'POST', - body: { - if (stream) ...{'stream': true}, - "messages": [ - {"role": "system", "content": inputPayload.systemPrompt}, - if (inputPayload.userPrompt.isNotEmpty) ...{ - {"role": "user", "content": inputPayload.userPrompt}, - } else ...{ - {"role": "user", "content": "Generate"}, - }, - ], - "temperature": - inputPayload - .configMap[LLMConfigName.temperature.name] - ?.configValue - .value - ?.$2 ?? - 0.5, - "top_p": - inputPayload - .configMap[LLMConfigName.top_p.name] - ?.configValue - .value - ?.$2 ?? - 0.95, - if (inputPayload.configMap[LLMConfigName.max_tokens.name] != null) ...{ - "max_tokens": inputPayload - .configMap[LLMConfigName.max_tokens.name]! - .configValue - .value, - }, - }, - ); - } - - @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/providers/gemini.dart b/packages/genai/lib/providers/gemini.dart deleted file mode 100644 index 65e2843d..00000000 --- a/packages/genai/lib/providers/gemini.dart +++ /dev/null @@ -1,95 +0,0 @@ -import '../llm_config.dart'; -import '../llm_input_payload.dart'; -import '../llm_model.dart'; -import '../llm_request.dart'; - -class GeminiModelController extends ModelController { - static final instance = GeminiModelController(); - @override - LLMInputPayload get inputPayload => LLMInputPayload( - endpoint: 'https://generativelanguage.googleapis.com/v1beta/models', - credential: '', - systemPrompt: '', - userPrompt: '', - configMap: { - //TODO: CHANGES TO THESE DO NOT APPLY TO OLDER REQUESTS!!!!!! - LLMConfigName.temperature.name: - defaultLLMConfigurations[LLMConfigName.temperature]!, - LLMConfigName.top_p.name: defaultLLMConfigurations[LLMConfigName.top_p]!, - LLMConfigName.stream.name: - defaultLLMConfigurations[LLMConfigName.stream]!, - }, - ).clone(); - - @override - LLMRequestDetails createRequest( - LLMModel model, - LLMInputPayload inputPayload, { - bool stream = false, - }) { - String endpoint = inputPayload.endpoint; - endpoint = - "$endpoint/${model.identifier}:generateContent?key=${inputPayload.credential}"; - if (stream) { - endpoint = endpoint.replaceAll( - 'generateContent?', - 'streamGenerateContent?alt=sse&', - ); - } - return LLMRequestDetails( - endpoint: endpoint, - headers: {}, - method: 'POST', - body: { - "model": model.identifier, - "contents": [ - { - "role": "user", - "parts": [ - {"text": inputPayload.userPrompt}, - ], - }, - ], - "systemInstruction": { - "role": "system", - "parts": [ - {"text": inputPayload.systemPrompt}, - ], - }, - "generationConfig": { - "temperature": - inputPayload - .configMap[LLMConfigName.temperature.name] - ?.configValue - .value - ?.$2 ?? - 0.5, - "topP": - inputPayload - .configMap[LLMConfigName.top_p.name] - ?.configValue - .value - ?.$2 ?? - 0.95, - if (inputPayload.configMap[LLMConfigName.max_tokens.name] != - null) ...{ - "maxOutputTokens": inputPayload - .configMap[LLMConfigName.max_tokens.name]! - .configValue - .value, - }, - }, - }, - ); - } - - @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/providers/ollama.dart b/packages/genai/lib/providers/ollama.dart deleted file mode 100644 index 44201acd..00000000 --- a/packages/genai/lib/providers/ollama.dart +++ /dev/null @@ -1,74 +0,0 @@ -import '../llm_config.dart'; -import '../llm_input_payload.dart'; -import '../llm_model.dart'; -import '../llm_request.dart'; - -class OllamaModelController extends ModelController { - static final instance = OllamaModelController(); - - @override - LLMInputPayload get inputPayload => LLMInputPayload( - endpoint: 'http://localhost:11434/v1/chat/completions', - credential: '', - systemPrompt: '', - userPrompt: '', - configMap: { - LLMConfigName.temperature.name: - defaultLLMConfigurations[LLMConfigName.temperature]!, - LLMConfigName.top_p.name: defaultLLMConfigurations[LLMConfigName.top_p]!, - LLMConfigName.stream.name: - defaultLLMConfigurations[LLMConfigName.stream]!, - }, - ).clone(); - - @override - LLMRequestDetails createRequest( - LLMModel model, - LLMInputPayload inputPayload, { - bool stream = false, - }) { - return LLMRequestDetails( - endpoint: inputPayload.endpoint, - headers: {}, - method: 'POST', - body: { - "model": model.identifier, - if (stream) ...{'stream': true}, - "messages": [ - {"role": "system", "content": inputPayload.systemPrompt}, - {"role": "user", "content": inputPayload.userPrompt}, - ], - "temperature": - inputPayload - .configMap[LLMConfigName.temperature.name] - ?.configValue - .value - ?.$2 ?? - 0.5, - "top_p": - inputPayload - .configMap[LLMConfigName.top_p.name] - ?.configValue - .value - ?.$2 ?? - 0.95, - if (inputPayload.configMap[LLMConfigName.max_tokens.name] != null) ...{ - "max_tokens": inputPayload - .configMap[LLMConfigName.max_tokens.name]! - .configValue - .value, - }, - }, - ); - } - - @override - String? outputFormatter(Map x) { - return x['choices']?[0]['message']?['content']; - } - - @override - String? streamOutputFormatter(Map x) { - return x['choices']?[0]['delta']?['content']; - } -} diff --git a/packages/genai/lib/providers/openai.dart b/packages/genai/lib/providers/openai.dart deleted file mode 100644 index 8ad3765e..00000000 --- a/packages/genai/lib/providers/openai.dart +++ /dev/null @@ -1,78 +0,0 @@ -import '../llm_config.dart'; -import '../llm_input_payload.dart'; -import '../llm_model.dart'; -import '../llm_request.dart'; - -class OpenAIModelController extends ModelController { - static final instance = OpenAIModelController(); - - @override - LLMInputPayload get inputPayload => LLMInputPayload( - endpoint: 'https://api.openai.com/v1/chat/completions', - credential: '', - systemPrompt: '', - userPrompt: '', - configMap: { - LLMConfigName.temperature.name: - defaultLLMConfigurations[LLMConfigName.temperature]!, - LLMConfigName.top_p.name: defaultLLMConfigurations[LLMConfigName.top_p]!, - LLMConfigName.stream.name: - defaultLLMConfigurations[LLMConfigName.stream]!, - }, - ).clone(); - - @override - LLMRequestDetails createRequest( - LLMModel model, - LLMInputPayload inputPayload, { - bool stream = false, - }) { - return LLMRequestDetails( - endpoint: inputPayload.endpoint, - headers: {'Authorization': "Bearer ${inputPayload.credential}"}, - method: 'POST', - body: { - 'model': model.identifier, - if (stream) ...{'stream': true}, - "messages": [ - {"role": "system", "content": inputPayload.systemPrompt}, - if (inputPayload.userPrompt.isNotEmpty) ...{ - {"role": "user", "content": inputPayload.userPrompt}, - } else ...{ - {"role": "user", "content": "Generate"}, - }, - ], - "temperature": - inputPayload - .configMap[LLMConfigName.temperature.name] - ?.configValue - .value - ?.$2 ?? - 0.5, - "top_p": - inputPayload - .configMap[LLMConfigName.top_p.name] - ?.configValue - .value - ?.$2 ?? - 0.95, - if (inputPayload.configMap[LLMConfigName.max_tokens.name] != null) ...{ - "max_tokens": inputPayload - .configMap[LLMConfigName.max_tokens.name]! - .configValue - .value, - }, - }, - ); - } - - @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/utils/ai_request_utils.dart b/packages/genai/lib/utils/ai_request_utils.dart new file mode 100644 index 00000000..33b27bd3 --- /dev/null +++ b/packages/genai/lib/utils/ai_request_utils.dart @@ -0,0 +1,149 @@ +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 { + final modelRequestData = aiRequestModel?.modelRequestData; + if (modelRequestData != null) { + try { + if (modelRequestData.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/ai_config_widgets.dart b/packages/genai/lib/widgets/ai_config_widgets.dart deleted file mode 100644 index 957c5c1f..00000000 --- a/packages/genai/lib/widgets/ai_config_widgets.dart +++ /dev/null @@ -1,99 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:genai/llm_config.dart'; - -class SliderAIConfig extends StatelessWidget { - final LLMModelConfiguration configuration; - final Function(LLMModelConfiguration) onSliderUpdated; - final bool readonly; - const SliderAIConfig({ - super.key, - required this.configuration, - required this.onSliderUpdated, - this.readonly = false, - }); - - @override - Widget build(BuildContext context) { - return Row( - children: [ - Expanded( - child: Slider( - min: (configuration.configValue.value as (double, double, double)) - .$1, - value: (configuration.configValue.value as (double, double, double)) - .$2, - max: (configuration.configValue.value as (double, double, double)) - .$3, - onChanged: (x) { - if (readonly) return; - final z = - configuration.configValue.value as (double, double, double); - configuration.configValue.value = (z.$1, x, z.$3); - onSliderUpdated(configuration); - }, - ), - ), - Text( - (configuration.configValue.value as (double, double, double)).$2 - .toStringAsFixed(2), - ), - ], - ); - } -} - -class WritableAIConfig extends StatelessWidget { - final bool numeric; - final LLMModelConfiguration configuration; - final Function(LLMModelConfiguration) onConfigUpdated; - final bool readonly; - const WritableAIConfig({ - super.key, - this.numeric = false, - required this.configuration, - required this.onConfigUpdated, - this.readonly = false, - }); - - @override - Widget build(BuildContext context) { - return TextFormField( - initialValue: configuration.configValue.value.toString(), - onChanged: (x) { - if (readonly) return; - if (numeric) { - if (x.isEmpty) x = '0'; - if (num.tryParse(x) == null) return; - configuration.configValue.value = num.parse(x); - } else { - configuration.configValue.value = x; - } - onConfigUpdated(configuration); - }, - ); - } -} - -class BooleanAIConfig extends StatelessWidget { - final LLMModelConfiguration configuration; - final Function(LLMModelConfiguration) onConfigUpdated; - final bool readonly; - const BooleanAIConfig({ - super.key, - required this.configuration, - required this.onConfigUpdated, - this.readonly = false, - }); - - @override - Widget build(BuildContext context) { - return Switch( - value: configuration.configValue.value as bool, - onChanged: (x) { - if (readonly) return; - configuration.configValue.value = x; - onConfigUpdated(configuration); - }, - ); - } -} 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 index b8156b72..5fcb9ad0 100644 --- a/packages/genai/models.json +++ b/packages/genai/models.json @@ -1,98 +1,152 @@ { - "openai": [ - [ - "gpt-4o", - "GPT-4o" - ], - [ - "gpt-4", - "GPT-4" - ], - [ - "gpt-4o-mini", - "GPT-4o Mini" - ], - [ - "gpt-4-turbo", - "GPT-4 Turbo" - ], - [ - "gpt-4.1", - "GPT-4.1" - ], - [ - "gpt-4.1-mini", - "GPT-4.1 Mini" - ], - [ - "gpt-4.1-nano", - "GPT-4.1 Nano" - ], - [ - "o1", - "o1" - ], - [ - "o3", - "o3" - ], - [ - "o3-mini", - "o3 Mini" - ], - [ - "gpt-3.5-turbo", - "GPT-3.5 Turbo" - ] - ], - "anthropic": [ - [ - "claude-3-opus-latest", - "Claude 3 Opus" - ], - [ - "claude-3-sonnet-latest", - "Claude 3 Sonnet" - ], - [ - "claude-3-haiku-latest", - "Claude 3 Haiku" - ], - [ - "claude-3-5-haiku-latest", - "Claude 3.5 Haiku" - ], - [ - "claude-3-5-sonnet-latest", - "Claude 3.5 Sonnet" - ] - ], - "gemini": [ - [ - "gemini-1.5-pro", - "Gemini 1.5 Pro" - ], - [ - "gemini-1.5-flash-8b", - "Gemini 1.5 Flash 8B" - ], - [ - "gemini-2.0-flash", - "Gemini 2.0 Flash" - ], - [ - "gemini-2.0-flash-lite", - "Gemini 2.0 Flash Lite" - ], - [ - "gemini-2.5-flash-preview-0520", - "Gemini 2.5 Flash Preview 0520" - ] - ], - "azureopenai": [ - [ - "custom", - "Custom" - ] - ] -} \ No newline at end of file + "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 index b5b6f696..13237382 100644 --- a/packages/genai/pubspec.yaml +++ b/packages/genai/pubspec.yaml @@ -11,9 +11,11 @@ environment: dependencies: flutter: sdk: flutter - shared_preferences: ^2.5.2 better_networking: path: ../better_networking + freezed_annotation: ^2.4.1 + json_annotation: ^4.9.0 + nanoid: ^1.0.0 dev_dependencies: flutter_test: @@ -22,5 +24,4 @@ dev_dependencies: flutter_lints: ^4.0.0 freezed: ^2.5.7 json_serializable: ^6.7.1 - -flutter: \ No newline at end of file + 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/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." From 8fa3433cf861991ca02e5a59a13df9869cb5bc50 Mon Sep 17 00:00:00 2001 From: Ankit Mahato Date: Wed, 27 Aug 2025 02:08:53 +0530 Subject: [PATCH 33/36] Update http_service.dart --- packages/better_networking/lib/services/http_service.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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); } From 6e1f2b47730752bf55cf8e225a1b81921ddc3fbd Mon Sep 17 00:00:00 2001 From: Ankit Mahato Date: Thu, 28 Aug 2025 06:42:32 +0530 Subject: [PATCH 34/36] Refactor AI model selection and config handling --- lib/consts.dart | 25 +- lib/main.dart | 6 +- lib/models/history_request_model.dart | 1 - lib/models/request_model.dart | 1 - lib/models/settings_model.dart | 44 +- lib/providers/ai_providers.dart | 5 + lib/providers/collection_providers.dart | 68 +-- lib/providers/providers.dart | 1 + lib/providers/settings_providers.dart | 7 +- lib/screens/common_widgets/ai/ai.dart | 3 + .../common_widgets/ai/ai_model_selector.dart | 41 ++ .../ai/ai_model_selector_button.dart | 41 ++ .../ai/ai_model_selector_dialog.dart | 243 +++++++++++ .../ai/dialog_add_ai_model.dart | 44 ++ .../common_widgets/api_type_dropdown.dart | 2 - .../common_widgets/common_widgets.dart | 1 + .../history_widgets/ai_history_page.dart | 103 ++--- .../history_widgets/his_request_pane.dart | 2 +- .../history/history_widgets/his_url_card.dart | 4 +- .../ai_request/aireq_authorization.dart | 27 +- .../ai_request/aireq_configs.dart | 122 +++--- .../request_pane/ai_request/aireq_prompt.dart | 43 +- .../ai_request/request_pane_ai.dart | 6 +- .../ai_request/widgets/llm_selector.dart | 388 ------------------ .../request_pane/request_pane.dart | 2 +- .../home_page/editor_pane/url_card.dart | 99 +---- lib/screens/settings_page.dart | 26 +- lib/widgets/field_text_bounded.dart | 61 +++ lib/widgets/response_body.dart | 25 +- lib/widgets/response_body_success.dart | 58 +-- lib/widgets/sse_display.dart | 33 +- lib/widgets/widgets.dart | 1 + packages/apidash_core/lib/apidash_core.dart | 1 + packages/apidash_core/pubspec.yaml | 2 + packages/apidash_core/pubspec_overrides.yaml | 4 +- .../lib/tokens/measurements.dart | 1 + packages/genai/genai_example/lib/main.dart | 19 +- packages/genai/lib/interface/consts.dart | 2 +- .../interface/model_providers/anthropic.dart | 32 +- .../model_providers/azureopenai.dart | 40 +- .../lib/interface/model_providers/gemini.dart | 35 +- .../lib/interface/model_providers/ollama.dart | 3 +- .../lib/interface/model_providers/openai.dart | 34 +- .../genai/lib/models/ai_request_model.dart | 37 +- .../lib/models/ai_request_model.freezed.dart | 252 +++++++++--- .../genai/lib/models/ai_request_model.g.dart | 30 +- .../genai/lib/models/available_models.dart | 11 + .../lib/models/available_models.freezed.dart | 8 +- packages/genai/lib/models/model_provider.dart | 4 +- .../genai/lib/models/model_request_data.dart | 33 -- .../models/model_request_data.freezed.dart | 339 --------------- .../lib/models/model_request_data.g.dart | 34 -- packages/genai/lib/models/models.dart | 1 - .../genai/lib/utils/ai_request_utils.dart | 5 +- pubspec.lock | 2 +- pubspec.yaml | 2 - 56 files changed, 1074 insertions(+), 1390 deletions(-) create mode 100644 lib/providers/ai_providers.dart create mode 100644 lib/screens/common_widgets/ai/ai.dart create mode 100644 lib/screens/common_widgets/ai/ai_model_selector.dart create mode 100644 lib/screens/common_widgets/ai/ai_model_selector_button.dart create mode 100644 lib/screens/common_widgets/ai/ai_model_selector_dialog.dart create mode 100644 lib/screens/common_widgets/ai/dialog_add_ai_model.dart delete mode 100644 lib/screens/home_page/editor_pane/details_card/request_pane/ai_request/widgets/llm_selector.dart create mode 100644 lib/widgets/field_text_bounded.dart delete mode 100644 packages/genai/lib/models/model_request_data.dart delete mode 100644 packages/genai/lib/models/model_request_data.freezed.dart delete mode 100644 packages/genai/lib/models/model_request_data.g.dart diff --git a/lib/consts.dart b/lib/consts.dart index 681d7698..71b88ee7 100644 --- a/lib/consts.dart +++ b/lib/consts.dart @@ -184,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 ]; @@ -201,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, @@ -231,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 a13230fc..c4457ad6 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,7 +1,6 @@ import 'package:apidash_design_system/apidash_design_system.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:genai/genai.dart'; import 'models/models.dart'; import 'providers/providers.dart'; import 'services/services.dart'; @@ -25,9 +24,8 @@ void main() async { settingsModel = settingsModel?.copyWithPath(workspaceFolderPath: null); } - //Load all LLMs - // await LLMManager.fetchAvailableLLMs(); - await LLMManager.loadAvailableLLMs(); + // TODO: Load all models at init + // await ModelManager.loadAvailableLLMs(); runApp( ProviderScope( diff --git a/lib/models/history_request_model.dart b/lib/models/history_request_model.dart index a2b3d84e..1169dc7e 100644 --- a/lib/models/history_request_model.dart +++ b/lib/models/history_request_model.dart @@ -1,5 +1,4 @@ import 'package:apidash_core/apidash_core.dart'; -import 'package:genai/genai.dart'; import 'models.dart'; part 'history_request_model.freezed.dart'; diff --git a/lib/models/request_model.dart b/lib/models/request_model.dart index 8513e938..25c36084 100644 --- a/lib/models/request_model.dart +++ b/lib/models/request_model.dart @@ -1,5 +1,4 @@ import 'package:apidash_core/apidash_core.dart'; -import 'package:genai/genai.dart'; part 'request_model.freezed.dart'; diff --git a/lib/models/settings_model.dart b/lib/models/settings_model.dart index 953746cf..47382f6e 100644 --- a/lib/models/settings_model.dart +++ b/lib/models/settings_model.dart @@ -1,7 +1,6 @@ import 'package:apidash_core/apidash_core.dart'; import 'package:flutter/material.dart'; import 'package:apidash/consts.dart'; -import 'package:genai/genai.dart'; @immutable class SettingsModel { @@ -19,7 +18,8 @@ class SettingsModel { this.workspaceFolderPath, this.isSSLDisabled = false, this.isDashBotEnabled = true, - this.defaultLLMSaveObject, + // TODO: Fix it + // this.defaultLLMSaveObject, }); final bool isDark; @@ -35,7 +35,8 @@ class SettingsModel { final String? workspaceFolderPath; final bool isSSLDisabled; final bool isDashBotEnabled; - final LLMSaveObject? defaultLLMSaveObject; + // TODO: Fix it + // final LLMSaveObject? defaultLLMSaveObject; SettingsModel copyWith({ bool? isDark, @@ -51,8 +52,9 @@ class SettingsModel { String? workspaceFolderPath, bool? isSSLDisabled, bool? isDashBotEnabled, - LLMSaveObject? def, - LLMSaveObject? defaultLLMSaveObject, + // TODO: Fix it + // LLMSaveObject? def, + // LLMSaveObject? defaultLLMSaveObject, }) { return SettingsModel( isDark: isDark ?? this.isDark, @@ -70,7 +72,8 @@ class SettingsModel { workspaceFolderPath: workspaceFolderPath ?? this.workspaceFolderPath, isSSLDisabled: isSSLDisabled ?? this.isSSLDisabled, isDashBotEnabled: isDashBotEnabled ?? this.isDashBotEnabled, - defaultLLMSaveObject: defaultLLMSaveObject ?? this.defaultLLMSaveObject, + // TODO: Fix it + // defaultLLMSaveObject: defaultLLMSaveObject ?? this.defaultLLMSaveObject, ); } @@ -91,7 +94,8 @@ class SettingsModel { workspaceFolderPath: workspaceFolderPath, isSSLDisabled: isSSLDisabled, isDashBotEnabled: isDashBotEnabled, - defaultLLMSaveObject: defaultLLMSaveObject, + // TODO: Fix it + // defaultLLMSaveObject: defaultLLMSaveObject, ); } @@ -148,11 +152,12 @@ class SettingsModel { final isSSLDisabled = data["isSSLDisabled"] as bool?; final isDashBotEnabled = data["isDashBotEnabled"] as bool?; - LLMSaveObject? defaultLLMSaveObject; - if (data["defaultLLMSaveObject"] != null) { - defaultLLMSaveObject = - LLMSaveObject.fromJSON(data["defaultLLMSaveObject"]); - } + // TODO: Fix it + // LLMSaveObject? defaultLLMSaveObject; + // if (data["defaultLLMSaveObject"] != null) { + // defaultLLMSaveObject = + // LLMSaveObject.fromJSON(data["defaultLLMSaveObject"]); + // } const sm = SettingsModel(); @@ -171,7 +176,8 @@ class SettingsModel { workspaceFolderPath: workspaceFolderPath, isSSLDisabled: isSSLDisabled, isDashBotEnabled: isDashBotEnabled, - defaultLLMSaveObject: defaultLLMSaveObject, + // TODO: Fix it + // defaultLLMSaveObject: defaultLLMSaveObject, ); } @@ -192,7 +198,8 @@ class SettingsModel { "workspaceFolderPath": workspaceFolderPath, "isSSLDisabled": isSSLDisabled, "isDashBotEnabled": isDashBotEnabled, - 'defaultLLMSaveObject': defaultLLMSaveObject?.toJSON(), + // TODO: Fix it + // 'defaultLLMSaveObject': defaultLLMSaveObject?.toJSON(), }; } @@ -218,8 +225,10 @@ class SettingsModel { other.historyRetentionPeriod == historyRetentionPeriod && other.workspaceFolderPath == workspaceFolderPath && other.isSSLDisabled == isSSLDisabled && - other.isDashBotEnabled == isDashBotEnabled && - other.defaultLLMSaveObject == defaultLLMSaveObject; + other.isDashBotEnabled == isDashBotEnabled; + // TODO: Fix it + // && + // other.defaultLLMSaveObject == defaultLLMSaveObject; } @override @@ -239,7 +248,8 @@ class SettingsModel { workspaceFolderPath, isSSLDisabled, isDashBotEnabled, - defaultLLMSaveObject, + // TODO: Fix it + // defaultLLMSaveObject, ); } } 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 fec5f0e0..4ef67bf9 100644 --- a/lib/providers/collection_providers.dart +++ b/lib/providers/collection_providers.dart @@ -1,12 +1,8 @@ import 'dart:async'; -import 'dart:convert'; - import 'package:apidash_core/apidash_core.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:apidash/consts.dart'; -import 'package:genai/genai.dart'; -import 'package:genai/models/ai_request_model.dart'; import 'providers.dart'; import '../models/models.dart'; import '../services/services.dart'; @@ -141,7 +137,6 @@ class CollectionStateNotifier final rId = id ?? ref.read(selectedIdStateProvider); if (rId == null || state?[rId] == null) return; var currentModel = state![rId]!; - final newModel = currentModel.copyWith( responseStatus: null, message: null, @@ -168,8 +163,8 @@ class CollectionStateNotifier requestTabIndex: 0, responseStatus: null, message: null, - httpRequestModel: currentModel.httpRequestModel, - aiRequestModel: currentModel.aiRequestModel?.clone(), + httpRequestModel: currentModel.httpRequestModel?.copyWith(), + aiRequestModel: currentModel.aiRequestModel?.copyWith(), httpResponseModel: null, isWorking: false, sendingTime: null, @@ -191,14 +186,13 @@ class CollectionStateNotifier var itemIds = ref.read(requestSequenceProvider); var currentModel = historyRequestModel; - final aT = currentModel.aiRequestModel != null ? APIType.ai : APIType.rest; - final newModel = RequestModel( - apiType: aT, + apiType: currentModel.metaData.apiType, id: newId, name: "${currentModel.metaData.name} (history)", - aiRequestModel: currentModel.aiRequestModel?.clone(), - httpRequestModel: currentModel.httpRequestModel ?? HttpRequestModel(), + aiRequestModel: currentModel.aiRequestModel?.copyWith(), + httpRequestModel: + currentModel.httpRequestModel?.copyWith() ?? HttpRequestModel(), responseStatus: currentModel.metaData.responseStatus, message: kResponseCodeReasons[currentModel.metaData.responseStatus], httpResponseModel: currentModel.httpResponseModel, @@ -217,9 +211,9 @@ class CollectionStateNotifier } void update({ + APIType? apiType, String? id, HTTPVerb? method, - APIType? apiType, AuthModel? authModel, String? url, String? name, @@ -245,21 +239,7 @@ class CollectionStateNotifier debugPrint("Unable to update as Request Id is null"); return; } - var currentModel = state![rId]!; - - if (apiType == APIType.ai) { - //Adding default AI Request Modoel - AIRequestModel? aiRM = currentModel.aiRequestModel; - LLMSaveObject? defaultLLMSO = ref - .watch(settingsProvider.notifier) - .settingsModel - ?.defaultLLMSaveObject; //Settings Default - if (aiRM == null) { - aiRequestModel = AIRequestModel.fromDefaultSaveObject(defaultLLMSO); - } - } - var currentHttpRequestModel = currentModel.httpRequestModel; final newModel = currentModel.copyWith( apiType: apiType ?? currentModel.apiType, @@ -332,14 +312,12 @@ class CollectionStateNotifier } APIType apiType = executionRequestModel.apiType; - AIRequestModel? aiRequestModel; bool noSSL = ref.read(settingsProvider).isSSLDisabled; HttpRequestModel substitutedHttpRequestModel; if (apiType == APIType.ai) { - aiRequestModel = requestModel.aiRequestModel!; - substitutedHttpRequestModel = - getSubstitutedHttpRequestModel(aiRequestModel.convertToHTTPRequest()); + substitutedHttpRequestModel = getSubstitutedHttpRequestModel( + executionRequestModel.aiRequestModel!.httpRequestModel!); } else { substitutedHttpRequestModel = getSubstitutedHttpRequestModel( executionRequestModel.httpRequestModel!); @@ -379,14 +357,6 @@ class CollectionStateNotifier final duration = rec.$3; final errorMessage = rec.$4; - if (isStreamingResponse == false) { - streamingMode = false; - if (!completer.isCompleted) { - completer.complete((response, duration, errorMessage)); - } - return; - } - if (isStreamingResponse) { httpResponseModel = httpResponseModel?.copyWith( time: duration, @@ -413,6 +383,8 @@ class CollectionStateNotifier .read(historyMetaStateNotifier.notifier) .editHistoryRequest(historyModel!); } + } else { + streamingMode = false; } if (!completer.isCompleted) { @@ -449,16 +421,12 @@ class CollectionStateNotifier ); //AI-FORMATTING for Non Streaming Varaint - if (streamingMode == false && apiType == APIType.ai) { - final mT = httpResponseModel?.mediaType; - final body = (mT?.subtype == kSubTypeJson) - ? utf8.decode(response.bodyBytes) - : response.body; - - final fb = response.statusCode == 200 - ? aiRequestModel?.model.provider.modelController - .outputFormatter(jsonDecode(body)) - : formatBody(body, mT); + 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); } @@ -483,7 +451,7 @@ class CollectionStateNotifier timeStamp: DateTime.now(), ), httpRequestModel: substitutedHttpRequestModel, - aiRequestModel: aiRequestModel, + 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 3f9e39a8..f4493abb 100644 --- a/lib/providers/settings_providers.dart +++ b/lib/providers/settings_providers.dart @@ -1,7 +1,6 @@ import 'package:apidash_core/apidash_core.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:genai/genai.dart'; import '../models/models.dart'; import '../services/services.dart'; import '../consts.dart'; @@ -35,7 +34,8 @@ class ThemeStateNotifier extends StateNotifier { String? workspaceFolderPath, bool? isSSLDisabled, bool? isDashBotEnabled, - LLMSaveObject? defaultLLMSaveObject, + // TODO: Fix it + // LLMSaveObject? defaultLLMSaveObject, }) async { state = state.copyWith( isDark: isDark, @@ -51,7 +51,8 @@ class ThemeStateNotifier extends StateNotifier { workspaceFolderPath: workspaceFolderPath, isSSLDisabled: isSSLDisabled, isDashBotEnabled: isDashBotEnabled, - defaultLLMSaveObject: defaultLLMSaveObject, + // TODO: Fix it + // defaultLLMSaveObject: defaultLLMSaveObject, ); 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..2ba45581 --- /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..9b6b53cb --- /dev/null +++ b/lib/screens/common_widgets/ai/ai_model_selector_dialog.dart @@ -0,0 +1,243 @@ +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(); + 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/api_type_dropdown.dart b/lib/screens/common_widgets/api_type_dropdown.dart index 7085049e..c35645d6 100644 --- a/lib/screens/common_widgets/api_type_dropdown.dart +++ b/lib/screens/common_widgets/api_type_dropdown.dart @@ -1,9 +1,7 @@ -import 'package:apidash_core/apidash_core.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:apidash/providers/providers.dart'; import 'package:apidash/widgets/widgets.dart'; -import 'package:genai/genai.dart'; class APITypeDropdown extends ConsumerWidget { const APITypeDropdown({super.key}); 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 index 72bb98bd..1a799b39 100644 --- a/lib/screens/history/history_widgets/ai_history_page.dart +++ b/lib/screens/history/history_widgets/ai_history_page.dart @@ -1,12 +1,9 @@ -import 'package:apidash/providers/collection_providers.dart'; -import 'package:apidash/providers/history_providers.dart'; -import 'package:apidash/widgets/editor.dart'; -import 'package:apidash_design_system/tokens/measurements.dart'; -import 'package:apidash_design_system/widgets/textfield_outlined.dart'; +import 'package: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:genai/llm_config.dart'; -import 'package:genai/widgets/ai_config_widgets.dart'; +import 'package:apidash/providers/providers.dart'; +import 'package:apidash/widgets/editor.dart'; class HisAIRequestPromptSection extends ConsumerWidget { const HisAIRequestPromptSection({super.key}); @@ -15,11 +12,10 @@ class HisAIRequestPromptSection extends ConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final selectedHistoryModel = ref.watch(selectedHistoryRequestModelProvider)!; - - final aiReqM = selectedHistoryModel.aiRequestModel!; - final payload = aiReqM.payload; - final systemPrompt = payload.systemPrompt; - final userPrompt = payload.userPrompt; + final aiReqM = selectedHistoryModel.aiRequestModel; + if (aiReqM == null) { + return kSizedBoxEmpty; + } return Container( padding: EdgeInsets.symmetric(vertical: 20), @@ -41,7 +37,7 @@ class HisAIRequestPromptSection extends ConsumerWidget { "${selectedHistoryModel.historyId}-aireq-sysprompt-body"), fieldKey: "${selectedHistoryModel.historyId}-aireq-sysprompt-body", - initialValue: systemPrompt, + initialValue: aiReqM.systemPrompt, readOnly: true, ), ), @@ -62,7 +58,7 @@ class HisAIRequestPromptSection extends ConsumerWidget { "${selectedHistoryModel.historyId}-aireq-userprompt-body"), fieldKey: "${selectedHistoryModel.historyId}-aireq-userprompt-body", - initialValue: userPrompt, + initialValue: aiReqM.userPrompt, readOnly: true, ), ), @@ -80,12 +76,10 @@ class HisAIRequestAuthorizationSection extends ConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final selectedHistoryModel = ref.watch(selectedHistoryRequestModelProvider)!; - - final aiReqM = selectedHistoryModel.aiRequestModel!; - - final payload = aiReqM.payload; - - final cred = payload.credential; + final aiReqM = selectedHistoryModel.aiRequestModel; + if (aiReqM == null) { + return kSizedBoxEmpty; + } return Container( padding: EdgeInsets.symmetric(vertical: 20), @@ -99,7 +93,7 @@ class HisAIRequestAuthorizationSection extends ConsumerWidget { "${selectedHistoryModel.historyId}-aireq-authvalue-body"), fieldKey: "${selectedHistoryModel.historyId}-aireq-authvalue-body", - initialValue: cred, + initialValue: aiReqM.apiKey, readOnly: true, ), ), @@ -117,55 +111,48 @@ class HisAIRequestConfigSection extends ConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final selectedHistoryModel = ref.watch(selectedHistoryRequestModelProvider)!; - - final aiReqM = selectedHistoryModel.aiRequestModel!; - - final payload = aiReqM.payload; - + final aiReqM = selectedHistoryModel.aiRequestModel; + if (aiReqM == null) { + return kSizedBoxEmpty; + } return SingleChildScrollView( padding: EdgeInsets.symmetric(vertical: 20), child: Column( key: ValueKey(selectedHistoryModel.historyId), children: [ - ...payload.configMap.values.map( + ...aiReqM.modelConfigs.map( (el) => ListTile( - title: Text(el.configName), + title: Text(el.name), subtitle: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - el.configDescription, + el.description, ), SizedBox(height: 5), - if (el.configType == LLMModelConfigurationType.boolean) ...[ - BooleanAIConfig( - readonly: true, - configuration: el, - onConfigUpdated: (x) {}, - ), - ] else if (el.configType == - LLMModelConfigurationType.numeric) ...[ - WritableAIConfig( - configuration: el, - onConfigUpdated: (x) {}, - readonly: true, - numeric: true, - ), - ] else if (el.configType == - LLMModelConfigurationType.text) ...[ - WritableAIConfig( - configuration: el, - onConfigUpdated: (x) {}, - readonly: true, - ), - ] else if (el.configType == - LLMModelConfigurationType.slider) ...[ - SliderAIConfig( - configuration: el, - onSliderUpdated: (x) {}, - readonly: true, - ), - ], + 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 859a2378..0b126600 100644 --- a/lib/screens/history/history_widgets/his_request_pane.dart +++ b/lib/screens/history/history_widgets/his_request_pane.dart @@ -1,4 +1,3 @@ -import 'package:apidash/screens/history/history_widgets/ai_history_page.dart'; import 'package:apidash_core/apidash_core.dart'; import 'package:apidash_design_system/apidash_design_system.dart'; import 'package:flutter/material.dart'; @@ -7,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 { diff --git a/lib/screens/history/history_widgets/his_url_card.dart b/lib/screens/history/history_widgets/his_url_card.dart index f6cb74b5..5825e31f 100644 --- a/lib/screens/history/history_widgets/his_url_card.dart +++ b/lib/screens/history/history_widgets/his_url_card.dart @@ -1,7 +1,7 @@ -import 'package:apidash/screens/home_page/editor_pane/url_card.dart'; import 'package:apidash_core/apidash_core.dart'; import 'package:apidash_design_system/apidash_design_system.dart'; import 'package:flutter/material.dart'; +import 'package:apidash/screens/common_widgets/common_widgets.dart'; import 'package:apidash/widgets/widgets.dart'; import 'package:apidash/models/models.dart'; import 'package:apidash/utils/utils.dart'; @@ -60,7 +60,7 @@ class HistoryURLCard extends StatelessWidget { isCompact ? kHSpacer10 : kHSpacer20, ], if (apiType == APIType.ai) ...[ - AIProviderSelector( + AIModelSelector( readOnlyModel: historyRequestModel?.aiRequestModel, ), SizedBox(width: 20), 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 index f17f6101..51ff7ba8 100644 --- 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 @@ -1,5 +1,6 @@ -import 'package:apidash/providers/collection_providers.dart'; -import 'package:apidash/widgets/editor.dart'; +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'; @@ -9,11 +10,15 @@ class AIRequestAuthorizationSection extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final selectedId = ref.watch(selectedIdStateProvider); - final reqM = ref.read(collectionStateNotifierProvider)![selectedId]!; - final aiReqM = reqM.aiRequestModel!; - final payload = aiReqM.payload; - - final cred = payload.credential; + 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), @@ -25,15 +30,11 @@ class AIRequestAuthorizationSection extends ConsumerWidget { child: TextFieldEditor( key: Key("$selectedId-aireq-authvalue-body"), fieldKey: "$selectedId-aireq-authvalue-body", - initialValue: cred, + initialValue: apiKey, onChanged: (String value) { - final aim = ref - .read(collectionStateNotifierProvider)![selectedId]! - .aiRequestModel!; - aim.payload.credential = value; ref .read(collectionStateNotifierProvider.notifier) - .update(aiRequestModel: aim.updatePayload(aim.payload)); + .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 index 2fcba89b..94e68307 100644 --- 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 @@ -1,36 +1,38 @@ -import 'package:apidash/providers/collection_providers.dart'; -import 'package:apidash/widgets/editor.dart'; -import 'package:apidash_design_system/widgets/textfield_outlined.dart'; +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'; -import 'package:genai/genai.dart'; -import 'package:genai/widgets/ai_config_widgets.dart'; -class AIRequestConfigSection extends ConsumerStatefulWidget { +class AIRequestConfigSection extends ConsumerWidget { const AIRequestConfigSection({super.key}); @override - ConsumerState createState() => - _AIRequestConfigSectionState(); -} - -class _AIRequestConfigSectionState - extends ConsumerState { - @override - Widget build(BuildContext context) { + Widget build(BuildContext context, WidgetRef ref) { final selectedId = ref.watch(selectedIdStateProvider); - final reqM = ref.read(collectionStateNotifierProvider)![selectedId]!; - final aiReqM = reqM.aiRequestModel!; - final payload = aiReqM.payload; + 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(LLMModelConfiguration el) { - final aim = ref - .read(collectionStateNotifierProvider)![selectedId]! - .aiRequestModel!; - aim.payload.configMap[el.configId] = el; - ref.read(collectionStateNotifierProvider.notifier).update( - aiRequestModel: aim.updatePayload(aim.payload), - ); + 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( @@ -38,53 +40,43 @@ class _AIRequestConfigSectionState child: Column( key: ValueKey(selectedId), children: [ - ...payload.configMap.values.map( + ...modelConfigs.map( (el) => ListTile( - title: Text(el.configName), + title: Text(el.name), subtitle: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - el.configDescription, + el.description, ), SizedBox(height: 5), - if (el.configType == LLMModelConfigurationType.boolean) ...[ - BooleanAIConfig( - configuration: el, - onConfigUpdated: (x) { - updateRequestModel(el); - setState(() {}); - }, - ), - ] else if (el.configType == - LLMModelConfigurationType.numeric) ...[ - WritableAIConfig( - configuration: el, - onConfigUpdated: (x) { - updateRequestModel(el); - setState(() {}); - }, - numeric: true, - ), - ] else if (el.configType == - LLMModelConfigurationType.text) ...[ - WritableAIConfig( - configuration: el, - onConfigUpdated: (x) { - updateRequestModel(el); - setState(() {}); - }, - ), - ] else if (el.configType == - LLMModelConfigurationType.slider) ...[ - SliderAIConfig( - configuration: el, - onSliderUpdated: (x) { - updateRequestModel(x); - setState(() {}); - }, - ), - ], + 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 index ce94f44e..d63d153e 100644 --- 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 @@ -1,6 +1,6 @@ -import 'package:apidash/providers/collection_providers.dart'; -import 'package:apidash/widgets/editor.dart'; -import 'package:apidash_design_system/tokens/measurements.dart'; +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'; @@ -10,12 +10,17 @@ class AIRequestPromptSection extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final selectedId = ref.watch(selectedIdStateProvider); - final reqM = ref.read(collectionStateNotifierProvider)![selectedId]!; - final aiReqM = reqM.aiRequestModel!; - final payload = aiReqM.payload; - - final systemPrompt = payload.systemPrompt; - final userPrompt = payload.userPrompt; + 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), @@ -37,13 +42,9 @@ class AIRequestPromptSection extends ConsumerWidget { fieldKey: "$selectedId-aireq-sysprompt-body", initialValue: systemPrompt, onChanged: (String value) { - final aim = ref - .read(collectionStateNotifierProvider)![selectedId]! - .aiRequestModel!; - aim.payload.systemPrompt = value; - ref - .read(collectionStateNotifierProvider.notifier) - .update(aiRequestModel: aim.updatePayload(aim.payload)); + ref.read(collectionStateNotifierProvider.notifier).update( + aiRequestModel: + aiRequestModel.copyWith(systemPrompt: value)); }, hintText: 'Enter System Prompt', ), @@ -65,13 +66,9 @@ class AIRequestPromptSection extends ConsumerWidget { fieldKey: "$selectedId-aireq-userprompt-body", initialValue: userPrompt, onChanged: (String value) { - final aim = ref - .read(collectionStateNotifierProvider)![selectedId]! - .aiRequestModel!; - aim.payload.userPrompt = value; - ref - .read(collectionStateNotifierProvider.notifier) - .update(aiRequestModel: aim.updatePayload(aim.payload)); + 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 index 31a490b8..29dffd06 100644 --- 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 @@ -1,10 +1,10 @@ -import 'package:apidash/screens/home_page/editor_pane/details_card/request_pane/ai_request/aireq_authorization.dart'; -import 'package:apidash/screens/home_page/editor_pane/details_card/request_pane/ai_request/aireq_configs.dart'; -import 'package:apidash/screens/home_page/editor_pane/details_card/request_pane/ai_request/aireq_prompt.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:apidash/providers/providers.dart'; import 'package:apidash/widgets/widgets.dart'; +import 'aireq_authorization.dart'; +import 'aireq_configs.dart'; +import 'aireq_prompt.dart'; class EditAIRequestPane extends ConsumerWidget { const EditAIRequestPane({super.key}); diff --git a/lib/screens/home_page/editor_pane/details_card/request_pane/ai_request/widgets/llm_selector.dart b/lib/screens/home_page/editor_pane/details_card/request_pane/ai_request/widgets/llm_selector.dart deleted file mode 100644 index a70a4d11..00000000 --- a/lib/screens/home_page/editor_pane/details_card/request_pane/ai_request/widgets/llm_selector.dart +++ /dev/null @@ -1,388 +0,0 @@ -import 'package:apidash_design_system/apidash_design_system.dart'; -import 'package:genai/genai.dart'; -import 'package:flutter/material.dart'; - -class DefaultLLMSelectorButton extends StatelessWidget { - final LLMSaveObject? defaultLLM; - final bool readonly; - final Function(LLMSaveObject) onDefaultLLMUpdated; - const DefaultLLMSelectorButton({ - super.key, - this.defaultLLM, - this.readonly = false, - required this.onDefaultLLMUpdated, - }); - - @override - Widget build(BuildContext context) { - return ElevatedButton( - onPressed: readonly - ? null - : () async { - final saveObject = await showDialog( - context: context, - builder: (context) { - return AlertDialog( - scrollable: true, - content: DefaultLLMSelectorDialog(defaultLLM: defaultLLM), - contentPadding: EdgeInsets.all(10), - ); - }, - ); - if (saveObject == null) return; - onDefaultLLMUpdated(saveObject); - }, - child: Text(defaultLLM?.selectedLLM.modelName ?? 'Select Model'), - ); - } -} - -class DefaultLLMSelectorDialog extends StatefulWidget { - final LLMSaveObject? defaultLLM; - const DefaultLLMSelectorDialog({super.key, this.defaultLLM}); - - @override - State createState() => - _DefaultLLMSelectorDialogState(); -} - -class _DefaultLLMSelectorDialogState extends State { - late LLMProvider selectedLLMProvider; - late LLMSaveObject llmSaveObject; - bool initialized = false; - - initialize() async { - final iP = LLMProvider.gemini.modelController.inputPayload; - llmSaveObject = widget.defaultLLM ?? - LLMSaveObject( - endpoint: iP.endpoint, - credential: '', - configMap: iP.configMap, - selectedLLM: - LLMProvider.gemini.getLLMByIdentifier('gemini-2.0-flash'), - provider: LLMProvider.ollama, - ); - selectedLLMProvider = llmSaveObject.provider; - initialized = true; - setState(() {}); - } - - @override - void initState() { - super.initState(); - initialize(); - } - - @override - Widget build(BuildContext context) { - if (!initialized) return SizedBox(); - - if (context.isMediumWindow) { - return Container( - padding: EdgeInsets.all(20), - width: MediaQuery.of(context).size.width * 0.8, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - ElevatedButton( - onPressed: () async { - await LLMManager.fetchAvailableLLMs(); - setState(() {}); - }, - child: Text('Fetch Models'), - ), - kVSpacer10, - Row( - children: [ - Text('Select Provider'), - kHSpacer20, - Expanded( - child: ADDropdownButton( - onChanged: (x) { - if (x == null) return; - selectedLLMProvider = x; - final models = x.models; - final mC = x.modelController; - final p = mC.inputPayload; - llmSaveObject = LLMSaveObject( - endpoint: p.endpoint, - credential: '', - configMap: p.configMap, - selectedLLM: models.first, - provider: x, - ); - setState(() {}); - }, - value: selectedLLMProvider, - values: LLMProvider.values - .where(((e) => e.models.isNotEmpty)) - .map((e) => (e, e.displayName)), - ), - ), - ], - ), - kVSpacer10, - _buildModelSelector(), - ], - ), - ); - } - - return Container( - padding: EdgeInsets.all(20), - width: MediaQuery.of(context).size.width * 0.8, - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Flexible( - flex: 1, - child: SingleChildScrollView( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - ElevatedButton( - onPressed: () async { - await LLMManager.fetchAvailableLLMs(); - setState(() {}); - }, - child: Text('Fetch Models'), - ), - SizedBox(height: 20), - ...LLMProvider.values.where(((e) => e.models.isNotEmpty)).map( - (x) => ListTile( - title: Text(x.displayName), - trailing: llmSaveObject.provider != x - ? null - : CircleAvatar( - radius: 5, - backgroundColor: Colors.green, - ), - onTap: () { - selectedLLMProvider = x; - final models = x.models; - final mC = x.modelController; - final p = mC.inputPayload; - llmSaveObject = LLMSaveObject( - endpoint: p.endpoint, - credential: '', - configMap: p.configMap, - selectedLLM: models.first, - provider: x, - ); - setState(() {}); - }, - ), - ), - ], - ), - ), - ), - SizedBox(width: 40), - Flexible( - flex: 3, - child: _buildModelSelector(), - ), - ], - ), - ); - } - - _buildModelSelector() { - return Container( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.max, - children: [ - Text( - selectedLLMProvider.displayName, - style: TextStyle(fontSize: 28), - ), - SizedBox(height: 20), - if (selectedLLMProvider != LLMProvider.ollama) ...[ - Text('API Key / Credential'), - kVSpacer8, - BoundedTextField( - onChanged: (x) { - llmSaveObject.credential = x; - setState(() {}); - }, - value: llmSaveObject.credential, - ), - kVSpacer10, - ], - Text('Endpoint'), - kVSpacer8, - BoundedTextField( - key: ValueKey(llmSaveObject.provider), - onChanged: (x) { - llmSaveObject.endpoint = x; - setState(() {}); - }, - value: llmSaveObject.endpoint, - ), - kVSpacer20, - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text('Models'), - IconButton(onPressed: addNewModel, 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: [ - ...selectedLLMProvider.models.map( - (x) => ListTile( - title: Text(x.modelName), - subtitle: Text(x.identifier), - trailing: Row( - mainAxisSize: MainAxisSize.min, - children: [ - if (llmSaveObject.selectedLLM.identifier == - x.identifier) - CircleAvatar( - radius: 5, - backgroundColor: Colors.green, - ), - ], - ), - onTap: () { - llmSaveObject.selectedLLM = x; - setState(() {}); - }, - ), - ), - ], - ), - ), - ), - ), - kVSpacer10, - Align( - alignment: Alignment.centerRight, - child: ElevatedButton( - onPressed: () { - llmSaveObject.provider = selectedLLMProvider; - Navigator.of(context).pop(llmSaveObject); - }, - child: Text('Save Changes'), - ), - ), - ], - ), - ); - } - - addNewModel() async { - TextEditingController iC = TextEditingController(); - TextEditingController nC = TextEditingController(); - final z = await showDialog( - context: context, - builder: (context) { - return AlertDialog( - title: Text('Add Custom Model'), - content: Container( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - ADOutlinedTextField( - controller: iC, - hintText: 'Model ID', - ), - kVSpacer10, - ADOutlinedTextField( - controller: nC, - hintText: 'Model Display Name', - ), - kVSpacer10, - Container( - width: double.infinity, - child: ElevatedButton( - onPressed: () { - Navigator.of(context).pop([ - iC.value.text, - nC.value.text, - ]); - }, - child: Text('Add Model'), - ), - ) - ], - ), - ), - ); - }); - if (z == null) return; - await LLMManager.addLLM(selectedLLMProvider.name, z[0], z[1]); - setState(() {}); - } -} - -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/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 40216103..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 @@ -1,9 +1,9 @@ -import 'package:apidash/screens/home_page/editor_pane/details_card/request_pane/ai_request/request_pane_ai.dart'; import 'package:apidash_core/apidash_core.dart'; import 'package:apidash_design_system/apidash_design_system.dart'; import 'package:flutter/material.dart'; 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'; diff --git a/lib/screens/home_page/editor_pane/url_card.dart b/lib/screens/home_page/editor_pane/url_card.dart index 6428820f..db9ab0f7 100644 --- a/lib/screens/home_page/editor_pane/url_card.dart +++ b/lib/screens/home_page/editor_pane/url_card.dart @@ -1,11 +1,9 @@ -import 'package:apidash/screens/home_page/editor_pane/details_card/request_pane/ai_request/widgets/llm_selector.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'; import 'package:apidash/providers/providers.dart'; import 'package:apidash/widgets/widgets.dart'; -import 'package:genai/genai.dart'; import '../../common_widgets/common_widgets.dart'; class EditorPaneRequestURLCard extends ConsumerWidget { @@ -16,8 +14,6 @@ class EditorPaneRequestURLCard extends ConsumerWidget { ref.watch(selectedIdStateProvider); final apiType = ref .watch(selectedRequestModelProvider.select((value) => value?.apiType)); - final aiHC = ref.watch(selectedRequestModelProvider - .select((v) => v?.aiRequestModel?.hashCode)); return Card( color: kColorTransparent, surfaceTintColor: kColorTransparent, @@ -39,17 +35,15 @@ class EditorPaneRequestURLCard extends ConsumerWidget { switch (apiType) { APIType.rest => const DropdownButtonHTTPMethod(), APIType.graphql => kSizedBoxEmpty, - APIType.ai => const AIProviderSelector(), + APIType.ai => const AIModelSelector(), null => kSizedBoxEmpty, }, switch (apiType) { APIType.rest => kHSpacer5, _ => kHSpacer8, }, - Expanded( - child: URLTextField( - key: aiHC == null ? null : ValueKey(aiHC), - ), + const Expanded( + child: URLTextField(), ), ], ) @@ -58,17 +52,15 @@ class EditorPaneRequestURLCard extends ConsumerWidget { switch (apiType) { APIType.rest => const DropdownButtonHTTPMethod(), APIType.graphql => kSizedBoxEmpty, - APIType.ai => const AIProviderSelector(), + APIType.ai => const AIModelSelector(), null => kSizedBoxEmpty, }, switch (apiType) { APIType.rest => kHSpacer20, _ => kHSpacer8, }, - Expanded( - child: URLTextField( - key: aiHC == null ? null : ValueKey(aiHC), - ), + const Expanded( + child: URLTextField(), ), kHSpacer20, const SizedBox( @@ -110,26 +102,20 @@ class URLTextField extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final selectedId = ref.watch(selectedIdStateProvider); - - final reqM = ref.read(collectionStateNotifierProvider)![selectedId]!; - final aiReqM = reqM.aiRequestModel; - final payload = aiReqM?.payload; - + final requestModel = ref + .read(collectionStateNotifierProvider.notifier) + .getRequestModel(selectedId!)!; return EnvURLField( - selectedId: selectedId!, - initialValue: payload?.endpoint ?? - ref - .read(collectionStateNotifierProvider.notifier) - .getRequestModel(selectedId) - ?.httpRequestModel - ?.url, + selectedId: selectedId, + initialValue: switch (requestModel.apiType) { + APIType.ai => requestModel.aiRequestModel?.url, + _ => requestModel.httpRequestModel?.url, + }, onChanged: (value) { - if (aiReqM != null) { - // Handle AI Endpoint Changes - aiReqM.payload.endpoint = value; - ref - .read(collectionStateNotifierProvider.notifier) - .update(aiRequestModel: aiReqM.updatePayload(aiReqM.payload)); + if (requestModel.apiType == APIType.ai) { + ref.read(collectionStateNotifierProvider.notifier).update( + aiRequestModel: + requestModel.aiRequestModel?.copyWith(url: value)); } else { ref.read(collectionStateNotifierProvider.notifier).update(url: value); } @@ -169,52 +155,3 @@ class SendRequestButton extends ConsumerWidget { ); } } - -class AIProviderSelector extends ConsumerWidget { - final AIRequestModel? readOnlyModel; - - const AIProviderSelector({ - super.key, - this.readOnlyModel, - }); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final selectedId = ref.watch(selectedIdStateProvider); - final req = ref.watch(collectionStateNotifierProvider)![selectedId]!; - AIRequestModel? aiRequestModel = readOnlyModel ?? req.aiRequestModel; - - if (aiRequestModel == null) { - return Container(); - } - - LLMSaveObject defaultLLMSO = LLMSaveObject( - endpoint: aiRequestModel.payload.endpoint, - credential: aiRequestModel.payload.credential, - configMap: aiRequestModel.payload.configMap, - selectedLLM: aiRequestModel.model, - provider: aiRequestModel.provider, - ); - - return DefaultLLMSelectorButton( - readonly: (readOnlyModel != null), - key: ValueKey(ref.watch(selectedIdStateProvider)), - defaultLLM: defaultLLMSO, - onDefaultLLMUpdated: (llmso) { - ref.read(collectionStateNotifierProvider.notifier).update( - aiRequestModel: AIRequestModel( - model: llmso.selectedLLM, - provider: llmso.provider, - payload: LLMInputPayload( - endpoint: llmso.endpoint, - credential: llmso.credential, - systemPrompt: aiRequestModel.payload.systemPrompt, - userPrompt: aiRequestModel.payload.userPrompt, - configMap: llmso.configMap, - ), - ), - ); - }, - ); - } -} diff --git a/lib/screens/settings_page.dart b/lib/screens/settings_page.dart index 91641db6..8131aadd 100644 --- a/lib/screens/settings_page.dart +++ b/lib/screens/settings_page.dart @@ -1,4 +1,3 @@ -import 'package:apidash/screens/home_page/editor_pane/details_card/request_pane/ai_request/widgets/llm_selector.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; @@ -115,18 +114,19 @@ class SettingsPage extends ConsumerWidget { }, ), ), - ListTile( - hoverColor: kColorTransparent, - title: const Text('Default Large Language Model (LLM)'), - trailing: DefaultLLMSelectorButton( - defaultLLM: settings.defaultLLMSaveObject, - onDefaultLLMUpdated: (d) { - ref - .read(settingsProvider.notifier) - .update(defaultLLMSaveObject: d); - }, - ), - ), + // TODO: Fix it + // ListTile( + // hoverColor: kColorTransparent, + // title: const Text('Default Large Language Model (LLM)'), + // trailing: DefaultLLMSelectorButton( + // defaultLLM: settings.defaultLLMSaveObject, + // onDefaultLLMUpdated: (d) { + // ref + // .read(settingsProvider.notifier) + // .update(defaultLLMSaveObject: d); + // }, + // ), + // ), CheckboxListTile( title: const Text("Save Responses"), subtitle: 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 84774837..7a679d8c 100644 --- a/lib/widgets/response_body.dart +++ b/lib/widgets/response_body.dart @@ -1,5 +1,3 @@ -import 'dart:convert'; - import 'package:flutter/material.dart'; import 'package:apidash_core/apidash_core.dart'; import 'package:apidash/models/models.dart'; @@ -19,7 +17,6 @@ class ResponseBody extends StatelessWidget { @override Widget build(BuildContext context) { final responseModel = selectedRequestModel?.httpResponseModel; - if (responseModel == null) { return const ErrorMessage( message: '$kNullResponseModelError $kUnexpectedRaiseIssue'); @@ -28,7 +25,6 @@ class ResponseBody extends StatelessWidget { final isSSE = responseModel.sseOutput?.isNotEmpty ?? false; var body = responseModel.body; var formattedBody = responseModel.formattedBody; - if (body == null) { return const ErrorMessage( message: '$kMsgNullBody $kUnexpectedRaiseIssue'); @@ -46,7 +42,6 @@ class ResponseBody extends StatelessWidget { final mediaType = responseModel.mediaType ?? MediaType(kTypeText, kSubTypePlain); - // Fix #415: Treat null Content-type as plain text instead of Error message // if (mediaType == null) { // return ErrorMessage( @@ -54,8 +49,9 @@ class ResponseBody extends StatelessWidget { // '$kMsgUnknowContentType - ${responseModel.contentType}. $kUnexpectedRaiseIssue'); // } - var responseBodyView = selectedRequestModel?.apiType == APIType.ai - ? ([ResponseBodyView.answer, ResponseBodyView.raw], 'text') + var responseBodyView = (selectedRequestModel?.apiType == APIType.ai && + (responseModel.sseOutput?.isNotEmpty ?? false)) + ? (kAnswerRawBodyViewOptions, kSubTypePlain) : getResponseBodyViewOptions(mediaType); var options = responseBodyView.$1; var highlightLanguage = responseBodyView.$2; @@ -65,18 +61,6 @@ class ResponseBody extends StatelessWidget { options.remove(ResponseBodyView.code); } - if (responseModel.sseOutput?.isNotEmpty ?? false) { - return ResponseBodySuccess( - key: Key("${selectedRequestModel!.id}-response"), - mediaType: MediaType('text', 'event-stream'), - options: [ResponseBodyView.sse, ResponseBodyView.raw], - bytes: utf8.encode((responseModel.sseOutput!).toString()), - body: jsonEncode(responseModel.sseOutput!), - formattedBody: responseModel.sseOutput!.join('\n'), - selectedModel: selectedRequestModel?.aiRequestModel?.model, - ); - } - return ResponseBodySuccess( key: Key("${selectedRequestModel!.id}-response"), mediaType: mediaType, @@ -85,7 +69,8 @@ class ResponseBody extends StatelessWidget { body: body, formattedBody: formattedBody, highlightLanguage: highlightLanguage, - selectedModel: selectedRequestModel?.aiRequestModel?.model, + sseOutput: responseModel.sseOutput, + aiRequestModel: selectedRequestModel?.aiRequestModel, ); } } diff --git a/lib/widgets/response_body_success.dart b/lib/widgets/response_body_success.dart index d7b1329c..a2ea46b9 100644 --- a/lib/widgets/response_body_success.dart +++ b/lib/widgets/response_body_success.dart @@ -1,5 +1,3 @@ -import 'dart:convert'; - import 'package:apidash_core/apidash_core.dart'; import 'package:apidash_design_system/apidash_design_system.dart'; import 'package:flutter/foundation.dart'; @@ -7,7 +5,6 @@ import 'package:flutter/material.dart'; import 'package:apidash/utils/utils.dart'; import 'package:apidash/widgets/widgets.dart'; import 'package:apidash/consts.dart'; -import 'package:genai/genai.dart'; import 'button_share.dart'; class ResponseBodySuccess extends StatefulWidget { @@ -19,15 +16,17 @@ class ResponseBodySuccess extends StatefulWidget { required this.bytes, this.formattedBody, this.highlightLanguage, - this.selectedModel, + this.sseOutput, + this.aiRequestModel, }); final MediaType mediaType; final List options; final String body; final Uint8List bytes; final String? formattedBody; + final List? sseOutput; final String? highlightLanguage; - final LLMModel? selectedModel; //ONLY FOR AI-REQUESTS + final AIRequestModel? aiRequestModel; @override State createState() => _ResponseBodySuccessState(); @@ -50,8 +49,6 @@ class _ResponseBodySuccessState extends State { borderRadius: kBorderRadius8, ); - final isAIRequest = widget.options.contains(ResponseBodyView.answer); - return LayoutBuilder( builder: (BuildContext context, BoxConstraints constraints) { var showLabel = showButtonLabelsInBodySuccess( @@ -93,33 +90,20 @@ class _ResponseBodySuccessState extends State { ), const Spacer(), ((widget.options == kPreviewRawBodyViewOptions) || - kCodeRawBodyViewOptions.contains(currentSeg) || - isAIRequest) + kCodeRawBodyViewOptions.contains(currentSeg)) ? CopyButton( - toCopy: (currentSeg == ResponseBodyView.answer) - ? widget.formattedBody! - : isAIRequest - ? formatBody(widget.body, widget.mediaType)! - : (widget.formattedBody ?? widget.body), + toCopy: widget.formattedBody ?? widget.body, showLabel: showLabel, ) : const SizedBox(), kIsMobile ? ShareButton( - toShare: (currentSeg == ResponseBodyView.answer) - ? widget.formattedBody! - : isAIRequest - ? formatBody(widget.body, widget.mediaType)! - : (widget.formattedBody ?? widget.body), + toShare: widget.formattedBody ?? widget.body, showLabel: showLabel, ) : SaveInDownloadsButton( - content: (currentSeg == ResponseBodyView.answer) - ? utf8.encode(widget.formattedBody!) - : widget.bytes, - mimeType: (currentSeg == ResponseBodyView.answer) - ? 'text/plain' - : widget.mediaType.mimeType, + content: widget.bytes, + mimeType: widget.mediaType.mimeType, showLabel: showLabel, ), ], @@ -153,25 +137,7 @@ class _ResponseBodySuccessState extends State { ), ), ), - ResponseBodyView.raw => Expanded( - child: Container( - width: double.maxFinite, - padding: kP8, - decoration: textContainerdecoration, - child: SingleChildScrollView( - child: SelectableText( - widget.options.contains(ResponseBodyView.answer) - ? formatBody( - widget.body, - MediaType(kTypeApplication, kSubTypeJson), - )! - : (widget.formattedBody ?? widget.body), - style: kCodeStyle, - ), - ), - ), - ), - ResponseBodyView.answer => Expanded( + ResponseBodyView.raw || ResponseBodyView.answer => Expanded( child: Container( width: double.maxFinite, padding: kP8, @@ -190,8 +156,8 @@ class _ResponseBodySuccessState extends State { padding: kP8, decoration: textContainerdecoration, child: SSEDisplay( - sseOutput: widget.formattedBody?.split('\n') ?? [], - selectedLLModel: widget.selectedModel, + sseOutput: widget.sseOutput, + aiRequestModel: widget.aiRequestModel, ), ), ), diff --git a/lib/widgets/sse_display.dart b/lib/widgets/sse_display.dart index 6d0bee1f..1fc681ca 100644 --- a/lib/widgets/sse_display.dart +++ b/lib/widgets/sse_display.dart @@ -1,28 +1,23 @@ import 'dart:convert'; +import 'package:apidash_core/apidash_core.dart'; import 'package:apidash_design_system/apidash_design_system.dart'; import 'package:flutter/material.dart'; -import 'package:genai/genai.dart'; -class SSEDisplay extends StatefulWidget { - final LLMModel? selectedLLModel; - final List sseOutput; +class SSEDisplay extends StatelessWidget { + final AIRequestModel? aiRequestModel; + final List? sseOutput; const SSEDisplay({ super.key, - required this.sseOutput, - this.selectedLLModel, + this.sseOutput, + this.aiRequestModel, }); - @override - State createState() => _SSEDisplayState(); -} - -class _SSEDisplayState extends State { @override Widget build(BuildContext context) { final theme = Theme.of(context); final fontSizeMedium = theme.textTheme.bodyMedium?.fontSize; final isDark = theme.brightness == Brightness.dark; - if (widget.sseOutput.isEmpty) { + if (sseOutput == null || sseOutput!.isEmpty) { return Text( 'No content', style: kCodeStyle.copyWith( @@ -32,11 +27,10 @@ class _SSEDisplayState extends State { ); } - if (widget.selectedLLModel != null) { + if (aiRequestModel != null) { // For RAW Text output (only AI Requests) String out = ""; - final mc = widget.selectedLLModel!.provider.modelController; - for (String x in widget.sseOutput) { + for (String x in sseOutput!) { x = x.trim(); if (x.isEmpty || x.contains('[DONE]')) { continue; @@ -50,10 +44,10 @@ class _SSEDisplayState extends State { Map? dec; try { dec = jsonDecode(x); - final z = mc.streamOutputFormatter(dec!); + final z = aiRequestModel?.getFormattedStreamOutput(dec!); out += z ?? ''; } catch (e) { - print("Error in JSONDEC $e"); + debugPrint("SSEDisplay -> Error in JSONDEC $e"); } } return SingleChildScrollView( @@ -63,9 +57,8 @@ class _SSEDisplayState extends State { return ListView( padding: kP1, - children: widget.sseOutput.reversed - .where((e) => e.trim() != '') - .map((chunk) { + children: + sseOutput!.reversed.where((e) => e.trim() != '').map((chunk) { Map? parsedJson; try { parsedJson = jsonDecode(chunk); 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/genai/genai_example/lib/main.dart b/packages/genai/genai_example/lib/main.dart index 85c7b12c..ed63985c 100644 --- a/packages/genai/genai_example/lib/main.dart +++ b/packages/genai/genai_example/lib/main.dart @@ -42,17 +42,12 @@ class _AIExampleState extends State { output = ""; }); callGenerativeModel( - AIRequestModel( - modelProvider: selectedProvider, - modelRequestData: kModelProvidersMap[selectedProvider] - ?.defaultRequestData - .copyWith( - model: selectedModel, - apiKey: credentialController.value.text, - systemPrompt: systemPromptController.value.text, - userPrompt: inputPromptController.value.text, - stream: stream, - ), + kModelProvidersMap[selectedProvider]?.defaultAIRequestModel.copyWith( + model: selectedModel, + apiKey: credentialController.value.text, + systemPrompt: systemPromptController.value.text, + userPrompt: inputPromptController.value.text, + stream: stream, ), onAnswer: (x) { setState(() { @@ -143,7 +138,7 @@ class _AIExampleState extends State { ], ), SizedBox(height: 30), - Container( + SizedBox( width: 400, child: Column( mainAxisAlignment: MainAxisAlignment.center, diff --git a/packages/genai/lib/interface/consts.dart b/packages/genai/lib/interface/consts.dart index cd09eec5..1b05c9be 100644 --- a/packages/genai/lib/interface/consts.dart +++ b/packages/genai/lib/interface/consts.dart @@ -17,7 +17,7 @@ const kGeminiUrl = 'https://generativelanguage.googleapis.com/v1beta/models'; const kOpenAIUrl = 'https://api.openai.com/v1/chat/completions'; const kOllamaUrl = '$kBaseOllamaUrl/v1/chat/completions'; -final kDefaultModelRequestData = ModelRequestData( +final kDefaultAiRequestModel = AIRequestModel( url: '', model: '', apiKey: '', diff --git a/packages/genai/lib/interface/model_providers/anthropic.dart b/packages/genai/lib/interface/model_providers/anthropic.dart index 88c18009..c6d1f955 100644 --- a/packages/genai/lib/interface/model_providers/anthropic.dart +++ b/packages/genai/lib/interface/model_providers/anthropic.dart @@ -6,32 +6,36 @@ class AnthropicModel extends ModelProvider { static final instance = AnthropicModel(); @override - ModelRequestData get defaultRequestData => - kDefaultModelRequestData.copyWith(url: kAnthropicUrl); + AIRequestModel get defaultAIRequestModel => kDefaultAiRequestModel.copyWith( + modelApiProvider: ModelAPIProvider.anthropic, + url: kAnthropicUrl, + ); @override - HttpRequestModel? createRequest(ModelRequestData? requestData) { - if (requestData == null) { + HttpRequestModel? createRequest(AIRequestModel? aiRequestModel) { + if (aiRequestModel == null) { return null; } return HttpRequestModel( method: HTTPVerb.post, - url: requestData.url, + url: aiRequestModel.url, headers: const [ NameValueModel(name: "anthropic-version", value: "2023-06-01"), ], - authModel: AuthModel( - type: APIAuthType.apiKey, - apikey: AuthApiKeyModel(key: requestData.apiKey), - ), + authModel: aiRequestModel.apiKey == null + ? null + : AuthModel( + type: APIAuthType.apiKey, + apikey: AuthApiKeyModel(key: aiRequestModel.apiKey!), + ), body: kJsonEncoder.convert({ - "model": requestData.model, + "model": aiRequestModel.model, "messages": [ - {"role": "system", "content": requestData.systemPrompt}, - {"role": "user", "content": requestData.userPrompt}, + {"role": "system", "content": aiRequestModel.systemPrompt}, + {"role": "user", "content": aiRequestModel.userPrompt}, ], - ...requestData.getModelConfigMap(), - if (requestData.stream ?? false) ...{'stream': true}, + ...aiRequestModel.getModelConfigMap(), + if (aiRequestModel.stream ?? false) ...{'stream': true}, }), ); } diff --git a/packages/genai/lib/interface/model_providers/azureopenai.dart b/packages/genai/lib/interface/model_providers/azureopenai.dart index 5f4da780..587c1c49 100644 --- a/packages/genai/lib/interface/model_providers/azureopenai.dart +++ b/packages/genai/lib/interface/model_providers/azureopenai.dart @@ -4,36 +4,44 @@ import '../consts.dart'; class AzureOpenAIModel extends ModelProvider { static final instance = AzureOpenAIModel(); - @override - ModelRequestData get defaultRequestData => kDefaultModelRequestData; @override - HttpRequestModel? createRequest(ModelRequestData? requestData) { - if (requestData == null) { + AIRequestModel get defaultAIRequestModel => kDefaultAiRequestModel.copyWith( + modelApiProvider: ModelAPIProvider.azureopenai, + ); + + @override + HttpRequestModel? createRequest(AIRequestModel? aiRequestModel) { + if (aiRequestModel == null) { return null; } - if (requestData.url.isEmpty) { + if (aiRequestModel.url.isEmpty) { throw Exception('MODEL ENDPOINT IS EMPTY'); } return HttpRequestModel( method: HTTPVerb.post, - url: requestData.url, - authModel: AuthModel( - type: APIAuthType.apiKey, - apikey: AuthApiKeyModel(key: requestData.apiKey, name: 'api-key'), - ), + url: aiRequestModel.url, + authModel: aiRequestModel.apiKey == null + ? null + : AuthModel( + type: APIAuthType.apiKey, + apikey: AuthApiKeyModel( + key: aiRequestModel.apiKey!, + name: 'api-key', + ), + ), body: kJsonEncoder.convert({ - "model": requestData.model, + "model": aiRequestModel.model, "messages": [ - {"role": "system", "content": requestData.systemPrompt}, - if (requestData.userPrompt.isNotEmpty) ...{ - {"role": "user", "content": requestData.userPrompt}, + {"role": "system", "content": aiRequestModel.systemPrompt}, + if (aiRequestModel.userPrompt.isNotEmpty) ...{ + {"role": "user", "content": aiRequestModel.userPrompt}, } else ...{ {"role": "user", "content": "Generate"}, }, ], - ...requestData.getModelConfigMap(), - if (requestData.stream ?? false) ...{'stream': true}, + ...aiRequestModel.getModelConfigMap(), + if (aiRequestModel.stream ?? false) ...{'stream': true}, }), ); } diff --git a/packages/genai/lib/interface/model_providers/gemini.dart b/packages/genai/lib/interface/model_providers/gemini.dart index 4ca3c313..f3c57867 100644 --- a/packages/genai/lib/interface/model_providers/gemini.dart +++ b/packages/genai/lib/interface/model_providers/gemini.dart @@ -6,7 +6,8 @@ class GeminiModel extends ModelProvider { static final instance = GeminiModel(); @override - ModelRequestData get defaultRequestData => kDefaultModelRequestData.copyWith( + AIRequestModel get defaultAIRequestModel => kDefaultAiRequestModel.copyWith( + modelApiProvider: ModelAPIProvider.gemini, url: kGeminiUrl, modelConfigs: [ kDefaultModelConfigTemperature, @@ -16,13 +17,13 @@ class GeminiModel extends ModelProvider { ); @override - HttpRequestModel? createRequest(ModelRequestData? requestData) { - if (requestData == null) { + HttpRequestModel? createRequest(AIRequestModel? aiRequestModel) { + if (aiRequestModel == null) { return null; } List params = []; - String endpoint = "${requestData.url}/${requestData.model}:"; - if (requestData.stream ?? false) { + String endpoint = "${aiRequestModel.url}/${aiRequestModel.model}:"; + if (aiRequestModel.stream ?? false) { endpoint += 'streamGenerateContent'; params.add(const NameValueModel(name: "alt", value: "sse")); } else { @@ -32,30 +33,32 @@ class GeminiModel extends ModelProvider { return HttpRequestModel( method: HTTPVerb.post, url: endpoint, - authModel: AuthModel( - type: APIAuthType.apiKey, - apikey: AuthApiKeyModel( - key: requestData.apiKey, - location: 'query', - name: 'key', - ), - ), + 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": requestData.userPrompt}, + {"text": aiRequestModel.userPrompt}, ], }, ], "systemInstruction": { "role": "system", "parts": [ - {"text": requestData.systemPrompt}, + {"text": aiRequestModel.systemPrompt}, ], }, - "generationConfig": requestData.getModelConfigMap(), + "generationConfig": aiRequestModel.getModelConfigMap(), }), ); } diff --git a/packages/genai/lib/interface/model_providers/ollama.dart b/packages/genai/lib/interface/model_providers/ollama.dart index 78c87cf3..6a6fc713 100644 --- a/packages/genai/lib/interface/model_providers/ollama.dart +++ b/packages/genai/lib/interface/model_providers/ollama.dart @@ -6,7 +6,8 @@ class OllamaModel extends OpenAIModel { static final instance = OllamaModel(); @override - ModelRequestData get defaultRequestData => kDefaultModelRequestData.copyWith( + 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 index f42b6c31..a2b08f3d 100644 --- a/packages/genai/lib/interface/model_providers/openai.dart +++ b/packages/genai/lib/interface/model_providers/openai.dart @@ -6,33 +6,37 @@ class OpenAIModel extends ModelProvider { static final instance = OpenAIModel(); @override - ModelRequestData get defaultRequestData => - kDefaultModelRequestData.copyWith(url: kOpenAIUrl); + AIRequestModel get defaultAIRequestModel => kDefaultAiRequestModel.copyWith( + modelApiProvider: ModelAPIProvider.openai, + url: kOpenAIUrl, + ); @override - HttpRequestModel? createRequest(ModelRequestData? requestData) { - if (requestData == null) { + HttpRequestModel? createRequest(AIRequestModel? aiRequestModel) { + if (aiRequestModel == null) { return null; } return HttpRequestModel( method: HTTPVerb.post, - url: requestData.url, - authModel: AuthModel( - type: APIAuthType.bearer, - bearer: AuthBearerModel(token: requestData.apiKey), - ), + url: aiRequestModel.url, + authModel: aiRequestModel.apiKey == null + ? null + : AuthModel( + type: APIAuthType.bearer, + bearer: AuthBearerModel(token: aiRequestModel.apiKey!), + ), body: kJsonEncoder.convert({ - "model": requestData.model, + "model": aiRequestModel.model, "messages": [ - {"role": "system", "content": requestData.systemPrompt}, - if (requestData.userPrompt.isNotEmpty) ...{ - {"role": "user", "content": requestData.userPrompt}, + {"role": "system", "content": aiRequestModel.systemPrompt}, + if (aiRequestModel.userPrompt.isNotEmpty) ...{ + {"role": "user", "content": aiRequestModel.userPrompt}, } else ...{ {"role": "user", "content": "Generate"}, }, ], - ...requestData.getModelConfigMap(), - if (requestData.stream ?? false) ...{'stream': true}, + ...aiRequestModel.getModelConfigMap(), + if (aiRequestModel.stream ?? false) ...{'stream': true}, }), ); } diff --git a/packages/genai/lib/models/ai_request_model.dart b/packages/genai/lib/models/ai_request_model.dart index 7ed9e4fc..e9a3a297 100644 --- a/packages/genai/lib/models/ai_request_model.dart +++ b/packages/genai/lib/models/ai_request_model.dart @@ -1,7 +1,7 @@ import 'package:better_networking/better_networking.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; import '../interface/interface.dart'; -import 'model_request_data.dart'; +import 'model_config.dart'; part 'ai_request_model.freezed.dart'; part 'ai_request_model.g.dart'; @@ -11,19 +11,44 @@ class AIRequestModel with _$AIRequestModel { @JsonSerializable(explicitToJson: true, anyMap: true) const factory AIRequestModel({ - ModelAPIProvider? modelProvider, - ModelRequestData? modelRequestData, + 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[modelProvider]?.createRequest(modelRequestData); + kModelProvidersMap[modelApiProvider]?.createRequest(this); String? getFormattedOutput(Map x) => - kModelProvidersMap[modelProvider]?.outputFormatter(x); + kModelProvidersMap[modelApiProvider]?.outputFormatter(x); String? getFormattedStreamOutput(Map x) => - kModelProvidersMap[modelProvider]?.streamOutputFormatter(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 index 7d9b3220..7cec0388 100644 --- a/packages/genai/lib/models/ai_request_model.freezed.dart +++ b/packages/genai/lib/models/ai_request_model.freezed.dart @@ -21,8 +21,17 @@ AIRequestModel _$AIRequestModelFromJson(Map json) { /// @nodoc mixin _$AIRequestModel { - ModelAPIProvider? get modelProvider => throw _privateConstructorUsedError; - ModelRequestData? get modelRequestData => throw _privateConstructorUsedError; + 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; @@ -42,11 +51,15 @@ abstract class $AIRequestModelCopyWith<$Res> { ) = _$AIRequestModelCopyWithImpl<$Res, AIRequestModel>; @useResult $Res call({ - ModelAPIProvider? modelProvider, - ModelRequestData? modelRequestData, + 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, }); - - $ModelRequestDataCopyWith<$Res>? get modelRequestData; } /// @nodoc @@ -64,37 +77,53 @@ class _$AIRequestModelCopyWithImpl<$Res, $Val extends AIRequestModel> @pragma('vm:prefer-inline') @override $Res call({ - Object? modelProvider = freezed, - Object? modelRequestData = freezed, + 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( - modelProvider: freezed == modelProvider - ? _value.modelProvider - : modelProvider // ignore: cast_nullable_to_non_nullable + modelApiProvider: freezed == modelApiProvider + ? _value.modelApiProvider + : modelApiProvider // ignore: cast_nullable_to_non_nullable as ModelAPIProvider?, - modelRequestData: freezed == modelRequestData - ? _value.modelRequestData - : modelRequestData // ignore: cast_nullable_to_non_nullable - as ModelRequestData?, + 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, ); } - - /// Create a copy of AIRequestModel - /// with the given fields replaced by the non-null parameter values. - @override - @pragma('vm:prefer-inline') - $ModelRequestDataCopyWith<$Res>? get modelRequestData { - if (_value.modelRequestData == null) { - return null; - } - - return $ModelRequestDataCopyWith<$Res>(_value.modelRequestData!, (value) { - return _then(_value.copyWith(modelRequestData: value) as $Val); - }); - } } /// @nodoc @@ -107,12 +136,15 @@ abstract class _$$AIRequestModelImplCopyWith<$Res> @override @useResult $Res call({ - ModelAPIProvider? modelProvider, - ModelRequestData? modelRequestData, + 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, }); - - @override - $ModelRequestDataCopyWith<$Res>? get modelRequestData; } /// @nodoc @@ -129,19 +161,49 @@ class __$$AIRequestModelImplCopyWithImpl<$Res> @pragma('vm:prefer-inline') @override $Res call({ - Object? modelProvider = freezed, - Object? modelRequestData = freezed, + 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( - modelProvider: freezed == modelProvider - ? _value.modelProvider - : modelProvider // ignore: cast_nullable_to_non_nullable + modelApiProvider: freezed == modelApiProvider + ? _value.modelApiProvider + : modelApiProvider // ignore: cast_nullable_to_non_nullable as ModelAPIProvider?, - modelRequestData: freezed == modelRequestData - ? _value.modelRequestData - : modelRequestData // ignore: cast_nullable_to_non_nullable - as ModelRequestData?, + 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?, ), ); } @@ -151,20 +213,55 @@ class __$$AIRequestModelImplCopyWithImpl<$Res> @JsonSerializable(explicitToJson: true, anyMap: true) class _$AIRequestModelImpl extends _AIRequestModel { - const _$AIRequestModelImpl({this.modelProvider, this.modelRequestData}) - : super._(); + 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? modelProvider; + final ModelAPIProvider? modelApiProvider; @override - final ModelRequestData? modelRequestData; + @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(modelProvider: $modelProvider, modelRequestData: $modelRequestData)'; + return 'AIRequestModel(modelApiProvider: $modelApiProvider, url: $url, model: $model, apiKey: $apiKey, systemPrompt: $systemPrompt, userPrompt: $userPrompt, modelConfigs: $modelConfigs, stream: $stream)'; } @override @@ -172,15 +269,35 @@ class _$AIRequestModelImpl extends _AIRequestModel { return identical(this, other) || (other.runtimeType == runtimeType && other is _$AIRequestModelImpl && - (identical(other.modelProvider, modelProvider) || - other.modelProvider == modelProvider) && - (identical(other.modelRequestData, modelRequestData) || - other.modelRequestData == modelRequestData)); + (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, modelProvider, modelRequestData); + 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. @@ -201,8 +318,14 @@ class _$AIRequestModelImpl extends _AIRequestModel { abstract class _AIRequestModel extends AIRequestModel { const factory _AIRequestModel({ - final ModelAPIProvider? modelProvider, - final ModelRequestData? modelRequestData, + 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._(); @@ -210,9 +333,24 @@ abstract class _AIRequestModel extends AIRequestModel { _$AIRequestModelImpl.fromJson; @override - ModelAPIProvider? get modelProvider; + ModelAPIProvider? get modelApiProvider; @override - ModelRequestData? get modelRequestData; + 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. diff --git a/packages/genai/lib/models/ai_request_model.g.dart b/packages/genai/lib/models/ai_request_model.g.dart index 182fe1cb..66db3eee 100644 --- a/packages/genai/lib/models/ai_request_model.g.dart +++ b/packages/genai/lib/models/ai_request_model.g.dart @@ -8,22 +8,34 @@ part of 'ai_request_model.dart'; _$AIRequestModelImpl _$$AIRequestModelImplFromJson(Map json) => _$AIRequestModelImpl( - modelProvider: $enumDecodeNullable( + modelApiProvider: $enumDecodeNullable( _$ModelAPIProviderEnumMap, - json['modelProvider'], + json['modelApiProvider'], ), - modelRequestData: json['modelRequestData'] == null - ? null - : ModelRequestData.fromJson( - Map.from(json['modelRequestData'] as Map), - ), + 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, ) => { - 'modelProvider': _$ModelAPIProviderEnumMap[instance.modelProvider], - 'modelRequestData': instance.modelRequestData?.toJson(), + '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 = { diff --git a/packages/genai/lib/models/available_models.dart b/packages/genai/lib/models/available_models.dart index ef9fc306..3b6d4c1d 100644 --- a/packages/genai/lib/models/available_models.dart +++ b/packages/genai/lib/models/available_models.dart @@ -5,6 +5,7 @@ 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'; @@ -34,6 +35,8 @@ class AvailableModels with _$AvailableModels { @freezed class AIModelProvider with _$AIModelProvider { + const AIModelProvider._(); + const factory AIModelProvider({ @JsonKey(name: "provider_id") ModelAPIProvider? providerId, @JsonKey(name: "provider_name") String? providerName, @@ -43,6 +46,14 @@ class AIModelProvider with _$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 diff --git a/packages/genai/lib/models/available_models.freezed.dart b/packages/genai/lib/models/available_models.freezed.dart index 725ef027..32ce0905 100644 --- a/packages/genai/lib/models/available_models.freezed.dart +++ b/packages/genai/lib/models/available_models.freezed.dart @@ -363,13 +363,14 @@ class __$$AIModelProviderImplCopyWithImpl<$Res> /// @nodoc @JsonSerializable() -class _$AIModelProviderImpl implements _AIModelProvider { +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; + }) : _models = models, + super._(); factory _$AIModelProviderImpl.fromJson(Map json) => _$$AIModelProviderImplFromJson(json); @@ -440,13 +441,14 @@ class _$AIModelProviderImpl implements _AIModelProvider { } } -abstract class _AIModelProvider implements AIModelProvider { +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; diff --git a/packages/genai/lib/models/model_provider.dart b/packages/genai/lib/models/model_provider.dart index 70b019fc..c929645a 100644 --- a/packages/genai/lib/models/model_provider.dart +++ b/packages/genai/lib/models/model_provider.dart @@ -2,9 +2,9 @@ import 'package:better_networking/better_networking.dart'; import '../models/models.dart'; abstract class ModelProvider { - ModelRequestData get defaultRequestData => throw UnimplementedError(); + AIRequestModel get defaultAIRequestModel => throw UnimplementedError(); - HttpRequestModel? createRequest(ModelRequestData? requestData) { + HttpRequestModel? createRequest(AIRequestModel? aiRequestModel) { throw UnimplementedError(); } diff --git a/packages/genai/lib/models/model_request_data.dart b/packages/genai/lib/models/model_request_data.dart deleted file mode 100644 index 7fa04e33..00000000 --- a/packages/genai/lib/models/model_request_data.dart +++ /dev/null @@ -1,33 +0,0 @@ -import 'package:freezed_annotation/freezed_annotation.dart'; -import 'model_config.dart'; -part 'model_request_data.freezed.dart'; -part 'model_request_data.g.dart'; - -@freezed -class ModelRequestData with _$ModelRequestData { - const ModelRequestData._(); - - @JsonSerializable(explicitToJson: true, anyMap: true) - const factory ModelRequestData({ - @Default("") String url, - @Default("") String model, - @Default("") 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, - }) = _ModelRequestData; - - factory ModelRequestData.fromJson(Map json) => - _$ModelRequestDataFromJson(json); - - Map getModelConfigMap() { - Map m = {}; - for (var config in modelConfigs) { - m[config.id] = config.value.getPayloadValue(); - } - return m; - } -} diff --git a/packages/genai/lib/models/model_request_data.freezed.dart b/packages/genai/lib/models/model_request_data.freezed.dart deleted file mode 100644 index 35c0a7a5..00000000 --- a/packages/genai/lib/models/model_request_data.freezed.dart +++ /dev/null @@ -1,339 +0,0 @@ -// 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 'model_request_data.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', -); - -ModelRequestData _$ModelRequestDataFromJson(Map json) { - return _ModelRequestData.fromJson(json); -} - -/// @nodoc -mixin _$ModelRequestData { - 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 ModelRequestData to a JSON map. - Map toJson() => throw _privateConstructorUsedError; - - /// Create a copy of ModelRequestData - /// with the given fields replaced by the non-null parameter values. - @JsonKey(includeFromJson: false, includeToJson: false) - $ModelRequestDataCopyWith get copyWith => - throw _privateConstructorUsedError; -} - -/// @nodoc -abstract class $ModelRequestDataCopyWith<$Res> { - factory $ModelRequestDataCopyWith( - ModelRequestData value, - $Res Function(ModelRequestData) then, - ) = _$ModelRequestDataCopyWithImpl<$Res, ModelRequestData>; - @useResult - $Res call({ - 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 _$ModelRequestDataCopyWithImpl<$Res, $Val extends ModelRequestData> - implements $ModelRequestDataCopyWith<$Res> { - _$ModelRequestDataCopyWithImpl(this._value, this._then); - - // ignore: unused_field - final $Val _value; - // ignore: unused_field - final $Res Function($Val) _then; - - /// Create a copy of ModelRequestData - /// with the given fields replaced by the non-null parameter values. - @pragma('vm:prefer-inline') - @override - $Res call({ - Object? url = null, - Object? model = null, - Object? apiKey = null, - Object? systemPrompt = null, - Object? userPrompt = null, - Object? modelConfigs = null, - Object? stream = freezed, - }) { - return _then( - _value.copyWith( - url: null == url - ? _value.url - : url // ignore: cast_nullable_to_non_nullable - as String, - model: null == model - ? _value.model - : model // ignore: cast_nullable_to_non_nullable - as String, - apiKey: null == 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 _$$ModelRequestDataImplCopyWith<$Res> - implements $ModelRequestDataCopyWith<$Res> { - factory _$$ModelRequestDataImplCopyWith( - _$ModelRequestDataImpl value, - $Res Function(_$ModelRequestDataImpl) then, - ) = __$$ModelRequestDataImplCopyWithImpl<$Res>; - @override - @useResult - $Res call({ - 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 __$$ModelRequestDataImplCopyWithImpl<$Res> - extends _$ModelRequestDataCopyWithImpl<$Res, _$ModelRequestDataImpl> - implements _$$ModelRequestDataImplCopyWith<$Res> { - __$$ModelRequestDataImplCopyWithImpl( - _$ModelRequestDataImpl _value, - $Res Function(_$ModelRequestDataImpl) _then, - ) : super(_value, _then); - - /// Create a copy of ModelRequestData - /// with the given fields replaced by the non-null parameter values. - @pragma('vm:prefer-inline') - @override - $Res call({ - Object? url = null, - Object? model = null, - Object? apiKey = null, - Object? systemPrompt = null, - Object? userPrompt = null, - Object? modelConfigs = null, - Object? stream = freezed, - }) { - return _then( - _$ModelRequestDataImpl( - url: null == url - ? _value.url - : url // ignore: cast_nullable_to_non_nullable - as String, - model: null == model - ? _value.model - : model // ignore: cast_nullable_to_non_nullable - as String, - apiKey: null == 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 _$ModelRequestDataImpl extends _ModelRequestData { - const _$ModelRequestDataImpl({ - this.url = "", - this.model = "", - this.apiKey = "", - @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 _$ModelRequestDataImpl.fromJson(Map json) => - _$$ModelRequestDataImplFromJson(json); - - @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 'ModelRequestData(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 _$ModelRequestDataImpl && - (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, - url, - model, - apiKey, - systemPrompt, - userPrompt, - const DeepCollectionEquality().hash(_modelConfigs), - stream, - ); - - /// Create a copy of ModelRequestData - /// with the given fields replaced by the non-null parameter values. - @JsonKey(includeFromJson: false, includeToJson: false) - @override - @pragma('vm:prefer-inline') - _$$ModelRequestDataImplCopyWith<_$ModelRequestDataImpl> get copyWith => - __$$ModelRequestDataImplCopyWithImpl<_$ModelRequestDataImpl>( - this, - _$identity, - ); - - @override - Map toJson() { - return _$$ModelRequestDataImplToJson(this); - } -} - -abstract class _ModelRequestData extends ModelRequestData { - const factory _ModelRequestData({ - 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, - }) = _$ModelRequestDataImpl; - const _ModelRequestData._() : super._(); - - factory _ModelRequestData.fromJson(Map json) = - _$ModelRequestDataImpl.fromJson; - - @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 ModelRequestData - /// with the given fields replaced by the non-null parameter values. - @override - @JsonKey(includeFromJson: false, includeToJson: false) - _$$ModelRequestDataImplCopyWith<_$ModelRequestDataImpl> get copyWith => - throw _privateConstructorUsedError; -} diff --git a/packages/genai/lib/models/model_request_data.g.dart b/packages/genai/lib/models/model_request_data.g.dart deleted file mode 100644 index 8522874a..00000000 --- a/packages/genai/lib/models/model_request_data.g.dart +++ /dev/null @@ -1,34 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'model_request_data.dart'; - -// ************************************************************************** -// JsonSerializableGenerator -// ************************************************************************** - -_$ModelRequestDataImpl _$$ModelRequestDataImplFromJson(Map json) => - _$ModelRequestDataImpl( - url: json['url'] as String? ?? "", - model: json['model'] as String? ?? "", - apiKey: json['apiKey'] as String? ?? "", - 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 _$$ModelRequestDataImplToJson( - _$ModelRequestDataImpl instance, -) => { - '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, -}; diff --git a/packages/genai/lib/models/models.dart b/packages/genai/lib/models/models.dart index 69ecfe07..5216053a 100644 --- a/packages/genai/lib/models/models.dart +++ b/packages/genai/lib/models/models.dart @@ -3,5 +3,4 @@ export 'available_models.dart'; export 'model_config_value.dart'; export 'model_config.dart'; export 'model_provider.dart'; -export 'model_request_data.dart'; export 'models_data.g.dart'; diff --git a/packages/genai/lib/utils/ai_request_utils.dart b/packages/genai/lib/utils/ai_request_utils.dart index 33b27bd3..a4b94481 100644 --- a/packages/genai/lib/utils/ai_request_utils.dart +++ b/packages/genai/lib/utils/ai_request_utils.dart @@ -96,10 +96,9 @@ Future callGenerativeModel( required Function(String?) onAnswer, required Function(dynamic) onError, }) async { - final modelRequestData = aiRequestModel?.modelRequestData; - if (modelRequestData != null) { + if (aiRequestModel != null) { try { - if (modelRequestData.stream ?? false) { + if (aiRequestModel.stream ?? false) { final answerStream = await streamGenAIRequest(aiRequestModel); processGenAIStreamOutput(answerStream, (w) { onAnswer('$w '); diff --git a/pubspec.lock b/pubspec.lock index 81107934..4f4c43ff 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -703,7 +703,7 @@ packages: source: hosted version: "0.32.1" genai: - dependency: "direct main" + dependency: transitive description: path: "packages/genai" relative: true diff --git a/pubspec.yaml b/pubspec.yaml index 4e40b9b5..0ef4fae7 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -14,8 +14,6 @@ dependencies: path: packages/apidash_core apidash_design_system: path: packages/apidash_design_system - genai: - path: packages/genai carousel_slider: ^5.0.0 code_builder: ^4.10.0 csv: ^6.0.0 From 7b7daa7dac013a491469101f249c6e12a0ce7410 Mon Sep 17 00:00:00 2001 From: Ankit Mahato Date: Thu, 28 Aug 2025 23:34:28 +0530 Subject: [PATCH 35/36] Add AI request model support and improve type handling Refactored collection state management to handle API type changes and AI request models. Updated widgets and tests to support nullable HTTP methods and AI request models, and improved response body rendering for AI responses. --- lib/providers/collection_providers.dart | 79 ++++++++++++------- .../common_widgets/ai/ai_model_selector.dart | 2 +- lib/screens/home_page/collection_pane.dart | 2 +- .../home_page/editor_pane/url_card.dart | 4 + lib/widgets/card_sidebar_request.dart | 4 +- lib/widgets/response_body.dart | 4 +- lib/widgets/response_body_success.dart | 19 ++++- lib/widgets/texts.dart | 6 +- pubspec.lock | 8 ++ test/models/history_models.dart | 1 + test/models/request_models.dart | 3 +- 11 files changed, 92 insertions(+), 40 deletions(-) diff --git a/lib/providers/collection_providers.dart b/lib/providers/collection_providers.dart index 4ef67bf9..3d9f91f0 100644 --- a/lib/providers/collection_providers.dart +++ b/lib/providers/collection_providers.dart @@ -241,34 +241,54 @@ 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, - aiRequestModel: aiRequestModel ?? currentModel.aiRequestModel, - ); + + RequestModel newModel; + + if (apiType != null && currentModel.apiType != apiType) { + 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: const AIRequestModel()), + }; + } 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; @@ -285,7 +305,8 @@ class CollectionStateNotifier } RequestModel? requestModel = state![requestId]; - if (requestModel?.httpRequestModel == null) { + if (requestModel?.httpRequestModel == null && + requestModel?.aiRequestModel == null) { return; } diff --git a/lib/screens/common_widgets/ai/ai_model_selector.dart b/lib/screens/common_widgets/ai/ai_model_selector.dart index 2ba45581..d746889a 100644 --- a/lib/screens/common_widgets/ai/ai_model_selector.dart +++ b/lib/screens/common_widgets/ai/ai_model_selector.dart @@ -15,7 +15,7 @@ class AIModelSelector extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { AIRequestModel? aiRequestModel; - if (readOnlyModel != null) { + if (readOnlyModel == null) { ref.watch(selectedIdStateProvider); aiRequestModel = ref.watch(selectedRequestModelProvider .select((value) => value?.aiRequestModel)); 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/url_card.dart b/lib/screens/home_page/editor_pane/url_card.dart index db9ab0f7..d84f27a7 100644 --- a/lib/screens/home_page/editor_pane/url_card.dart +++ b/lib/screens/home_page/editor_pane/url_card.dart @@ -102,6 +102,10 @@ 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!)!; 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/response_body.dart b/lib/widgets/response_body.dart index 7a679d8c..f1c866e9 100644 --- a/lib/widgets/response_body.dart +++ b/lib/widgets/response_body.dart @@ -49,8 +49,7 @@ class ResponseBody extends StatelessWidget { // '$kMsgUnknowContentType - ${responseModel.contentType}. $kUnexpectedRaiseIssue'); // } - var responseBodyView = (selectedRequestModel?.apiType == APIType.ai && - (responseModel.sseOutput?.isNotEmpty ?? false)) + var responseBodyView = selectedRequestModel?.apiType == APIType.ai ? (kAnswerRawBodyViewOptions, kSubTypePlain) : getResponseBodyViewOptions(mediaType); var options = responseBodyView.$1; @@ -70,6 +69,7 @@ class ResponseBody extends StatelessWidget { formattedBody: formattedBody, 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 a2ea46b9..44c9b28a 100644 --- a/lib/widgets/response_body_success.dart +++ b/lib/widgets/response_body_success.dart @@ -17,6 +17,7 @@ class ResponseBodySuccess extends StatefulWidget { this.formattedBody, this.highlightLanguage, this.sseOutput, + this.isAIResponse = false, this.aiRequestModel, }); final MediaType mediaType; @@ -26,6 +27,7 @@ class ResponseBodySuccess extends StatefulWidget { final String? formattedBody; final List? sseOutput; final String? highlightLanguage; + final bool isAIResponse; final AIRequestModel? aiRequestModel; @override @@ -137,7 +139,7 @@ class _ResponseBodySuccessState extends State { ), ), ), - ResponseBodyView.raw || ResponseBodyView.answer => Expanded( + ResponseBodyView.answer => Expanded( child: Container( width: double.maxFinite, padding: kP8, @@ -150,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, diff --git a/lib/widgets/texts.dart b/lib/widgets/texts.dart index 1ec47eb4..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,7 +18,7 @@ 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, }, diff --git a/pubspec.lock b/pubspec.lock index 4f4c43ff..c6d22be0 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1117,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 From 9d50e3f09c514c07b8c2eb3cfe972d8017c1720b Mon Sep 17 00:00:00 2001 From: Ankit Mahato Date: Fri, 29 Aug 2025 01:27:32 +0530 Subject: [PATCH 36/36] Add default AI model selection to settings Replaces the previous placeholder for default LLM selection with a working default AI model selector in settings. Updates SettingsModel and related providers to support storing and updating the default AI model as a JSON object. Integrates the selector UI and ensures new AI requests use the default model if set. --- lib/models/settings_model.dart | 40 +++++-------------- lib/providers/collection_providers.dart | 5 ++- lib/providers/settings_providers.dart | 6 +-- .../ai/ai_model_selector_dialog.dart | 4 ++ lib/screens/common_widgets/code_pane.dart | 3 +- lib/screens/settings_page.dart | 31 ++++++++------ 6 files changed, 41 insertions(+), 48 deletions(-) diff --git a/lib/models/settings_model.dart b/lib/models/settings_model.dart index 47382f6e..71a58ebb 100644 --- a/lib/models/settings_model.dart +++ b/lib/models/settings_model.dart @@ -18,8 +18,7 @@ class SettingsModel { this.workspaceFolderPath, this.isSSLDisabled = false, this.isDashBotEnabled = true, - // TODO: Fix it - // this.defaultLLMSaveObject, + this.defaultAIModel, }); final bool isDark; @@ -35,8 +34,7 @@ class SettingsModel { final String? workspaceFolderPath; final bool isSSLDisabled; final bool isDashBotEnabled; - // TODO: Fix it - // final LLMSaveObject? defaultLLMSaveObject; + final Map? defaultAIModel; SettingsModel copyWith({ bool? isDark, @@ -52,9 +50,7 @@ class SettingsModel { String? workspaceFolderPath, bool? isSSLDisabled, bool? isDashBotEnabled, - // TODO: Fix it - // LLMSaveObject? def, - // LLMSaveObject? defaultLLMSaveObject, + Map? defaultAIModel, }) { return SettingsModel( isDark: isDark ?? this.isDark, @@ -72,8 +68,7 @@ class SettingsModel { workspaceFolderPath: workspaceFolderPath ?? this.workspaceFolderPath, isSSLDisabled: isSSLDisabled ?? this.isSSLDisabled, isDashBotEnabled: isDashBotEnabled ?? this.isDashBotEnabled, - // TODO: Fix it - // defaultLLMSaveObject: defaultLLMSaveObject ?? this.defaultLLMSaveObject, + defaultAIModel: defaultAIModel ?? this.defaultAIModel, ); } @@ -94,8 +89,7 @@ class SettingsModel { workspaceFolderPath: workspaceFolderPath, isSSLDisabled: isSSLDisabled, isDashBotEnabled: isDashBotEnabled, - // TODO: Fix it - // defaultLLMSaveObject: defaultLLMSaveObject, + defaultAIModel: defaultAIModel, ); } @@ -151,14 +145,7 @@ class SettingsModel { final workspaceFolderPath = data["workspaceFolderPath"] as String?; final isSSLDisabled = data["isSSLDisabled"] as bool?; final isDashBotEnabled = data["isDashBotEnabled"] as bool?; - - // TODO: Fix it - // LLMSaveObject? defaultLLMSaveObject; - // if (data["defaultLLMSaveObject"] != null) { - // defaultLLMSaveObject = - // LLMSaveObject.fromJSON(data["defaultLLMSaveObject"]); - // } - + final defaultAIModel = data["defaultAIModel"] as Map?; const sm = SettingsModel(); return sm.copyWith( @@ -176,8 +163,7 @@ class SettingsModel { workspaceFolderPath: workspaceFolderPath, isSSLDisabled: isSSLDisabled, isDashBotEnabled: isDashBotEnabled, - // TODO: Fix it - // defaultLLMSaveObject: defaultLLMSaveObject, + defaultAIModel: defaultAIModel, ); } @@ -198,8 +184,7 @@ class SettingsModel { "workspaceFolderPath": workspaceFolderPath, "isSSLDisabled": isSSLDisabled, "isDashBotEnabled": isDashBotEnabled, - // TODO: Fix it - // 'defaultLLMSaveObject': defaultLLMSaveObject?.toJSON(), + 'defaultLLMSaveObject': defaultAIModel, }; } @@ -225,10 +210,8 @@ class SettingsModel { other.historyRetentionPeriod == historyRetentionPeriod && other.workspaceFolderPath == workspaceFolderPath && other.isSSLDisabled == isSSLDisabled && - other.isDashBotEnabled == isDashBotEnabled; - // TODO: Fix it - // && - // other.defaultLLMSaveObject == defaultLLMSaveObject; + other.isDashBotEnabled == isDashBotEnabled && + other.defaultAIModel == defaultAIModel; } @override @@ -248,8 +231,7 @@ class SettingsModel { workspaceFolderPath, isSSLDisabled, isDashBotEnabled, - // TODO: Fix it - // defaultLLMSaveObject, + defaultAIModel, ); } } diff --git a/lib/providers/collection_providers.dart b/lib/providers/collection_providers.dart index 3d9f91f0..c6e9db55 100644 --- a/lib/providers/collection_providers.dart +++ b/lib/providers/collection_providers.dart @@ -245,6 +245,7 @@ class CollectionStateNotifier 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, @@ -257,7 +258,9 @@ class CollectionStateNotifier name: name ?? currentModel.name, description: description ?? currentModel.description, httpRequestModel: null, - aiRequestModel: const AIRequestModel()), + aiRequestModel: defaultModel == null + ? const AIRequestModel() + : AIRequestModel.fromJson(defaultModel)), }; } else { newModel = currentModel.copyWith( diff --git a/lib/providers/settings_providers.dart b/lib/providers/settings_providers.dart index f4493abb..be09cc6e 100644 --- a/lib/providers/settings_providers.dart +++ b/lib/providers/settings_providers.dart @@ -34,8 +34,7 @@ class ThemeStateNotifier extends StateNotifier { String? workspaceFolderPath, bool? isSSLDisabled, bool? isDashBotEnabled, - // TODO: Fix it - // LLMSaveObject? defaultLLMSaveObject, + Map? defaultAIModel, }) async { state = state.copyWith( isDark: isDark, @@ -51,8 +50,7 @@ class ThemeStateNotifier extends StateNotifier { workspaceFolderPath: workspaceFolderPath, isSSLDisabled: isSSLDisabled, isDashBotEnabled: isDashBotEnabled, - // TODO: Fix it - // defaultLLMSaveObject: defaultLLMSaveObject, + defaultAIModel: defaultAIModel, ); await setSettingsToSharedPrefs(state); } diff --git a/lib/screens/common_widgets/ai/ai_model_selector_dialog.dart b/lib/screens/common_widgets/ai/ai_model_selector_dialog.dart index 9b6b53cb..be6f7e73 100644 --- a/lib/screens/common_widgets/ai/ai_model_selector_dialog.dart +++ b/lib/screens/common_widgets/ai/ai_model_selector_dialog.dart @@ -22,6 +22,10 @@ class _AIModelSelectorDialogState extends ConsumerState { @override void initState() { super.initState(); + selectedProvider = widget.aiRequestModel?.modelApiProvider; + if (selectedProvider != null && widget.aiRequestModel?.model != null) { + newAIRequestModel = widget.aiRequestModel?.copyWith(); + } aM = ModelManager.fetchAvailableModels(); } diff --git a/lib/screens/common_widgets/code_pane.dart b/lib/screens/common_widgets/code_pane.dart index c4031fae..5c08cc69 100644 --- a/lib/screens/common_widgets/code_pane.dart +++ b/lib/screens/common_widgets/code_pane.dart @@ -29,7 +29,8 @@ class CodePane extends ConsumerWidget { ? getRequestModelFromHistoryModel(selectedHistoryRequestModel!) : ref.watch(selectedRequestModelProvider); - if (selectedRequestModel!.apiType == APIType.ai) { + // TODO: Add AI Request Codegen + if (selectedRequestModel?.apiType == APIType.ai) { return const ErrorMessage( message: "Code generation for AI Requests is currently not available.", ); diff --git a/lib/screens/settings_page.dart b/lib/screens/settings_page.dart index 8131aadd..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,19 +116,22 @@ class SettingsPage extends ConsumerWidget { }, ), ), - // TODO: Fix it - // ListTile( - // hoverColor: kColorTransparent, - // title: const Text('Default Large Language Model (LLM)'), - // trailing: DefaultLLMSelectorButton( - // defaultLLM: settings.defaultLLMSaveObject, - // onDefaultLLMUpdated: (d) { - // ref - // .read(settingsProvider.notifier) - // .update(defaultLLMSaveObject: d); - // }, - // ), - // ), + 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: