diff --git a/README.md b/README.md index fab24a65..cb12c55a 100644 --- a/README.md +++ b/README.md @@ -2,17 +2,7 @@ [![Discord Server Invite](https://img.shields.io/badge/DISCORD-JOIN%20SERVER-5663F7?style=for-the-badge&logo=discord&logoColor=white)](https://discord.com/invite/bBeSdtJ6Ue) -### 🚨🚨 API Dash is participating in GSoC 2025! Check out the details below: - -GSoC - -| | Link | -|--|--| -| Learn about GSoC | [Link](https://summerofcode.withgoogle.com) | -| API Dash GSoC Page | [Link](https://summerofcode.withgoogle.com/programs/2025/organizations/api-dash) | -| Project Ideas List | [Link](https://github.com/foss42/apidash/discussions/565) | -| Application Guide | [Link](https://github.com/foss42/apidash/discussions/564) | -| Discord Channel | [Link](https://discord.com/invite/bBeSdtJ6Ue) | +GSoC ### Please support this initiative by giving this project a Star ⭐️ @@ -94,6 +84,7 @@ API Dash can be downloaded from the links below: | HTTP | ✅ | | GraphQL | ✅ | | SSE/Streaming | ✅ | +| AI | https://github.com/foss42/apidash/issues/871 | | WebSocket | https://github.com/foss42/apidash/issues/15 | | MQTT | https://github.com/foss42/apidash/issues/115 | | gRPC | https://github.com/foss42/apidash/issues/14 | diff --git a/doc/gsoc/2025/codes/example4.dart b/doc/gsoc/2025/codes/example4.dart new file mode 100644 index 00000000..83344c99 --- /dev/null +++ b/doc/gsoc/2025/codes/example4.dart @@ -0,0 +1,743 @@ +// ignore_for_file: use_key_in_widget_constructors, use_full_hex_values_for_flutter_colors + +import 'package:flutter/material.dart'; + +void main() { + runApp(const MyApp()); +} + +class MyApp extends StatelessWidget { + const MyApp({super.key}); + + @override + Widget build(BuildContext context) { + return MaterialApp( + debugShowCheckedModeBanner: false, + home: Scaffold(body: Center(child: SDUIWidget())), + ); + } +} + +class SDUIWidget extends StatelessWidget { + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: const Color(0xFF303030), + body: Column( + children: [ + Text( + "Different Dog Breeds", + style: TextStyle( + color: const Color(0xFFFF00), + fontSize: 30.0, + ), + ), + Expanded( + child: GridView.count( + padding: const EdgeInsets.fromLTRB(10, 10, 10, 10), + crossAxisCount: 3, + mainAxisSpacing: 10.0, + crossAxisSpacing: 10.0, + childAspectRatio: 1.0, + children: [ + Card( + color: const Color(0xFFFFFFE0), + child: Expanded( + child: Padding( + padding: const EdgeInsets.fromLTRB(8, 8, 8, 8), + child: SingleChildScrollView( + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Card( + color: const Color(0xFFCCCAC0), + child: Padding( + padding: const EdgeInsets.fromLTRB(4, 4, 4, 4), + child: Text( + "Name: Affenpinscher", + style: TextStyle( + fontSize: 16.8, + fontWeight: FontWeight.w700, + color: const Color(0xFFFFFFFF), + ), + ), + ), + ), + Card( + color: const Color(0xFFCCCAC0), + child: Padding( + padding: const EdgeInsets.fromLTRB(4, 4, 4, 4), + child: Text( + "Description: The Affenpinscher is a small and playful breed of dog that was originally bred in Germany for hunting small game. They are intelligent, energetic, and affectionate, and make excellent companion dogs.", + style: TextStyle( + fontSize: 16.8, + color: const Color(0xFFFFFFFF), + ), + ), + ), + ), + Card( + color: const Color(0xFFCCCAC0), + child: Padding( + padding: const EdgeInsets.fromLTRB(4, 4, 4, 4), + child: Text( + "Life Expectancy: 14 - 16 years", + style: TextStyle( + fontSize: 16.8, + fontWeight: FontWeight.w700, + color: const Color(0xFFFFFFFF), + ), + ), + ), + ), + Card( + color: const Color(0xFFCCCAC0), + child: Padding( + padding: const EdgeInsets.fromLTRB(4, 4, 4, 4), + child: Text( + "Male Weight: 3 - 5 kg", + style: TextStyle( + fontSize: 16.8, + fontWeight: FontWeight.w700, + color: const Color(0xFFFFFFFF), + ), + ), + ), + ), + Card( + color: const Color(0xFFCCCAC0), + child: Padding( + padding: const EdgeInsets.fromLTRB(4, 4, 4, 4), + child: Text( + "Female Weight: 3 - 5 kg", + style: TextStyle( + fontSize: 16.8, + fontWeight: FontWeight.w700, + color: const Color(0xFFFFFFFF), + ), + ), + ), + ), + Card( + color: const Color(0xFFCCCAC0), + child: Padding( + padding: const EdgeInsets.fromLTRB(4, 4, 4, 4), + child: Text( + "Hypoallergenic: true", + style: TextStyle( + fontSize: 16.8, + fontWeight: FontWeight.w700, + color: const Color(0xFFFFFFFF), + ), + ), + ), + ), + ], + ), + ), + ), + ), + ), + Card( + color: const Color(0xFF90EE90), + child: Expanded( + child: Padding( + padding: const EdgeInsets.fromLTRB(8, 8, 8, 8), + child: SingleChildScrollView( + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Card( + color: const Color(0xFF75C475), + child: Padding( + padding: const EdgeInsets.fromLTRB(4, 4, 4, 4), + child: Text( + "Name: Afghan Hound", + style: TextStyle( + fontSize: 16.8, + fontWeight: FontWeight.w700, + color: const Color(0xFFFFFFFF), + ), + ), + ), + ), + Card( + color: const Color(0xFF75C475), + child: Padding( + padding: const EdgeInsets.fromLTRB(4, 4, 4, 4), + child: Text( + "Description: The Afghan Hound is a large and elegant breed of dog that was originally bred in Afghanistan for hunting small game. They are intelligent, independent, and athletic, and make excellent companion dogs.", + style: TextStyle( + fontSize: 16.8, + color: const Color(0xFFFFFFFF), + ), + ), + ), + ), + Card( + color: const Color(0xFF75C475), + child: Padding( + padding: const EdgeInsets.fromLTRB(4, 4, 4, 4), + child: Text( + "Life Expectancy: 12 - 14 years", + style: TextStyle( + fontSize: 16.8, + fontWeight: FontWeight.w700, + color: const Color(0xFFFFFFFF), + ), + ), + ), + ), + Card( + color: const Color(0xFF75C475), + child: Padding( + padding: const EdgeInsets.fromLTRB(4, 4, 4, 4), + child: Text( + "Male Weight: 23 - 27 kg", + style: TextStyle( + fontSize: 16.8, + fontWeight: FontWeight.w700, + color: const Color(0xFFFFFFFF), + ), + ), + ), + ), + Card( + color: const Color(0xFF75C475), + child: Padding( + padding: const EdgeInsets.fromLTRB(4, 4, 4, 4), + child: Text( + "Female Weight: 20 - 25 kg", + style: TextStyle( + fontSize: 16.8, + fontWeight: FontWeight.w700, + color: const Color(0xFFFFFFFF), + ), + ), + ), + ), + Card( + color: const Color(0xFF75C475), + child: Padding( + padding: const EdgeInsets.fromLTRB(4, 4, 4, 4), + child: Text( + "Hypoallergenic: false", + style: TextStyle( + fontSize: 16.8, + fontWeight: FontWeight.w700, + color: const Color(0xFFFFFFFF), + ), + ), + ), + ), + ], + ), + ), + ), + ), + ), + Card( + color: const Color(0xFFADD8E6), + child: Expanded( + child: Padding( + padding: const EdgeInsets.fromLTRB(8, 8, 8, 8), + child: SingleChildScrollView( + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Card( + color: const Color(0xFF95BDCC), + child: Padding( + padding: const EdgeInsets.fromLTRB(4, 4, 4, 4), + child: Text( + "Name: Akita", + style: TextStyle( + fontSize: 16.8, + fontWeight: FontWeight.w700, + color: const Color(0xFFFFFFFF), + ), + ), + ), + ), + Card( + color: const Color(0xFF95BDCC), + child: Padding( + padding: const EdgeInsets.fromLTRB(4, 4, 4, 4), + child: Text( + "Description: The Akita is a large, muscular dog breed that originated in Japan. They are known for their loyalty and courage.", + style: TextStyle( + fontSize: 16.8, + color: const Color(0xFFFFFFFF), + ), + ), + ), + ), + Card( + color: const Color(0xFF95BDCC), + child: Padding( + padding: const EdgeInsets.fromLTRB(4, 4, 4, 4), + child: Text( + "Life Expectancy: 10 - 12 years", + style: TextStyle( + fontSize: 16.8, + fontWeight: FontWeight.w700, + color: const Color(0xFFFFFFFF), + ), + ), + ), + ), + Card( + color: const Color(0xFF95BDCC), + child: Padding( + padding: const EdgeInsets.fromLTRB(4, 4, 4, 4), + child: Text( + "Male Weight: 35 - 60 kg", + style: TextStyle( + fontSize: 16.8, + fontWeight: FontWeight.w700, + color: const Color(0xFFFFFFFF), + ), + ), + ), + ), + Card( + color: const Color(0xFF95BDCC), + child: Padding( + padding: const EdgeInsets.fromLTRB(4, 4, 4, 4), + child: Text( + "Female Weight: 35 - 50 kg", + style: TextStyle( + fontSize: 16.8, + fontWeight: FontWeight.w700, + color: const Color(0xFFFFFFFF), + ), + ), + ), + ), + Card( + color: const Color(0xFF95BDCC), + child: Padding( + padding: const EdgeInsets.fromLTRB(4, 4, 4, 4), + child: Text( + "Hypoallergenic: false", + style: TextStyle( + fontSize: 16.8, + fontWeight: FontWeight.w700, + color: const Color(0xFFFFFFFF), + ), + ), + ), + ), + ], + ), + ), + ), + ), + ), + Card( + color: const Color(0xFFE6E6FA), + child: Expanded( + child: Padding( + padding: const EdgeInsets.fromLTRB(8, 8, 8, 8), + child: SingleChildScrollView( + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Card( + color: const Color(0xFFD0D0E0), + child: Padding( + padding: const EdgeInsets.fromLTRB(4, 4, 4, 4), + child: Text( + "Name: Alaskan Klee Kai", + style: TextStyle( + fontSize: 16.8, + fontWeight: FontWeight.w700, + color: const Color(0xFFFFFFFF), + ), + ), + ), + ), + Card( + color: const Color(0xFFD0D0E0), + child: Padding( + padding: const EdgeInsets.fromLTRB(4, 4, 4, 4), + child: Text( + "Description: The Alaskan Klee Kai is a small to medium-sized breed of dog that was developed in Alaska in the 1970s. It is an active and intelligent breed that is loyal and friendly. The Alaskan Klee Kai stands between 13-17 inches at the shoulder and has a double-coat that can come in various colors and patterns.", + style: TextStyle( + fontSize: 16.8, + color: const Color(0xFFFFFFFF), + ), + ), + ), + ), + Card( + color: const Color(0xFFD0D0E0), + child: Padding( + padding: const EdgeInsets.fromLTRB(4, 4, 4, 4), + child: Text( + "Life Expectancy: 12 - 15 years", + style: TextStyle( + fontSize: 16.8, + fontWeight: FontWeight.w700, + color: const Color(0xFFFFFFFF), + ), + ), + ), + ), + Card( + color: const Color(0xFFD0D0E0), + child: Padding( + padding: const EdgeInsets.fromLTRB(4, 4, 4, 4), + child: Text( + "Male Weight: 6 - 7 kg", + style: TextStyle( + fontSize: 16.8, + fontWeight: FontWeight.w700, + color: const Color(0xFFFFFFFF), + ), + ), + ), + ), + Card( + color: const Color(0xFFD0D0E0), + child: Padding( + padding: const EdgeInsets.fromLTRB(4, 4, 4, 4), + child: Text( + "Female Weight: 6 - 7 kg", + style: TextStyle( + fontSize: 16.8, + fontWeight: FontWeight.w700, + color: const Color(0xFFFFFFFF), + ), + ), + ), + ), + Card( + color: const Color(0xFFD0D0E0), + child: Padding( + padding: const EdgeInsets.fromLTRB(4, 4, 4, 4), + child: Text( + "Hypoallergenic: false", + style: TextStyle( + fontSize: 16.8, + fontWeight: FontWeight.w700, + color: const Color(0xFFFFFFFF), + ), + ), + ), + ), + ], + ), + ), + ), + ), + ), + Card( + color: const Color(0xFFF08080), + child: Expanded( + child: Padding( + padding: const EdgeInsets.fromLTRB(8, 8, 8, 8), + child: SingleChildScrollView( + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Card( + color: const Color(0xFFD16767), + child: Padding( + padding: const EdgeInsets.fromLTRB(4, 4, 4, 4), + child: Text( + "Name: Alaskan Malamute", + style: TextStyle( + fontSize: 16.8, + fontWeight: FontWeight.w700, + color: const Color(0xFFFFFFFF), + ), + ), + ), + ), + Card( + color: const Color(0xFFD16767), + child: Padding( + padding: const EdgeInsets.fromLTRB(4, 4, 4, 4), + child: Text( + "Description: The Alaskan Malamute is a large and powerful sled dog from Alaska. They are strong and hardworking, yet friendly and loyal. Alaskan Malamutes have a thick, double coat that can be any color. They are active and require plenty of exercise and mental stimulation to stay healthy and happy.", + style: TextStyle( + fontSize: 16.8, + color: const Color(0xFFFFFFFF), + ), + ), + ), + ), + Card( + color: const Color(0xFFD16767), + child: Padding( + padding: const EdgeInsets.fromLTRB(4, 4, 4, 4), + child: Text( + "Life Expectancy: 12 - 15 years", + style: TextStyle( + fontSize: 16.8, + fontWeight: FontWeight.w700, + color: const Color(0xFFFFFFFF), + ), + ), + ), + ), + Card( + color: const Color(0xFFD16767), + child: Padding( + padding: const EdgeInsets.fromLTRB(4, 4, 4, 4), + child: Text( + "Male Weight: 34 - 39 kg", + style: TextStyle( + fontSize: 16.8, + fontWeight: FontWeight.w700, + color: const Color(0xFFFFFFFF), + ), + ), + ), + ), + Card( + color: const Color(0xFFD16767), + child: Padding( + padding: const EdgeInsets.fromLTRB(4, 4, 4, 4), + child: Text( + "Female Weight: 25 - 34 kg", + style: TextStyle( + fontSize: 16.8, + fontWeight: FontWeight.w700, + color: const Color(0xFFFFFFFF), + ), + ), + ), + ), + Card( + color: const Color(0xFFD16767), + child: Padding( + padding: const EdgeInsets.fromLTRB(4, 4, 4, 4), + child: Text( + "Hypoallergenic: false", + style: TextStyle( + fontSize: 16.8, + fontWeight: FontWeight.w700, + color: const Color(0xFFFFFFFF), + ), + ), + ), + ), + ], + ), + ), + ), + ), + ), + Card( + color: const Color(0xFFFFFFE0), + child: Expanded( + child: Padding( + padding: const EdgeInsets.fromLTRB(8, 8, 8, 8), + child: SingleChildScrollView( + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Card( + color: const Color(0xFFCCCAC0), + child: Padding( + padding: const EdgeInsets.fromLTRB(4, 4, 4, 4), + child: Text( + "Name: American Bulldog", + style: TextStyle( + fontSize: 16.8, + fontWeight: FontWeight.w700, + color: const Color(0xFFFFFFFF), + ), + ), + ), + ), + Card( + color: const Color(0xFFCCCAC0), + child: Padding( + padding: const EdgeInsets.fromLTRB(4, 4, 4, 4), + child: Text( + "Description: The American Bulldog is a large and powerful breed of dog that was originally bred in the United States for working on farms. They are intelligent, loyal, and protective, and make excellent guard dogs.", + style: TextStyle( + fontSize: 16.8, + color: const Color(0xFFFFFFFF), + ), + ), + ), + ), + Card( + color: const Color(0xFFCCCAC0), + child: Padding( + padding: const EdgeInsets.fromLTRB(4, 4, 4, 4), + child: Text( + "Life Expectancy: 12 - 14 years", + style: TextStyle( + fontSize: 16.8, + fontWeight: FontWeight.w700, + color: const Color(0xFFFFFFFF), + ), + ), + ), + ), + Card( + color: const Color(0xFFCCCAC0), + child: Padding( + padding: const EdgeInsets.fromLTRB(4, 4, 4, 4), + child: Text( + "Male Weight: 25 - 50 kg", + style: TextStyle( + fontSize: 16.8, + fontWeight: FontWeight.w700, + color: const Color(0xFFFFFFFF), + ), + ), + ), + ), + Card( + color: const Color(0xFFCCCAC0), + child: Padding( + padding: const EdgeInsets.fromLTRB(4, 4, 4, 4), + child: Text( + "Female Weight: 25 - 45 kg", + style: TextStyle( + fontSize: 16.8, + fontWeight: FontWeight.w700, + color: const Color(0xFFFFFFFF), + ), + ), + ), + ), + Card( + color: const Color(0xFFCCCAC0), + child: Padding( + padding: const EdgeInsets.fromLTRB(4, 4, 4, 4), + child: Text( + "Hypoallergenic: false", + style: TextStyle( + fontSize: 16.8, + fontWeight: FontWeight.w700, + color: const Color(0xFFFFFFFF), + ), + ), + ), + ), + ], + ), + ), + ), + ), + ), + Card( + color: const Color(0xFF90EE90), + child: Expanded( + child: Padding( + padding: const EdgeInsets.fromLTRB(8, 8, 8, 8), + child: SingleChildScrollView( + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Card( + color: const Color(0xFF75C475), + child: Padding( + padding: const EdgeInsets.fromLTRB(4, 4, 4, 4), + child: Text( + "Name: American English Coonhound", + style: TextStyle( + fontSize: 16.8, + fontWeight: FontWeight.w700, + color: const Color(0xFFFFFFFF), + ), + ), + ), + ), + Card( + color: const Color(0xFF75C475), + child: Padding( + padding: const EdgeInsets.fromLTRB(4, 4, 4, 4), + child: Text( + "Description: The American English Coonhound is a large and athletic breed of dog that was originally bred in the United States for hunting raccoons. They are intelligent, energetic, and determined, and make excellent hunting dogs.", + style: TextStyle( + fontSize: 16.8, + color: const Color(0xFFFFFFFF), + ), + ), + ), + ), + Card( + color: const Color(0xFF75C475), + child: Padding( + padding: const EdgeInsets.fromLTRB(4, 4, 4, 4), + child: Text( + "Life Expectancy: 12 - 14 years", + style: TextStyle( + fontSize: 16.8, + fontWeight: FontWeight.w700, + color: const Color(0xFFFFFFFF), + ), + ), + ), + ), + Card( + color: const Color(0xFF75C475), + child: Padding( + padding: const EdgeInsets.fromLTRB(4, 4, 4, 4), + child: Text( + "Male Weight: 20 - 30 kg", + style: TextStyle( + fontSize: 16.8, + fontWeight: FontWeight.w700, + color: const Color(0xFFFFFFFF), + ), + ), + ), + ), + Card( + color: const Color(0xFF75C475), + child: Padding( + padding: const EdgeInsets.fromLTRB(4, 4, 4, 4), + child: Text( + "Female Weight: 20 - 30 kg", + style: TextStyle( + fontSize: 16.8, + fontWeight: FontWeight.w700, + color: const Color(0xFFFFFFFF), + ), + ), + ), + ), + Card( + color: const Color(0xFF75C475), + child: Padding( + padding: const EdgeInsets.fromLTRB(4, 4, 4, 4), + child: Text( + "Hypoallergenic: false", + style: TextStyle( + fontSize: 16.8, + fontWeight: FontWeight.w700, + color: const Color(0xFFFFFFFF), + ), + ), + ), + ), + ], + ), + ), + ), + ), + ), + ], + ), + ), + ], + ), + ); + } +} diff --git a/doc/gsoc/2025/codes/example5.dart b/doc/gsoc/2025/codes/example5.dart new file mode 100644 index 00000000..d7d85ac1 --- /dev/null +++ b/doc/gsoc/2025/codes/example5.dart @@ -0,0 +1,286 @@ +// ignore_for_file: use_key_in_widget_constructors + +import 'package:flutter/material.dart'; + +void main() { + runApp(const MyApp()); +} + +class MyApp extends StatelessWidget { + const MyApp({super.key}); + + @override + Widget build(BuildContext context) { + return MaterialApp( + debugShowCheckedModeBanner: false, + home: Scaffold(body: Center(child: SDUIWidget())), + ); + } +} + +class SDUIWidget extends StatelessWidget { + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: const Color(0xFFFFFFE0), + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text( + "Dog Emojis", + style: TextStyle( + fontSize: 40, + fontWeight: FontWeight.w700, + ), + ), + SizedBox(height: 20), + Table( + columnWidths: { + 0: FixedColumnWidth(200), + 1: FixedColumnWidth(250), + 2: FixedColumnWidth(250), + 3: FixedColumnWidth(100), + }, + defaultColumnWidth: FlexColumnWidth(1), + textDirection: TextDirection.ltr, + defaultVerticalAlignment: TableCellVerticalAlignment.bottom, + border: TableBorder.all( + color: const Color(0xFF428AF5), + width: 1.0, + borderRadius: BorderRadius.circular(16), + ), + children: [ + TableRow( + children: [ + TableCell( + child: Padding( + padding: const EdgeInsets.only( + top: 10, left: 10, right: 10, bottom: 10), + child: Container( + color: const Color(0xFFFFFF00), + height: 50.0, + child: Center( + child: Text( + "Name", + style: TextStyle(fontSize: 21.84), + ), + ), + ), + ), + ), + TableCell( + child: Padding( + padding: const EdgeInsets.only( + top: 10, left: 10, right: 10, bottom: 10), + child: Container( + color: const Color(0xFFFFFF00), + height: 50.0, + child: Center( + child: Text( + "Category", + style: TextStyle(fontSize: 21.84), + ), + ), + ), + ), + ), + TableCell( + child: Padding( + padding: const EdgeInsets.only( + top: 10, left: 10, right: 10, bottom: 10), + child: Container( + color: const Color(0xFFFFFF00), + height: 50.0, + child: Center( + child: Text( + "Group", + style: TextStyle(fontSize: 21.84), + ), + ), + ), + ), + ), + TableCell( + child: Padding( + padding: const EdgeInsets.only( + top: 10, left: 10, right: 10, bottom: 10), + child: Container( + color: const Color(0xFFFFFF00), + height: 50.0, + child: Center( + child: Text( + "Emoji", + style: TextStyle(fontSize: 21.84), + ), + ), + ), + ), + ), + ], + ), + TableRow( + children: [ + TableCell( + child: Padding( + padding: const EdgeInsets.only( + top: 10, left: 10, right: 10, bottom: 10), + child: Center( + child: Text( + "dog face", + style: TextStyle(fontSize: 21.84), + ), + ), + ), + ), + TableCell( + child: Padding( + padding: const EdgeInsets.only( + top: 10, left: 10, right: 10, bottom: 10), + child: Center( + child: Text( + "animals and nature", + style: TextStyle(fontSize: 21.84), + ), + ), + ), + ), + TableCell( + child: Padding( + padding: const EdgeInsets.only( + top: 10, left: 10, right: 10, bottom: 10), + child: Center( + child: Text( + "animal mammal", + style: TextStyle(fontSize: 21.84), + ), + ), + ), + ), + TableCell( + child: Padding( + padding: const EdgeInsets.only( + top: 10, left: 10, right: 10, bottom: 10), + child: Center( + child: Text( + "🐶", + style: TextStyle(fontSize: 30.576), + ), + ), + ), + ), + ], + ), + TableRow( + children: [ + TableCell( + child: Padding( + padding: const EdgeInsets.only( + top: 10, left: 10, right: 10, bottom: 10), + child: Center( + child: Text( + "dog", + style: TextStyle(fontSize: 21.84), + ), + ), + ), + ), + TableCell( + child: Padding( + padding: const EdgeInsets.only( + top: 10, left: 10, right: 10, bottom: 10), + child: Center( + child: Text( + "animals and nature", + style: TextStyle(fontSize: 21.84), + ), + ), + ), + ), + TableCell( + child: Padding( + padding: const EdgeInsets.only( + top: 10, left: 10, right: 10, bottom: 10), + child: Center( + child: Text( + "animal mammal", + style: TextStyle(fontSize: 21.84), + ), + ), + ), + ), + TableCell( + child: Padding( + padding: const EdgeInsets.only( + top: 10, left: 10, right: 10, bottom: 10), + child: Center( + child: Text( + "🐕", + style: TextStyle(fontSize: 30.576), + ), + ), + ), + ), + ], + ), + TableRow( + children: [ + TableCell( + child: Padding( + padding: const EdgeInsets.only( + top: 10, left: 10, right: 10, bottom: 10), + child: Center( + child: Text( + "hot dog", + style: TextStyle(fontSize: 21.84), + ), + ), + ), + ), + TableCell( + child: Padding( + padding: const EdgeInsets.only( + top: 10, left: 10, right: 10, bottom: 10), + child: Center( + child: Text( + "food and drink", + style: TextStyle(fontSize: 21.84), + ), + ), + ), + ), + TableCell( + child: Padding( + padding: const EdgeInsets.only( + top: 10, left: 10, right: 10, bottom: 10), + child: Center( + child: Text( + "food prepared", + style: TextStyle(fontSize: 21.84), + ), + ), + ), + ), + TableCell( + child: Padding( + padding: const EdgeInsets.only( + top: 10, left: 10, right: 10, bottom: 10), + child: Center( + child: Text( + "🌭", + style: TextStyle(fontSize: 30.576), + ), + ), + ), + ), + ], + ), + ], + ), + ], + ), + ), + ); + } +} diff --git a/doc/gsoc/2025/images/aireq1.png b/doc/gsoc/2025/images/aireq1.png new file mode 100644 index 00000000..ee4d72ad Binary files /dev/null and b/doc/gsoc/2025/images/aireq1.png differ diff --git a/doc/gsoc/2025/images/aireq2.png b/doc/gsoc/2025/images/aireq2.png new file mode 100644 index 00000000..77464427 Binary files /dev/null and b/doc/gsoc/2025/images/aireq2.png differ diff --git a/doc/gsoc/2025/images/aiuiexample1.png b/doc/gsoc/2025/images/aiuiexample1.png new file mode 100644 index 00000000..674ac298 Binary files /dev/null and b/doc/gsoc/2025/images/aiuiexample1.png differ diff --git a/doc/gsoc/2025/images/apischema.png b/doc/gsoc/2025/images/apischema.png new file mode 100644 index 00000000..813294a4 Binary files /dev/null and b/doc/gsoc/2025/images/apischema.png differ diff --git a/doc/gsoc/2025/images/bnetlcov.png b/doc/gsoc/2025/images/bnetlcov.png new file mode 100644 index 00000000..0b6133a3 Binary files /dev/null and b/doc/gsoc/2025/images/bnetlcov.png differ diff --git a/doc/gsoc/2025/images/codecovold.png b/doc/gsoc/2025/images/codecovold.png new file mode 100644 index 00000000..789908d6 Binary files /dev/null and b/doc/gsoc/2025/images/codecovold.png differ diff --git a/doc/gsoc/2025/images/example5ui.png b/doc/gsoc/2025/images/example5ui.png new file mode 100644 index 00000000..2eef5235 Binary files /dev/null and b/doc/gsoc/2025/images/example5ui.png differ diff --git a/doc/gsoc/2025/images/exmp2.png b/doc/gsoc/2025/images/exmp2.png new file mode 100644 index 00000000..363da4ca Binary files /dev/null and b/doc/gsoc/2025/images/exmp2.png differ diff --git a/doc/gsoc/2025/images/gencomp.png b/doc/gsoc/2025/images/gencomp.png new file mode 100644 index 00000000..42db9613 Binary files /dev/null and b/doc/gsoc/2025/images/gencomp.png differ diff --git a/doc/gsoc/2025/images/llmarch.png b/doc/gsoc/2025/images/llmarch.png new file mode 100644 index 00000000..f8442003 Binary files /dev/null and b/doc/gsoc/2025/images/llmarch.png differ diff --git a/doc/gsoc/2025/images/modelselector1.png b/doc/gsoc/2025/images/modelselector1.png new file mode 100644 index 00000000..a48fe4b6 Binary files /dev/null and b/doc/gsoc/2025/images/modelselector1.png differ diff --git a/doc/gsoc/2025/images/modelselector2.png b/doc/gsoc/2025/images/modelselector2.png new file mode 100644 index 00000000..a39afd58 Binary files /dev/null and b/doc/gsoc/2025/images/modelselector2.png differ diff --git a/doc/gsoc/2025/images/pincodetable.png b/doc/gsoc/2025/images/pincodetable.png new file mode 100644 index 00000000..f19cfdd7 Binary files /dev/null and b/doc/gsoc/2025/images/pincodetable.png differ diff --git a/doc/gsoc/2025/images/sse_ex1.png b/doc/gsoc/2025/images/sse_ex1.png new file mode 100644 index 00000000..03ea73b6 Binary files /dev/null and b/doc/gsoc/2025/images/sse_ex1.png differ diff --git a/doc/gsoc/2025/images/stacreq.png b/doc/gsoc/2025/images/stacreq.png new file mode 100644 index 00000000..0f16a788 Binary files /dev/null and b/doc/gsoc/2025/images/stacreq.png differ diff --git a/doc/gsoc/2025/images/stacreq2.png b/doc/gsoc/2025/images/stacreq2.png new file mode 100644 index 00000000..931587dd Binary files /dev/null and b/doc/gsoc/2025/images/stacreq2.png differ diff --git a/doc/gsoc/2025/images/toolgen.png b/doc/gsoc/2025/images/toolgen.png new file mode 100644 index 00000000..0da50583 Binary files /dev/null and b/doc/gsoc/2025/images/toolgen.png differ diff --git a/doc/gsoc/2025/manas_hejmadi.md b/doc/gsoc/2025/manas_hejmadi.md index 8b137891..2d5bede4 100644 --- a/doc/gsoc/2025/manas_hejmadi.md +++ b/doc/gsoc/2025/manas_hejmadi.md @@ -1 +1,758 @@ +# GSoC'25 - AI-Based API Response to Dynamic UI and Tool Generator +> Final report summarizing my contributions to the project as part of GSoC'25 + +## Project Details +1. **Contributor** : Manas M Hejmadi +2. **Mentors** : Ashita P, Ankit M, Ragul Raj M +3. **Organization**: API Dash +4. **Project**: AI-Based API Response to Dynamic UI and Tool Generator + +#### Quick Links +- [GSoC Project Page](https://summerofcode.withgoogle.com/programs/2025/projects/hhUUM8wl) +- [Code Repository](https://github.com/foss42/apidash) +- [Discussion Logs](https://github.com/foss42/apidash/discussions/852) + +## Project Description + +The primary objective of this project was to extend the API Dash client with new generative AI capabilities that go far beyond the scope of traditional API testing clients. This will position API Dash as an OpenSource AI-Native API Testing client. + +Our initial vision was to develop an AI-powered agent capable of transforming raw API responses into structured UI schemas and fully functional UI components that could be directly exported and used in frontend applications. Additionally, we wanted to enable dynamic customization of UI components through natural language prompts, allowing developers to customise the design and layout according to their personal preference. This UI code could then be exported and used directly in their Flutter projects. + +As mentioned in the project proposal, we were also aiming to create a one-click `API Request to Tool` generation pipeline, allowing external AI agents to independently interact with APIs. This is a crucial requirement for modern agentic workflows and the idea was that API Dash must be ready to serve these needs. + +However, during the planning phase it became clear that these ambitious features required strong foundational infrastructure to work at a production level. Under the guidance of my mentors, we identified and implemented several core architectural improvements, such as: + +- Refactoring the networking layer into a modular, standalone package to enhance testability and maintainability. +- Adding streaming support via Server-Sent Events (SSE) to enable real-time AI interactions. +- Introducing AI request handling and a dedicated AI primitives package. This ensures that any future API Dash feature that would need generative ai, can directly import this primitives package instead of implementing everything again. this saves both time and effort. + +All in all, these completion of these improvements will establish API Dash as a modern, industry ready platform for developers and AI-driven workflows alike. + + +## Feature Description + +### `better_networking` package creation & project-wide refactor + +`Package Link`: https://pub.dev/packages/better_networking + +`Associated Pull Request`: [#857](https://github.com/foss42/apidash/pull/857) + +Initially, the entire networking constructs that API Dash relied on was fully written inside a module named `apidash_core`. +The networking code was fairly advanced including support for GraphQL, request cancellations and a lot of other good features. However, as it was tightly coupled with API Dash, we were unable to allow the rest of the flutter developer community to use these features. We believe in giving back to the open source community whenever we can and hence the mentors and I decided to refactor everything into a new package. +During discussions, I came up with the name `better_networking` and we envisioned it to be the go-to package for everything related to networking for a flutter application. + +This is an example of how better_networking simplifies request handling + + +```dart +final model = HttpRequestModel( + url: 'https://api.example.com/data', + method: HTTPVerb.post, + headers: [ + NameValueModel(name: 'Authorization', value: 'Bearer '), + ], + body: '{"key": "value"}', +); + +//Sending HTTP Requests +final (resp, duration, err) = await sendHttpRequest( + 'unique-request-id', + APIType.rest, + model, +); + +// To cancel the request +cancelHttpRequest('unique-request-id'); +``` + +This proved to be a great decision, as we were able to separate it completely, publish it on [pub.dev](https://pub.dev/packages/better_networking), and achieve over 95% code coverage through isolated testing. + +Code coverage before refactor: +![Code Coverage Report](./images/codecovold.png) + +Code coverage after Refactor: +![Code Coverage Report](./images/bnetlcov.png) + +--- + +### Added SSE and Streaming Support to the Client +`Associated Pull Request`: [#861](https://github.com/foss42/apidash/pull/861) + +![Code Coverage Report](./images/sse_ex1.png) + +SSE Support was a long pending [issue](https://github.com/foss42/apidash/issues/116) (since 2024). Once I completed the initial version of better_networking, the mentors asked me to look if SSE support can be added within the package and by extension into API Dash, as AI responses are usually transmitted in this format. After doing some research and a review into the existing PRs by other contributors for this feature, I noticed that everyone created new Request and Response Models for SSE in code. + +However, I did not agree with this approach as SSE is just a different content-type is not a fundamentally separate request type like GraphQL. +To demonstrate this, I wrote up a quick demo with SSE baked into the existing API Dash foundations. + +This new mechanism is very simple and elegant. Basically, every request in API Dash is executed in streaming mode using `StreamedResponse` in dart. If the response headers specify a content-type marked as streaming, the listener remains active and statefully saves all incoming values into the sseOutput attribute of the response model. If the content-type does not match any supported streaming type, the listener terminates and the output is returned immediately. In this way, the existing request/response model can handle both Streaming and Normal HTTP Requests + +This is an example of how I rewrote the original implementation of `sendHttpRequest` in terms of this new SSE handler +```dart +Future<(HttpResponse?, Duration?, String?)> sendHttpRequest( + String requestId, + APIType apiType, + HttpRequestModel requestModel, { + SupportedUriSchemes defaultUriScheme = kDefaultUriScheme, + bool noSSL = false, +}) async { + final stream = await streamHttpRequest( + requestId, + apiType, + requestModel, + defaultUriScheme: defaultUriScheme, + noSSL: noSSL, + ); + final output = await stream.first; + return (output?.$2, output?.$3, output?.$4); +} +``` +The mentors were impressed with this approach as it was far more maintainable and sensible than creating new models specifically for SSE. +This way, everything stays unified and we reduce the amount of duplication + +--- + +### Added Agents and AI Requests Support + +`Associated Pull Request`: [#870](https://github.com/foss42/apidash/pull/870) + +`Package Link`: https://pub.dev/packages/genai + +Since my project involved sending AI API requests, the next step involved adding support for AI requests and an interface for building agentic systems in Dart & Flutter. Hence, I started developing a comprehensive AI Requests feature. + +The user initiates a new request by selecting “AI”, then chooses a model and provides the required credentials through the Authorization tab. The request can be further configured by specifying system and user prompts and adjusting parameters such as `temperature`, `topP`, and `streaming or non-streaming mode`. Upon submission, the response is generated and presented either as cleaned plaintext/Markdown or in raw format, based on the user’s selection. + + +![AI Requests](./images/aireq1.png) +![AI Requests](./images/aireq2.png) + +My initial implementation used tightly coupled LLM providers (e.g., gemini, openai) with specific models (e.g., gemini-2.0-flash) through hardcoded enums. These enums were directly referenced in code, which on closer review proved unsustainable. Given the rapid pace of innovation in LLMs, models become obsolete quickly, and maintaining hardcoded enums would require frequent code changes and was looking quite impractical. +Furthermore, using hardcoded enums prevents runtime dynamic loading, restricting users to only the models we explicitly provide. This limits flexibility and creates a poor experience, especially for advanced users who may need access to less common or custom models. + +To address this, we adopted a remote model fetch system, where model identifiers are stored in a `models.json` file within the public API Dash repository. Clients fetch this file at runtime, enabling over-the-air updates to model availability. In addition, we added support for custom model identifiers directly within the ModelSelector, giving users full flexibility to configure their own models. + +Currently, we support several standard providers—such as Google Gemini, OpenAI, Anthropic, and Ollama—which offers a strong baseline of options while still allowing advanced customization. + +![LLM Provider Selector](./images/modelselector1.png) + +The AI Requests feature is built on top of the foundational genai package, which serves as the core layer for all AI-related functionality within API Dash. +This package provides the complete set of API callers, methods, and formatters required to abstract away the complexities of interacting with AI tool APIs. By exposing a generalized interface across multiple providers, it eliminates the need to handle provider-specific details directly. +As a result, developers can easily build features that leverage generative AI without worrying about low-level implementation details—leaving the intricacies of API communication and formatting to the genai package. + +Example of simplified usage (model-agnostic, works with any LLM out of the box) +```dart +final request = AIRequestModel( + modelApiProvider: ModelAPIProvider.gemini, // or openai, anthropic, etc. + model: "gemini-2.0-flash", + apiKey: "", + url: kGeminiUrl, + systemPrompt: "You are a helpful assistant.", + userPrompt: "Explain quantum entanglement simply.", + stream: false, // set true for streaming +); +await callGenerativeModel( + request, + onAnswer: (ans) => print("AI Output: $ans"), + onError: (err) => print("Error: $err"), +); +``` + +#### Agentic Infrastructure + +![Agentic Infrastructure](./images/llmarch.png) + +When developing AI-powered features in any application, the process typically involves steps such as system prompting, data validation, and output formatting. However, repeating this workflow for multiple features while taking care of errors and retry logic quickly becomes very cumbersom. To simplify this, we designed a well-defined architecture for building AI agents directly within code. + +The core idea is straightforward: an AI agent in API Dash is simply a Dart file containing a class that extends the base class `APIDashAIAgent`, defined as: +```dart +abstract class AIAgent { + String get agentName; + String getSystemPrompt(); + Future validator(String aiResponse); + Future outputFormatter(String validatedResponse); +} +``` +This base class provides the necessary hooks for implementing an agent. Developers can either rely on the default implementations or override these handlers with custom logic. The result is a fully abstracted, self-contained agent that can be invoked seamlessly from within the application. + +These agents operate within an orchestrator and governor framework that manages everything behind the scenes. This design ensures that developers only need to invoke the agent, while background processes handle concerns such as automatic retries, exponential backoff, and error recovery seamlessly. This saves a lot of time and effort and allows developers to spend more time on improving their actual feature implementation. + + +#### Sample Agent Code + +```dart +//simple_func_agent.dart + +class SimpleFuncGenerator extends AIAgent { + @override + String get agentName => 'SIMPLE_FUNCGEN'; + + @override + String getSystemPrompt() { + return """you are a programming language function generator. + your only task is to take whatever requirement is provided and convert + it into a valid function named func in the provided programming language + + LANGUAGE: :LANG: + REQUIREMENT: :REQUIREMENT: +"""; //suports templating via :: + } + + @override + Future validator(String aiResponse) async { + return aiResponse.contains("func"); + } + + @override + Future outputFormatter(String validatedResponse) async { + validatedResponse = validatedResponse + .replaceAll(RegExp(r'```[a-zA-Z]*\n?'), '') + .replaceAll('```', ''); + return { + 'FUNC': validatedResponse, + }; + } +} + +//Calling an agent + final res = await APIDashAgentCaller.instance.call( + SimpleFuncGenerator(), + ref: ref, + input: AgentInputs(variables: { + 'REQUIREMENT': 'take the median of the given array', + 'TARGET_LANGUAGE': 'python3', + }), + ); + +``` + +--- + +### Created the API Tool Generator +As proposed in my GSoC proposal, I set out to implement an API Tool Call Generator within the application. It consists of an in-app agent that processes API request details and converts them into standardized tool-call code compatible with providers such as OpenAI, Gemini, LangChain, and others. + +This feature is fully built on top of the agentic foundation established by `genai`. Once a user executes an API request and receives a response, they can click “Generate Tool”, which opens a tool generation dialog. Here, the user selects their preferred agent framework along with the target output language (currently Python or Node.js). The client then consolidates all request details, sends them to an LLM, and generates the corresponding function callers. These are subsequently integrated into a predefined, well-researched API Tool template, ensuring reliability and consistency. + +![Tool Generation](./images/toolgen.png) + +Here's the generated tool code +```python +import requests +def func(): + """ + Retrieves a list of users from the reqres.in API. + """ + url = "https://reqres.in/api/users" + headers = { + "X-Api-Key": "reqres-free-v1" + } + try: + response = requests.get(url, headers=headers) + response.raise_for_status() + return response.json() + except requests.exceptions.RequestException as e: + print(f"Request failed: {e}") + return None + +api_tool = { + "function": func, + "definition": { + "name": "GetUsers", + "description": "Retrieves a list of users from the reqres.in API.", + "parameters": { + "type": "object", + "properties": {}, + "required": [], + "additionalProperties": False + } + } +} + +__all__ = ["api_tool"] +``` + +--- + +### Implemented the API Schema to Flutter UI Generator + +With the foundational infrastructure in place, I was finally getting closer to my GSoC goal of building the AI UI Designer. + +The purpose of this feature is straightforward yet powerful—take API responses and automatically transform them into suitable UI components, while also providing the ability to modify the design through natural language instructions. Additionally, the generated UI can be exported as Flutter code, enabling seamless integration into frontend applications. + +A Proof of Concept (PoC) for this functionality had already been demonstrated during the initial phase of GSoC. The remaining work involved converting the PoC into production-ready code, addressing error handling, improving stability, and ensuring it could scale as a fully integrated feature within API Dash. + +This marks a significant milestone, as the AI UI Designer bridges the gap between raw API responses and usable frontend components—removing boilerplate work and streamlining the developer workflow. + +![API Response](./images/apischema.png) + +With the AI UI Designer, the response returned from the above API can be automatically converted into a Flutter widget. This widget is generated and rendered using the Server-Driven UI (SDUI) approach, powered by the [Stac](https://pub.dev/packages/stac) package. + +This is what the generated component looks like: + +![Generated Widget](./images/gencomp.png) + +The prompts that were used to achieve this final result: + +> 1. Use a ListTile based layout instead of cards and add uniform padding +> 2. Make the page background as light yellow and change the appbar's background color to black and foreground color to white + +### Additional Examples of Generated UI Components + +Here's the Entire Creation Flow + +https://github.com/user-attachments/assets/a4074f28-2aaa-471a-b9bb-623d731b7515 + +#### Example 1 +`GET` https://rickandmortyapi.com/api/character/[1,2,3,4,5,6,7] +```json +[ + { + "id": 1, + "name": "Rick Sanchez", + "status": "Alive", + "species": "Human", + "type": "", + "gender": "Male", + "origin": { + "name": "Earth (C-137)", + "url": "https://rickandmortyapi.com/api/location/1" + }, + "location": { + "name": "Citadel of Ricks", + "url": "https://rickandmortyapi.com/api/location/3" + }, + "image": "https://rickandmortyapi.com/api/character/avatar/1.jpeg", + "episode": [ + "https://rickandmortyapi.com/api/episode/1", + "https://rickandmortyapi.com/api/episode/2", + "https://rickandmortyapi.com/api/episode/3", + ], + "url": "https://rickandmortyapi.com/api/character/1", + "created": "2017-11-04T18:48:46.250Z" + }, + { + "id": 2, + "name": "Morty Smith", + "status": "Alive", + "species": "Human", + "type": "", + "gender": "Male", + "origin": { + "name": "unknown", + "url": "" + }, + "location": { + "name": "Citadel of Ricks", + "url": "https://rickandmortyapi.com/api/location/3" + }, + "image": "https://rickandmortyapi.com/api/character/avatar/2.jpeg", + "url": "https://rickandmortyapi.com/api/character/2", + "created": "2017-11-04T18:50:21.651Z" + }, + ... +] +``` +The Generated Component Preview looks like + +![Generated Widget](./images/aiuiexample1.png) + + +#### Example 2 +`GET` https://api.nasa.gov/planetary/apod?date=&start_date=&end_date=&count=&thumbs +```json +{ + "copyright": "Marina Prol", + "date": "2025-08-30", + "explanation": "A young crescent moon can be hard to see. That's because when the Moon shows its crescent phase (young or old) it can never be far from the Sun in planet Earth's sky. But even though the sky is still bright, a slender sunlit lunar crescent is clearly visible in this early evening skyscape. The telephoto snapshot was captured on August 24, with the Moon very near the western horizon at sunset. Seen in a narrow crescent phase about 1.5 days old, the visible sunlit portion is a mere two percent of the surface of the Moon's familiar nearside. At the Canary Islands Space Centre, a steerable radio dish for communication with spacecraft is tilted in the direction of the two percent Moon. The sunset sky's pastel pinkish coloring is partly due to fine sand and dust from the Sahara Desert blown by the prevailing winds.", + "hdurl": "https://apod.nasa.gov/apod/image/2508/IMG_4081.jpeg", + "media_type": "image", + "service_version": "v1", + "title": "A Two Percent Moon", + "url": "https://apod.nasa.gov/apod/image/2508/IMG_4081_1024.jpeg" +} +``` +Generated UI: + +![Generated Widget](./images/exmp2.png) + +Modification Prompts: +``` +- separate the single card layout into (image+title) in one card and +description in another card +- Resize the image to be 300x300 with a fit of "contain" +- implement dark theme in the whole page with the cards being darker +- app bar must be dark themed with yellow font colour +- the title "A Two Percent Moon" should be centered and yellow coloured +- description text can be light yellow coloured with decent padding +remove any copyright +``` + +Exported Flutter Code: +```dart +class SDUIWidget extends StatelessWidget { + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: const Color(0xFF333333), + appBar: AppBar( + backgroundColor: const Color(0xFF222222), + title: const Text( + "Astronomy Picture of the Day", + style: TextStyle( + color: Color(0xFFFFDA63), + ), + ), + ), + body: SingleChildScrollView( + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Card( + color: const Color(0xFF121212), + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Image.network( + "https://apod.nasa.gov/apod/image/2508/IMG_4081_1024.jpeg", + width: 300, + height: 300, + fit: BoxFit.contain, + ), + const Padding( + padding: EdgeInsets.fromLTRB(8, 8, 8, 8), + child: Text( + "A Two Percent Moon", + style: TextStyle( + color: Color(0xFFFFDA63), + fontSize: 24, + ), + textAlign: TextAlign.center, + ), + ), + ], + ), + ), + Card( + color: const Color(0xFF121212), + child: SingleChildScrollView( + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Padding( + padding: EdgeInsets.fromLTRB(8, 8, 8, 8), + child: Text( + "A young crescent moon can be hard to see. That's because when the Moon shows its crescent phase (young or old) it can never be far from the Sun in planet Earth's sky. But even though the sky is still bright, a slender sunlit lunar crescent is clearly visible in this early evening skyscape. The telephoto snapshot was captured on August 24, with the Moon very near the western horizon at sunset. Seen in a narrow crescent phase about 1.5 days old, the visible sunlit portion is a mere two percent of the surface of the Moon's familiar nearside. At the Canary Islands Space Centre, a steerable radio dish for communication with spacecraft is tilted in the direction of the two percent Moon. The sunset sky's pastel pinkish coloring is partly due to fine sand and dust from the Sahara Desert blown by the prevailing winds.", + style: TextStyle( + color: Color(0xFFFFFACD), + fontSize: 16, + ), + ), + ), + ], + ), + ), + ), + ], + ), + ), + ); + } +} +``` + +Click here to view the [Complete Process Demo](https://youtu.be/QCW9Vt_PcYE) + +#### Example 3 +`GET`: https://api.postalpincode.in/pincode/560078 +```json +[ + { + "Message": "Number of pincode(s) found:4", + "Status": "Success", + "PostOffice": [ + { + "Name": "J P Nagar", + "Description": null, + "BranchType": "Sub Post Office", + "DeliveryStatus": "Delivery", + "Circle": "Karnataka", + "District": "Bangalore", + "Division": "Bangalore South", + "Region": "Bangalore HQ", + "Block": "Bangalore South", + "State": "Karnataka", + "Country": "India", + "Pincode": "560078" + }, + { + "Name": "JP Nagar III Phase", + "Description": null, + "BranchType": "Sub Post Office", + "DeliveryStatus": "Non-Delivery", + "Circle": "Karnataka", + "District": "Bangalore", + "Division": "Bangalore South", + "Region": "Bangalore HQ", + "Block": "Bangalore South", + "State": "Karnataka", + "Country": "India", + "Pincode": "560078" + }, + { + "Name": "Kumaraswamy Layout", + "Description": null, + "BranchType": "Sub Post Office", + "DeliveryStatus": "Non-Delivery", + "Circle": "Karnataka", + "District": "Bangalore", + "Division": "Bangalore South", + "Region": "Bangalore HQ", + "Block": "Bangalore South", + "State": "Karnataka", + "Country": "India", + "Pincode": "560078" + }, + { + "Name": "Yelachenahalli", + "Description": null, + "BranchType": "Sub Post Office", + "DeliveryStatus": "Non-Delivery", + "Circle": "Karnataka", + "District": "Bangalore", + "Division": "Bangalore South", + "Region": "Bangalore HQ", + "Block": "Bangalore South", + "State": "Karnataka", + "Country": "India", + "Pincode": "560078" + } + ] + } +] +``` +Generated UI: + +![](./images/pincodetable.png) + +Modification Prompts Used: +```- remove any scrollviews +- use a Table for the data (name, branch type, delivery status, district, state only) +- add padding to each text element in table cells +- bold the names of the city areas except "Bangalore" +- make the font size 3x and dark green and bold +- add a right and left margin to table of 20px +- vertically center everything +- light green background +- bold the names of the areas except "Bangalore" +- increase the font size of table cell contents by 25% +``` + +Click here to view the [Complete Process Demo](https://youtu.be/T3-yp6ZHltA) + +#### Example 4 +`GET` https://dogapi.dog/api/v2/breeds +```json +{ + "data": [ + { + "id": "667c7359-a739-4f2b-abb4-98867671e375", + "type": "breed", + "attributes": { + "name": "Alaskan Klee Kai", + "description": "The Alaskan Klee Kai is a small to medium-sized breed of dog that was developed in Alaska in the 1970s. It is an active and intelligent breed that is loyal and friendly. The Alaskan Klee Kai stands between 13-17 inches at the shoulder and has a double-coat that can come in various colors and patterns.", + "life": { + "max": 15, + "min": 12 + }, + "male_weight": { + "max": 7, + "min": 6 + }, + "female_weight": { + "max": 7, + "min": 6 + }, + "hypoallergenic": false + }, + "relationships": { + "group": { + "data": { + "id": "8000793f-a1ae-4ec4-8d55-ef83f1f644e5", + "type": "group" + } + } + } + }, + ... + ], + ... +} +``` +Generated UI: + +https://github.com/user-attachments/assets/5ac6794e-c92d-41f3-9e6e-4ffa544827b1 + +Modification Prompts Used: +``` +- remove scrollview +- convert into 3x3 gridview with all children having childAspectRatio of 1.3 +- make the elements in each card of grid view fit its entire card +and make background as dark theme +- Add some padding for each card and also change the background color of the cards to a random one between light yellow, light green, light blue, light purple and light red +- for each text item inside the cards, wrap it with its own card with slightly dark and transparent background color , let font colour be white +- dont separare the cards for the key and value, kep it together and add padding to the inner cards also +``` + +Exported Source Code: [Link](./codes/example4.dart) + +Click here to view the [Complete Process Demo](https://youtu.be/ZXADJOCR9L8) + +#### Example 5 +`GET` https://emojihub.yurace.pro/api/search?q=dog +```json +[ + { + "name": "dog face", + "category": "animals and nature", + "group": "animal mammal", + "htmlCode": [ + "🐶" + ], + "unicode": [ + "U+1F436" + ] + }, + { + "name": "dog", + "category": "animals and nature", + "group": "animal mammal", + "htmlCode": [ + "🐕" + ], + "unicode": [ + "U+1F415" + ] + }, + { + "name": "hot dog", + "category": "food and drink", + "group": "food prepared", + "htmlCode": [ + "🌭" + ], + "unicode": [ + "U+1F32D" + ] + } +] +``` +Generated UI: + +![](./images/example5ui.png) + + +Exported Source Code: [Link](./codes/example5.dart) + +Modification Prompts Used: +``` +- remove the HTML code and convert the whole thing into a table with headers (name, Category, group, emoji). Also add a scaffold with light yellow background color & convert the unicode into EMOJIs +- increase the font size by 2.5x & put the table in the center horizontally and vertically +- let the table be bigger and not width constrained +- add padding for each table cell of 10px +- the shading added to the table header must be yellow color +- add a big size text boldened that says "Dog Emojis" Above the table, centered, spaced by 20px on the bottom +``` + +Click here to view the [Complete Process Demo](https://youtu.be/HBFjmSUHSxM) + +--- + +## Complete Pull Request Report + + + +| Feature | PR | Issue | Status | Comments | +|---|---|---|---|---| +|Proof of Concept & Proposal Doc|[#755](https://github.com/foss42/apidash/pull/755)||Merged|| +|FIX: `` exception|[#780](https://github.com/foss42/apidash/pull/780)|[#782](https://github.com/foss42/apidash/issues/782)|Merged|| +|AI Requests Feature Initial Implementation|[#850](https://github.com/foss42/apidash/pull/850)||Closed|multiple modifications suggested| +|AI Requests Feature Fine tuning|[#856](https://github.com/foss42/apidash/pull/856)||Closed|SSE and Separate Networking layer was deemed necessary before this PR| +|`better_networking` Package Creation|[#857](https://github.com/foss42/apidash/pull/857)||Merged| +`genai` package foundations|[#859](https://github.com/foss42/apidash/pull/859)||Closed|Mentor requested for a new PR after making some changes| +|SSE Feature Foundations|[#860](https://github.com/foss42/apidash/pull/860)||Closed|Mentor requested changes and rebase to main branch| +|SSE & Streaming Support|[#861](https://github.com/foss42/apidash/pull/861)|[#116](https://github.com/foss42/apidash/issues/116)|Merged|| +|`genai` & AI Requests Feature|[#870](https://github.com/foss42/apidash/pull/870)|[#871](https://github.com/foss42/apidash/issues/871)|Merged|| +|`genai` package: Testing|[#882](https://github.com/foss42/apidash/pull/882)||Merged|| +Foundations: Agents & AI UI Designer + Tool Generation |[#874](https://github.com/foss42/apidash/pull/874)||Closed|Mentor requested to make a new PR that was based on top of main branch code| +|AI UI Designer & Tool Generator|[#880](https://github.com/foss42/apidash/pull/880)|[#617](https://github.com/foss42/apidash/issues/617), [#884](https://github.com/foss42/apidash/issues/884)|Merged|| +|Final Report Documentation|[#878](https://github.com/foss42/apidash/pull/878)||Merged|| +--- + +## Challenges Faced + +#### Incomplete Responses after SSE implmentation +After migrating to SSE, API Dash was designed to first listen to a stream and return immediately if the content type wasn’t streaming-related. This worked fine until I discovered an edge case with very long responses: the HTTP protocol splits such responses into multiple packets. Because of the initial stream design, only the first packet was returned, resulting in incomplete outputs. +To fix this, I implemented a manual chunking mechanism where all incoming packets are collected until the stream ends, after which they are concatenated into the complete response. This resolved the issue and ensured correctness for long streaming outputs. + +#### Component Rendering Dilemma +The core feature of the AI UI Designer is an in-app dynamic component renderer. Implementing this is challenging because Dart does not support full runtime reflection for Flutter widgets. In other words, a Flutter program cannot directly execute or render dynamically generated Flutter code at runtime. +I experimented with the available reflection mechanisms in Dart, but they are limited to the language itself and do not extend to Flutter’s widget tree. As a result, I was only able to render very basic elements such as Text widgets. Anything more complex was practically impossible to achieve with Dart’s restricted reflection capabilities. + +Next, I considered using the Dart SDK to build the code into a Flutter Web app and display it to the user through a localhost iframe. However, this would require bundling the Dart SDK with the application, making it significantly heavier. Moreover, it would involve writing platform channel code for macOS, Windows, and other platforms, which would be highly impractical. + +Disappointed with these limitations, I devised a new approach: instead of attempting in-app rendering, I generated the Flutter code and sent it to an external service that could immediately build and deploy it as a Flutter web application, which could then be displayed within an iframe. I implemented this as a project called [FlutRun](https://github.com/synapsecode/AI_UI_designer_prototype) and successfully demonstrated it to the mentors. +As API Dash is a privacy-first API client that prohibits sending user requests to any external servers (with the exception of user specified AI API calls), even routing requests to our own servers is restricted which made this solution impractical to implement. + +Lastly, after some research and discussions with my mentors, I was introduced to the concept of **Server-Driven User Interfaces (SDUI)**. The core idea is to represent UI as a parseable structure (such as JSON) and then dynamically render it using a rendering pipeline written in Flutter. This approach proved to be both practical and efficient. In fact, I came across the Stac package, which implemented this concept seamlessly, and that ultimately became the solution we adopted. + +#### Lack of Error Handling in Stac +Stac (the JSON-based representation of Flutter UIs) is opinionated and differs slightly from native Flutter code. As a result, when LLMs generate Stac code, they often produce small mistakes. + +The challenge is that the Stac framework surfaces these mistakes only as console errors—they don’t bubble up as exceptions to the caller. I attempted to capture them using Flutter’s ErrorZones and similar mechanisms, but without success. + +This limitation is significant because I wanted to implement a reset feature: if the LLM generates invalid Stac code, the app should be able to roll back to the last known good state. With the current design, this isn’t feasible. I even reached out to the Stac founders, who confirmed that proper error bubbling is planned but won’t be available anytime soon. + +The only real workaround right now would be to fork Stac and patch it manually—something I’m still debating. For the time being, we’ve mitigated the issue by tuning the system prompt and restricting generations to a small, well-understood subset of Stac. This approach has been working decently so far. + Have also discussed with the maintainer's of Stac regarding this issue: + + ![Stac Discussion](./images/stacreq.png) + ![Stac Discussion](./images/stacreq2.png) + +#### Stac Code Clipping +When the API response is highly complex, agents may generate a verbose UI specification. This can cause the resulting Stac JSON to become so large that it exceeds the LLM’s output context window, resulting in clipped (incomplete) JSON. +The problem is difficult to detect because Stac lacks proper error handling, so truncated JSON cannot be validated reliably. When this occurs, the system encounters an ugly visual crash. +Unlike plain text, JSON cannot be trivially streamed or concatenated in parts, since partial structures may break schema integrity. At present, this remains an open issue without a clean solution. + +#### Limitations of Prompting +Because of the platform’s privacy-first design, we cannot send user response data to external models. As a result, we must rely entirely on system prompts to enable this feature. +Stac code is highly specialized, and while fine-tuning an existing Flutter-focused or JSON-focused model would have been an effective approach, this option is not permitted under our constraints. This creates a significant challenge, since system prompting alone has limited capacity before context loss begins to degrade output quality. +Our temporary solution is to restrict the feature to a smaller subset of Stac. However, a long-term solution will be necessary to overcome these limitations and support the full scope of functionality. + +--- + +## Future Work +- **Error Handling in Stac** +Stac’s current error handling is limited, making debugging and reliability difficult. A future step would be to improve structured error messages and fallback behaviors for invalid or incomplete code paths. + +- **Expanding Restricted Stac SDUI Widget Library** +At present, the Stac-driven UI generation supports only a subset of widget types. This restricts the richness of UIs generated from complex API responses. Future work can focus on extending the widget library to cover advanced layout controls, interactive inputs, and custom components. This expansion will allow the system to handle more nuanced use cases and generate production-grade UIs directly from structured data. + +- **Integration Tests for AI Features** +Automated testing remains critical to ensure reliability and prevent regressions in AI-driven workflows. Integration tests will be built for tool generation and AI UI Designer + +--- + +## Design and Prototypes Link +- [API Tool Generation Research Document ](https://docs.google.com/document/d/17wjzrJcE-HlSy3i3UdgQUEneCXXEKb-XNNiHSp-ECVg) +- [AI UI Designer prototype](https://github.com/synapsecode/AI_UI_designer_prototype) +- [FlutRun (My custom remote flutter component rendering service)](https://github.com/synapsecode/FlutRun) + +--- + +## Conclusion +Google Summer of Code 2025 with API Dash has been a truly amazing experience. My work throughout this project centered on building the core infrastructure that will be the heart of API Dash's next-gen features, and I believe I have successfully laid a strong foundation. I look forward to seeing future contributors build upon it and take the project even further. + +--- diff --git a/lib/apitoolgen/request_consolidator.dart b/lib/apitoolgen/request_consolidator.dart new file mode 100644 index 00000000..0f86dbe7 --- /dev/null +++ b/lib/apitoolgen/request_consolidator.dart @@ -0,0 +1,97 @@ +class APIDashRequestDescription { + final String endpoint; + final String method; + final Map? queryParams; + final List? formData; + final Map? headers; + final String? bodyTXT; + final Map? bodyJSON; + final String? responseType; + final dynamic response; + + APIDashRequestDescription({ + required this.endpoint, + required this.method, + this.queryParams, + this.formData, + this.headers, + this.bodyTXT, + this.bodyJSON, + this.responseType, + this.response, + }); + + String get generateREQDATA { + //Note Down the Query parameters + String queryParamStr = ''; + if (queryParams != null) { + for (final x in queryParams!.keys) { + queryParamStr += + '\t$x: ${queryParams![x]} <${queryParams![x].runtimeType}>\n'; + } + queryParamStr = 'QUERY_PARAMETERS: {\n$queryParamStr}'; + } + + //Note Down the Headers + String headersStr = ''; + if (headers != null) { + for (final x in headers!.keys) { + headersStr += '\t$x: ${headers![x]} <${headers![x].runtimeType}>\n'; + } + headersStr = 'HEADERS: {\n$headersStr}'; + } + + String bodyDetails = ''; + if (bodyTXT != null) { + bodyDetails = 'BODY_TYPE: TEXT\nBODY_TEXT:$bodyTXT'; + } else if (bodyJSON != null) { + //Note Down the JSONData + String jsonBodyStr = ''; + if (bodyJSON != null) { + getTyp(input, [i = 0]) { + String indent = "\t"; + for (int j = 0; j < i; j++) indent += "\t"; + if (input.runtimeType.toString().toLowerCase().contains('map')) { + String typd = '{'; + for (final z in input.keys) { + typd += "$indent$z: TYPE: ${getTyp(input[z], i + 1)}\n"; + } + return "$indent$typd}"; + } + return input.runtimeType.toString(); + } + + for (final x in bodyJSON!.keys) { + jsonBodyStr += '\t$x: TYPE: <${getTyp(bodyJSON![x])}>\n'; + } + jsonBodyStr = 'BODY_JSON: {\n$jsonBodyStr}'; + } + bodyDetails = 'BODY_TYPE: JSON\n$jsonBodyStr'; + } else if (formData != null) { + //Note Down the FormData + String formDataStr = ''; + if (formData != null) { + for (final x in formData!) { + formDataStr += '\t$x\n'; + } + formDataStr = 'BODY_FORM_DATA: {\n$formDataStr}'; + } + bodyDetails = 'BODY_TYPE: FORM-DATA\n$formDataStr'; + } + + String responseDetails = ''; + if (responseType != null && response != null) { + responseDetails = + '-----RESPONSE_DETAILS-----\nRESPONSE_TYPE: $responseType\nRESPONSE_BODY: $response'; + } + + return """REST API (HTTP) +METHOD: $method +ENDPOINT: $endpoint +HEADERS: ${headersStr.isEmpty ? '{}' : headersStr} +$queryParamStr +$bodyDetails +$responseDetails +"""; + } +} diff --git a/lib/consts.dart b/lib/consts.dart index 66972b14..1d7aa6e4 100644 --- a/lib/consts.dart +++ b/lib/consts.dart @@ -159,6 +159,7 @@ enum ResponseBodyView { preview("Preview", Icons.visibility_rounded), code("Preview", Icons.code_rounded), raw("Raw", Icons.text_snippet_rounded), + answer("Answer", Icons.abc), sse("SSE", Icons.stream), none("Preview", Icons.warning); @@ -183,8 +184,9 @@ const kPreviewCodeRawBodyViewOptions = [ ResponseBodyView.code, ResponseBodyView.raw ]; -const kPreviewSSERawBodyViewOptions = [ - ResponseBodyView.sse, +const kSSERawBodyViewOptions = [ResponseBodyView.sse, ResponseBodyView.raw]; +const kAnswerRawBodyViewOptions = [ + ResponseBodyView.answer, ResponseBodyView.raw ]; @@ -200,15 +202,15 @@ const Map>> kSubTypeYaml: kCodeRawBodyViewOptions, kSubTypeXYaml: kCodeRawBodyViewOptions, kSubTypeYml: kCodeRawBodyViewOptions, - kSubTypeXNdjson: kPreviewSSERawBodyViewOptions, - kSubTypeNdjson: kPreviewSSERawBodyViewOptions, - kSubTypeJsonSeq: kPreviewSSERawBodyViewOptions, - kSubTypeXLdjson: kPreviewSSERawBodyViewOptions, - kSubTypeLdjson: kPreviewSSERawBodyViewOptions, - kSubTypeXJsonStream: kPreviewSSERawBodyViewOptions, - kSubTypeJsonStream: kPreviewSSERawBodyViewOptions, - kSubTypeJsonstream: kPreviewSSERawBodyViewOptions, - kSubTypeStreamJson: kPreviewSSERawBodyViewOptions, + kSubTypeXNdjson: kSSERawBodyViewOptions, + kSubTypeNdjson: kSSERawBodyViewOptions, + kSubTypeJsonSeq: kSSERawBodyViewOptions, + kSubTypeXLdjson: kSSERawBodyViewOptions, + kSubTypeLdjson: kSSERawBodyViewOptions, + kSubTypeXJsonStream: kSSERawBodyViewOptions, + kSubTypeJsonStream: kSSERawBodyViewOptions, + kSubTypeJsonstream: kSSERawBodyViewOptions, + kSubTypeStreamJson: kSSERawBodyViewOptions, }, kTypeImage: { kSubTypeDefaultViewOptions: kPreviewBodyViewOptions, @@ -230,7 +232,7 @@ const Map>> kSubTypeTextXml: kCodeRawBodyViewOptions, kSubTypeTextYaml: kCodeRawBodyViewOptions, kSubTypeTextYml: kCodeRawBodyViewOptions, - kSubTypeEventStream: kPreviewSSERawBodyViewOptions, + kSubTypeEventStream: kSSERawBodyViewOptions, }, }; @@ -508,3 +510,4 @@ const kMsgClearHistory = const kMsgClearHistorySuccess = 'History cleared successfully'; const kMsgClearHistoryError = 'Error clearing history'; const kMsgShareError = "Unable to share"; +const kLabelGenerateUI = "Generate UI"; diff --git a/lib/main.dart b/lib/main.dart index 2b43ca94..9be1fb49 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,6 +1,8 @@ +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:stac/stac.dart'; import 'models/models.dart'; import 'providers/providers.dart'; import 'services/services.dart'; @@ -9,6 +11,12 @@ import 'app.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); + await Stac.initialize(); + + //Load all LLMs + // await LLMManager.fetchAvailableLLMs(); + await ModelManager.fetchAvailableModels(); + var settingsModel = await getSettingsFromSharedPrefs(); var onboardingStatus = await getOnboardingStatusFromSharedPrefs(); initializeJsRuntime(); @@ -23,6 +31,9 @@ void main() async { settingsModel = settingsModel?.copyWithPath(workspaceFolderPath: null); } + // TODO: Load all models at init + // await ModelManager.loadAvailableLLMs(); + runApp( ProviderScope( overrides: [ diff --git a/lib/models/history_meta_model.g.dart b/lib/models/history_meta_model.g.dart index da184793..ede68a29 100644 --- a/lib/models/history_meta_model.g.dart +++ b/lib/models/history_meta_model.g.dart @@ -34,6 +34,7 @@ Map _$$HistoryMetaModelImplToJson( const _$APITypeEnumMap = { APIType.rest: 'rest', + APIType.ai: 'ai', APIType.graphql: 'graphql', }; diff --git a/lib/models/history_request_model.dart b/lib/models/history_request_model.dart index f46382a7..1169dc7e 100644 --- a/lib/models/history_request_model.dart +++ b/lib/models/history_request_model.dart @@ -14,7 +14,8 @@ class HistoryRequestModel with _$HistoryRequestModel { const factory HistoryRequestModel({ required String historyId, required HistoryMetaModel metaData, - required HttpRequestModel httpRequestModel, + HttpRequestModel? httpRequestModel, + AIRequestModel? aiRequestModel, required HttpResponseModel httpResponseModel, String? preRequestScript, String? postRequestScript, diff --git a/lib/models/history_request_model.freezed.dart b/lib/models/history_request_model.freezed.dart index 020ea13d..dd553171 100644 --- a/lib/models/history_request_model.freezed.dart +++ b/lib/models/history_request_model.freezed.dart @@ -22,7 +22,8 @@ HistoryRequestModel _$HistoryRequestModelFromJson(Map json) { mixin _$HistoryRequestModel { String get historyId => throw _privateConstructorUsedError; HistoryMetaModel get metaData => throw _privateConstructorUsedError; - HttpRequestModel get httpRequestModel => throw _privateConstructorUsedError; + HttpRequestModel? get httpRequestModel => throw _privateConstructorUsedError; + AIRequestModel? get aiRequestModel => throw _privateConstructorUsedError; HttpResponseModel get httpResponseModel => throw _privateConstructorUsedError; String? get preRequestScript => throw _privateConstructorUsedError; String? get postRequestScript => throw _privateConstructorUsedError; @@ -47,14 +48,16 @@ abstract class $HistoryRequestModelCopyWith<$Res> { $Res call( {String historyId, HistoryMetaModel metaData, - HttpRequestModel httpRequestModel, + HttpRequestModel? httpRequestModel, + AIRequestModel? aiRequestModel, HttpResponseModel httpResponseModel, String? preRequestScript, String? postRequestScript, AuthModel? authModel}); $HistoryMetaModelCopyWith<$Res> get metaData; - $HttpRequestModelCopyWith<$Res> get httpRequestModel; + $HttpRequestModelCopyWith<$Res>? get httpRequestModel; + $AIRequestModelCopyWith<$Res>? get aiRequestModel; $HttpResponseModelCopyWith<$Res> get httpResponseModel; $AuthModelCopyWith<$Res>? get authModel; } @@ -76,7 +79,8 @@ class _$HistoryRequestModelCopyWithImpl<$Res, $Val extends HistoryRequestModel> $Res call({ Object? historyId = null, Object? metaData = null, - Object? httpRequestModel = null, + Object? httpRequestModel = freezed, + Object? aiRequestModel = freezed, Object? httpResponseModel = null, Object? preRequestScript = freezed, Object? postRequestScript = freezed, @@ -91,10 +95,14 @@ class _$HistoryRequestModelCopyWithImpl<$Res, $Val extends HistoryRequestModel> ? _value.metaData : metaData // ignore: cast_nullable_to_non_nullable as HistoryMetaModel, - httpRequestModel: null == httpRequestModel + httpRequestModel: freezed == httpRequestModel ? _value.httpRequestModel : httpRequestModel // ignore: cast_nullable_to_non_nullable - as HttpRequestModel, + as HttpRequestModel?, + aiRequestModel: freezed == aiRequestModel + ? _value.aiRequestModel + : aiRequestModel // ignore: cast_nullable_to_non_nullable + as AIRequestModel?, httpResponseModel: null == httpResponseModel ? _value.httpResponseModel : httpResponseModel // ignore: cast_nullable_to_non_nullable @@ -128,12 +136,30 @@ class _$HistoryRequestModelCopyWithImpl<$Res, $Val extends HistoryRequestModel> /// with the given fields replaced by the non-null parameter values. @override @pragma('vm:prefer-inline') - $HttpRequestModelCopyWith<$Res> get httpRequestModel { - return $HttpRequestModelCopyWith<$Res>(_value.httpRequestModel, (value) { + $HttpRequestModelCopyWith<$Res>? get httpRequestModel { + if (_value.httpRequestModel == null) { + return null; + } + + return $HttpRequestModelCopyWith<$Res>(_value.httpRequestModel!, (value) { return _then(_value.copyWith(httpRequestModel: value) as $Val); }); } + /// Create a copy of HistoryRequestModel + /// with the given fields replaced by the non-null parameter values. + @override + @pragma('vm:prefer-inline') + $AIRequestModelCopyWith<$Res>? get aiRequestModel { + if (_value.aiRequestModel == null) { + return null; + } + + return $AIRequestModelCopyWith<$Res>(_value.aiRequestModel!, (value) { + return _then(_value.copyWith(aiRequestModel: value) as $Val); + }); + } + /// Create a copy of HistoryRequestModel /// with the given fields replaced by the non-null parameter values. @override @@ -170,7 +196,8 @@ abstract class _$$HistoryRequestModelImplCopyWith<$Res> $Res call( {String historyId, HistoryMetaModel metaData, - HttpRequestModel httpRequestModel, + HttpRequestModel? httpRequestModel, + AIRequestModel? aiRequestModel, HttpResponseModel httpResponseModel, String? preRequestScript, String? postRequestScript, @@ -179,7 +206,9 @@ abstract class _$$HistoryRequestModelImplCopyWith<$Res> @override $HistoryMetaModelCopyWith<$Res> get metaData; @override - $HttpRequestModelCopyWith<$Res> get httpRequestModel; + $HttpRequestModelCopyWith<$Res>? get httpRequestModel; + @override + $AIRequestModelCopyWith<$Res>? get aiRequestModel; @override $HttpResponseModelCopyWith<$Res> get httpResponseModel; @override @@ -201,7 +230,8 @@ class __$$HistoryRequestModelImplCopyWithImpl<$Res> $Res call({ Object? historyId = null, Object? metaData = null, - Object? httpRequestModel = null, + Object? httpRequestModel = freezed, + Object? aiRequestModel = freezed, Object? httpResponseModel = null, Object? preRequestScript = freezed, Object? postRequestScript = freezed, @@ -216,10 +246,14 @@ class __$$HistoryRequestModelImplCopyWithImpl<$Res> ? _value.metaData : metaData // ignore: cast_nullable_to_non_nullable as HistoryMetaModel, - httpRequestModel: null == httpRequestModel + httpRequestModel: freezed == httpRequestModel ? _value.httpRequestModel : httpRequestModel // ignore: cast_nullable_to_non_nullable - as HttpRequestModel, + as HttpRequestModel?, + aiRequestModel: freezed == aiRequestModel + ? _value.aiRequestModel + : aiRequestModel // ignore: cast_nullable_to_non_nullable + as AIRequestModel?, httpResponseModel: null == httpResponseModel ? _value.httpResponseModel : httpResponseModel // ignore: cast_nullable_to_non_nullable @@ -247,7 +281,8 @@ class _$HistoryRequestModelImpl implements _HistoryRequestModel { const _$HistoryRequestModelImpl( {required this.historyId, required this.metaData, - required this.httpRequestModel, + this.httpRequestModel, + this.aiRequestModel, required this.httpResponseModel, this.preRequestScript, this.postRequestScript, @@ -261,7 +296,9 @@ class _$HistoryRequestModelImpl implements _HistoryRequestModel { @override final HistoryMetaModel metaData; @override - final HttpRequestModel httpRequestModel; + final HttpRequestModel? httpRequestModel; + @override + final AIRequestModel? aiRequestModel; @override final HttpResponseModel httpResponseModel; @override @@ -273,7 +310,7 @@ class _$HistoryRequestModelImpl implements _HistoryRequestModel { @override String toString() { - return 'HistoryRequestModel(historyId: $historyId, metaData: $metaData, httpRequestModel: $httpRequestModel, httpResponseModel: $httpResponseModel, preRequestScript: $preRequestScript, postRequestScript: $postRequestScript, authModel: $authModel)'; + return 'HistoryRequestModel(historyId: $historyId, metaData: $metaData, httpRequestModel: $httpRequestModel, aiRequestModel: $aiRequestModel, httpResponseModel: $httpResponseModel, preRequestScript: $preRequestScript, postRequestScript: $postRequestScript, authModel: $authModel)'; } @override @@ -287,6 +324,8 @@ class _$HistoryRequestModelImpl implements _HistoryRequestModel { other.metaData == metaData) && (identical(other.httpRequestModel, httpRequestModel) || other.httpRequestModel == httpRequestModel) && + (identical(other.aiRequestModel, aiRequestModel) || + other.aiRequestModel == aiRequestModel) && (identical(other.httpResponseModel, httpResponseModel) || other.httpResponseModel == httpResponseModel) && (identical(other.preRequestScript, preRequestScript) || @@ -304,6 +343,7 @@ class _$HistoryRequestModelImpl implements _HistoryRequestModel { historyId, metaData, httpRequestModel, + aiRequestModel, httpResponseModel, preRequestScript, postRequestScript, @@ -330,7 +370,8 @@ abstract class _HistoryRequestModel implements HistoryRequestModel { const factory _HistoryRequestModel( {required final String historyId, required final HistoryMetaModel metaData, - required final HttpRequestModel httpRequestModel, + final HttpRequestModel? httpRequestModel, + final AIRequestModel? aiRequestModel, required final HttpResponseModel httpResponseModel, final String? preRequestScript, final String? postRequestScript, @@ -344,7 +385,9 @@ abstract class _HistoryRequestModel implements HistoryRequestModel { @override HistoryMetaModel get metaData; @override - HttpRequestModel get httpRequestModel; + HttpRequestModel? get httpRequestModel; + @override + AIRequestModel? get aiRequestModel; @override HttpResponseModel get httpResponseModel; @override diff --git a/lib/models/history_request_model.g.dart b/lib/models/history_request_model.g.dart index 66aee83b..2548ce76 100644 --- a/lib/models/history_request_model.g.dart +++ b/lib/models/history_request_model.g.dart @@ -11,8 +11,14 @@ _$HistoryRequestModelImpl _$$HistoryRequestModelImplFromJson(Map json) => historyId: json['historyId'] as String, metaData: HistoryMetaModel.fromJson( Map.from(json['metaData'] as Map)), - httpRequestModel: HttpRequestModel.fromJson( - Map.from(json['httpRequestModel'] as Map)), + httpRequestModel: json['httpRequestModel'] == null + ? null + : HttpRequestModel.fromJson( + Map.from(json['httpRequestModel'] as Map)), + aiRequestModel: json['aiRequestModel'] == null + ? null + : AIRequestModel.fromJson( + Map.from(json['aiRequestModel'] as Map)), httpResponseModel: HttpResponseModel.fromJson( Map.from(json['httpResponseModel'] as Map)), preRequestScript: json['preRequestScript'] as String?, @@ -28,7 +34,8 @@ Map _$$HistoryRequestModelImplToJson( { 'historyId': instance.historyId, 'metaData': instance.metaData.toJson(), - 'httpRequestModel': instance.httpRequestModel.toJson(), + 'httpRequestModel': instance.httpRequestModel?.toJson(), + 'aiRequestModel': instance.aiRequestModel?.toJson(), 'httpResponseModel': instance.httpResponseModel.toJson(), 'preRequestScript': instance.preRequestScript, 'postRequestScript': instance.postRequestScript, diff --git a/lib/models/request_model.dart b/lib/models/request_model.dart index b7052f81..25c36084 100644 --- a/lib/models/request_model.dart +++ b/lib/models/request_model.dart @@ -25,6 +25,7 @@ class RequestModel with _$RequestModel { @JsonKey(includeToJson: false) @Default(false) bool isStreaming, String? preRequestScript, String? postRequestScript, + AIRequestModel? aiRequestModel, }) = _RequestModel; factory RequestModel.fromJson(Map json) => diff --git a/lib/models/request_model.freezed.dart b/lib/models/request_model.freezed.dart index 3ba8979b..d700dba9 100644 --- a/lib/models/request_model.freezed.dart +++ b/lib/models/request_model.freezed.dart @@ -39,6 +39,7 @@ mixin _$RequestModel { bool get isStreaming => throw _privateConstructorUsedError; String? get preRequestScript => throw _privateConstructorUsedError; String? get postRequestScript => throw _privateConstructorUsedError; + AIRequestModel? get aiRequestModel => throw _privateConstructorUsedError; /// Serializes this RequestModel to a JSON map. Map toJson() => throw _privateConstructorUsedError; @@ -70,10 +71,12 @@ abstract class $RequestModelCopyWith<$Res> { @JsonKey(includeToJson: false) DateTime? sendingTime, @JsonKey(includeToJson: false) bool isStreaming, String? preRequestScript, - String? postRequestScript}); + String? postRequestScript, + AIRequestModel? aiRequestModel}); $HttpRequestModelCopyWith<$Res>? get httpRequestModel; $HttpResponseModelCopyWith<$Res>? get httpResponseModel; + $AIRequestModelCopyWith<$Res>? get aiRequestModel; } /// @nodoc @@ -105,6 +108,7 @@ class _$RequestModelCopyWithImpl<$Res, $Val extends RequestModel> Object? isStreaming = null, Object? preRequestScript = freezed, Object? postRequestScript = freezed, + Object? aiRequestModel = freezed, }) { return _then(_value.copyWith( id: null == id @@ -163,6 +167,10 @@ class _$RequestModelCopyWithImpl<$Res, $Val extends RequestModel> ? _value.postRequestScript : postRequestScript // ignore: cast_nullable_to_non_nullable as String?, + aiRequestModel: freezed == aiRequestModel + ? _value.aiRequestModel + : aiRequestModel // ignore: cast_nullable_to_non_nullable + as AIRequestModel?, ) as $Val); } @@ -193,6 +201,20 @@ class _$RequestModelCopyWithImpl<$Res, $Val extends RequestModel> return _then(_value.copyWith(httpResponseModel: value) as $Val); }); } + + /// Create a copy of RequestModel + /// with the given fields replaced by the non-null parameter values. + @override + @pragma('vm:prefer-inline') + $AIRequestModelCopyWith<$Res>? get aiRequestModel { + if (_value.aiRequestModel == null) { + return null; + } + + return $AIRequestModelCopyWith<$Res>(_value.aiRequestModel!, (value) { + return _then(_value.copyWith(aiRequestModel: value) as $Val); + }); + } } /// @nodoc @@ -217,12 +239,15 @@ abstract class _$$RequestModelImplCopyWith<$Res> @JsonKey(includeToJson: false) DateTime? sendingTime, @JsonKey(includeToJson: false) bool isStreaming, String? preRequestScript, - String? postRequestScript}); + String? postRequestScript, + AIRequestModel? aiRequestModel}); @override $HttpRequestModelCopyWith<$Res>? get httpRequestModel; @override $HttpResponseModelCopyWith<$Res>? get httpResponseModel; + @override + $AIRequestModelCopyWith<$Res>? get aiRequestModel; } /// @nodoc @@ -252,6 +277,7 @@ class __$$RequestModelImplCopyWithImpl<$Res> Object? isStreaming = null, Object? preRequestScript = freezed, Object? postRequestScript = freezed, + Object? aiRequestModel = freezed, }) { return _then(_$RequestModelImpl( id: null == id @@ -309,6 +335,10 @@ class __$$RequestModelImplCopyWithImpl<$Res> ? _value.postRequestScript : postRequestScript // ignore: cast_nullable_to_non_nullable as String?, + aiRequestModel: freezed == aiRequestModel + ? _value.aiRequestModel + : aiRequestModel // ignore: cast_nullable_to_non_nullable + as AIRequestModel?, )); } } @@ -331,7 +361,8 @@ class _$RequestModelImpl implements _RequestModel { @JsonKey(includeToJson: false) this.sendingTime, @JsonKey(includeToJson: false) this.isStreaming = false, this.preRequestScript, - this.postRequestScript}); + this.postRequestScript, + this.aiRequestModel}); factory _$RequestModelImpl.fromJson(Map json) => _$$RequestModelImplFromJson(json); @@ -371,10 +402,12 @@ class _$RequestModelImpl implements _RequestModel { final String? preRequestScript; @override final String? postRequestScript; + @override + final AIRequestModel? aiRequestModel; @override String toString() { - return 'RequestModel(id: $id, apiType: $apiType, name: $name, description: $description, requestTabIndex: $requestTabIndex, httpRequestModel: $httpRequestModel, responseStatus: $responseStatus, message: $message, httpResponseModel: $httpResponseModel, isWorking: $isWorking, sendingTime: $sendingTime, isStreaming: $isStreaming, preRequestScript: $preRequestScript, postRequestScript: $postRequestScript)'; + return 'RequestModel(id: $id, apiType: $apiType, name: $name, description: $description, requestTabIndex: $requestTabIndex, httpRequestModel: $httpRequestModel, responseStatus: $responseStatus, message: $message, httpResponseModel: $httpResponseModel, isWorking: $isWorking, sendingTime: $sendingTime, isStreaming: $isStreaming, preRequestScript: $preRequestScript, postRequestScript: $postRequestScript, aiRequestModel: $aiRequestModel)'; } @override @@ -405,7 +438,9 @@ class _$RequestModelImpl implements _RequestModel { (identical(other.preRequestScript, preRequestScript) || other.preRequestScript == preRequestScript) && (identical(other.postRequestScript, postRequestScript) || - other.postRequestScript == postRequestScript)); + other.postRequestScript == postRequestScript) && + (identical(other.aiRequestModel, aiRequestModel) || + other.aiRequestModel == aiRequestModel)); } @JsonKey(includeFromJson: false, includeToJson: false) @@ -425,7 +460,8 @@ class _$RequestModelImpl implements _RequestModel { sendingTime, isStreaming, preRequestScript, - postRequestScript); + postRequestScript, + aiRequestModel); /// Create a copy of RequestModel /// with the given fields replaced by the non-null parameter values. @@ -458,7 +494,8 @@ abstract class _RequestModel implements RequestModel { @JsonKey(includeToJson: false) final DateTime? sendingTime, @JsonKey(includeToJson: false) final bool isStreaming, final String? preRequestScript, - final String? postRequestScript}) = _$RequestModelImpl; + final String? postRequestScript, + final AIRequestModel? aiRequestModel}) = _$RequestModelImpl; factory _RequestModel.fromJson(Map json) = _$RequestModelImpl.fromJson; @@ -495,6 +532,8 @@ abstract class _RequestModel implements RequestModel { String? get preRequestScript; @override String? get postRequestScript; + @override + AIRequestModel? get aiRequestModel; /// Create a copy of RequestModel /// with the given fields replaced by the non-null parameter values. diff --git a/lib/models/request_model.g.dart b/lib/models/request_model.g.dart index 8e0a5a68..272bfca0 100644 --- a/lib/models/request_model.g.dart +++ b/lib/models/request_model.g.dart @@ -30,6 +30,10 @@ _$RequestModelImpl _$$RequestModelImplFromJson(Map json) => _$RequestModelImpl( isStreaming: json['isStreaming'] as bool? ?? false, preRequestScript: json['preRequestScript'] as String?, postRequestScript: json['postRequestScript'] as String?, + aiRequestModel: json['aiRequestModel'] == null + ? null + : AIRequestModel.fromJson( + Map.from(json['aiRequestModel'] as Map)), ); Map _$$RequestModelImplToJson(_$RequestModelImpl instance) => @@ -44,9 +48,11 @@ Map _$$RequestModelImplToJson(_$RequestModelImpl instance) => 'httpResponseModel': instance.httpResponseModel?.toJson(), 'preRequestScript': instance.preRequestScript, 'postRequestScript': instance.postRequestScript, + 'aiRequestModel': instance.aiRequestModel?.toJson(), }; const _$APITypeEnumMap = { APIType.rest: 'rest', + APIType.ai: 'ai', APIType.graphql: 'graphql', }; diff --git a/lib/models/settings_model.dart b/lib/models/settings_model.dart index a06b1e59..b3222bf0 100644 --- a/lib/models/settings_model.dart +++ b/lib/models/settings_model.dart @@ -1,4 +1,5 @@ import 'package:apidash_core/apidash_core.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:apidash/consts.dart'; @@ -18,6 +19,7 @@ class SettingsModel { this.workspaceFolderPath, this.isSSLDisabled = false, this.isDashBotEnabled = true, + this.defaultAIModel, }); final bool isDark; @@ -33,6 +35,7 @@ class SettingsModel { final String? workspaceFolderPath; final bool isSSLDisabled; final bool isDashBotEnabled; + final Map? defaultAIModel; SettingsModel copyWith({ bool? isDark, @@ -48,6 +51,7 @@ class SettingsModel { String? workspaceFolderPath, bool? isSSLDisabled, bool? isDashBotEnabled, + Map? defaultAIModel, }) { return SettingsModel( isDark: isDark ?? this.isDark, @@ -65,6 +69,7 @@ class SettingsModel { workspaceFolderPath: workspaceFolderPath ?? this.workspaceFolderPath, isSSLDisabled: isSSLDisabled ?? this.isSSLDisabled, isDashBotEnabled: isDashBotEnabled ?? this.isDashBotEnabled, + defaultAIModel: defaultAIModel ?? this.defaultAIModel, ); } @@ -85,6 +90,7 @@ class SettingsModel { workspaceFolderPath: workspaceFolderPath, isSSLDisabled: isSSLDisabled, isDashBotEnabled: isDashBotEnabled, + defaultAIModel: defaultAIModel, ); } @@ -140,7 +146,9 @@ class SettingsModel { final workspaceFolderPath = data["workspaceFolderPath"] as String?; final isSSLDisabled = data["isSSLDisabled"] as bool?; final isDashBotEnabled = data["isDashBotEnabled"] as bool?; - + final defaultAIModel = data["defaultAIModel"] == null + ? null + : Map.from(data["defaultAIModel"]); const sm = SettingsModel(); return sm.copyWith( @@ -158,6 +166,7 @@ class SettingsModel { workspaceFolderPath: workspaceFolderPath, isSSLDisabled: isSSLDisabled, isDashBotEnabled: isDashBotEnabled, + defaultAIModel: defaultAIModel, ); } @@ -178,6 +187,7 @@ class SettingsModel { "workspaceFolderPath": workspaceFolderPath, "isSSLDisabled": isSSLDisabled, "isDashBotEnabled": isDashBotEnabled, + "defaultAIModel": defaultAIModel, }; } @@ -203,7 +213,8 @@ class SettingsModel { other.historyRetentionPeriod == historyRetentionPeriod && other.workspaceFolderPath == workspaceFolderPath && other.isSSLDisabled == isSSLDisabled && - other.isDashBotEnabled == isDashBotEnabled; + other.isDashBotEnabled == isDashBotEnabled && + mapEquals(other.defaultAIModel, defaultAIModel); } @override @@ -223,6 +234,7 @@ class SettingsModel { workspaceFolderPath, isSSLDisabled, isDashBotEnabled, + defaultAIModel, ); } } diff --git a/lib/providers/ai_providers.dart b/lib/providers/ai_providers.dart new file mode 100644 index 00000000..9955c010 --- /dev/null +++ b/lib/providers/ai_providers.dart @@ -0,0 +1,5 @@ +// import 'package:apidash_core/apidash_core.dart'; +// import 'package:riverpod/riverpod.dart'; + +// final aiApiCredentialProvider = +// StateProvider>((ref) => {}); diff --git a/lib/providers/collection_providers.dart b/lib/providers/collection_providers.dart index 525185a6..c6e9db55 100644 --- a/lib/providers/collection_providers.dart +++ b/lib/providers/collection_providers.dart @@ -163,6 +163,8 @@ class CollectionStateNotifier requestTabIndex: 0, responseStatus: null, message: null, + httpRequestModel: currentModel.httpRequestModel?.copyWith(), + aiRequestModel: currentModel.aiRequestModel?.copyWith(), httpResponseModel: null, isWorking: false, sendingTime: null, @@ -183,10 +185,14 @@ class CollectionStateNotifier var itemIds = ref.read(requestSequenceProvider); var currentModel = historyRequestModel; + final newModel = RequestModel( + apiType: currentModel.metaData.apiType, id: newId, name: "${currentModel.metaData.name} (history)", - httpRequestModel: currentModel.httpRequestModel, + aiRequestModel: currentModel.aiRequestModel?.copyWith(), + httpRequestModel: + currentModel.httpRequestModel?.copyWith() ?? HttpRequestModel(), responseStatus: currentModel.metaData.responseStatus, message: kResponseCodeReasons[currentModel.metaData.responseStatus], httpResponseModel: currentModel.httpResponseModel, @@ -205,9 +211,9 @@ class CollectionStateNotifier } void update({ + APIType? apiType, String? id, HTTPVerb? method, - APIType? apiType, AuthModel? authModel, String? url, String? name, @@ -226,6 +232,7 @@ class CollectionStateNotifier HttpResponseModel? httpResponseModel, String? preRequestScript, String? postRequestScript, + AIRequestModel? aiRequestModel, }) { final rId = id ?? ref.read(selectedIdStateProvider); if (rId == null) { @@ -234,33 +241,57 @@ class CollectionStateNotifier } var currentModel = state![rId]!; var currentHttpRequestModel = currentModel.httpRequestModel; - final newModel = currentModel.copyWith( - apiType: apiType ?? currentModel.apiType, - name: name ?? currentModel.name, - description: description ?? currentModel.description, - requestTabIndex: requestTabIndex ?? currentModel.requestTabIndex, - httpRequestModel: currentHttpRequestModel?.copyWith( - method: method ?? currentHttpRequestModel.method, - url: url ?? currentHttpRequestModel.url, - headers: headers ?? currentHttpRequestModel.headers, - params: params ?? currentHttpRequestModel.params, - authModel: authModel ?? currentHttpRequestModel.authModel, - isHeaderEnabledList: - isHeaderEnabledList ?? currentHttpRequestModel.isHeaderEnabledList, - isParamEnabledList: - isParamEnabledList ?? currentHttpRequestModel.isParamEnabledList, - bodyContentType: - bodyContentType ?? currentHttpRequestModel.bodyContentType, - body: body ?? currentHttpRequestModel.body, - query: query ?? currentHttpRequestModel.query, - formData: formData ?? currentHttpRequestModel.formData, - ), - responseStatus: responseStatus ?? currentModel.responseStatus, - message: message ?? currentModel.message, - httpResponseModel: httpResponseModel ?? currentModel.httpResponseModel, - preRequestScript: preRequestScript ?? currentModel.preRequestScript, - postRequestScript: postRequestScript ?? currentModel.postRequestScript, - ); + + RequestModel newModel; + + if (apiType != null && currentModel.apiType != apiType) { + final defaultModel = ref.read(settingsProvider).defaultAIModel; + newModel = switch (apiType) { + APIType.rest || APIType.graphql => currentModel.copyWith( + apiType: apiType, + name: name ?? currentModel.name, + description: description ?? currentModel.description, + httpRequestModel: const HttpRequestModel(), + aiRequestModel: null), + APIType.ai => currentModel.copyWith( + apiType: apiType, + name: name ?? currentModel.name, + description: description ?? currentModel.description, + httpRequestModel: null, + aiRequestModel: defaultModel == null + ? const AIRequestModel() + : AIRequestModel.fromJson(defaultModel)), + }; + } else { + newModel = currentModel.copyWith( + apiType: apiType ?? currentModel.apiType, + name: name ?? currentModel.name, + description: description ?? currentModel.description, + requestTabIndex: requestTabIndex ?? currentModel.requestTabIndex, + httpRequestModel: currentHttpRequestModel?.copyWith( + method: method ?? currentHttpRequestModel.method, + url: url ?? currentHttpRequestModel.url, + headers: headers ?? currentHttpRequestModel.headers, + params: params ?? currentHttpRequestModel.params, + authModel: authModel ?? currentHttpRequestModel.authModel, + isHeaderEnabledList: isHeaderEnabledList ?? + currentHttpRequestModel.isHeaderEnabledList, + isParamEnabledList: + isParamEnabledList ?? currentHttpRequestModel.isParamEnabledList, + bodyContentType: + bodyContentType ?? currentHttpRequestModel.bodyContentType, + body: body ?? currentHttpRequestModel.body, + query: query ?? currentHttpRequestModel.query, + formData: formData ?? currentHttpRequestModel.formData, + ), + responseStatus: responseStatus ?? currentModel.responseStatus, + message: message ?? currentModel.message, + httpResponseModel: httpResponseModel ?? currentModel.httpResponseModel, + preRequestScript: preRequestScript ?? currentModel.preRequestScript, + postRequestScript: postRequestScript ?? currentModel.postRequestScript, + aiRequestModel: aiRequestModel ?? currentModel.aiRequestModel, + ); + } var map = {...state!}; map[rId] = newModel; @@ -277,7 +308,8 @@ class CollectionStateNotifier } RequestModel? requestModel = state![requestId]; - if (requestModel?.httpRequestModel == null) { + if (requestModel?.httpRequestModel == null && + requestModel?.aiRequestModel == null) { return; } @@ -304,9 +336,16 @@ class CollectionStateNotifier } APIType apiType = executionRequestModel.apiType; - HttpRequestModel substitutedHttpRequestModel = - getSubstitutedHttpRequestModel(executionRequestModel.httpRequestModel!); bool noSSL = ref.read(settingsProvider).isSSLDisabled; + HttpRequestModel substitutedHttpRequestModel; + + if (apiType == APIType.ai) { + substitutedHttpRequestModel = getSubstitutedHttpRequestModel( + executionRequestModel.aiRequestModel!.httpRequestModel!); + } else { + substitutedHttpRequestModel = getSubstitutedHttpRequestModel( + executionRequestModel.httpRequestModel!); + } // Set model to working and streaming state = { @@ -316,6 +355,7 @@ class CollectionStateNotifier sendingTime: DateTime.now(), ), }; + bool streamingMode = true; //Default: Streaming First final stream = await streamHttpRequest( requestId, @@ -367,6 +407,8 @@ class CollectionStateNotifier .read(historyMetaStateNotifier.notifier) .editHistoryRequest(historyModel!); } + } else { + streamingMode = false; } if (!completer.isCompleted) { @@ -402,6 +444,16 @@ class CollectionStateNotifier isStreamingResponse: isStreamingResponse, ); + //AI-FORMATTING for Non Streaming Varaint + if (!streamingMode && + apiType == APIType.ai && + response.statusCode == 200) { + final fb = executionRequestModel.aiRequestModel?.getFormattedOutput( + kJsonDecoder + .convert(httpResponseModel?.body ?? "Error parsing body")); + httpResponseModel = httpResponseModel?.copyWith(formattedBody: fb); + } + newRequestModel = newRequestModel.copyWith( responseStatus: statusCode, message: kResponseCodeReasons[statusCode], @@ -423,6 +475,7 @@ class CollectionStateNotifier timeStamp: DateTime.now(), ), httpRequestModel: substitutedHttpRequestModel, + aiRequestModel: executionRequestModel.aiRequestModel, httpResponseModel: httpResponseModel!, preRequestScript: requestModel.preRequestScript, postRequestScript: requestModel.postRequestScript, diff --git a/lib/providers/providers.dart b/lib/providers/providers.dart index 29fc6e59..1a906ce5 100644 --- a/lib/providers/providers.dart +++ b/lib/providers/providers.dart @@ -1,3 +1,4 @@ +export 'ai_providers.dart'; export 'collection_providers.dart'; export 'environment_providers.dart'; export 'history_providers.dart'; diff --git a/lib/providers/settings_providers.dart b/lib/providers/settings_providers.dart index d3cb9f2f..be09cc6e 100644 --- a/lib/providers/settings_providers.dart +++ b/lib/providers/settings_providers.dart @@ -34,6 +34,7 @@ class ThemeStateNotifier extends StateNotifier { String? workspaceFolderPath, bool? isSSLDisabled, bool? isDashBotEnabled, + Map? defaultAIModel, }) async { state = state.copyWith( isDark: isDark, @@ -49,6 +50,7 @@ class ThemeStateNotifier extends StateNotifier { workspaceFolderPath: workspaceFolderPath, isSSLDisabled: isSSLDisabled, isDashBotEnabled: isDashBotEnabled, + defaultAIModel: defaultAIModel, ); await setSettingsToSharedPrefs(state); } diff --git a/lib/screens/common_widgets/agentic_ui_features/ai_ui_designer/framework_selector.dart b/lib/screens/common_widgets/agentic_ui_features/ai_ui_designer/framework_selector.dart new file mode 100644 index 00000000..7cc1eac0 --- /dev/null +++ b/lib/screens/common_widgets/agentic_ui_features/ai_ui_designer/framework_selector.dart @@ -0,0 +1,119 @@ +import 'package:apidash/consts.dart'; +import 'package:apidash_design_system/apidash_design_system.dart'; +import 'package:flutter/material.dart'; + +class FrameWorkSelectorPage extends StatefulWidget { + final String content; + final Function(String, String) onNext; + const FrameWorkSelectorPage( + {super.key, required this.content, required this.onNext}); + + @override + State createState() => _FrameWorkSelectorPageState(); +} + +class _FrameWorkSelectorPageState extends State { + String? selectedFramework; + TextEditingController controller = TextEditingController(); + + @override + void initState() { + controller.text = widget.content; + super.initState(); + } + + @override + Widget build(BuildContext context) { + final textContainerdecoration = BoxDecoration( + color: Color.alphaBlend( + (Theme.of(context).brightness == Brightness.dark + ? Theme.of(context).colorScheme.onPrimaryContainer + : Theme.of(context).colorScheme.primaryContainer) + .withValues(alpha: kForegroundOpacity), + Theme.of(context).colorScheme.surface), + border: Border.all( + color: Theme.of(context).colorScheme.surfaceContainerHighest), + borderRadius: kBorderRadius8, + ); + + return Container( + // width: MediaQuery.of(context).size.width * 0.6, // Large dialog + padding: EdgeInsets.all(20), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: Container( + width: double.maxFinite, + padding: kP8, + decoration: textContainerdecoration, + child: SingleChildScrollView( + child: TextField( + controller: controller, + maxLines: null, + style: kCodeStyle, + ), + ), + ), + ), + kVSpacer20, + // Text( + // "Select Framework", + // style: TextStyle( + // color: Colors.white, + // fontSize: 18, + // fontWeight: FontWeight.bold, + // ), + // ), + // SizedBox(height: 10), + // DropdownButtonFormField( + // dropdownColor: Color(0xFF2D2D2D), + // decoration: InputDecoration( + // filled: true, + // fillColor: Color(0xFF2D2D2D), + // border: OutlineInputBorder( + // borderRadius: BorderRadius.circular(8.0), + // ), + // ), + // value: selectedFramework, + // items: ["Flutter", "ReactJS"].map((String value) { + // return DropdownMenuItem( + // value: value, + // child: Text( + // value, + // style: TextStyle(color: Colors.white), + // ), + // ); + // }).toList(), + // onChanged: (newValue) { + // selectedFramework = newValue; + // setState(() {}); + // }, + // ), + // kVSpacer20, + Align( + alignment: Alignment.centerRight, + child: FilledButton.tonalIcon( + style: FilledButton.styleFrom( + padding: kPh12, + minimumSize: const Size(44, 44), + ), + onPressed: () { + widget.onNext(controller.value.text, "FLUTTER"); + }, + icon: Icon( + Icons.generating_tokens, + ), + label: const SizedBox( + child: Text( + kLabelGenerateUI, + ), + ), + ), + ), + ], + ), + ); + } +} diff --git a/lib/screens/common_widgets/agentic_ui_features/ai_ui_designer/generate_ui_dialog.dart b/lib/screens/common_widgets/agentic_ui_features/ai_ui_designer/generate_ui_dialog.dart new file mode 100644 index 00000000..33967d8f --- /dev/null +++ b/lib/screens/common_widgets/agentic_ui_features/ai_ui_designer/generate_ui_dialog.dart @@ -0,0 +1,191 @@ +import 'package:apidash/consts.dart'; +import 'package:apidash/providers/collection_providers.dart'; +import 'package:apidash/services/agentic_services/apidash_agent_calls.dart'; +import 'package:apidash/widgets/widget_sending.dart'; +import 'package:apidash_design_system/apidash_design_system.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'framework_selector.dart'; +import 'sdui_preview.dart'; + +void showCustomDialog(BuildContext context, Widget dialogContent) { + showDialog( + context: context, + builder: (BuildContext context) { + return Dialog( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12.0), + ), + child: dialogContent, + ); + }, + ); +} + +class GenerateUIDialog extends ConsumerStatefulWidget { + final String content; + const GenerateUIDialog({ + super.key, + required this.content, + }); + + @override + ConsumerState createState() => _GenerateUIDialogState(); +} + +class _GenerateUIDialogState extends ConsumerState { + int index = 0; + TextEditingController controller = TextEditingController(); + + String generatedSDUI = '{}'; + + Future generateSDUICode(String apiResponse) async { + try { + setState(() { + index = 1; //Induce Loading + }); + final res = await generateSDUICodeFromResponse( + ref: ref, + apiResponse: apiResponse, + ); + if (res == null) { + setState(() { + index = 0; + }); + ScaffoldMessenger.of(context).showSnackBar(SnackBar( + content: Text( + "Preview Generation Failed!", + style: TextStyle(color: Colors.white), + ), + backgroundColor: Colors.redAccent, + )); + return null; + } + return res; + } catch (e) { + String errMsg = 'Unexpected Error Occured'; + if (e.toString().contains('NO_DEFAULT_LLM')) { + errMsg = "Please Select Default AI Model in Settings"; + } + ScaffoldMessenger.of(context).showSnackBar(SnackBar( + content: Text( + errMsg, + style: TextStyle(color: Colors.white), + ), + backgroundColor: Colors.redAccent, + )); + Navigator.pop(context); + return null; + } + } + + Future modifySDUICode(String modificationRequest) async { + setState(() { + index = 1; //Induce Loading + }); + final res = await modifySDUICodeUsingPrompt( + generatedSDUI: generatedSDUI, + ref: ref, + modificationRequest: modificationRequest, + ); + if (res == null) { + setState(() { + index = 2; + }); + ScaffoldMessenger.of(context).showSnackBar(SnackBar( + content: Text( + "Modification Request Failed!", + style: TextStyle(color: Colors.white), + ), + backgroundColor: Colors.redAccent, + )); + return; + } + setState(() { + generatedSDUI = res; + index = 2; + }); + } + + @override + Widget build(BuildContext context) { + return Stack( + children: [ + if (index == 0) + FrameWorkSelectorPage( + content: widget.content, + onNext: (apiResponse, targetLanguage) async { + print("Generating SDUI Code"); + final sdui = await generateSDUICode(apiResponse); + if (sdui == null) return; + setState(() { + index = 2; + generatedSDUI = sdui; + }); + }, + ), + if (index == 1) + SizedBox( + // width: MediaQuery.of(context).size.width * 0.6, + child: Center( + child: Padding( + padding: const EdgeInsets.only(top: 40.0), + child: Container( + height: 500, + child: SendingWidget( + startSendingTime: DateTime.now(), + showTimeElapsed: false, + ), + ), + ), + ), + ), + if (index == 2) + SDUIPreviewPage( + key: ValueKey(generatedSDUI.hashCode), + onModificationRequestMade: modifySDUICode, + sduiCode: generatedSDUI, + ) + ], + ); + } +} + +class AIGenerateUIButton extends ConsumerWidget { + const AIGenerateUIButton({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + return FilledButton.tonalIcon( + style: FilledButton.styleFrom( + padding: kPh12, + minimumSize: const Size(44, 44), + ), + onPressed: () { + final model = ref.watch(selectedRequestModelProvider + .select((value) => value?.httpResponseModel)); + if (model == null) return; + + String data = ""; + if (model.sseOutput != null) { + data = model.sseOutput!.join(''); + } else { + data = model.formattedBody ?? "<>"; + } + + showCustomDialog( + context, + GenerateUIDialog(content: data), + ); + }, + icon: Icon( + Icons.generating_tokens, + ), + label: const SizedBox( + child: Text( + kLabelGenerateUI, + ), + ), + ); + } +} diff --git a/lib/screens/common_widgets/agentic_ui_features/ai_ui_designer/sdui_preview.dart b/lib/screens/common_widgets/agentic_ui_features/ai_ui_designer/sdui_preview.dart new file mode 100644 index 00000000..a984533d --- /dev/null +++ b/lib/screens/common_widgets/agentic_ui_features/ai_ui_designer/sdui_preview.dart @@ -0,0 +1,195 @@ +import 'package:apidash/screens/common_widgets/agentic_ui_features/ai_ui_designer/sdui_renderer.dart'; +import 'package:apidash/services/agentic_services/agent_caller.dart'; +import 'package:apidash/services/agentic_services/agents/stac_to_flutter.dart'; +import 'package:apidash_core/apidash_core.dart'; +import 'package:apidash_design_system/apidash_design_system.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/services.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +class SDUIPreviewPage extends ConsumerStatefulWidget { + final String sduiCode; + final Function(String) onModificationRequestMade; + const SDUIPreviewPage({ + super.key, + required this.onModificationRequestMade, + required this.sduiCode, + }); + + @override + ConsumerState createState() => _SDUIPreviewPageState(); +} + +class _SDUIPreviewPageState extends ConsumerState { + bool exportingCode = false; + String modificationRequest = ""; + + exportCode() async { + setState(() { + exportingCode = true; + }); + final ans = await APIDashAgentCaller.instance.call( + StacToFlutterBot(), + ref: ref, + input: AgentInputs( + variables: {'VAR_CODE': widget.sduiCode}, + ), + ); + final exportedCode = ans?['CODE']; + + if (exportedCode == null) { + setState(() { + exportingCode = false; + }); + ScaffoldMessenger.of(context).showSnackBar(SnackBar( + content: Text( + "Export Failed", + style: TextStyle(color: Colors.white), + ), + backgroundColor: Colors.redAccent, + )); + print("exportCode: Failed; ABORTING"); + return; + } + + Clipboard.setData(ClipboardData(text: ans['CODE'])); + ScaffoldMessenger.of(context) + .showSnackBar(SnackBar(content: Text("Copied to clipboard!"))); + setState(() { + exportingCode = false; + }); + } + + @override + Widget build(BuildContext context) { + return Container( + // width: MediaQuery.of(context).size.width * 0.6, // Large dialog + padding: EdgeInsets.all(20), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Text( + "Generated Component", + style: TextStyle( + fontSize: 20, + ), + ), + Spacer(), + IconButton( + onPressed: () { + Navigator.pop(context); + }, + icon: Icon(Icons.close)), + ], + ), + kVSpacer20, + Expanded( + child: Center( + child: Container( + padding: EdgeInsets.all(20), + decoration: BoxDecoration( + border: Border.all(color: Colors.white), + ), + child: AspectRatio( + aspectRatio: 16 / 9, + child: StacRenderer( + stacRepresentation: widget.sduiCode, + onError: () { + Future.delayed(Duration(milliseconds: 200), () { + ScaffoldMessenger.of(context).showSnackBar(SnackBar( + content: Text( + "Failed to Display Preview", + style: TextStyle(color: Colors.white), + ), + backgroundColor: Colors.redAccent, + )); + }); + }, + ), + ), + ), + ), + ), + kVSpacer20, + if (!exportingCode) ...[ + Container( + width: double.infinity, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(15), + ), + child: ADOutlinedTextField( + hintText: 'Any Modifications?', + onChanged: (z) { + setState(() { + modificationRequest = z; + }); + }, + maxLines: 3, // Makes the text box taller + ), + ), + kVSpacer20, + ], + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Align( + alignment: Alignment.centerRight, + child: (exportingCode) + ? Container( + child: CircularProgressIndicator( + strokeWidth: 1, + ), + margin: EdgeInsets.only(right: 10), + ) + : FilledButton.tonalIcon( + style: FilledButton.styleFrom( + padding: kPh12, + minimumSize: const Size(44, 44), + ), + onPressed: exportCode, + icon: Icon( + Icons.download, + ), + label: const SizedBox( + child: Text( + "Export Code", + ), + ), + ), + ), + kHSpacer10, + if (!exportingCode) + Align( + alignment: Alignment.centerRight, + child: FilledButton.tonalIcon( + style: FilledButton.styleFrom( + padding: kPh12, + minimumSize: const Size(44, 44), + ), + onPressed: () { + if (modificationRequest.isNotEmpty) { + widget.onModificationRequestMade(modificationRequest); + } + }, + icon: Icon( + Icons.generating_tokens, + ), + label: const SizedBox( + child: Text( + "Make Modifications", + ), + ), + ), + ), + ], + ), + ], + ), + ); + } +} diff --git a/lib/screens/common_widgets/agentic_ui_features/ai_ui_designer/sdui_renderer.dart b/lib/screens/common_widgets/agentic_ui_features/ai_ui_designer/sdui_renderer.dart new file mode 100644 index 00000000..a9128475 --- /dev/null +++ b/lib/screens/common_widgets/agentic_ui_features/ai_ui_designer/sdui_renderer.dart @@ -0,0 +1,45 @@ +import 'dart:convert'; + +import 'package:flutter/material.dart'; +import 'package:stac/stac.dart' as stac; + +class StacRenderer extends StatefulWidget { + final String stacRepresentation; + final VoidCallback onError; + const StacRenderer( + {super.key, required this.stacRepresentation, required this.onError}); + + @override + State createState() => _StacRendererState(); +} + +class _StacRendererState extends State { + Map? sduiCode; + + @override + void initState() { + super.initState(); + try { + sduiCode = jsonDecode(widget.stacRepresentation); + } catch (e) { + widget.onError(); + } + } + + @override + Widget build(BuildContext context) { + // return SingleChildScrollView( + // child: SelectableText(sduiCode?.toString() ?? ""), + // ); + if (sduiCode == null || sduiCode!.isEmpty) { + return Container(); + } + return stac.StacApp( + title: 'Component Preview', + homeBuilder: (context) => Material( + color: Colors.transparent, + child: stac.Stac.fromJson(sduiCode!.cast(), context), + ), + ); + } +} diff --git a/lib/screens/common_widgets/agentic_ui_features/tool_generation/generate_tool_dialog.dart b/lib/screens/common_widgets/agentic_ui_features/tool_generation/generate_tool_dialog.dart new file mode 100644 index 00000000..f7f7b510 --- /dev/null +++ b/lib/screens/common_widgets/agentic_ui_features/tool_generation/generate_tool_dialog.dart @@ -0,0 +1,207 @@ +import 'package:apidash/apitoolgen/request_consolidator.dart'; +import 'package:apidash/providers/collection_providers.dart'; +import 'package:apidash/screens/common_widgets/agentic_ui_features/ai_ui_designer/generate_ui_dialog.dart'; +import 'package:apidash/services/agentic_services/apidash_agent_calls.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 'generated_tool_codecopy.dart'; +import 'tool_requirements_selector.dart'; + +class GenerateToolButton extends ConsumerWidget { + const GenerateToolButton({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + return FilledButton.tonalIcon( + style: FilledButton.styleFrom( + padding: kPh12, + minimumSize: const Size(44, 44), + ), + onPressed: () async { + GenerateToolDialog.show(context, ref); + }, + icon: Icon( + Icons.token_outlined, + ), + label: const SizedBox( + child: Text( + "Generate Tool", + ), + ), + ); + } +} + +class GenerateToolDialog extends ConsumerStatefulWidget { + final APIDashRequestDescription requestDesc; + const GenerateToolDialog({ + super.key, + required this.requestDesc, + }); + + static show(BuildContext context, WidgetRef ref) { + final aiRequestModel = ref.watch( + selectedRequestModelProvider.select((value) => value?.aiRequestModel)); + HttpRequestModel? requestModel = ref.watch(selectedRequestModelProvider + .select((value) => value?.httpRequestModel)); + final responseModel = ref.watch(selectedRequestModelProvider + .select((value) => value?.httpResponseModel)); + + if (aiRequestModel == null && requestModel == null) return; + if (requestModel == null) return; + if (responseModel == null) return; + + String? bodyTXT; + Map? bodyJSON; + List? bodyFormData; + + if (aiRequestModel != null) { + requestModel = aiRequestModel.httpRequestModel!; + } + + final reqDesModel = APIDashRequestDescription( + endpoint: requestModel.url, + method: requestModel.method.name.toUpperCase(), + responseType: responseModel.contentType.toString(), + headers: requestModel.headersMap, + response: responseModel.body, + formData: bodyFormData, + bodyTXT: bodyTXT, + bodyJSON: bodyJSON, + ); + + showCustomDialog( + context, + GenerateToolDialog( + requestDesc: reqDesModel, + ), + ); + } + + @override + ConsumerState createState() => _GenerateToolDialogState(); +} + +class _GenerateToolDialogState extends ConsumerState { + int index = 0; + TextEditingController controller = TextEditingController(); + + String selectedLanguage = 'PYTHON'; + String selectedAgent = 'GEMINI'; + String? generatedToolCode = ''; + + generateAPITool() async { + try { + setState(() { + generatedToolCode = null; + index = 1; + }); + final res = await generateAPIToolUsingRequestData( + ref: ref, + requestData: widget.requestDesc.generateREQDATA, + targetLanguage: selectedLanguage, + selectedAgent: selectedAgent, + ); + if (res == null) { + setState(() { + generatedToolCode = ''; + index = 0; + }); + ScaffoldMessenger.of(context).showSnackBar(SnackBar( + content: Text( + "API Tool generation failed!", + style: TextStyle(color: Colors.white), + ), + backgroundColor: Colors.redAccent, + )); + return; + } + setState(() { + generatedToolCode = res; + index = 1; + }); + } catch (e) { + setState(() { + index = 0; + }); + String errMsg = 'Unexpected Error Occured'; + if (e.toString().contains('NO_DEFAULT_LLM')) { + errMsg = "Please Select Default AI Model in Settings"; + } + ScaffoldMessenger.of(context).showSnackBar(SnackBar( + content: Text( + errMsg, + style: TextStyle(color: Colors.white), + ), + backgroundColor: Colors.redAccent, + )); + Navigator.pop(context); + } + } + + @override + Widget build(BuildContext context) { + return LayoutBuilder(builder: (context, constraints) { + final dialogWidth = constraints.maxWidth; + final isExpandedWindow = dialogWidth > WindowWidth.expanded.value; + final isLargeWindow = dialogWidth > WindowWidth.large.value; + final isExtraLargeWindow = dialogWidth > WindowWidth.large.value; + + if (isExtraLargeWindow || isLargeWindow || isExpandedWindow) { + return Container( + height: 600, + width: MediaQuery.of(context).size.width * 0.8, + child: Row( + children: [ + Flexible( + flex: 2, + child: ToolRequirementSelectorPage( + onGenerateCallback: (agent, lang) { + setState(() { + selectedLanguage = lang; + selectedAgent = agent; + }); + generateAPITool(); + }, + )), + Expanded( + flex: 3, + child: GeneratedToolCodeCopyPage( + toolCode: generatedToolCode, + language: selectedLanguage.trim(), + ), + ), + ], + ), + ); + } else { + return Container( + height: 600, + // width: MediaQuery.of(context).size.width * 0.8, + child: IndexedStack( + index: index, + children: [ + Center( + child: ToolRequirementSelectorPage( + onGenerateCallback: (agent, lang) { + setState(() { + selectedLanguage = lang; + selectedAgent = agent; + }); + generateAPITool(); + }, + ), + ), + GeneratedToolCodeCopyPage( + toolCode: generatedToolCode, + language: selectedLanguage.trim(), + ), + ], + ), + ); + } + }); + } +} diff --git a/lib/screens/common_widgets/agentic_ui_features/tool_generation/generated_tool_codecopy.dart b/lib/screens/common_widgets/agentic_ui_features/tool_generation/generated_tool_codecopy.dart new file mode 100644 index 00000000..bf3bf34a --- /dev/null +++ b/lib/screens/common_widgets/agentic_ui_features/tool_generation/generated_tool_codecopy.dart @@ -0,0 +1,65 @@ +import 'package:apidash/widgets/button_copy.dart'; +import 'package:apidash/widgets/previewer_code.dart'; +import 'package:apidash/widgets/widget_sending.dart'; +import 'package:apidash_design_system/tokens/tokens.dart'; +import 'package:flutter/material.dart'; + +class GeneratedToolCodeCopyPage extends StatelessWidget { + final String? toolCode; + final String language; + const GeneratedToolCodeCopyPage( + {super.key, required this.toolCode, required this.language}); + + @override + Widget build(BuildContext context) { + final lightMode = Theme.of(context).brightness == Brightness.light; + var codeTheme = lightMode ? kLightCodeTheme : kDarkCodeTheme; + + if (toolCode == null) { + return SendingWidget( + startSendingTime: DateTime.now(), + showTimeElapsed: false, + ); + } + + if (toolCode!.isEmpty) { + return Padding( + padding: const EdgeInsets.only(right: 40), + child: Center( + child: Icon( + Icons.token_outlined, + color: lightMode ? Colors.black12 : Colors.white12, + size: 500, + ), + ), + ); + } + + return Container( + color: const Color.fromARGB(26, 123, 123, 123), + padding: EdgeInsets.all(20), + child: Column( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + CopyButton( + toCopy: toolCode!, + showLabel: true, + ), + Expanded( + child: SingleChildScrollView( + child: Container( + width: double.infinity, + child: CodePreviewer( + code: toolCode!, + theme: codeTheme, + language: language.toLowerCase(), + textStyle: kCodeStyle, + ), + ), + ), + ), + ], + ), + ); + } +} diff --git a/lib/screens/common_widgets/agentic_ui_features/tool_generation/tool_requirements_selector.dart b/lib/screens/common_widgets/agentic_ui_features/tool_generation/tool_requirements_selector.dart new file mode 100644 index 00000000..68556b06 --- /dev/null +++ b/lib/screens/common_widgets/agentic_ui_features/tool_generation/tool_requirements_selector.dart @@ -0,0 +1,203 @@ +import 'package:apidash/providers/settings_providers.dart'; +import 'package:apidash/screens/common_widgets/ai/ai_model_selector_button.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 ToolRequirementSelectorPage extends StatefulWidget { + final Function(String agent, String lang) onGenerateCallback; + const ToolRequirementSelectorPage( + {super.key, required this.onGenerateCallback}); + + @override + State createState() => + _ToolRequirementSelectorPageState(); +} + +class _ToolRequirementSelectorPageState + extends State { + String targetLanguage = 'PYTHON'; + String agentFramework = 'GEMINI'; + + Map frameworkMapping = { + 'GEMINI': 'Gemini', + 'OPENAI': 'OpenAI', + 'LANGCHAIN': 'LangChain', + 'MICROSOFT_AUTOGEN': 'Microsoft AutoGen', + 'MISTRAL': 'Mistral', + 'ANTRHOPIC': 'Anthropic', + }; + + Map languageMapping = { + 'PYTHON': 'Python 3', + 'JAVASCRIPT': 'JavaScript / NodeJS' + }; + + @override + Widget build(BuildContext context) { + final lightMode = Theme.of(context).brightness == Brightness.light; + + return Container( + constraints: BoxConstraints.expand(), + padding: EdgeInsets.all(30), + child: Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text( + "Generate API Tool", + style: TextStyle( + fontSize: 24, + ), + ), + kVSpacer5, + Padding( + padding: EdgeInsets.only(left: 3), + child: Text( + "Select an agent framework & language", + style: TextStyle( + color: lightMode ? Colors.black54 : Colors.white60, + fontSize: 15), + ), + ), + kVSpacer20, + Padding( + padding: EdgeInsets.only(left: 3), + child: Text( + "Agent Framework", + style: TextStyle( + color: lightMode ? Colors.black54 : Colors.white60, + ), + ), + ), + kVSpacer8, + ADPopupMenu( + value: frameworkMapping[agentFramework], + values: [ + ...frameworkMapping.keys + .map((e) => (e.toString(), frameworkMapping[e].toString())), + ], + width: MediaQuery.of(context).size.width * 0.35, + tooltip: '', + onChanged: (x) { + setState(() { + agentFramework = x ?? 'OPENAI'; + + //AutoGen is Python-Only + if (agentFramework == 'MICROSOFT_AUTOGEN') { + targetLanguage = 'PYTHON'; + } + }); + }, + isOutlined: true, + ), + kVSpacer20, + Padding( + padding: EdgeInsets.only(left: 3), + child: Text( + "Target Language", + style: TextStyle( + color: lightMode ? Colors.black54 : Colors.white60, + ), + ), + ), + kVSpacer8, + ADPopupMenu( + value: languageMapping[targetLanguage], + values: [ + ...languageMapping.keys + .map((e) => (e.toString(), languageMapping[e].toString())), + ], + width: MediaQuery.of(context).size.width * 0.35, + tooltip: '', + onChanged: (x) { + setState(() { + targetLanguage = x ?? 'PYTHON'; + + //AutoGen is Python-Only + if (agentFramework == 'MICROSOFT_AUTOGEN') { + targetLanguage = 'PYTHON'; + } + }); + }, + isOutlined: true, + ), + kVSpacer20, + Wrap( + runSpacing: 10, + alignment: WrapAlignment.center, + runAlignment: WrapAlignment.center, + // mainAxisAlignment: MainAxisAlignment.center, + children: [ + FilledButton.tonalIcon( + style: FilledButton.styleFrom( + padding: kPh12, + minimumSize: const Size(44, 44), + ), + onPressed: () { + widget.onGenerateCallback(agentFramework, targetLanguage); + }, + icon: Icon( + Icons.token_outlined, + ), + label: const SizedBox( + child: Text( + "Generate Tool", + ), + ), + ), + kHSpacer5, + DefaultLLModelSelectorWidget(), + ], + ), + ], + ), + ); + } +} + +class DefaultLLModelSelectorWidget extends ConsumerWidget { + const DefaultLLModelSelectorWidget({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final settings = ref.watch(settingsProvider); + return Opacity( + opacity: 0.8, + child: Container( + width: 200, + child: Row( + children: [ + Padding( + padding: EdgeInsets.only(left: 3), + child: Text( + "with", + style: TextStyle( + color: Theme.of(context).brightness == Brightness.light + ? Colors.black54 + : Colors.white60, + fontSize: 15), + ), + ), + SizedBox(width: 5), + AIModelSelectorButton( + aiRequestModel: + AIRequestModel.fromJson(settings.defaultAIModel ?? {}), + onModelUpdated: (d) { + ref.read(settingsProvider.notifier).update( + defaultAIModel: d.copyWith( + modelConfigs: [], + stream: null, + systemPrompt: '', + userPrompt: '').toJson()); + }, + ), + kVSpacer5, + ], + ), + ), + ); + } +} diff --git a/lib/screens/common_widgets/ai/ai.dart b/lib/screens/common_widgets/ai/ai.dart new file mode 100644 index 00000000..20b1371b --- /dev/null +++ b/lib/screens/common_widgets/ai/ai.dart @@ -0,0 +1,3 @@ +export 'ai_model_selector_button.dart'; +export 'ai_model_selector_dialog.dart'; +export 'ai_model_selector.dart'; diff --git a/lib/screens/common_widgets/ai/ai_model_selector.dart b/lib/screens/common_widgets/ai/ai_model_selector.dart new file mode 100644 index 00000000..d746889a --- /dev/null +++ b/lib/screens/common_widgets/ai/ai_model_selector.dart @@ -0,0 +1,41 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:apidash_core/apidash_core.dart'; +import 'package:apidash/providers/providers.dart'; +import 'ai_model_selector_button.dart'; + +class AIModelSelector extends ConsumerWidget { + final AIRequestModel? readOnlyModel; + + const AIModelSelector({ + super.key, + this.readOnlyModel, + }); + + @override + Widget build(BuildContext context, WidgetRef ref) { + AIRequestModel? aiRequestModel; + if (readOnlyModel == null) { + ref.watch(selectedIdStateProvider); + aiRequestModel = ref.watch(selectedRequestModelProvider + .select((value) => value?.aiRequestModel)); + } else { + aiRequestModel = readOnlyModel; + } + + if (aiRequestModel == null) { + return Container(); + } + + return AIModelSelectorButton( + readonly: (readOnlyModel != null), + key: ValueKey(ref.watch(selectedIdStateProvider)), + aiRequestModel: aiRequestModel, + onModelUpdated: (newAIRequestModel) { + ref + .read(collectionStateNotifierProvider.notifier) + .update(aiRequestModel: newAIRequestModel.copyWith()); + }, + ); + } +} diff --git a/lib/screens/common_widgets/ai/ai_model_selector_button.dart b/lib/screens/common_widgets/ai/ai_model_selector_button.dart new file mode 100644 index 00000000..72aeb60c --- /dev/null +++ b/lib/screens/common_widgets/ai/ai_model_selector_button.dart @@ -0,0 +1,41 @@ +import 'package:apidash_core/apidash_core.dart'; +import 'package:apidash_design_system/apidash_design_system.dart'; +import 'package:flutter/material.dart'; +import 'ai_model_selector_dialog.dart'; + +class AIModelSelectorButton extends StatelessWidget { + final AIRequestModel? aiRequestModel; + final bool readonly; + final Function(AIRequestModel)? onModelUpdated; + const AIModelSelectorButton({ + super.key, + this.aiRequestModel, + this.readonly = false, + this.onModelUpdated, + }); + + @override + Widget build(BuildContext context) { + return ElevatedButton( + onPressed: readonly + ? null + : () async { + final newAIRequestModel = await showDialog( + context: context, + builder: (context) { + return AlertDialog( + scrollable: true, + content: AIModelSelectorDialog( + aiRequestModel: aiRequestModel, + ), + contentPadding: kP10, + ); + }, + ); + if (newAIRequestModel == null) return; + onModelUpdated?.call(newAIRequestModel); + }, + child: Text(aiRequestModel?.model ?? 'Select Model'), + ); + } +} diff --git a/lib/screens/common_widgets/ai/ai_model_selector_dialog.dart b/lib/screens/common_widgets/ai/ai_model_selector_dialog.dart new file mode 100644 index 00000000..53680e22 --- /dev/null +++ b/lib/screens/common_widgets/ai/ai_model_selector_dialog.dart @@ -0,0 +1,251 @@ +// import 'package:apidash/providers/providers.dart'; +import 'package:apidash/widgets/widgets.dart'; +import 'package:apidash_core/apidash_core.dart'; +import 'package:apidash_design_system/apidash_design_system.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +class AIModelSelectorDialog extends ConsumerStatefulWidget { + final AIRequestModel? aiRequestModel; + const AIModelSelectorDialog({super.key, this.aiRequestModel}); + + @override + ConsumerState createState() => + _AIModelSelectorDialogState(); +} + +class _AIModelSelectorDialogState extends ConsumerState { + late final Future aM; + ModelAPIProvider? selectedProvider; + AIRequestModel? newAIRequestModel; + + @override + void initState() { + super.initState(); + selectedProvider = widget.aiRequestModel?.modelApiProvider; + if (selectedProvider != null && widget.aiRequestModel?.model != null) { + newAIRequestModel = widget.aiRequestModel?.copyWith(); + } + aM = ModelManager.fetchAvailableModels(); + } + + @override + Widget build(BuildContext context) { + // ref.watch(aiApiCredentialProvider); + final width = MediaQuery.of(context).size.width * 0.8; + return FutureBuilder( + future: aM, + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.done && + snapshot.hasData && + snapshot.data != null) { + final data = snapshot.data!; + final mappedData = data.map; + if (context.isMediumWindow) { + return Container( + padding: kP20, + width: width, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ElevatedButton( + onPressed: null, + // TODO: Add update model logic + //() async { + // await LLMManager.fetchAvailableLLMs(); + // setState(() {}); + //}, + child: Text('Update Models'), + ), + kVSpacer10, + Row( + children: [ + Text('Select Model Provider'), + kHSpacer20, + Expanded( + child: ADDropdownButton( + onChanged: (x) { + setState(() { + selectedProvider = x; + newAIRequestModel = mappedData[selectedProvider] + ?.toAiRequestModel(); + }); + }, + value: selectedProvider, + values: data.modelProviders + .map((e) => (e.providerId!, e.providerName)), + ), + ), + ], + ), + kVSpacer10, + _buildModelSelector(mappedData[selectedProvider]), + ], + ), + ); + } + + return Container( + padding: kP20, + width: width, + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Flexible( + flex: 1, + child: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ElevatedButton( + onPressed: null, + // TODO: Add update model logic + //() async { + // await LLMManager.fetchAvailableLLMs(); + // setState(() {}); + //}, + child: Text('Update Models'), + ), + SizedBox(height: 20), + ...data.modelProviders.map( + (x) => ListTile( + title: Text(x.providerName ?? ""), + trailing: selectedProvider != x.providerId + ? null + : CircleAvatar( + radius: 5, + backgroundColor: Colors.green, + ), + onTap: () { + setState(() { + selectedProvider = x.providerId; + newAIRequestModel = mappedData[selectedProvider] + ?.toAiRequestModel(); + }); + }, + ), + ), + ], + ), + ), + ), + SizedBox(width: 40), + Flexible( + flex: 3, + child: _buildModelSelector(mappedData[selectedProvider]), + ), + ], + ), + ); + } + return CircularProgressIndicator(); + }, + ); + } + + _buildModelSelector(AIModelProvider? aiModelProvider) { + if (aiModelProvider == null) { + return Center(child: Text("Please select an AI API Provider")); + } + // final currentCredential = + // ref.watch(aiApiCredentialProvider)[aiModelProvider.providerId!] ?? ""; + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.max, + children: [ + Text( + aiModelProvider.providerName ?? "", + style: TextStyle(fontSize: 28), + ), + SizedBox(height: 20), + if (aiModelProvider.providerId != ModelAPIProvider.ollama) ...[ + Text('API Key / Credential'), + kVSpacer8, + BoundedTextField( + onChanged: (x) { + // ref.read(aiApiCredentialProvider.notifier).state = { + // ...ref.read(aiApiCredentialProvider), + // aiModelProvider.providerId!: x + // }; + setState(() { + newAIRequestModel = newAIRequestModel?.copyWith(apiKey: x); + }); + }, + value: newAIRequestModel?.apiKey ?? "", + // value: currentCredential, + ), + kVSpacer10, + ], + Text('Endpoint'), + kVSpacer8, + BoundedTextField( + key: ValueKey(aiModelProvider.providerName ?? ""), + onChanged: (x) { + setState(() { + newAIRequestModel = newAIRequestModel?.copyWith(url: x); + }); + }, + value: newAIRequestModel?.url ?? "", + ), + kVSpacer20, + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text('Models'), + // IconButton( + // onPressed: () => addNewModel(context), icon: Icon(Icons.add)) + ], + ), + kVSpacer8, + Container( + height: 300, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(10), + color: const Color.fromARGB(27, 0, 0, 0), + ), + child: Material( + color: Colors.transparent, + child: SingleChildScrollView( + clipBehavior: Clip.hardEdge, + child: Column( + children: [ + ...(aiModelProvider.models ?? []).map( + (x) => ListTile( + title: Text(x.name ?? ""), + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + if (newAIRequestModel?.model == x.id) + CircleAvatar( + radius: 5, + backgroundColor: Colors.green, + ), + ], + ), + onTap: () { + setState(() { + newAIRequestModel = + newAIRequestModel?.copyWith(model: x.id); + }); + }, + ), + ), + ], + ), + ), + ), + ), + kVSpacer10, + Align( + alignment: Alignment.centerRight, + child: ElevatedButton( + onPressed: () { + Navigator.of(context).pop(newAIRequestModel); + }, + child: Text('Save'), + ), + ), + ], + ); + } +} diff --git a/lib/screens/common_widgets/ai/dialog_add_ai_model.dart b/lib/screens/common_widgets/ai/dialog_add_ai_model.dart new file mode 100644 index 00000000..7873b31b --- /dev/null +++ b/lib/screens/common_widgets/ai/dialog_add_ai_model.dart @@ -0,0 +1,44 @@ +import 'package:apidash_design_system/apidash_design_system.dart'; +import 'package:flutter/material.dart'; + +Future addNewModel(BuildContext context) async { + TextEditingController iC = TextEditingController(); + TextEditingController nC = TextEditingController(); + final z = await showDialog( + context: context, + builder: (context) { + return AlertDialog( + title: Text('Add Custom Model'), + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + ADOutlinedTextField( + controller: iC, + hintText: 'Model ID', + ), + kVSpacer10, + ADOutlinedTextField( + controller: nC, + hintText: 'Model Display Name', + ), + kVSpacer10, + SizedBox( + width: double.infinity, + child: ElevatedButton( + onPressed: () { + Navigator.of(context).pop([ + iC.value.text, + nC.value.text, + ]); + }, + child: Text('Add Model'), + ), + ) + ], + ), + ); + }); + if (z == null) return; + // TODO: Add logic to add a new model + // setState(() {}); +} diff --git a/lib/screens/common_widgets/code_pane.dart b/lib/screens/common_widgets/code_pane.dart index 654b9440..5c08cc69 100644 --- a/lib/screens/common_widgets/code_pane.dart +++ b/lib/screens/common_widgets/code_pane.dart @@ -28,6 +28,14 @@ class CodePane extends ConsumerWidget { final selectedRequestModel = isHistoryRequest ? getRequestModelFromHistoryModel(selectedHistoryRequestModel!) : ref.watch(selectedRequestModelProvider); + + // TODO: Add AI Request Codegen + if (selectedRequestModel?.apiType == APIType.ai) { + return const ErrorMessage( + message: "Code generation for AI Requests is currently not available.", + ); + } + final defaultUriScheme = ref.watch(settingsProvider.select((value) => value.defaultUriScheme)); diff --git a/lib/screens/common_widgets/common_widgets.dart b/lib/screens/common_widgets/common_widgets.dart index ff914477..e68b01fb 100644 --- a/lib/screens/common_widgets/common_widgets.dart +++ b/lib/screens/common_widgets/common_widgets.dart @@ -1,3 +1,4 @@ +export 'ai/ai.dart'; export 'auth/auth.dart'; export 'api_type_dropdown.dart'; export 'button_navbar.dart'; diff --git a/lib/screens/history/history_widgets/ai_history_page.dart b/lib/screens/history/history_widgets/ai_history_page.dart new file mode 100644 index 00000000..1a799b39 --- /dev/null +++ b/lib/screens/history/history_widgets/ai_history_page.dart @@ -0,0 +1,165 @@ +import 'package:apidash_core/apidash_core.dart'; +import 'package:apidash_design_system/apidash_design_system.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:apidash/providers/providers.dart'; +import 'package:apidash/widgets/editor.dart'; + +class HisAIRequestPromptSection extends ConsumerWidget { + const HisAIRequestPromptSection({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final selectedHistoryModel = + ref.watch(selectedHistoryRequestModelProvider)!; + final aiReqM = selectedHistoryModel.aiRequestModel; + if (aiReqM == null) { + return kSizedBoxEmpty; + } + + return Container( + padding: EdgeInsets.symmetric(vertical: 20), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.only(left: 25.0), + child: Text( + 'System Prompt', + ), + ), + kVSpacer10, + Expanded( + child: Container( + padding: EdgeInsets.symmetric(horizontal: 20), + child: TextFieldEditor( + key: Key( + "${selectedHistoryModel.historyId}-aireq-sysprompt-body"), + fieldKey: + "${selectedHistoryModel.historyId}-aireq-sysprompt-body", + initialValue: aiReqM.systemPrompt, + readOnly: true, + ), + ), + ), + SizedBox(height: 10), + Padding( + padding: const EdgeInsets.only(left: 25.0), + child: Text( + 'User Prompt / Input', + ), + ), + kVSpacer10, + Expanded( + child: Container( + padding: EdgeInsets.symmetric(horizontal: 20), + child: TextFieldEditor( + key: Key( + "${selectedHistoryModel.historyId}-aireq-userprompt-body"), + fieldKey: + "${selectedHistoryModel.historyId}-aireq-userprompt-body", + initialValue: aiReqM.userPrompt, + readOnly: true, + ), + ), + ), + ], + ), + ); + } +} + +class HisAIRequestAuthorizationSection extends ConsumerWidget { + const HisAIRequestAuthorizationSection({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final selectedHistoryModel = + ref.watch(selectedHistoryRequestModelProvider)!; + final aiReqM = selectedHistoryModel.aiRequestModel; + if (aiReqM == null) { + return kSizedBoxEmpty; + } + + return Container( + padding: EdgeInsets.symmetric(vertical: 20), + child: Column( + children: [ + Expanded( + child: Container( + padding: EdgeInsets.symmetric(horizontal: 20), + child: TextFieldEditor( + key: Key( + "${selectedHistoryModel.historyId}-aireq-authvalue-body"), + fieldKey: + "${selectedHistoryModel.historyId}-aireq-authvalue-body", + initialValue: aiReqM.apiKey, + readOnly: true, + ), + ), + ), + ], + ), + ); + } +} + +class HisAIRequestConfigSection extends ConsumerWidget { + const HisAIRequestConfigSection({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final selectedHistoryModel = + ref.watch(selectedHistoryRequestModelProvider)!; + final aiReqM = selectedHistoryModel.aiRequestModel; + if (aiReqM == null) { + return kSizedBoxEmpty; + } + return SingleChildScrollView( + padding: EdgeInsets.symmetric(vertical: 20), + child: Column( + key: ValueKey(selectedHistoryModel.historyId), + children: [ + ...aiReqM.modelConfigs.map( + (el) => ListTile( + title: Text(el.name), + subtitle: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + el.description, + ), + SizedBox(height: 5), + switch (el.type) { + ConfigType.boolean => AIConfigBool( + readonly: true, + configuration: el, + onConfigUpdated: (x) {}, + ), + ConfigType.numeric => AIConfigField( + readonly: true, + configuration: el, + onConfigUpdated: (x) {}, + numeric: true, + ), + ConfigType.text => AIConfigField( + readonly: true, + configuration: el, + onConfigUpdated: (x) {}, + ), + ConfigType.slider => AIConfigSlider( + readonly: true, + configuration: el, + onSliderUpdated: (x) {}, + ), + }, + SizedBox(height: 10), + ], + ), + ), + ), + ], + ), + ); + } +} diff --git a/lib/screens/history/history_widgets/his_request_pane.dart b/lib/screens/history/history_widgets/his_request_pane.dart index 6766ad1b..0b126600 100644 --- a/lib/screens/history/history_widgets/his_request_pane.dart +++ b/lib/screens/history/history_widgets/his_request_pane.dart @@ -6,6 +6,7 @@ import 'package:apidash/providers/providers.dart'; import 'package:apidash/widgets/widgets.dart'; import 'package:apidash/consts.dart'; import '../../common_widgets/common_widgets.dart'; +import 'ai_history_page.dart'; import 'his_scripts_tab.dart'; class HistoryRequestPane extends ConsumerWidget { @@ -22,23 +23,39 @@ class HistoryRequestPane extends ConsumerWidget { final codePaneVisible = ref.watch(historyCodePaneVisibleStateProvider); final apiType = ref.watch(selectedHistoryRequestModelProvider .select((value) => value?.metaData.apiType)); - final headersMap = ref.watch(selectedHistoryRequestModelProvider - .select((value) => value?.httpRequestModel.headersMap)) ?? + + final headersMap = ref.watch(selectedHistoryRequestModelProvider.select( + (value) { + if (apiType == APIType.ai) return {}; + return value?.httpRequestModel!.headersMap; + }, + )) ?? {}; final headerLength = headersMap.length; - final paramsMap = ref.watch(selectedHistoryRequestModelProvider - .select((value) => value?.httpRequestModel.paramsMap)) ?? + final paramsMap = ref.watch(selectedHistoryRequestModelProvider.select( + (value) { + if (apiType == APIType.ai) return {}; + return value?.httpRequestModel!.paramsMap; + }, + )) ?? {}; final paramLength = paramsMap.length; - final hasBody = ref.watch(selectedHistoryRequestModelProvider - .select((value) => value?.httpRequestModel.hasBody)) ?? + final hasBody = ref.watch(selectedHistoryRequestModelProvider.select( + (value) { + if (apiType == APIType.ai) return false; + return value?.httpRequestModel!.hasBody; + }, + )) ?? false; - final hasQuery = ref.watch(selectedHistoryRequestModelProvider - .select((value) => value?.httpRequestModel.hasQuery)) ?? - false; + final hasQuery = + ref.watch(selectedHistoryRequestModelProvider.select((value) { + if (apiType == APIType.ai) return false; + return value?.httpRequestModel!.hasQuery; + })) ?? + false; final scriptsLength = ref.watch(selectedHistoryRequestModelProvider .select((value) => value?.preRequestScript?.length)) ?? @@ -127,6 +144,27 @@ class HistoryRequestPane extends ConsumerWidget { const HistoryScriptsTab(), ], ), + APIType.ai => RequestPane( + key: const Key("history-request-pane-ai"), + selectedId: selectedId, + codePaneVisible: codePaneVisible, + onPressedCodeButton: () { + ref.read(historyCodePaneVisibleStateProvider.notifier).state = + !codePaneVisible; + }, + showViewCodeButton: !isCompact, + showIndicators: [ + false, + false, + false, + ], + tabLabels: const ["Prompts", "Authorization", "Configuration"], + children: [ + const HisAIRequestPromptSection(), + const HisAIRequestAuthorizationSection(), + const HisAIRequestConfigSection(), + ], + ), _ => kSizedBoxEmpty, }; } diff --git a/lib/screens/history/history_widgets/his_response_pane.dart b/lib/screens/history/history_widgets/his_response_pane.dart index f90390cb..b63b2d2a 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( @@ -31,6 +35,7 @@ class HistoryResponsePane extends ConsumerWidget { children: [ ResponseBody( selectedRequestModel: requestModel, + isPartOfHistory: true, ), ResponseHeaders( responseHeaders: historyHttpResponseModel?.headers ?? {}, diff --git a/lib/screens/history/history_widgets/his_url_card.dart b/lib/screens/history/history_widgets/his_url_card.dart index 7a6153b4..5825e31f 100644 --- a/lib/screens/history/history_widgets/his_url_card.dart +++ b/lib/screens/history/history_widgets/his_url_card.dart @@ -1,6 +1,7 @@ import 'package:apidash_core/apidash_core.dart'; import 'package:apidash_design_system/apidash_design_system.dart'; import 'package:flutter/material.dart'; +import 'package:apidash/screens/common_widgets/common_widgets.dart'; import 'package:apidash/widgets/widgets.dart'; import 'package:apidash/models/models.dart'; import 'package:apidash/utils/utils.dart'; @@ -58,6 +59,12 @@ class HistoryURLCard extends StatelessWidget { ), isCompact ? kHSpacer10 : kHSpacer20, ], + if (apiType == APIType.ai) ...[ + AIModelSelector( + readOnlyModel: historyRequestModel?.aiRequestModel, + ), + SizedBox(width: 20), + ], Expanded( child: ReadOnlyTextField( initialValue: url, diff --git a/lib/screens/home_page/collection_pane.dart b/lib/screens/home_page/collection_pane.dart index 4a014971..cc1a2a3b 100644 --- a/lib/screens/home_page/collection_pane.dart +++ b/lib/screens/home_page/collection_pane.dart @@ -193,7 +193,7 @@ class RequestItem extends ConsumerWidget { return SidebarRequestCard( id: id, apiType: requestModel.apiType, - method: requestModel.httpRequestModel!.method, + method: requestModel.httpRequestModel?.method, name: requestModel.name, url: requestModel.httpRequestModel?.url, selectedId: selectedId, diff --git a/lib/screens/home_page/editor_pane/details_card/request_pane/ai_request/aireq_authorization.dart b/lib/screens/home_page/editor_pane/details_card/request_pane/ai_request/aireq_authorization.dart new file mode 100644 index 00000000..51ff7ba8 --- /dev/null +++ b/lib/screens/home_page/editor_pane/details_card/request_pane/ai_request/aireq_authorization.dart @@ -0,0 +1,47 @@ +import 'package:apidash/providers/providers.dart'; +import 'package:apidash/widgets/widgets.dart'; +import 'package:apidash_design_system/apidash_design_system.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +class AIRequestAuthorizationSection extends ConsumerWidget { + const AIRequestAuthorizationSection({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final selectedId = ref.watch(selectedIdStateProvider); + final apiKey = ref.watch(selectedRequestModelProvider + .select((value) => value?.aiRequestModel?.apiKey)); + final requestModel = ref + .read(collectionStateNotifierProvider.notifier) + .getRequestModel(selectedId!); + final aiReqM = requestModel?.aiRequestModel; + if (aiReqM == null) { + return kSizedBoxEmpty; + } + + return Container( + padding: EdgeInsets.symmetric(vertical: 20), + child: Column( + children: [ + Expanded( + child: Container( + padding: EdgeInsets.symmetric(horizontal: 20), + child: TextFieldEditor( + key: Key("$selectedId-aireq-authvalue-body"), + fieldKey: "$selectedId-aireq-authvalue-body", + initialValue: apiKey, + onChanged: (String value) { + ref + .read(collectionStateNotifierProvider.notifier) + .update(aiRequestModel: aiReqM.copyWith(apiKey: value)); + }, + hintText: 'Enter API key or Authorization Credentials', + ), + ), + ), + ], + ), + ); + } +} diff --git a/lib/screens/home_page/editor_pane/details_card/request_pane/ai_request/aireq_configs.dart b/lib/screens/home_page/editor_pane/details_card/request_pane/ai_request/aireq_configs.dart new file mode 100644 index 00000000..94e68307 --- /dev/null +++ b/lib/screens/home_page/editor_pane/details_card/request_pane/ai_request/aireq_configs.dart @@ -0,0 +1,89 @@ +import 'package:apidash/providers/providers.dart'; +import 'package:apidash_core/apidash_core.dart'; +import 'package:apidash_design_system/apidash_design_system.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +class AIRequestConfigSection extends ConsumerWidget { + const AIRequestConfigSection({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final selectedId = ref.watch(selectedIdStateProvider); + final modelConfigs = ref.watch(selectedRequestModelProvider + .select((value) => value?.aiRequestModel?.modelConfigs)); + final requestModel = ref + .read(collectionStateNotifierProvider.notifier) + .getRequestModel(selectedId!); + final aiReqM = requestModel?.aiRequestModel; + if (aiReqM == null || modelConfigs == null) { + return kSizedBoxEmpty; + } + + updateRequestModel(ModelConfig modelConfig) { + final aiRequestModel = ref + .read(collectionStateNotifierProvider.notifier) + .getRequestModel(selectedId) + ?.aiRequestModel; + final idx = aiRequestModel?.getModelConfigIdx(modelConfig.id); + if (idx != null && aiRequestModel != null) { + var l = [...aiRequestModel.modelConfigs]; + l[idx] = modelConfig; + ref.read(collectionStateNotifierProvider.notifier).update( + aiRequestModel: aiRequestModel.copyWith(modelConfigs: l), + ); + } + } + + return SingleChildScrollView( + padding: EdgeInsets.symmetric(vertical: 20), + child: Column( + key: ValueKey(selectedId), + children: [ + ...modelConfigs.map( + (el) => ListTile( + title: Text(el.name), + subtitle: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + el.description, + ), + SizedBox(height: 5), + switch (el.type) { + ConfigType.boolean => AIConfigBool( + configuration: el, + onConfigUpdated: (x) { + updateRequestModel(x); + }, + ), + ConfigType.numeric => AIConfigField( + configuration: el, + onConfigUpdated: (x) { + updateRequestModel(x); + }, + numeric: true, + ), + ConfigType.text => AIConfigField( + configuration: el, + onConfigUpdated: (x) { + updateRequestModel(x); + }, + ), + ConfigType.slider => AIConfigSlider( + configuration: el, + onSliderUpdated: (x) { + updateRequestModel(x); + }, + ), + }, + SizedBox(height: 10), + ], + ), + ), + ), + ], + ), + ); + } +} diff --git a/lib/screens/home_page/editor_pane/details_card/request_pane/ai_request/aireq_prompt.dart b/lib/screens/home_page/editor_pane/details_card/request_pane/ai_request/aireq_prompt.dart new file mode 100644 index 00000000..d63d153e --- /dev/null +++ b/lib/screens/home_page/editor_pane/details_card/request_pane/ai_request/aireq_prompt.dart @@ -0,0 +1,81 @@ +import 'package:apidash/providers/providers.dart'; +import 'package:apidash/widgets/widgets.dart'; +import 'package:apidash_design_system/apidash_design_system.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +class AIRequestPromptSection extends ConsumerWidget { + const AIRequestPromptSection({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final selectedId = ref.watch(selectedIdStateProvider); + final systemPrompt = ref.watch(selectedRequestModelProvider + .select((value) => value?.aiRequestModel?.systemPrompt)); + final userPrompt = ref.watch(selectedRequestModelProvider + .select((value) => value?.aiRequestModel?.userPrompt)); + final aiRequestModel = ref + .read(collectionStateNotifierProvider.notifier) + .getRequestModel(selectedId!) + ?.aiRequestModel; + if (aiRequestModel == null) { + return kSizedBoxEmpty; + } + + return Container( + padding: EdgeInsets.symmetric(vertical: 20), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.only(left: 25.0), + child: Text( + 'System Prompt', + ), + ), + kVSpacer10, + Expanded( + child: Container( + padding: EdgeInsets.symmetric(horizontal: 20), + child: TextFieldEditor( + key: Key("$selectedId-aireq-sysprompt-body"), + fieldKey: "$selectedId-aireq-sysprompt-body", + initialValue: systemPrompt, + onChanged: (String value) { + ref.read(collectionStateNotifierProvider.notifier).update( + aiRequestModel: + aiRequestModel.copyWith(systemPrompt: value)); + }, + hintText: 'Enter System Prompt', + ), + ), + ), + SizedBox(height: 10), + Padding( + padding: const EdgeInsets.only(left: 25.0), + child: Text( + 'User Prompt / Input', + ), + ), + kVSpacer10, + Expanded( + child: Container( + padding: EdgeInsets.symmetric(horizontal: 20), + child: TextFieldEditor( + key: Key("$selectedId-aireq-userprompt-body"), + fieldKey: "$selectedId-aireq-userprompt-body", + initialValue: userPrompt, + onChanged: (String value) { + ref.read(collectionStateNotifierProvider.notifier).update( + aiRequestModel: + aiRequestModel.copyWith(userPrompt: value)); + }, + hintText: 'Enter User Prompt', + ), + ), + ), + ], + ), + ); + } +} diff --git a/lib/screens/home_page/editor_pane/details_card/request_pane/ai_request/request_pane_ai.dart b/lib/screens/home_page/editor_pane/details_card/request_pane/ai_request/request_pane_ai.dart new file mode 100644 index 00000000..29dffd06 --- /dev/null +++ b/lib/screens/home_page/editor_pane/details_card/request_pane/ai_request/request_pane_ai.dart @@ -0,0 +1,49 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:apidash/providers/providers.dart'; +import 'package:apidash/widgets/widgets.dart'; +import 'aireq_authorization.dart'; +import 'aireq_configs.dart'; +import 'aireq_prompt.dart'; + +class EditAIRequestPane extends ConsumerWidget { + const EditAIRequestPane({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final selectedId = ref.watch(selectedIdStateProvider); + final codePaneVisible = ref.watch(codePaneVisibleStateProvider); + final tabIndex = ref.watch( + selectedRequestModelProvider.select((value) => value?.requestTabIndex)); + + return RequestPane( + selectedId: selectedId, + codePaneVisible: codePaneVisible, + tabIndex: tabIndex, + onPressedCodeButton: () { + ref.read(codePaneVisibleStateProvider.notifier).state = + !codePaneVisible; + }, + onTapTabBar: (index) { + ref + .read(collectionStateNotifierProvider.notifier) + .update(requestTabIndex: index); + }, + showIndicators: [ + false, + false, + false, + ], + tabLabels: const [ + "Prompt", + "Authorization", + "Configurations", + ], + children: const [ + AIRequestPromptSection(), + AIRequestAuthorizationSection(), + AIRequestConfigSection(), + ], + ); + } +} diff --git a/lib/screens/home_page/editor_pane/details_card/request_pane/request_pane.dart b/lib/screens/home_page/editor_pane/details_card/request_pane/request_pane.dart index 9c852c71..af82951e 100644 --- a/lib/screens/home_page/editor_pane/details_card/request_pane/request_pane.dart +++ b/lib/screens/home_page/editor_pane/details_card/request_pane/request_pane.dart @@ -3,6 +3,7 @@ import 'package:apidash_design_system/apidash_design_system.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:apidash/providers/providers.dart'; +import 'ai_request/request_pane_ai.dart'; import 'request_pane_graphql.dart'; import 'request_pane_rest.dart'; @@ -17,6 +18,7 @@ class EditRequestPane extends ConsumerWidget { return switch (apiType) { APIType.rest => const EditRestRequestPane(), APIType.graphql => const EditGraphQLRequestPane(), + APIType.ai => const EditAIRequestPane(), _ => kSizedBoxEmpty, }; } diff --git a/lib/screens/home_page/editor_pane/details_card/response_pane.dart b/lib/screens/home_page/editor_pane/details_card/response_pane.dart index 367bb412..0e701d5f 100644 --- a/lib/screens/home_page/editor_pane/details_card/response_pane.dart +++ b/lib/screens/home_page/editor_pane/details_card/response_pane.dart @@ -106,12 +106,18 @@ class ResponseHeadersTab extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final requestHeaders = ref.watch(selectedRequestModelProvider - .select((value) => value?.httpResponseModel?.requestHeaders)) ?? - {}; - final responseHeaders = ref.watch(selectedRequestModelProvider - .select((value) => value?.httpResponseModel?.headers)) ?? - {}; + final requestHeaders = + ref.watch(selectedRequestModelProvider.select((value) { + return value?.httpResponseModel!.requestHeaders; + })) ?? + {}; + + final responseHeaders = + ref.watch(selectedRequestModelProvider.select((value) { + return value?.httpResponseModel!.headers; + })) ?? + {}; + return ResponseHeaders( responseHeaders: responseHeaders, requestHeaders: requestHeaders, diff --git a/lib/screens/home_page/editor_pane/url_card.dart b/lib/screens/home_page/editor_pane/url_card.dart index 5aa1ce02..d84f27a7 100644 --- a/lib/screens/home_page/editor_pane/url_card.dart +++ b/lib/screens/home_page/editor_pane/url_card.dart @@ -35,6 +35,7 @@ class EditorPaneRequestURLCard extends ConsumerWidget { switch (apiType) { APIType.rest => const DropdownButtonHTTPMethod(), APIType.graphql => kSizedBoxEmpty, + APIType.ai => const AIModelSelector(), null => kSizedBoxEmpty, }, switch (apiType) { @@ -51,6 +52,7 @@ class EditorPaneRequestURLCard extends ConsumerWidget { switch (apiType) { APIType.rest => const DropdownButtonHTTPMethod(), APIType.graphql => kSizedBoxEmpty, + APIType.ai => const AIModelSelector(), null => kSizedBoxEmpty, }, switch (apiType) { @@ -100,15 +102,27 @@ class URLTextField extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final selectedId = ref.watch(selectedIdStateProvider); + ref.watch(selectedRequestModelProvider + .select((value) => value?.aiRequestModel?.url)); + ref.watch(selectedRequestModelProvider + .select((value) => value?.httpRequestModel?.url)); + final requestModel = ref + .read(collectionStateNotifierProvider.notifier) + .getRequestModel(selectedId!)!; return EnvURLField( - selectedId: selectedId!, - initialValue: ref - .read(collectionStateNotifierProvider.notifier) - .getRequestModel(selectedId) - ?.httpRequestModel - ?.url, + selectedId: selectedId, + initialValue: switch (requestModel.apiType) { + APIType.ai => requestModel.aiRequestModel?.url, + _ => requestModel.httpRequestModel?.url, + }, onChanged: (value) { - ref.read(collectionStateNotifierProvider.notifier).update(url: value); + if (requestModel.apiType == APIType.ai) { + ref.read(collectionStateNotifierProvider.notifier).update( + aiRequestModel: + requestModel.aiRequestModel?.copyWith(url: value)); + } else { + ref.read(collectionStateNotifierProvider.notifier).update(url: value); + } }, onFieldSubmitted: (value) { ref.read(collectionStateNotifierProvider.notifier).sendRequest(); diff --git a/lib/screens/settings_page.dart b/lib/screens/settings_page.dart index 606ef516..4da2a48f 100644 --- a/lib/screens/settings_page.dart +++ b/lib/screens/settings_page.dart @@ -1,3 +1,4 @@ +import 'package:apidash_core/apidash_core.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; @@ -7,6 +8,7 @@ import '../services/services.dart'; import '../utils/utils.dart'; import '../widgets/widgets.dart'; import '../consts.dart'; +import 'common_widgets/common_widgets.dart'; class SettingsPage extends ConsumerWidget { const SettingsPage({super.key}); @@ -114,6 +116,22 @@ class SettingsPage extends ConsumerWidget { }, ), ), + ListTile( + hoverColor: kColorTransparent, + title: const Text('Default Large Language Model (LLM)'), + trailing: AIModelSelectorButton( + aiRequestModel: + AIRequestModel.fromJson(settings.defaultAIModel ?? {}), + onModelUpdated: (d) { + ref.read(settingsProvider.notifier).update( + defaultAIModel: d.copyWith( + modelConfigs: [], + stream: null, + systemPrompt: '', + userPrompt: '').toJson()); + }, + ), + ), CheckboxListTile( title: const Text("Save Responses"), subtitle: diff --git a/lib/services/agentic_services/agent_caller.dart b/lib/services/agentic_services/agent_caller.dart new file mode 100644 index 00000000..c9939770 --- /dev/null +++ b/lib/services/agentic_services/agent_caller.dart @@ -0,0 +1,27 @@ +import 'package:apidash/providers/providers.dart'; +import 'package:apidash_core/apidash_core.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +class APIDashAgentCaller { + static APIDashAgentCaller instance = APIDashAgentCaller(); + + Future call( + AIAgent agent, { + required WidgetRef ref, + required AgentInputs input, + }) async { + final defaultAIModel = + ref.read(settingsProvider.select((e) => e.defaultAIModel)); + if (defaultAIModel == null) { + throw Exception('NO_DEFAULT_LLM'); + } + final baseAIRequestObject = AIRequestModel.fromJson(defaultAIModel); + final ans = await AIAgentService.callAgent( + agent, + baseAIRequestObject, + query: input.query, + variables: input.variables, + ); + return ans; + } +} diff --git a/lib/services/agentic_services/agents/agents.dart b/lib/services/agentic_services/agents/agents.dart new file mode 100644 index 00000000..e0b355ba --- /dev/null +++ b/lib/services/agentic_services/agents/agents.dart @@ -0,0 +1,7 @@ +export 'intermediate_rep_gen.dart'; +export 'semantic_analyser.dart'; +export 'stac_to_flutter.dart'; +export 'stacgen.dart'; +export 'stacmodifier.dart'; +export 'apitool_funcgen.dart'; +export 'apitool_bodygen.dart'; diff --git a/lib/services/agentic_services/agents/apitool_bodygen.dart b/lib/services/agentic_services/agents/apitool_bodygen.dart new file mode 100644 index 00000000..628d777f --- /dev/null +++ b/lib/services/agentic_services/agents/apitool_bodygen.dart @@ -0,0 +1,32 @@ +import 'package:apidash/templates/templates.dart'; +import 'package:apidash_core/apidash_core.dart'; + +class ApiToolBodyGen extends AIAgent { + @override + String get agentName => 'APITOOL_BODYGEN'; + + @override + String getSystemPrompt() { + return kPromptAPIToolBodyGen; + } + + @override + Future validator(String aiResponse) async { + //Add any specific validations here as needed + return true; + } + + @override + Future outputFormatter(String validatedResponse) async { + validatedResponse = validatedResponse + .replaceAll('```python', '') + .replaceAll('```python\n', '') + .replaceAll('```javascript', '') + .replaceAll('```javascript\n', '') + .replaceAll('```', ''); + + return { + 'TOOL': validatedResponse, + }; + } +} diff --git a/lib/services/agentic_services/agents/apitool_funcgen.dart b/lib/services/agentic_services/agents/apitool_funcgen.dart new file mode 100644 index 00000000..b870332f --- /dev/null +++ b/lib/services/agentic_services/agents/apitool_funcgen.dart @@ -0,0 +1,32 @@ +import 'package:apidash/templates/templates.dart'; +import 'package:apidash_core/apidash_core.dart'; + +class APIToolFunctionGenerator extends AIAgent { + @override + String get agentName => 'APITOOL_FUNCGEN'; + + @override + String getSystemPrompt() { + return kPromptAPIToolFuncGen; + } + + @override + Future validator(String aiResponse) async { + //Add any specific validations here as needed + return true; + } + + @override + Future outputFormatter(String validatedResponse) async { + validatedResponse = validatedResponse + .replaceAll('```python', '') + .replaceAll('```python\n', '') + .replaceAll('```javascript', '') + .replaceAll('```javascript\n', '') + .replaceAll('```', ''); + + return { + 'FUNC': validatedResponse, + }; + } +} diff --git a/lib/services/agentic_services/agents/intermediate_rep_gen.dart b/lib/services/agentic_services/agents/intermediate_rep_gen.dart new file mode 100644 index 00000000..f8523555 --- /dev/null +++ b/lib/services/agentic_services/agents/intermediate_rep_gen.dart @@ -0,0 +1,29 @@ +import 'package:apidash/templates/templates.dart'; +import 'package:apidash_core/apidash_core.dart'; + +class IntermediateRepresentationGen extends AIAgent { + @override + String get agentName => 'INTERMEDIATE_REP_GEN'; + + @override + String getSystemPrompt() { + return kPromptIntermediateRepGen; + } + + @override + Future validator(String aiResponse) async { + //Add any specific validations here as needed + return true; + } + + @override + Future outputFormatter(String validatedResponse) async { + validatedResponse = validatedResponse + .replaceAll('```yaml', '') + .replaceAll('```yaml\n', '') + .replaceAll('```', ''); + return { + 'INTERMEDIATE_REPRESENTATION': validatedResponse, + }; + } +} diff --git a/lib/services/agentic_services/agents/semantic_analyser.dart b/lib/services/agentic_services/agents/semantic_analyser.dart new file mode 100644 index 00000000..861a5cac --- /dev/null +++ b/lib/services/agentic_services/agents/semantic_analyser.dart @@ -0,0 +1,25 @@ +import 'package:apidash/templates/templates.dart'; +import 'package:apidash_core/apidash_core.dart'; + +class ResponseSemanticAnalyser extends AIAgent { + @override + String get agentName => 'RESP_SEMANTIC_ANALYSER'; + + @override + String getSystemPrompt() { + return kPromptSemanticAnalyser; + } + + @override + Future validator(String aiResponse) async { + //Add any specific validations here as needed + return true; + } + + @override + Future outputFormatter(String validatedResponse) async { + return { + 'SEMANTIC_ANALYSIS': validatedResponse, + }; + } +} diff --git a/lib/services/agentic_services/agents/stac_to_flutter.dart b/lib/services/agentic_services/agents/stac_to_flutter.dart new file mode 100644 index 00000000..af0b15c8 --- /dev/null +++ b/lib/services/agentic_services/agents/stac_to_flutter.dart @@ -0,0 +1,30 @@ +import 'package:apidash/templates/templates.dart'; +import 'package:apidash_core/apidash_core.dart'; + +class StacToFlutterBot extends AIAgent { + @override + String get agentName => 'STAC_TO_FLUTTER'; + + @override + String getSystemPrompt() { + return kPromptStacToFlutter; + } + + @override + Future validator(String aiResponse) async { + //Add any specific validations here as needed + return true; + } + + @override + Future outputFormatter(String validatedResponse) async { + validatedResponse = validatedResponse + .replaceAll('```dart', '') + .replaceAll('```dart\n', '') + .replaceAll('```', ''); + + return { + 'CODE': validatedResponse, + }; + } +} diff --git a/lib/services/agentic_services/agents/stacgen.dart b/lib/services/agentic_services/agents/stacgen.dart new file mode 100644 index 00000000..940611cb --- /dev/null +++ b/lib/services/agentic_services/agents/stacgen.dart @@ -0,0 +1,42 @@ +import 'dart:convert'; +import 'package:apidash/templates/templates.dart'; +import 'package:apidash_core/apidash_core.dart'; +import 'package:flutter/material.dart'; + +class StacGenBot extends AIAgent { + @override + String get agentName => 'STAC_GEN'; + + @override + String getSystemPrompt() { + return kPromptStacGen; + } + + @override + Future validator(String aiResponse) async { + aiResponse = aiResponse.replaceAll('```json', '').replaceAll('```', ''); + //JSON CHECK + try { + jsonDecode(aiResponse); + } catch (e) { + debugPrint("JSON PARSE ERROR: $e"); + return false; + } + return true; + } + + @override + Future outputFormatter(String validatedResponse) async { + validatedResponse = validatedResponse + .replaceAll('```json', '') + .replaceAll('```json\n', '') + .replaceAll('```', ''); + + //Stac Specific Changes + validatedResponse = validatedResponse.replaceAll('bold', 'w700'); + + return { + 'STAC': validatedResponse, + }; + } +} diff --git a/lib/services/agentic_services/agents/stacmodifier.dart b/lib/services/agentic_services/agents/stacmodifier.dart new file mode 100644 index 00000000..c13874ca --- /dev/null +++ b/lib/services/agentic_services/agents/stacmodifier.dart @@ -0,0 +1,42 @@ +import 'dart:convert'; +import 'package:apidash/templates/templates.dart'; +import 'package:apidash_core/apidash_core.dart'; +import 'package:flutter/material.dart'; + +class StacModifierBot extends AIAgent { + @override + String get agentName => 'STAC_MODIFIER'; + + @override + String getSystemPrompt() { + return kPromptStacModifier; + } + + @override + Future validator(String aiResponse) async { + aiResponse = aiResponse.replaceAll('```json', '').replaceAll('```', ''); + //JSON CHECK + try { + jsonDecode(aiResponse); + } catch (e) { + debugPrint("JSON PARSE ERROR: $e"); + return false; + } + return true; + } + + @override + Future outputFormatter(String validatedResponse) async { + validatedResponse = validatedResponse + .replaceAll('```json', '') + .replaceAll('```json\n', '') + .replaceAll('```', ''); + + //Stac Specific Changes + validatedResponse = validatedResponse.replaceAll('bold', 'w700'); + + return { + 'STAC': validatedResponse, + }; + } +} diff --git a/lib/services/agentic_services/apidash_agent_calls.dart b/lib/services/agentic_services/apidash_agent_calls.dart new file mode 100644 index 00000000..f9a24469 --- /dev/null +++ b/lib/services/agentic_services/apidash_agent_calls.dart @@ -0,0 +1,104 @@ +import 'package:apidash/services/agentic_services/agent_caller.dart'; +import 'package:apidash/services/agentic_services/agents/agents.dart'; +import 'package:apidash/templates/tool_templates.dart'; +import 'package:apidash_core/apidash_core.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +Future generateSDUICodeFromResponse({ + required WidgetRef ref, + required String apiResponse, +}) async { + final step1Res = await Future.wait([ + APIDashAgentCaller.instance.call( + ResponseSemanticAnalyser(), + ref: ref, + input: AgentInputs(query: apiResponse), + ), + APIDashAgentCaller.instance.call( + IntermediateRepresentationGen(), + ref: ref, + input: AgentInputs(variables: { + 'VAR_API_RESPONSE': apiResponse, + }), + ), + ]); + final sa = step1Res[0]?['SEMANTIC_ANALYSIS']; + final ir = step1Res[1]?['INTERMEDIATE_REPRESENTATION']; + + if (sa == null || ir == null) { + return null; + } + + debugPrint("Semantic Analysis: $sa"); + debugPrint("Intermediate Representation: $ir"); + + final sduiCode = await APIDashAgentCaller.instance.call( + StacGenBot(), + ref: ref, + input: AgentInputs(variables: { + 'VAR_RAW_API_RESPONSE': apiResponse, + 'VAR_INTERMEDIATE_REPR': ir, + 'VAR_SEMANTIC_ANALYSIS': sa, + }), + ); + final stacCode = sduiCode?['STAC']?.toString(); + if (stacCode == null) { + return null; + } + + return sduiCode['STAC'].toString(); +} + +Future modifySDUICodeUsingPrompt({ + required WidgetRef ref, + required String generatedSDUI, + required String modificationRequest, +}) async { + final res = await APIDashAgentCaller.instance.call( + StacModifierBot(), + ref: ref, + input: AgentInputs(variables: { + 'VAR_CODE': generatedSDUI, + 'VAR_CLIENT_REQUEST': modificationRequest, + }), + ); + final sdui = res?['STAC']; + return sdui; +} + +Future generateAPIToolUsingRequestData({ + required WidgetRef ref, + required String requestData, + required String targetLanguage, + required String selectedAgent, +}) async { + final toolfuncRes = await APIDashAgentCaller.instance.call( + APIToolFunctionGenerator(), + ref: ref, + input: AgentInputs(variables: { + 'REQDATA': requestData, + 'TARGET_LANGUAGE': targetLanguage, + }), + ); + if (toolfuncRes == null) { + return null; + } + + String toolCode = toolfuncRes!['FUNC']; + + final toolres = await APIDashAgentCaller.instance.call( + ApiToolBodyGen(), + ref: ref, + input: AgentInputs(variables: { + 'TEMPLATE': + APIToolGenTemplateSelector.getTemplate(targetLanguage, selectedAgent) + .substitutePromptVariable('FUNC', toolCode), + }), + ); + if (toolres == null) { + return null; + } + String toolDefinition = toolres!['TOOL']; + return toolDefinition; +} diff --git a/lib/templates/rulesets/stac_ruleset.dart b/lib/templates/rulesets/stac_ruleset.dart new file mode 100644 index 00000000..d32df1a0 --- /dev/null +++ b/lib/templates/rulesets/stac_ruleset.dart @@ -0,0 +1,329 @@ +const kRulesetStac = """ +### Scaffold +``` +{ + "type": "scaffold", + "appBar": { + "type": "appBar", + "title": { + "type": "text", + "data": "App Bar Title" + } + }, + "body": {}, + "backgroundColor": "#FFFFFF" +} +``` +--- +### Align +``` +{ + "type": "align", + "alignment": "topEnd", + "child": {...} +} +``` +--- +### Card +``` +{ + "type": "card", + "color": "#FFFFFF", + "shadowColor": "#000000", + "surfaceTintColor": "#FF0000", + "elevation": 5.0, + "shape": { + "type": "roundedRectangle", + "borderRadius": 10.0 + }, + "borderOnForeground": true, + "margin": { + "left": 10, + "top": 20, + "right": 10, + "bottom": 20 + }, + "clipBehavior": "antiAlias", + "child": {}, + "semanticContainer": true +} +``` +--- +### Center +``` +{ + "type": "center", + "child": { + "type": "text", + "data": "Hello, World!" + } +} +``` +--- +### Circle Avatar +``` +{ + "type": "circleAvatar", + "backgroundColor": "#FF0000", + "foregroundColor": "#FFFFFF", + "backgroundImage": "https://raw.githubusercontent.com/StacDev/stac/refs/heads/dev/assets/companies/bettrdo.jpg", + "radius": 50, + "child": { + "type": "text", + "data": "A" + } +} +``` +--- +### Column +``` +{ + "type": "column", + "mainAxisAlignment": "center", + "crossAxisAlignment": "start", + "mainAxisSize": "min", + "verticalDirection": "up", + "spacing": 10, + "children": [ + { + "type": "text", + "data": "Hello, World!" + }, + { + "type": "container", + "width": 100, + "height": 100, + "color": "#FF0000" + } + ] +} +``` +--- +### Container +``` +{ + "type": "container", + "alignment": "center", + "padding": { + "top": 16.0, + "bottom": 16.0, + "left": 16.0, + "right": 16.0 + }, + "decoration": { + "color": "#FF5733", + "borderRadius": { + "topLeft": 16.0, + "topRight": 16.0, + "bottomLeft": 16.0, + "bottomRight": 16.0 + } + }, + "width": 200.0, + "height": 200.0, + "child": { + "type": "text", + "data": "Hello, World!", + "style": { + "color": "#FFFFFF", + "fontSize": 24.0 + } + } +} +``` +--- +### GridView +``` +{ + "type": "gridView", + "physics": "never", + "shrinkWrap": true, + "padding": { + "left": 10, + "top": 10, + "right": 10, + "bottom": 10 + }, + "crossAxisCount": 2, + "mainAxisSpacing": 10.0, + "crossAxisSpacing": 10.0, + "children": [ + { + "type": "text", + "data": "Item 1" + }, + { + "type": "text", + "data": "Item 2" + } + ], +} +``` +--- +### Icon +``` +{ + "type": "icon", + "icon": "home", + "size": 24.0, + "color": "#000000", + "semanticLabel": "Home Icon", + "textDirection": "ltr" +} +``` +--- +### Image +``` +{ + "type": "image", + "src": "https://example.com/image.png", + "alignment": "center", + "imageType": "network", + "color": "#FFFFFF", + "width": 200.0, + "height": 100.0, + "fit": "contain" +} +``` +--- +### ListTile +``` +{ + "type": "listTile", + "leading": { + "type": "image", + "src": "https://cdn-icons-png.flaticon.com/512/3135/3135715.png" + }, + "title": {}, + "subtitle": {}, + "trailing": {} +} +``` +--- +### Padding +``` +{ + "type": "padding", + "padding": { + "top": 80, + "left": 24, + "right": 24, + "bottom": 24 + }, + "child": {...} +} +``` +--- +### Row +``` +{ + "type": "row", + "mainAxisAlignment": "center", + "crossAxisAlignment": "center", + "spacing": 12, + "children": [] +} +``` +--- +### SingleChildScrollView +``` +{ + "type": "singleChildScrollView", + "child": { + "type": "column", + "children": [ + + ] + } +} +``` +--- +### SizedBox +``` +{ + "type": "sizedBox", + "height": 25 +} +{ + "type": "sizedBox", + "width": 25 +} +``` +--- +### Table +``` +{ + "type": "table", + "columnWidths": { + "1": { "type": "fixedColumnWidth", "value": 200 } + }, + "defaultColumnWidth": { "type": "flexColumnWidth", "value": 1 }, + "textDirection": "ltr", + "defaultVerticalAlignment": "bottom", + "border": { + "color": "#428AF5", + "width": 1.0, + "borderRadius": 16 + }, + "children": [ + { + "type": "tableRow", + "children": [ + { "type": "tableCell", "child": { "type": "text", "data": "Header 1" } }, + ] + }, + ] +} +``` +--- +### TableCell +``` +{ + "type": "tableCell", + "verticalAlignment": "top", + "child": { + "type": "container", + "color": "#40000000", + "height": 50.0, + "child": { + "type": "center", + "child": { + "type": "text", + "data": "Header 1" + } + } + } +} +``` + +## Stac Styles (Analogous to Flutter Styles) + +### Border Radius +``` +//implicit +{ + "borderRadius": 16.0 +} +//explicit +{ + "borderRadius": { + "topLeft": 16.0, + "topRight": 16.0, + "bottomLeft": 16.0, + "bottomRight": 16.0 + } +} +``` +--- +### Border +``` +{ + "border": { + "color": "#FF0000", + "width": 2.0, + "borderStyle": "solid", + "strokeAlign": 0.0 + } +} +``` +"""; diff --git a/lib/templates/system_prompt_templates/apitool_bodygen_prompt.dart b/lib/templates/system_prompt_templates/apitool_bodygen_prompt.dart new file mode 100644 index 00000000..bc4f6640 --- /dev/null +++ b/lib/templates/system_prompt_templates/apitool_bodygen_prompt.dart @@ -0,0 +1,29 @@ +const String kPromptAPIToolBodyGen = """ +You are an expert API Tool Format Corrector Agent + +An API tool is a predefined or dynamically generated interface that the AI can call to perform specific external actions—such as fetching data, executing computations, or triggering real-world services—through an Application Programming Interface (API). + +You will be provided with a partially complete API tool template that will contain the api calling function named func and the tool definition +Your job is to correct any mistakes and provide the correct output. + +The template will contain the following variables (A Variable is marked by :: +Wherever you find this pattern replace it with the appropriate values) +`TOOL_NAME`: The name of the API Tool, infer it from the function code +`TOOL_DESCRIPTION`: The Description of the Tool, generate it based on the tool name +`TOOL_PARAMS`: The example of parameters have been provided below, infer the parameters needed from the func body, it must be a dictionary +`REQUIRED_PARAM_NAMES`: infer what parameters are required and add thier names in a list +`INPUT_SCHEMA`: if this variable exists, then create a StructuredTool or DynamicStructuredTool schema of the input according to the language of the tool itself. + +this is the general format of parameters: +"ARG_NAME": { + "type": "ARG_TYPE", + "description: "ARG_DESC" +} + +ALWAYS return the output as code only and do not start or begin with any introduction or conclusion. ONLY THE CODE. + +Here's the Template: +``` +:TEMPLATE: +``` +"""; diff --git a/lib/templates/system_prompt_templates/apitool_funcgen_prompt.dart b/lib/templates/system_prompt_templates/apitool_funcgen_prompt.dart new file mode 100644 index 00000000..e5dc2a2e --- /dev/null +++ b/lib/templates/system_prompt_templates/apitool_funcgen_prompt.dart @@ -0,0 +1,38 @@ +const String kPromptAPIToolFuncGen = """ +You are an expert LANGUAGE-SPECIFIC API CALL METHOD generator. + +You will always be provided with: +1. (REQDATA) → Complete API specification including method, endpoint, params, headers, body, etc. +2. (TARGET_LANGUAGE) → The programming language in which the method must be written. + +Your task: +- Generate a single method **explicitly named `func`** in the target language. +- The method must accept all dynamic variables (from query params, path params, request body fields, etc.) as function arguments. +- Embed all fixed/static values from REQDATA (e.g., Authorization tokens, fixed headers, constant body fields) directly inside the method. Do **not** expect them to be passed as arguments. + +Strict rules: +1. **No extra output** — only return the code for the function `func`, nothing else. +2. **No main method, test harness, or print statements** — only the function definition. +3. **Headers & Authorization**: + - If REQDATA specifies headers (including `Authorization`), hardcode them inside the method. + - Never expose these as parameters unless explicitly marked as variable in REQDATA. +4. **Request Body Handling**: + - If `REQDATA.BODY_TYPE == TEXT`: send the raw text as-is. + - If `REQDATA.BODY_TYPE == JSON` or `FORM-DATA`: create function arguments for the variable fields and serialize them according to best practices in the target language. +5. **Parameters**: + - Query params and path params must be represented as function arguments. + - Ensure correct encoding/escaping as per target language conventions. +6. **Error Handling**: + - Implement minimal, idiomatic error handling for the target language (e.g., try/except, promise rejection handling). +7. **Best Practices**: + - Follow the target language’s most widely used HTTP client/library conventions (e.g., `requests` in Python, `fetch`/`axios` in JavaScript, `http.Client` in Go). + - Keep the function minimal, clean, and production-ready. + +Inputs: +REQDATA: :REQDATA: +TARGET_LANGUAGE: :TARGET_LANGUAGE: + +Output: +- ONLY the function definition named `func` in the target language. +- Do not add explanations, comments, or surrounding text. Code only. +"""; diff --git a/lib/templates/system_prompt_templates/intermediate_rep_gen_prompt.dart b/lib/templates/system_prompt_templates/intermediate_rep_gen_prompt.dart new file mode 100644 index 00000000..9d61f345 --- /dev/null +++ b/lib/templates/system_prompt_templates/intermediate_rep_gen_prompt.dart @@ -0,0 +1,55 @@ +const String kPromptIntermediateRepGen = """ +You are an expert UI architect specializing in converting structured API responses into high-quality user interface designs. + +Your task is to analyze the given API response (`API_RESPONSE`) and return a **UI schema** in a clean, human-readable **Markdown format**. This schema will later be used by another system to generate the actual UI. + +### Your Output Must: +- Be in structured Markdown format (no Flutter code or JSON) +- Represent a layout hierarchy using indentation +- Only use the following allowed UI elements (Flutter-based): + - Text + - Row, Column + - GridView, SingleChildScrollView, Expanded + - Image + - ElevatedButton + - Icon + - Padding, SizedBox, Card, Container, Spacer, ListTile + - Table + +### Guidelines: +- Pick the best layout based on the structure and type of data +- Use rows/columns/tables where appropriate +- Use Cards to group related info +- Add short labels to explain each component's purpose +- Only use allowed elements — no custom widgets or other components +- if there are actual image links in the incoming data, please use them + +You must **include alignment information** where relevant, using the following format: +[ElementType] Label (alignment: ..., mainAxis: ..., crossAxis: ...) + +### Example Markdown Schema: +``` +- **[Column] Root layout** *(mainAxis: start, crossAxis: stretch)* + - **[Card] Match Info** + - **[Text]** "India vs Australia" *(alignment: centerLeft)* + - **[Text]** "Date: Aug 15, 2025" *(alignment: centerLeft)* + - **[Row] Pagination Info** *(mainAxis: spaceBetween, crossAxis: center)* + - **[Text]** "Page: 1" + - **[Text]** "Total: 12" + - **[ListView] User Cards** *(scrollDirection: vertical)* + - **[Card] User Item (George)** + - **[Row] Avatar and Info** *(mainAxis: start, crossAxis: center)* + - **[Image]** Avatar *(alignment: center, fit: cover)* + - **[Column] User Info** *(mainAxis: start, crossAxis: start)* + - **[Text]** Name: George Bluth + - **[Text]** Email: george@example.com +``` + +# Inputs +API_RESPONSE: ```json +:VAR_API_RESPONSE: +``` + +Return only the Schema and nothing else and MAKE SURE TO USE the Actual VALUES instead of text placeholders. this is very important +If you notice the content is too long then please include a Single Child Scroll Viewbut make sure you are handing cases wherein multiple scroll views are used and stuff + """; diff --git a/lib/templates/system_prompt_templates/semantic_analyser_prompt.dart b/lib/templates/system_prompt_templates/semantic_analyser_prompt.dart new file mode 100644 index 00000000..2bc10a21 --- /dev/null +++ b/lib/templates/system_prompt_templates/semantic_analyser_prompt.dart @@ -0,0 +1,15 @@ +const String kPromptSemanticAnalyser = """ +You are an expert at understanding and semantically interpreting JSON API responses. When provided with a sample API response in JSON format, your task is to produce a clear and concise semantic analysis that identifies the core data structures, their meaning, and what parts are relevant for a user interface. + +Your output must be in **plain text only** — no markdown, no formatting, no lists — just a single well-structured paragraph. This paragraph will be fed into a separate UI generation system, so it must be tailored accordingly. + +Focus only on the fields and data structures that are useful for generating a UI. Omit or instruct to ignore fields that are irrelevant for display purposes (e.g., metadata, internal identifiers, pagination if not needed visually, etc.). + +Describe: +- What the data represents (e.g., a list of users, product details, etc.) +- What UI elements or components would be ideal to display this data (e.g., cards, tables, images, lists) +- Which fields should be highlighted or emphasized +- Any structural or layout suggestions that would help a UI builder understand how to present the information + +Do **not** use formatting of any kind. Do **not** start or end the response with any extra commentary or boilerplate. Just return the pure semantic explanation of the data in a clean paragraph, ready for use by another LLM. + """; diff --git a/lib/templates/system_prompt_templates/stac_gen_prompt.dart b/lib/templates/system_prompt_templates/stac_gen_prompt.dart new file mode 100644 index 00000000..7d5c0a6c --- /dev/null +++ b/lib/templates/system_prompt_templates/stac_gen_prompt.dart @@ -0,0 +1,59 @@ +import '../rulesets/stac_ruleset.dart'; + +const String kPromptStacGen = """ +You are an expert agent whose one and only task is to generate Server Driven UI Code (json-like) representation from the given inputs. + +You will be provided with the Rules of the SDUI language, schema, text description as follows: + +SDUI CODE RULES: +( +$kRulesetStac +) + +DO NOT CREATE YOUR OWN SYNTAX. ONLY USE WHAT IS PROVIDED BY THE ABOVE RULES + +# Style/Formatting Rules +- No trailing commas. No comments. No undefined props. +- Strings for enums like mainAxisAlignment: "center". +- padding/margin objects may include any of: left,right,top,bottom,all,horizontal,vertical. +- style objects are opaque key-value maps (e.g., in text.style, elevatedButton.style); include only needed keys. + +#Behavior Conventions +- Use sizedBox for minor spacing; spacer/expanded for flexible space. +- Use listView for long, homogeneous lists; column for short static stacks. +- Always ensure images have at least src; add fit if necessary (e.g., "cover"). +- Prefer card for grouped content with elevation. +- Use gridView only if there are 2+ columns of similar items. + +# Validation Checklist (apply before emitting) +- Widgets/props only from catalog. +- All required props present (type, leaf essentials like text.data, image.src). +- Property types correct; no nulls/empties. +- Keys ordered deterministically. + +# Inputs +SCHEMA: ```:VAR_INTERMEDIATE_REPR:``` +DESCRIPTION: ```:VAR_SEMANTIC_ANALYSIS:``` + +# Generation Steps (follow silently) +- Read SCHEMA to identify concrete entities/IDs; read DESCRIPTION for layout intent. +- Pick widgets from the catalog that best express the layout. +- Compose from coarse to fine: page → sections → rows/columns → leaf widgets. +- Apply sensible defaults (alignment, spacing) only when needed. +- Validate: catalog-only widgets/props, property types, no unused fields, deterministic ordering. + +# Hard Output Contract +- Output MUST be ONLY the SDUI JSON. No prose, no code fences, no comments. Must start with { and end with }. +- Use only widgets and properties from the Widget Catalog below. +- Prefer minimal, valid trees. Omit null/empty props. +- Numeric where numeric, booleans where booleans, strings for enums/keys. +- Color strings allowed (e.g., "#RRGGBB"). +- Keep key order consistent: type, then layout/meta props, then child/children. + +# Final Instruction +Using SCHEMA and DESCRIPTION, output only the SDUI JSON that satisfies the rules above. DO NOT START OR END THE RESPONSE WITH ANYTHING ELSE. + +if there are no scrollable elements then wrap the whole content with a single child scroll view, if there are scrollable contents inside, then apply shrinkWrap and handle accordingly like +you would do in Flutter but in this Stac Representation + +"""; diff --git a/lib/templates/system_prompt_templates/stac_modifier_prompt.dart b/lib/templates/system_prompt_templates/stac_modifier_prompt.dart new file mode 100644 index 00000000..33c60ccc --- /dev/null +++ b/lib/templates/system_prompt_templates/stac_modifier_prompt.dart @@ -0,0 +1,28 @@ +import '../rulesets/stac_ruleset.dart'; + +const String kPromptStacModifier = """ +You are an expert agent whose sole JOB is to accept FLutter-SDUI (json-like) representation +and modify it to match the requests of the client. + +SDUI CODE RULES: +$kRulesetStac + +# Inputs +PREVIOUS_CODE: ```:VAR_CODE:``` +CLIENT_REQUEST: ```:VAR_CLIENT_REQUEST:``` + + +# Hard Output Contract +- Output MUST be ONLY the SDUI JSON. No prose, no code fences, no comments. Must start with { and end with }. +- Use only widgets and properties from the Widget Catalog below. +- Prefer minimal, valid trees. Omit null/empty props. +- Numeric where numeric, booleans where booleans, strings for enums/keys. +- Color strings allowed (e.g., "#RRGGBB"). +- Keep key order consistent: type, then layout/meta props, then child/children. + + +# Final Instruction +DO NOT CHANGE ANYTHING UNLESS SPECIFICALLY ASKED TO +use the CLIENT_REQUEST to modify the PREVIOUS_CODE while following the existing FLutter-SDUI (json-like) representation +ONLY FLutter-SDUI Representation NOTHING ELSE. DO NOT START OR END WITH TEXT, ONLY FLutter-SDUI Representatiin. +"""; diff --git a/lib/templates/system_prompt_templates/stac_to_flutter_prompt.dart b/lib/templates/system_prompt_templates/stac_to_flutter_prompt.dart new file mode 100644 index 00000000..d2d7b30c --- /dev/null +++ b/lib/templates/system_prompt_templates/stac_to_flutter_prompt.dart @@ -0,0 +1,15 @@ +const String kPromptStacToFlutter = """ +You are an expert agent whose sole JOB is to accept FLutter-SDUI (json-like) representation +and convert it into actual working FLutter component. + +This is fairly easy to do as FLutter-SDUI is literally a one-one representation of Flutter Code + +SDUI_CODE: ```:VAR_CODE:``` + +use the Above SDUI_CODE and convert it into Flutter Code that is effectively same as what the SDUI_CODE represents + +DO NOT CHANGE CONTENT, just convert everything one-by-one +Output ONLY Code Representation NOTHING ELSE. DO NOT START OR END WITH TEXT, ONLY Code + +DO NOT WRITE CODE TO PARSE SDUI, ACTUALLY CONVERT IT TO REAL DART CODE +"""; diff --git a/lib/templates/templates.dart b/lib/templates/templates.dart new file mode 100644 index 00000000..9df768d8 --- /dev/null +++ b/lib/templates/templates.dart @@ -0,0 +1,8 @@ +export 'system_prompt_templates/apitool_bodygen_prompt.dart'; +export 'system_prompt_templates/apitool_funcgen_prompt.dart'; +export 'system_prompt_templates/intermediate_rep_gen_prompt.dart'; +export 'system_prompt_templates/semantic_analyser_prompt.dart'; +export 'system_prompt_templates/stac_to_flutter_prompt.dart'; +export 'system_prompt_templates/stac_gen_prompt.dart'; +export 'system_prompt_templates/stac_modifier_prompt.dart'; +export 'tool_templates.dart'; diff --git a/lib/templates/tool_templates.dart b/lib/templates/tool_templates.dart new file mode 100644 index 00000000..3ecf0a3e --- /dev/null +++ b/lib/templates/tool_templates.dart @@ -0,0 +1,112 @@ +const GENERAL_ARG_PROPERTY_FORMAT_PY = """:ARG_NAME: { + "type": ":ARG_TYPE:", + "description: ":ARG_DESC:" +}"""; + +const GENERAL_PYTHON_TOOL_FORMAT = """ +:FUNC: + +api_tool = { + "function": func, + "definition": { + "name": ":TOOL_NAME:", + "description": ":TOOL_DESCRIPTION:", + "parameters": { + "type": "object", + "properties": :TOOL_PARAMS:, + "required": [:REQUIRED_PARAM_NAMES:], + "additionalProperties": False + } + } +} + +__all__ = ["api_tool"] +"""; + +const GENERAL_JAVASCRIPT_TOOL_FORMAT = """ +:FUNC: + +const apiTool = { + function: func, + definition: { + type: 'function', + function: { + name: ':TOOL_NAME:', + description: ':TOOL_DESCRIPTION:', + parameters: { + type: 'object', + properties: :TOOL_PARAMS:, + required: [:REQUIRED_PARAM_NAMES:] + additionalProperties: false + } + } + } +}; + +export { apiTool }; +"""; + +const LANGCHAIN_PYTHON_TOOL_FORMAT = """ +from langchain.tools import StructuredTool + +:INPUT_SCHEMA: + +:FUNC: + +api_tool = StructuredTool.from_function( + func=func, + name=":TOOL_NAME:", + description=":TOOL_DESCRIPTION:", + args_schema=INPUT_SCHEMA, +) +__all__ = ["api_tool"] +"""; + +const LANGCHAIN_JAVASCRIPT_TOOL_FORMAT = """ +import { DynamicStructuredTool } from 'langchain/tools'; +import { z } from 'zod'; + +:INPUT_SCHEMA: + +:FUNC: + +const apiTool = new DynamicStructuredTool({ + func: func, + name: ':TOOL_NAME:', + description: ':TOOL_DESCRIPTION:', + schema: INPUT_SCHEMA +}); + +export { apiTool }; +"""; + +const MICROSOFT_AUTOGEN_TOOL_FORMAT = """ +:FUNC: + +api_tool = { + "function": func, + "name": ":TOOL_NAME:", + "description": ":TOOL_DESCRIPTION:" +} + +__all__ = ["api_tool"] +"""; + +class APIToolGenTemplateSelector { + static String getTemplate(String language, String agent) { + if (language == 'PYTHON') { + if (agent == 'MICROSOFT_AUTOGEN') { + return MICROSOFT_AUTOGEN_TOOL_FORMAT; + } else if (agent == 'LANGCHAIN') { + return LANGCHAIN_PYTHON_TOOL_FORMAT; + } + return GENERAL_PYTHON_TOOL_FORMAT; + } else if (language == 'JAVASCRIPT') { + if (agent == 'LANGCHAIN') { + return LANGCHAIN_JAVASCRIPT_TOOL_FORMAT; + } + return GENERAL_JAVASCRIPT_TOOL_FORMAT; + } + return 'NO_TEMPLATE'; + } +} diff --git a/lib/utils/history_utils.dart b/lib/utils/history_utils.dart index d1c3dbbc..60ea7ae3 100644 --- a/lib/utils/history_utils.dart +++ b/lib/utils/history_utils.dart @@ -13,6 +13,7 @@ RequestModel getRequestModelFromHistoryModel(HistoryRequestModel model) { name: model.metaData.name, responseStatus: model.httpResponseModel.statusCode, message: kResponseCodeReasons[model.httpResponseModel.statusCode], + aiRequestModel: model.aiRequestModel, httpRequestModel: model.httpRequestModel, httpResponseModel: model.httpResponseModel, ); diff --git a/lib/utils/ui_utils.dart b/lib/utils/ui_utils.dart index 192d816b..52b5fcc9 100644 --- a/lib/utils/ui_utils.dart +++ b/lib/utils/ui_utils.dart @@ -36,6 +36,7 @@ Color getAPIColor( method, ), APIType.graphql => kColorGQL, + APIType.ai => Colors.amber, }; if (brightness == Brightness.dark) { col = col.toDark; diff --git a/lib/widgets/card_sidebar_request.dart b/lib/widgets/card_sidebar_request.dart index 87dab9e4..372d88b5 100644 --- a/lib/widgets/card_sidebar_request.dart +++ b/lib/widgets/card_sidebar_request.dart @@ -11,7 +11,7 @@ class SidebarRequestCard extends StatelessWidget { super.key, required this.id, required this.apiType, - required this.method, + this.method, this.name, this.url, this.selectedId, @@ -30,7 +30,7 @@ class SidebarRequestCard extends StatelessWidget { final APIType apiType; final String? name; final String? url; - final HTTPVerb method; + final HTTPVerb? method; final String? selectedId; final String? editRequestId; final void Function()? onTap; diff --git a/lib/widgets/field_text_bounded.dart b/lib/widgets/field_text_bounded.dart new file mode 100644 index 00000000..18adeadf --- /dev/null +++ b/lib/widgets/field_text_bounded.dart @@ -0,0 +1,61 @@ +import 'package:apidash_design_system/apidash_design_system.dart'; +import 'package:flutter/material.dart'; + +class BoundedTextField extends StatefulWidget { + const BoundedTextField({ + super.key, + required this.value, + required this.onChanged, + }); + + final String value; + final void Function(String value) onChanged; + + @override + State createState() => _BoundedTextFieldState(); +} + +class _BoundedTextFieldState extends State { + TextEditingController controller = TextEditingController(); + @override + void initState() { + controller.text = widget.value; + super.initState(); + } + + @override + void didUpdateWidget(covariant BoundedTextField oldWidget) { + //Assisting in Resetting on Change + if (widget.value == '') { + controller.text = widget.value; + } + super.didUpdateWidget(oldWidget); + } + + @override + Widget build(BuildContext context) { + // final double width = context.isCompactWindow ? 150 : 220; + return Container( + height: 40, + decoration: BoxDecoration( + border: Border.all( + color: Theme.of(context).colorScheme.surfaceContainerHighest, + ), + borderRadius: kBorderRadius8, + ), + width: double.infinity, + child: Container( + transform: Matrix4.translationValues(0, -5, 0), + child: TextField( + controller: controller, + // obscureText: true, + decoration: InputDecoration( + border: InputBorder.none, + contentPadding: EdgeInsets.only(left: 10), + ), + onChanged: widget.onChanged, + ), + ), + ); + } +} diff --git a/lib/widgets/response_body.dart b/lib/widgets/response_body.dart index d0af2baa..090fdcbb 100644 --- a/lib/widgets/response_body.dart +++ b/lib/widgets/response_body.dart @@ -10,9 +10,11 @@ class ResponseBody extends StatelessWidget { const ResponseBody({ super.key, this.selectedRequestModel, + this.isPartOfHistory = false, }); final RequestModel? selectedRequestModel; + final bool isPartOfHistory; @override Widget build(BuildContext context) { @@ -22,9 +24,8 @@ class ResponseBody extends StatelessWidget { message: '$kNullResponseModelError $kUnexpectedRaiseIssue'); } - final isSSE = responseModel.sseOutput?.isNotEmpty ?? false; var body = responseModel.body; - var formattedBody = responseModel.formattedBody; + if (body == null) { return const ErrorMessage( message: '$kMsgNullBody $kUnexpectedRaiseIssue'); @@ -36,9 +37,6 @@ class ResponseBody extends StatelessWidget { showIssueButton: false, ); } - if (isSSE) { - body = responseModel.sseOutput!.join(); - } final mediaType = responseModel.mediaType ?? MediaType(kTypeText, kSubTypePlain); @@ -49,10 +47,17 @@ class ResponseBody extends StatelessWidget { // '$kMsgUnknowContentType - ${responseModel.contentType}. $kUnexpectedRaiseIssue'); // } - var responseBodyView = getResponseBodyViewOptions(mediaType); + var responseBodyView = selectedRequestModel?.apiType == APIType.ai + ? (kAnswerRawBodyViewOptions, kSubTypePlain) + : getResponseBodyViewOptions(mediaType); var options = responseBodyView.$1; var highlightLanguage = responseBodyView.$2; + final isSSE = responseModel.sseOutput?.isNotEmpty ?? false; + var formattedBody = isSSE + ? responseModel.sseOutput!.join('\n') + : responseModel.formattedBody; + if (formattedBody == null) { options = [...options]; options.remove(ResponseBodyView.code); @@ -65,8 +70,11 @@ class ResponseBody extends StatelessWidget { bytes: responseModel.bodyBytes!, body: body, formattedBody: formattedBody, - sseOutput: responseModel.sseOutput, highlightLanguage: highlightLanguage, + sseOutput: responseModel.sseOutput, + isAIResponse: selectedRequestModel?.apiType == APIType.ai, + aiRequestModel: selectedRequestModel?.aiRequestModel, + isPartOfHistory: isPartOfHistory, ); } } diff --git a/lib/widgets/response_body_success.dart b/lib/widgets/response_body_success.dart index 9b22a4b9..a120f1e4 100644 --- a/lib/widgets/response_body_success.dart +++ b/lib/widgets/response_body_success.dart @@ -1,3 +1,5 @@ +import 'package:apidash/screens/common_widgets/agentic_ui_features/ai_ui_designer/generate_ui_dialog.dart'; +import 'package:apidash/screens/common_widgets/agentic_ui_features/tool_generation/generate_tool_dialog.dart'; import 'package:apidash_core/apidash_core.dart'; import 'package:apidash_design_system/apidash_design_system.dart'; import 'package:flutter/foundation.dart'; @@ -8,15 +10,19 @@ import 'package:apidash/consts.dart'; import 'button_share.dart'; class ResponseBodySuccess extends StatefulWidget { - const ResponseBodySuccess( - {super.key, - required this.mediaType, - required this.body, - required this.options, - required this.bytes, - this.formattedBody, - this.sseOutput, - this.highlightLanguage}); + const ResponseBodySuccess({ + super.key, + required this.mediaType, + required this.body, + required this.options, + required this.bytes, + this.formattedBody, + this.highlightLanguage, + this.sseOutput, + this.isAIResponse = false, + this.aiRequestModel, + this.isPartOfHistory = false, + }); final MediaType mediaType; final List options; final String body; @@ -24,6 +30,9 @@ class ResponseBodySuccess extends StatefulWidget { final String? formattedBody; final List? sseOutput; final String? highlightLanguage; + final bool isAIResponse; + final AIRequestModel? aiRequestModel; + final bool isPartOfHistory; @override State createState() => _ResponseBodySuccessState(); @@ -56,6 +65,16 @@ class _ResponseBodySuccessState extends State { padding: kP10, child: Column( children: [ + if (!widget.isPartOfHistory) + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Expanded(child: GenerateToolButton()), + SizedBox(width: 10), + Expanded(child: AIGenerateUIButton()), + ], + ), + kVSpacer10, Row( children: [ (widget.options == kRawBodyViewOptions) @@ -134,7 +153,7 @@ class _ResponseBodySuccessState extends State { ), ), ), - ResponseBodyView.raw => Expanded( + ResponseBodyView.answer => Expanded( child: Container( width: double.maxFinite, padding: kP8, @@ -147,6 +166,21 @@ class _ResponseBodySuccessState extends State { ), ), ), + ResponseBodyView.raw => Expanded( + child: Container( + width: double.maxFinite, + padding: kP8, + decoration: textContainerdecoration, + child: SingleChildScrollView( + child: SelectableText( + widget.isAIResponse + ? widget.body + : (widget.formattedBody ?? widget.body), + style: kCodeStyle, + ), + ), + ), + ), ResponseBodyView.sse => Expanded( child: Container( width: double.maxFinite, @@ -154,6 +188,7 @@ class _ResponseBodySuccessState extends State { decoration: textContainerdecoration, child: SSEDisplay( sseOutput: widget.sseOutput, + aiRequestModel: widget.aiRequestModel, ), ), ), diff --git a/lib/widgets/sse_display.dart b/lib/widgets/sse_display.dart index efa65f43..1fc681ca 100644 --- a/lib/widgets/sse_display.dart +++ b/lib/widgets/sse_display.dart @@ -1,12 +1,15 @@ import 'dart:convert'; +import 'package:apidash_core/apidash_core.dart'; import 'package:apidash_design_system/apidash_design_system.dart'; import 'package:flutter/material.dart'; class SSEDisplay extends StatelessWidget { + final AIRequestModel? aiRequestModel; final List? sseOutput; const SSEDisplay({ super.key, this.sseOutput, + this.aiRequestModel, }); @override @@ -24,9 +27,38 @@ class SSEDisplay extends StatelessWidget { ); } + if (aiRequestModel != null) { + // For RAW Text output (only AI Requests) + String out = ""; + for (String x in sseOutput!) { + x = x.trim(); + if (x.isEmpty || x.contains('[DONE]')) { + continue; + } + + // Start with JSON + final pos = x.indexOf('{'); + if (pos == -1) continue; + x = x.substring(pos); + + Map? dec; + try { + dec = jsonDecode(x); + final z = aiRequestModel?.getFormattedStreamOutput(dec!); + out += z ?? ''; + } catch (e) { + debugPrint("SSEDisplay -> Error in JSONDEC $e"); + } + } + return SingleChildScrollView( + child: Text(out), + ); + } + return ListView( padding: kP1, - children: sseOutput!.reversed.where((e) => e != '').map((chunk) { + children: + sseOutput!.reversed.where((e) => e.trim() != '').map((chunk) { Map? parsedJson; try { parsedJson = jsonDecode(chunk); diff --git a/lib/widgets/texts.dart b/lib/widgets/texts.dart index c64a71b2..9d2e1a84 100644 --- a/lib/widgets/texts.dart +++ b/lib/widgets/texts.dart @@ -7,10 +7,10 @@ class SidebarRequestCardTextBox extends StatelessWidget { const SidebarRequestCardTextBox({ super.key, required this.apiType, - required this.method, + this.method, }); final APIType apiType; - final HTTPVerb method; + final HTTPVerb? method; @override Widget build(BuildContext context) { @@ -18,8 +18,9 @@ class SidebarRequestCardTextBox extends StatelessWidget { width: 24, child: Text( switch (apiType) { - APIType.rest => method.abbr, + APIType.rest => method!.abbr, APIType.graphql => apiType.abbr, + APIType.ai => apiType.abbr, }, textAlign: TextAlign.center, style: TextStyle( diff --git a/lib/widgets/widget_sending.dart b/lib/widgets/widget_sending.dart index 8c949061..d50b7318 100644 --- a/lib/widgets/widget_sending.dart +++ b/lib/widgets/widget_sending.dart @@ -7,9 +7,11 @@ import 'package:apidash/consts.dart'; class SendingWidget extends StatefulWidget { final DateTime? startSendingTime; + final bool showTimeElapsed; const SendingWidget({ super.key, required this.startSendingTime, + this.showTimeElapsed = true, }); @override @@ -51,33 +53,34 @@ class _SendingWidgetState extends State { Center( child: Lottie.asset(kAssetSendingLottie), ), - Padding( - padding: kPh20t40, - child: Visibility( - visible: _millisecondsElapsed >= 0, - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon( - Icons.alarm, - color: Theme.of(context).colorScheme.onSurfaceVariant, - ), - const SizedBox( - width: 10, - ), - Text( - 'Time elapsed: ${humanizeDuration(Duration(milliseconds: _millisecondsElapsed))}', - textAlign: TextAlign.center, - overflow: TextOverflow.fade, - softWrap: false, - style: kTextStyleButton.copyWith( + if (widget.showTimeElapsed) + Padding( + padding: kPh20t40, + child: Visibility( + visible: _millisecondsElapsed >= 0, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.alarm, color: Theme.of(context).colorScheme.onSurfaceVariant, ), - ), - ], + const SizedBox( + width: 10, + ), + Text( + 'Time elapsed: ${humanizeDuration(Duration(milliseconds: _millisecondsElapsed))}', + textAlign: TextAlign.center, + overflow: TextOverflow.fade, + softWrap: false, + style: kTextStyleButton.copyWith( + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ), + ], + ), ), ), - ), ], ); } 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..d9910e0c 100644 --- a/packages/apidash_core/lib/apidash_core.dart +++ b/packages/apidash_core/lib/apidash_core.dart @@ -8,4 +8,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..c340f8af 100644 --- a/packages/apidash_core/pubspec.yaml +++ b/packages/apidash_core/pubspec.yaml @@ -11,11 +11,11 @@ environment: dependencies: flutter: sdk: flutter - better_networking: - path: ../better_networking curl_parser: path: ../curl_parser freezed_annotation: ^2.4.1 + genai: + path: ../genai har: path: ../har insomnia_collection: diff --git a/packages/apidash_core/pubspec_overrides.yaml b/packages/apidash_core/pubspec_overrides.yaml index 87d1d244..1df19ae7 100644 --- a/packages/apidash_core/pubspec_overrides.yaml +++ b/packages/apidash_core/pubspec_overrides.yaml @@ -1,9 +1,11 @@ -# melos_managed_dependency_overrides: better_networking,curl_parser,har,insomnia_collection,postman,seed +# melos_managed_dependency_overrides: better_networking,curl_parser,har,insomnia_collection,postman,seed,genai dependency_overrides: better_networking: path: ../better_networking curl_parser: path: ../curl_parser + genai: + path: ../genai har: path: ../har insomnia_collection: diff --git a/packages/apidash_design_system/lib/tokens/measurements.dart b/packages/apidash_design_system/lib/tokens/measurements.dart index 876c69fb..a1fc7a91 100644 --- a/packages/apidash_design_system/lib/tokens/measurements.dart +++ b/packages/apidash_design_system/lib/tokens/measurements.dart @@ -28,6 +28,7 @@ const kP6 = EdgeInsets.all(6); const kP8 = EdgeInsets.all(8); const kP10 = EdgeInsets.all(10); const kP12 = EdgeInsets.all(12); +const kP20 = EdgeInsets.all(20); const kPs8 = EdgeInsets.only(left: 8); const kPs2 = EdgeInsets.only(left: 2); const kPe4 = EdgeInsets.only(right: 4); diff --git a/packages/better_networking/CHANGELOG.md b/packages/better_networking/CHANGELOG.md index 59f048f2..91219bf0 100644 --- a/packages/better_networking/CHANGELOG.md +++ b/packages/better_networking/CHANGELOG.md @@ -1,3 +1,11 @@ +## 0.0.3 + +- Loosen dart SDK constraints. + +## 0.0.2 + +- Improvement of various core features. + ## 0.0.1 -* Intial release. +- Intial release. diff --git a/packages/better_networking/better_networking_example/.gitignore b/packages/better_networking/example/.gitignore similarity index 100% rename from packages/better_networking/better_networking_example/.gitignore rename to packages/better_networking/example/.gitignore diff --git a/packages/better_networking/better_networking_example/README.md b/packages/better_networking/example/README.md similarity index 100% rename from packages/better_networking/better_networking_example/README.md rename to packages/better_networking/example/README.md diff --git a/packages/better_networking/better_networking_example/analysis_options.yaml b/packages/better_networking/example/analysis_options.yaml similarity index 100% rename from packages/better_networking/better_networking_example/analysis_options.yaml rename to packages/better_networking/example/analysis_options.yaml diff --git a/packages/better_networking/better_networking_example/lib/main.dart b/packages/better_networking/example/lib/main.dart similarity index 100% rename from packages/better_networking/better_networking_example/lib/main.dart rename to packages/better_networking/example/lib/main.dart diff --git a/packages/better_networking/better_networking_example/pubspec.lock b/packages/better_networking/example/pubspec.lock similarity index 99% rename from packages/better_networking/better_networking_example/pubspec.lock rename to packages/better_networking/example/pubspec.lock index 79fa58be..2cf904b4 100644 --- a/packages/better_networking/better_networking_example/pubspec.lock +++ b/packages/better_networking/example/pubspec.lock @@ -23,7 +23,7 @@ packages: path: ".." relative: true source: path - version: "0.0.1" + version: "0.0.3" boolean_selector: dependency: transitive description: diff --git a/packages/better_networking/better_networking_example/pubspec.yaml b/packages/better_networking/example/pubspec.yaml similarity index 100% rename from packages/better_networking/better_networking_example/pubspec.yaml rename to packages/better_networking/example/pubspec.yaml diff --git a/packages/better_networking/better_networking_example/pubspec_overrides.yaml b/packages/better_networking/example/pubspec_overrides.yaml similarity index 100% rename from packages/better_networking/better_networking_example/pubspec_overrides.yaml rename to packages/better_networking/example/pubspec_overrides.yaml diff --git a/packages/better_networking/lib/consts.dart b/packages/better_networking/lib/consts.dart index 4719a64d..002cc2e7 100644 --- a/packages/better_networking/lib/consts.dart +++ b/packages/better_networking/lib/consts.dart @@ -2,11 +2,19 @@ import 'dart:convert'; enum APIType { rest("HTTP", "HTTP"), + ai("AI", "AI"), graphql("GraphQL", "GQL"); const APIType(this.label, this.abbr); final String label; final String abbr; + + static fromMethod(String method) { + return HTTPVerb.values.firstWhere( + (model) => model.name == method.toLowerCase(), + orElse: () => throw ArgumentError('INVALID HTTP METHOD'), + ); + } } enum APIAuthType { diff --git a/packages/better_networking/lib/services/http_service.dart b/packages/better_networking/lib/services/http_service.dart index e5f3ff63..78ae2349 100644 --- a/packages/better_networking/lib/services/http_service.dart +++ b/packages/better_networking/lib/services/http_service.dart @@ -23,11 +23,11 @@ final httpClientManager = HttpClientManager(); Future<(HttpResponse?, Duration?, String?)> sendHttpRequestV1( String requestId, APIType apiType, - AuthModel? authData, HttpRequestModel requestModel, { SupportedUriSchemes defaultUriScheme = kDefaultUriScheme, bool noSSL = false, }) async { + final authData = requestModel.authModel; if (httpClientManager.wasRequestCancelled(requestId)) { httpClientManager.removeCancelledRequest(requestId); } diff --git a/packages/better_networking/lib/utils/http_request_utils.dart b/packages/better_networking/lib/utils/http_request_utils.dart index c13fab75..6540395a 100644 --- a/packages/better_networking/lib/utils/http_request_utils.dart +++ b/packages/better_networking/lib/utils/http_request_utils.dart @@ -93,6 +93,7 @@ String? getRequestBody(APIType type, HttpRequestModel httpRequestModel) { ? httpRequestModel.body : null, APIType.graphql => getGraphQLBody(httpRequestModel), + APIType.ai => null, //TODO: TAKE A LOOK }; } diff --git a/packages/better_networking/pubspec.yaml b/packages/better_networking/pubspec.yaml index 9ad62859..d9b33719 100644 --- a/packages/better_networking/pubspec.yaml +++ b/packages/better_networking/pubspec.yaml @@ -1,6 +1,6 @@ name: better_networking description: "Simplified Networking. Support for sending REST & GraphQL API Requests." -version: 0.0.1 +version: 0.0.3 homepage: https://github.com/foss42/apidash topics: @@ -11,7 +11,7 @@ topics: - graphql environment: - sdk: ^3.8.0 + sdk: ">=3.0.0 <4.0.0" flutter: ">=1.17.0" dependencies: diff --git a/packages/genai/.gitignore b/packages/genai/.gitignore new file mode 100644 index 00000000..383c7b14 --- /dev/null +++ b/packages/genai/.gitignore @@ -0,0 +1,31 @@ +# 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/ +coverage/ diff --git a/packages/genai/.pubignore b/packages/genai/.pubignore new file mode 100644 index 00000000..8f49ea12 --- /dev/null +++ b/packages/genai/.pubignore @@ -0,0 +1,12 @@ +pubspec.lock +melos_genai.iml +build/ +coverage/ +dart_test.yaml +doc/ +test/ +pubspec_overrides.yaml +genai_example/melos_genai_example.iml +genai_example/pubspec_overrides.yaml +tool/ +models.json diff --git a/packages/genai/CHANGELOG.md b/packages/genai/CHANGELOG.md new file mode 100644 index 00000000..4ebb3af0 --- /dev/null +++ b/packages/genai/CHANGELOG.md @@ -0,0 +1,3 @@ +## 0.0.1 + +- Introducing a unified Dart/Flutter package for working with multiple Generative AI providers (Google Gemini, OpenAI, Anthropic, Azure OpenAI, Ollama, etc.). diff --git a/packages/genai/LICENSE b/packages/genai/LICENSE new file mode 100644 index 00000000..65d4cee9 --- /dev/null +++ b/packages/genai/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + +Copyright 2025 Ashita Prasad, Ankit Mahato + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/packages/genai/README.md b/packages/genai/README.md new file mode 100644 index 00000000..6d43b408 --- /dev/null +++ b/packages/genai/README.md @@ -0,0 +1,191 @@ +# genai + +A **unified Dart/Flutter package** for working with multiple Generative AI providers (Google Gemini, OpenAI, Anthropic, Azure OpenAI, Ollama, etc.) using a **single request model**. + +- ✅ Supports **normal & streaming** responses +- ✅ Unified `AIRequestModel` across providers +- ✅ Configurable parameters (temperature, top-p, max tokens, etc.) +- ✅ Simple request utilities (`executeGenAIRequest`, `streamGenAIRequest`) +- ✅ Extensible — add your own provider easily + +--- + +## 🚀 Installation + +Add `genai` to your `pubspec.yaml`: + +```yaml +dependencies: + genai: ^0.1.0 +``` + +Then run: + +```bash +flutter pub get +``` + +--- + +## ⚡ Quick Start + +### 1. Import the package + +```dart +import 'package:genai/genai.dart'; +``` + +### 2. Create a request + +```dart +final request = AIRequestModel( + modelApiProvider: ModelAPIProvider.gemini, // or openai, anthropic, etc. + model: "gemini-2.0-flash", + apiKey: "", + url: kGeminiUrl, + systemPrompt: "You are a helpful assistant.", + userPrompt: "Explain quantum entanglement simply.", + stream: false, // set true for streaming +); +``` + +### 3. Run a non-streaming request + +```dart +final answer = await executeGenAIRequest(request); +print("AI Answer: $answer"); +``` + +### 4. Run a streaming request (SSE) + +```dart +final stream = await streamGenAIRequest(request.copyWith(stream: true)); +stream.listen((chunk) { + print("AI Stream Chunk: $chunk"); +}, onError: (err) { + print("Stream Error: $err"); +}); +``` + +### 5. Auto-handle both (recommended) + +```dart +await callGenerativeModel( + request, + onAnswer: (ans) => print("AI Output: $ans"), + onError: (err) => print("Error: $err"), +); +``` + +--- + +## ⚙️ Configuration + +Each request accepts `modelConfigs` to fine-tune output. + +Available configs (defaults provided): + +- `temperature` → controls randomness +- `top_p` / `topP` → nucleus sampling probability +- `max_tokens` / `maxOutputTokens` → maximum length of output +- `stream` → enables streaming + +Example: + +```dart +final request = request.copyWith( + modelConfigs: [ + kDefaultModelConfigTemperature.copyWith( + value: ConfigSliderValue(value: (0, 0.8, 1)), + ), + kDefaultGeminiModelConfigMaxTokens.copyWith( + value: ConfigNumericValue(value: 2048), + ), + ], +); +``` + +--- + +## 📡 Supported Providers + +| Provider | Enum Value | Default URL | +| ------------ | ------------------------------ | --------------------------------------------------------- | +| OpenAI | `ModelAPIProvider.openai` | `https://api.openai.com/v1/chat/completions` | +| Gemini | `ModelAPIProvider.gemini` | `https://generativelanguage.googleapis.com/v1beta/models` | +| Anthropic | `ModelAPIProvider.anthropic` | `https://api.anthropic.com/v1/messages` | +| Azure OpenAI | `ModelAPIProvider.azureopenai` | Provided by Azure deployment | +| Ollama | `ModelAPIProvider.ollama` | `$kBaseOllamaUrl/v1/chat/completions` | + +--- + +## 🛠️ Advanced Streaming (Word-by-Word) + +```dart +final stream = await streamGenAIRequest(request.copyWith(stream: true)); + +processGenAIStreamOutput( + stream, + (word) => print("Word: $word"), // called for each word + (err) => print("Error: $err"), +); +``` + +--- + +## 🔒 Authentication + +- **OpenAI / Anthropic / Azure OpenAI** → API key passed as HTTP header. +- **Gemini** → API key passed as query param `?key=YOUR_API_KEY`. +- **Ollama** → local server, no key required. + +Just set `apiKey` in your `AIRequestModel`. + +--- + +## 📦 Extending with New Providers + +Want to add a new AI provider? + +1. Extend `ModelProvider` +2. Implement: + + - `defaultAIRequestModel` + - `createRequest()` + - `outputFormatter()` + - `streamOutputFormatter()` + +3. Register in `kModelProvidersMap` + +That’s it — it plugs into the same unified request flow. + +--- + +## ✅ Example: Gemini + +```dart +final request = GeminiModel.instance.defaultAIRequestModel.copyWith( + model: "gemini-pro", + apiKey: "", + userPrompt: "Write me a haiku about Flutter.", +); + +final answer = await executeGenAIRequest(request); +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(s) + +- Ankit Mahato ([GitHub](https://github.com/animator), [LinkedIn](https://www.linkedin.com/in/ankitmahato/), [X](https://x.com/ankitmahato)) +- Ashita Prasad ([GitHub](https://github.com/ashitaprasad), [LinkedIn](https://www.linkedin.com/in/ashitaprasad/), [X](https://x.com/ashitaprasad)) +- Manas Hejmadi (contributor) ([GitHub](https://github.com/synapsecode)) + +## License + +This project is licensed under the [Apache License 2.0](https://github.com/foss42/apidash/blob/main/packages/genai/LICENSE). diff --git a/packages/genai/analysis_options.yaml b/packages/genai/analysis_options.yaml new file mode 100644 index 00000000..9a1eabb4 --- /dev/null +++ b/packages/genai/analysis_options.yaml @@ -0,0 +1,11 @@ +include: package:flutter_lints/flutter.yaml + +analyzer: + errors: + invalid_annotation_target: ignore + exclude: + - "**/*.freezed.dart" + - "**/*.g.dart" + +linter: + rules: diff --git a/packages/genai/genai_example/.gitignore b/packages/genai/genai_example/.gitignore new file mode 100644 index 00000000..79c113f9 --- /dev/null +++ b/packages/genai/genai_example/.gitignore @@ -0,0 +1,45 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.build/ +.buildlog/ +.history +.svn/ +.swiftpm/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +**/doc/api/ +**/ios/Flutter/.last_build_id +.dart_tool/ +.flutter-plugins +.flutter-plugins-dependencies +.pub-cache/ +.pub/ +/build/ + +# Symbolication related +app.*.symbols + +# Obfuscation related +app.*.map.json + +# Android Studio will place build artifacts here +/android/app/debug +/android/app/profile +/android/app/release diff --git a/packages/genai/genai_example/README.md b/packages/genai/genai_example/README.md new file mode 100644 index 00000000..70e37aa4 --- /dev/null +++ b/packages/genai/genai_example/README.md @@ -0,0 +1,59 @@ +# GenAI Example + +This project is a simple demonstration of how to use the GenAI package + +### Fetch all available Remote LLMs +```dart +await LLMManager.fetchAvailableLLMs(); +``` + +### Getting LLM Models for a given Provider +```dart +final List models = LLMProvider.gemini.models; +``` + +### Calling a GenAI Model using the provided helper +```dart +final LLMModel geminiModel = LLMProvider.gemini.getLLMByIdentifier('gemini-2.0-flash'); +final ModelController controller = model.provider.modelController; +GenerativeAI.callGenerativeModel( + geminiModel, + onAnswer: (x) { + print(x); + }, + onError: (e){}, + systemPrompt: 'Give a 100 word summary of the provided word. Only give the answer', + userPrompt: 'Pizza', + credential: 'AIza.....', +); +``` + +### Calling a GenAI model (with Streaming) +```dart +final LLMModel geminiModel = LLMProvider.gemini.getLLMByIdentifier('gemini-2.0-flash'); +final ModelController controller = model.provider.modelController; +GenerativeAI.callGenerativeModel( + geminiModel, + onAnswer: (x) { + stdout.write(x); //each word in the stream + }, + onError: (e){}, + systemPrompt: 'Give a 100 word summary of the provided word. Only give the answer', + userPrompt: 'Pizza', + credential: 'AIza.....', + stream: true, +); +``` + +### Directly Using a Model (eg: Gemini) +```dart +final LLMModel model = LLMProvider.gemini.getLLMByIdentifier('gemini-2.0-flash'); +final ModelController controller = model.provider.modelController; +final payload = controller.inputPayload; +payload.systemPrompt = 'Say YES or NO'; +payload.userPrompt = 'The sun sets in the west'; +payload.credential = 'AIza....'; +final genAIRequest = controller.createRequest(model, payload); +final answer = await GenerativeAI.executeGenAIRequest(model, genAIRequest); +print(answer) +``` \ No newline at end of file diff --git a/packages/genai/genai_example/analysis_options.yaml b/packages/genai/genai_example/analysis_options.yaml new file mode 100644 index 00000000..0d290213 --- /dev/null +++ b/packages/genai/genai_example/analysis_options.yaml @@ -0,0 +1,28 @@ +# This file configures the analyzer, which statically analyzes Dart code to +# check for errors, warnings, and lints. +# +# The issues identified by the analyzer are surfaced in the UI of Dart-enabled +# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be +# invoked from the command line by running `flutter analyze`. + +# The following line activates a set of recommended lints for Flutter apps, +# packages, and plugins designed to encourage good coding practices. +include: package:flutter_lints/flutter.yaml + +linter: + # The lint rules applied to this project can be customized in the + # section below to disable rules from the `package:flutter_lints/flutter.yaml` + # included above or to enable additional rules. A list of all available lints + # and their documentation is published at https://dart.dev/lints. + # + # Instead of disabling a lint rule for the entire project in the + # section below, it can also be suppressed for a single line of code + # or a specific dart file by using the `// ignore: name_of_lint` and + # `// ignore_for_file: name_of_lint` syntax on the line or in the file + # producing the lint. + rules: + # avoid_print: false # Uncomment to disable the `avoid_print` rule + # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule + +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options diff --git a/packages/genai/genai_example/lib/main.dart b/packages/genai/genai_example/lib/main.dart new file mode 100644 index 00000000..ed63985c --- /dev/null +++ b/packages/genai/genai_example/lib/main.dart @@ -0,0 +1,191 @@ +import 'package:flutter/material.dart'; +import 'package:genai/genai.dart'; + +void main() { + runApp(const GenAIExample()); +} + +class GenAIExample extends StatelessWidget { + const GenAIExample({super.key}); + @override + Widget build(BuildContext context) { + return MaterialApp( + title: 'GenAI Example', + theme: ThemeData( + colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple), + ), + home: AIExample(), + ); + } +} + +class AIExample extends StatefulWidget { + const AIExample({super.key}); + + @override + State createState() => _AIExampleState(); +} + +class _AIExampleState extends State { + late final Future aM; + + @override + void initState() { + super.initState(); + aM = ModelManager.fetchAvailableModels(); //fetch latest LLMs + systemPromptController.text = 'Give me a 200 word essay on the given topic'; + inputPromptController.text = 'Apple'; + } + + generateAIResponse({bool stream = false}) { + setState(() { + output = ""; + }); + callGenerativeModel( + kModelProvidersMap[selectedProvider]?.defaultAIRequestModel.copyWith( + model: selectedModel, + apiKey: credentialController.value.text, + systemPrompt: systemPromptController.value.text, + userPrompt: inputPromptController.value.text, + stream: stream, + ), + onAnswer: (x) { + setState(() { + output += "$x "; + }); + }, + onError: (e) { + debugPrint(e); + }, + ); + } + + String output = ""; + ModelAPIProvider selectedProvider = ModelAPIProvider.ollama; + String selectedModel = ""; + + TextEditingController systemPromptController = TextEditingController(); + TextEditingController inputPromptController = TextEditingController(); + TextEditingController credentialController = TextEditingController(); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: Text('GenAI Example')), + body: FutureBuilder( + future: aM, + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.done && + snapshot.hasData && + snapshot.data != null) { + final data = snapshot.data!; + final mappedData = data.map; + return SingleChildScrollView( + padding: EdgeInsets.all(20), + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text('Providers'), + SizedBox(height: 10), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + ...data.modelProviders.map( + (x) => Container( + padding: EdgeInsets.only(right: 10), + child: GestureDetector( + onTap: () { + setState(() { + selectedProvider = x.providerId!; + }); + }, + child: Chip( + label: Text(x.providerName ?? ""), + backgroundColor: selectedProvider == x.providerId + ? Colors.blue[50] + : Colors.transparent, + ), + ), + ), + ), + ], + ), + SizedBox(height: 20), + Text('Models'), + SizedBox(height: 10), + Wrap( + spacing: 5, + runSpacing: 5, + children: [ + ...(mappedData[selectedProvider]?.models ?? []).map( + (x) => Container( + padding: EdgeInsets.only(right: 10), + child: GestureDetector( + onTap: () { + setState(() { + selectedModel = x.id!; + }); + }, + child: Chip( + label: Text(x.name ?? ""), + backgroundColor: selectedModel == x.id + ? Colors.blue[50] + : Colors.transparent, + ), + ), + ), + ), + ], + ), + SizedBox(height: 30), + SizedBox( + width: 400, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text('Input Prompt'), + TextField(controller: inputPromptController), + SizedBox(height: 20), + Text('System Prompt'), + TextField(controller: systemPromptController), + SizedBox(height: 20), + Text('Credential'), + TextField(controller: credentialController), + SizedBox(height: 20), + ], + ), + ), + SizedBox(height: 30), + Row( + mainAxisSize: MainAxisSize.min, + children: [ + ElevatedButton( + onPressed: () { + generateAIResponse(); + }, + child: Text('Generate Response (SINGLE-RESPONSE)'), + ), + SizedBox(width: 20), + ElevatedButton( + onPressed: () { + generateAIResponse(stream: true); + }, + child: Text('Generate Response (STREAM)'), + ), + ], + ), + SizedBox(height: 30), + Divider(), + SizedBox(height: 20), + + Text(output), + ], + ), + ); + } + return CircularProgressIndicator(); + }, + ), + ); + } +} diff --git a/packages/genai/genai_example/pubspec.lock b/packages/genai/genai_example/pubspec.lock new file mode 100644 index 00000000..7526a4ce --- /dev/null +++ b/packages/genai/genai_example/pubspec.lock @@ -0,0 +1,370 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + adaptive_number: + dependency: transitive + description: + name: adaptive_number + sha256: "3a567544e9b5c9c803006f51140ad544aedc79604fd4f3f2c1380003f97c1d77" + url: "https://pub.dev" + source: hosted + version: "1.0.0" + async: + dependency: transitive + description: + name: async + sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb" + url: "https://pub.dev" + source: hosted + version: "2.13.0" + better_networking: + dependency: "direct overridden" + description: + path: "../../better_networking" + relative: true + source: path + version: "0.0.3" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + characters: + dependency: transitive + description: + name: characters + sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 + url: "https://pub.dev" + source: hosted + version: "1.4.0" + clock: + dependency: transitive + description: + name: clock + sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b + url: "https://pub.dev" + source: hosted + version: "1.1.2" + collection: + dependency: transitive + description: + name: collection + sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" + url: "https://pub.dev" + source: hosted + version: "1.19.1" + convert: + dependency: transitive + description: + name: convert + sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68 + url: "https://pub.dev" + source: hosted + version: "3.1.2" + crypto: + dependency: transitive + description: + name: crypto + sha256: "1e445881f28f22d6140f181e07737b22f1e099a5e1ff94b0af2f9e4a463f4855" + url: "https://pub.dev" + source: hosted + version: "3.0.6" + cupertino_icons: + dependency: "direct main" + description: + name: cupertino_icons + sha256: ba631d1c7f7bef6b729a622b7b752645a2d076dba9976925b8f25725a30e1ee6 + url: "https://pub.dev" + source: hosted + version: "1.0.8" + dart_jsonwebtoken: + dependency: transitive + description: + name: dart_jsonwebtoken + sha256: "21ce9f8a8712f741e8d6876a9c82c0f8a257fe928c4378a91d8527b92a3fd413" + url: "https://pub.dev" + source: hosted + version: "3.2.0" + ed25519_edwards: + dependency: transitive + description: + name: ed25519_edwards + sha256: "6ce0112d131327ec6d42beede1e5dfd526069b18ad45dcf654f15074ad9276cd" + url: "https://pub.dev" + source: hosted + version: "0.3.1" + fake_async: + dependency: transitive + description: + name: fake_async + sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" + url: "https://pub.dev" + source: hosted + version: "1.3.3" + fixnum: + dependency: transitive + description: + name: fixnum + sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be + url: "https://pub.dev" + source: hosted + version: "1.1.1" + flutter: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_lints: + dependency: "direct dev" + description: + name: flutter_lints + sha256: "5398f14efa795ffb7a33e9b6a08798b26a180edac4ad7db3f231e40f82ce11e1" + url: "https://pub.dev" + source: hosted + version: "5.0.0" + flutter_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" + freezed_annotation: + dependency: transitive + description: + name: freezed_annotation + sha256: c2e2d632dd9b8a2b7751117abcfc2b4888ecfe181bd9fca7170d9ef02e595fe2 + url: "https://pub.dev" + source: hosted + version: "2.4.4" + genai: + dependency: "direct main" + description: + path: ".." + relative: true + source: path + version: "0.0.1" + http: + dependency: transitive + description: + name: http + sha256: "2c11f3f94c687ee9bad77c171151672986360b2b001d109814ee7140b2cf261b" + url: "https://pub.dev" + source: hosted + version: "1.4.0" + http_parser: + dependency: transitive + description: + name: http_parser + sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571" + url: "https://pub.dev" + source: hosted + version: "4.1.2" + json5: + dependency: transitive + description: + name: json5 + sha256: b67d6e06c9e225c8277d3c43f796677af7975a2a2b0669ff12ba38ff466a31f4 + url: "https://pub.dev" + source: hosted + version: "0.8.2" + json_annotation: + dependency: transitive + description: + name: json_annotation + sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1" + url: "https://pub.dev" + source: hosted + version: "4.9.0" + leak_tracker: + dependency: transitive + description: + name: leak_tracker + sha256: "6bb818ecbdffe216e81182c2f0714a2e62b593f4a4f13098713ff1685dfb6ab0" + url: "https://pub.dev" + source: hosted + version: "10.0.9" + leak_tracker_flutter_testing: + dependency: transitive + description: + name: leak_tracker_flutter_testing + sha256: f8b613e7e6a13ec79cfdc0e97638fddb3ab848452eff057653abd3edba760573 + url: "https://pub.dev" + source: hosted + version: "3.0.9" + leak_tracker_testing: + dependency: transitive + description: + name: leak_tracker_testing + sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3" + url: "https://pub.dev" + source: hosted + version: "3.0.1" + lints: + dependency: transitive + description: + name: lints + sha256: c35bb79562d980e9a453fc715854e1ed39e24e7d0297a880ef54e17f9874a9d7 + url: "https://pub.dev" + source: hosted + version: "5.1.1" + matcher: + dependency: transitive + description: + name: matcher + sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 + url: "https://pub.dev" + source: hosted + version: "0.12.17" + material_color_utilities: + dependency: transitive + description: + name: material_color_utilities + sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec + url: "https://pub.dev" + source: hosted + version: "0.11.1" + meta: + dependency: transitive + description: + name: meta + sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c + url: "https://pub.dev" + source: hosted + version: "1.16.0" + nanoid: + dependency: transitive + description: + name: nanoid + sha256: be3f8752d9046c825df2f3914195151eb876f3ad64b9d833dd0b799b77b8759e + url: "https://pub.dev" + source: hosted + version: "1.0.0" + path: + dependency: transitive + description: + name: path + sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" + url: "https://pub.dev" + source: hosted + version: "1.9.1" + petitparser: + dependency: transitive + description: + name: petitparser + sha256: "07c8f0b1913bcde1ff0d26e57ace2f3012ccbf2b204e070290dad3bb22797646" + url: "https://pub.dev" + source: hosted + version: "6.1.0" + pointycastle: + dependency: transitive + description: + name: pointycastle + sha256: "92aa3841d083cc4b0f4709b5c74fd6409a3e6ba833ffc7dc6a8fee096366acf5" + url: "https://pub.dev" + source: hosted + version: "4.0.0" + seed: + dependency: "direct overridden" + description: + path: "../../seed" + relative: true + source: path + version: "0.0.3" + sky_engine: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + source_span: + dependency: transitive + description: + name: source_span + sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c" + url: "https://pub.dev" + source: hosted + version: "1.10.1" + stack_trace: + dependency: transitive + description: + name: stack_trace + sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" + url: "https://pub.dev" + source: hosted + version: "1.12.1" + stream_channel: + dependency: transitive + description: + name: stream_channel + sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + string_scanner: + dependency: transitive + description: + name: string_scanner + sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" + url: "https://pub.dev" + source: hosted + version: "1.4.1" + term_glyph: + dependency: transitive + description: + name: term_glyph + sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" + url: "https://pub.dev" + source: hosted + version: "1.2.2" + test_api: + dependency: transitive + description: + name: test_api + sha256: fb31f383e2ee25fbbfe06b40fe21e1e458d14080e3c67e7ba0acfde4df4e0bbd + url: "https://pub.dev" + source: hosted + version: "0.7.4" + typed_data: + dependency: transitive + description: + name: typed_data + sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 + url: "https://pub.dev" + source: hosted + version: "1.4.0" + vector_math: + dependency: transitive + description: + name: vector_math + sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + vm_service: + dependency: transitive + description: + name: vm_service + sha256: ddfa8d30d89985b96407efce8acbdd124701f96741f2d981ca860662f1c0dc02 + url: "https://pub.dev" + source: hosted + version: "15.0.0" + web: + dependency: transitive + description: + name: web + sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a" + url: "https://pub.dev" + source: hosted + version: "1.1.1" + xml: + dependency: transitive + description: + name: xml + sha256: b015a8ad1c488f66851d762d3090a21c600e479dc75e68328c52774040cf9226 + url: "https://pub.dev" + source: hosted + version: "6.5.0" +sdks: + dart: ">=3.8.0 <4.0.0" + flutter: ">=3.18.0-18.0.pre.54" diff --git a/packages/genai/genai_example/pubspec.yaml b/packages/genai/genai_example/pubspec.yaml new file mode 100644 index 00000000..fbfc5abb --- /dev/null +++ b/packages/genai/genai_example/pubspec.yaml @@ -0,0 +1,90 @@ +name: genai_example +description: "A new Flutter project." +# The following line prevents the package from being accidentally published to +# pub.dev using `flutter pub publish`. This is preferred for private packages. +publish_to: "none" # Remove this line if you wish to publish to pub.dev + +# The following defines the version and build number for your application. +# A version number is three numbers separated by dots, like 1.2.43 +# followed by an optional build number separated by a +. +# Both the version and the builder number may be overridden in flutter +# build by specifying --build-name and --build-number, respectively. +# In Android, build-name is used as versionName while build-number used as versionCode. +# Read more about Android versioning at https://developer.android.com/studio/publish/versioning +# In iOS, build-name is used as CFBundleShortVersionString while build-number is used as CFBundleVersion. +# Read more about iOS versioning at +# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html +# In Windows, build-name is used as the major, minor, and patch parts +# of the product and file versions while build-number is used as the build suffix. +version: 1.0.0+1 + +environment: + sdk: ^3.8.0 + +# Dependencies specify other packages that your package needs in order to work. +# To automatically upgrade your package dependencies to the latest versions +# consider running `flutter pub upgrade --major-versions`. Alternatively, +# dependencies can be manually updated by changing the version numbers below to +# the latest version available on pub.dev. To see which dependencies have newer +# versions available, run `flutter pub outdated`. +dependencies: + flutter: + sdk: flutter + genai: + path: .. + + # The following adds the Cupertino Icons font to your application. + # Use with the CupertinoIcons class for iOS style icons. + cupertino_icons: ^1.0.8 + +dev_dependencies: + flutter_test: + sdk: flutter + + # The "flutter_lints" package below contains a set of recommended lints to + # encourage good coding practices. The lint set provided by the package is + # activated in the `analysis_options.yaml` file located at the root of your + # package. See that file for information about deactivating specific lint + # rules and activating additional ones. + flutter_lints: ^5.0.0 + +# For information on the generic Dart part of this file, see the +# following page: https://dart.dev/tools/pub/pubspec + +# The following section is specific to Flutter packages. +flutter: + # The following line ensures that the Material Icons font is + # included with your application, so that you can use the icons in + # the material Icons class. + uses-material-design: true + + # To add assets to your application, add an assets section, like this: + # assets: + # - images/a_dot_burr.jpeg + # - images/a_dot_ham.jpeg + + # An image asset can refer to one or more resolution-specific "variants", see + # https://flutter.dev/to/resolution-aware-images + + # For details regarding adding assets from package dependencies, see + # https://flutter.dev/to/asset-from-package + + # To add custom fonts to your application, add a fonts section here, + # in this "flutter" section. Each entry in this list should have a + # "family" key with the font family name, and a "fonts" key with a + # list giving the asset and other descriptors for the font. For + # example: + # fonts: + # - family: Schyler + # fonts: + # - asset: fonts/Schyler-Regular.ttf + # - asset: fonts/Schyler-Italic.ttf + # style: italic + # - family: Trajan Pro + # fonts: + # - asset: fonts/TrajanPro.ttf + # - asset: fonts/TrajanPro_Bold.ttf + # weight: 700 + # + # For details regarding fonts from package dependencies, + # see https://flutter.dev/to/font-from-package diff --git a/packages/genai/genai_example/pubspec_overrides.yaml b/packages/genai/genai_example/pubspec_overrides.yaml new file mode 100644 index 00000000..6581ff92 --- /dev/null +++ b/packages/genai/genai_example/pubspec_overrides.yaml @@ -0,0 +1,8 @@ +# melos_managed_dependency_overrides: better_networking,genai,seed +dependency_overrides: + better_networking: + path: ../../better_networking + genai: + path: .. + seed: + path: ../../seed diff --git a/packages/genai/lib/agentic_engine/agent_service.dart b/packages/genai/lib/agentic_engine/agent_service.dart new file mode 100644 index 00000000..d218ccd1 --- /dev/null +++ b/packages/genai/lib/agentic_engine/agent_service.dart @@ -0,0 +1,90 @@ +import 'package:genai/agentic_engine/blueprint.dart'; +import 'package:genai/genai.dart'; + +class AIAgentService { + static Future _call_provider({ + required AIRequestModel baseAIRequestObject, + required String systemPrompt, + required String input, + }) async { + final aiRequest = baseAIRequestObject.copyWith( + systemPrompt: systemPrompt, + userPrompt: input, + ); + return await executeGenAIRequest(aiRequest); + } + + static Future _orchestrator( + AIAgent agent, + AIRequestModel baseAIRequestObject, { + String? query, + Map? variables, + }) async { + String sP = agent.getSystemPrompt(); + + //Perform Templating + if (variables != null) { + for (final v in variables.keys) { + sP = sP.substitutePromptVariable(v, variables[v]); + } + } + + return await _call_provider( + systemPrompt: sP, + input: query ?? '', + baseAIRequestObject: baseAIRequestObject, + ); + } + + static Future _governor( + AIAgent agent, + AIRequestModel baseAIRequestObject, { + String? query, + Map? variables, + }) async { + int RETRY_COUNT = 0; + List backoffDelays = [200, 400, 800, 1600, 3200]; + do { + try { + final res = await _orchestrator( + agent, + baseAIRequestObject, + query: query, + variables: variables, + ); + if (res != null) { + if (await agent.validator(res)) { + return agent.outputFormatter(res); + } + } + } catch (e) { + "AIAgentService::Governor: Exception Occured: $e"; + } + // Exponential Backoff + if (RETRY_COUNT < backoffDelays.length) { + await Future.delayed( + Duration(milliseconds: backoffDelays[RETRY_COUNT]), + ); + } + RETRY_COUNT += 1; + print( + "Retrying AgentCall for (${agent.agentName}): ATTEMPT: $RETRY_COUNT", + ); + } while (RETRY_COUNT < 5); + return null; + } + + static Future callAgent( + AIAgent agent, + AIRequestModel baseAIRequestObject, { + String? query, + Map? variables, + }) async { + return await _governor( + agent, + baseAIRequestObject, + query: query, + variables: variables, + ); + } +} diff --git a/packages/genai/lib/agentic_engine/agentic_engine.dart b/packages/genai/lib/agentic_engine/agentic_engine.dart new file mode 100644 index 00000000..abc52f3b --- /dev/null +++ b/packages/genai/lib/agentic_engine/agentic_engine.dart @@ -0,0 +1,2 @@ +export 'agent_service.dart'; +export 'blueprint.dart'; diff --git a/packages/genai/lib/agentic_engine/blueprint.dart b/packages/genai/lib/agentic_engine/blueprint.dart new file mode 100644 index 00000000..14b9bfc1 --- /dev/null +++ b/packages/genai/lib/agentic_engine/blueprint.dart @@ -0,0 +1,18 @@ +abstract class AIAgent { + String get agentName; + String getSystemPrompt(); + Future validator(String aiResponse); + Future outputFormatter(String validatedResponse); +} + +extension SystemPromptTemplating on String { + String substitutePromptVariable(String variable, String value) { + return this.replaceAll(":$variable:", value); + } +} + +class AgentInputs { + final String? query; + final Map? variables; + AgentInputs({this.query, this.variables}); +} diff --git a/packages/genai/lib/consts.dart b/packages/genai/lib/consts.dart new file mode 100644 index 00000000..27543c9b --- /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/refs/heads/main/packages/genai/models.json'; + +const kBaseOllamaUrl = 'http://localhost:11434'; diff --git a/packages/genai/lib/genai.dart b/packages/genai/lib/genai.dart new file mode 100644 index 00000000..9ea90ed2 --- /dev/null +++ b/packages/genai/lib/genai.dart @@ -0,0 +1,8 @@ +export 'models/models.dart'; +export 'interface/interface.dart'; +export 'utils/utils.dart'; +export 'widgets/widgets.dart'; +export 'agentic_engine/agentic_engine.dart'; + +// Export 3rd party +export 'package:better_networking/better_networking.dart'; diff --git a/packages/genai/lib/interface/consts.dart b/packages/genai/lib/interface/consts.dart new file mode 100644 index 00000000..1b05c9be --- /dev/null +++ b/packages/genai/lib/interface/consts.dart @@ -0,0 +1,71 @@ +import '../consts.dart'; +import '../models/models.dart'; +import 'model_providers/model_providers.dart'; + +enum ModelAPIProvider { openai, anthropic, gemini, azureopenai, ollama } + +final kModelProvidersMap = { + ModelAPIProvider.openai: OpenAIModel.instance, + ModelAPIProvider.anthropic: AnthropicModel.instance, + ModelAPIProvider.gemini: GeminiModel.instance, + ModelAPIProvider.azureopenai: AzureOpenAIModel.instance, + ModelAPIProvider.ollama: OllamaModel.instance, +}; + +const kAnthropicUrl = 'https://api.anthropic.com/v1/messages'; +const kGeminiUrl = 'https://generativelanguage.googleapis.com/v1beta/models'; +const kOpenAIUrl = 'https://api.openai.com/v1/chat/completions'; +const kOllamaUrl = '$kBaseOllamaUrl/v1/chat/completions'; + +final kDefaultAiRequestModel = AIRequestModel( + url: '', + model: '', + apiKey: '', + systemPrompt: '', + userPrompt: '', + modelConfigs: [ + kDefaultModelConfigTemperature, + kDefaultModelConfigTopP, + kDefaultModelConfigMaxTokens, + ], + stream: false, +); + +final kDefaultModelConfigTemperature = ModelConfig( + id: 'temperature', + name: 'Temperature', + description: 'The Temperature of the Model', + type: ConfigType.slider, + value: ConfigSliderValue(value: (0, 0.5, 1)), +); + +final kDefaultModelConfigTopP = ModelConfig( + id: 'top_p', + name: 'Top P', + description: 'The Top P of the Model', + type: ConfigType.slider, + value: ConfigSliderValue(value: (0, 0.95, 1)), +); + +final kDefaultModelConfigMaxTokens = ModelConfig( + id: 'max_tokens', + name: 'Maximum Tokens', + description: 'The maximum number of tokens allowed in the output', + type: ConfigType.numeric, + value: ConfigNumericValue(value: 1024), +); + +final kDefaultModelConfigStream = ModelConfig( + id: 'stream', + name: 'Enable Streaming Mode', + description: 'The LLM output will be sent in a stream instead of all at once', + type: ConfigType.boolean, + value: ConfigBooleanValue(value: false), +); + +final kDefaultGeminiModelConfigTopP = kDefaultModelConfigTopP.copyWith( + id: 'topP', +); + +final kDefaultGeminiModelConfigMaxTokens = kDefaultModelConfigMaxTokens + .copyWith(id: 'maxOutputTokens'); diff --git a/packages/genai/lib/interface/interface.dart b/packages/genai/lib/interface/interface.dart new file mode 100644 index 00000000..4ab63517 --- /dev/null +++ b/packages/genai/lib/interface/interface.dart @@ -0,0 +1,2 @@ +export 'model_providers/model_providers.dart'; +export 'consts.dart'; diff --git a/packages/genai/lib/interface/model_providers/anthropic.dart b/packages/genai/lib/interface/model_providers/anthropic.dart new file mode 100644 index 00000000..c6d1f955 --- /dev/null +++ b/packages/genai/lib/interface/model_providers/anthropic.dart @@ -0,0 +1,52 @@ +import 'package:better_networking/better_networking.dart'; +import '../../models/models.dart'; +import '../consts.dart'; + +class AnthropicModel extends ModelProvider { + static final instance = AnthropicModel(); + + @override + AIRequestModel get defaultAIRequestModel => kDefaultAiRequestModel.copyWith( + modelApiProvider: ModelAPIProvider.anthropic, + url: kAnthropicUrl, + ); + + @override + HttpRequestModel? createRequest(AIRequestModel? aiRequestModel) { + if (aiRequestModel == null) { + return null; + } + return HttpRequestModel( + method: HTTPVerb.post, + url: aiRequestModel.url, + headers: const [ + NameValueModel(name: "anthropic-version", value: "2023-06-01"), + ], + authModel: aiRequestModel.apiKey == null + ? null + : AuthModel( + type: APIAuthType.apiKey, + apikey: AuthApiKeyModel(key: aiRequestModel.apiKey!), + ), + body: kJsonEncoder.convert({ + "model": aiRequestModel.model, + "messages": [ + {"role": "system", "content": aiRequestModel.systemPrompt}, + {"role": "user", "content": aiRequestModel.userPrompt}, + ], + ...aiRequestModel.getModelConfigMap(), + if (aiRequestModel.stream ?? false) ...{'stream': true}, + }), + ); + } + + @override + String? outputFormatter(Map x) { + return x['content']?[0]['text']; + } + + @override + String? streamOutputFormatter(Map x) { + return x['text']; + } +} diff --git a/packages/genai/lib/interface/model_providers/azureopenai.dart b/packages/genai/lib/interface/model_providers/azureopenai.dart new file mode 100644 index 00000000..587c1c49 --- /dev/null +++ b/packages/genai/lib/interface/model_providers/azureopenai.dart @@ -0,0 +1,58 @@ +import 'package:better_networking/better_networking.dart'; +import '../../models/models.dart'; +import '../consts.dart'; + +class AzureOpenAIModel extends ModelProvider { + static final instance = AzureOpenAIModel(); + + @override + AIRequestModel get defaultAIRequestModel => kDefaultAiRequestModel.copyWith( + modelApiProvider: ModelAPIProvider.azureopenai, + ); + + @override + HttpRequestModel? createRequest(AIRequestModel? aiRequestModel) { + if (aiRequestModel == null) { + return null; + } + if (aiRequestModel.url.isEmpty) { + throw Exception('MODEL ENDPOINT IS EMPTY'); + } + return HttpRequestModel( + method: HTTPVerb.post, + url: aiRequestModel.url, + authModel: aiRequestModel.apiKey == null + ? null + : AuthModel( + type: APIAuthType.apiKey, + apikey: AuthApiKeyModel( + key: aiRequestModel.apiKey!, + name: 'api-key', + ), + ), + body: kJsonEncoder.convert({ + "model": aiRequestModel.model, + "messages": [ + {"role": "system", "content": aiRequestModel.systemPrompt}, + if (aiRequestModel.userPrompt.isNotEmpty) ...{ + {"role": "user", "content": aiRequestModel.userPrompt}, + } else ...{ + {"role": "user", "content": "Generate"}, + }, + ], + ...aiRequestModel.getModelConfigMap(), + if (aiRequestModel.stream ?? false) ...{'stream': true}, + }), + ); + } + + @override + String? outputFormatter(Map x) { + return x["choices"]?[0]["message"]?["content"]?.trim(); + } + + @override + String? streamOutputFormatter(Map x) { + return x["choices"]?[0]["delta"]?["content"]; + } +} diff --git a/packages/genai/lib/interface/model_providers/gemini.dart b/packages/genai/lib/interface/model_providers/gemini.dart new file mode 100644 index 00000000..f3c57867 --- /dev/null +++ b/packages/genai/lib/interface/model_providers/gemini.dart @@ -0,0 +1,75 @@ +import 'package:better_networking/better_networking.dart'; +import '../../models/models.dart'; +import '../consts.dart'; + +class GeminiModel extends ModelProvider { + static final instance = GeminiModel(); + + @override + AIRequestModel get defaultAIRequestModel => kDefaultAiRequestModel.copyWith( + modelApiProvider: ModelAPIProvider.gemini, + url: kGeminiUrl, + modelConfigs: [ + kDefaultModelConfigTemperature, + kDefaultGeminiModelConfigTopP, + kDefaultGeminiModelConfigMaxTokens, + ], + ); + + @override + HttpRequestModel? createRequest(AIRequestModel? aiRequestModel) { + if (aiRequestModel == null) { + return null; + } + List params = []; + String endpoint = "${aiRequestModel.url}/${aiRequestModel.model}:"; + if (aiRequestModel.stream ?? false) { + endpoint += 'streamGenerateContent'; + params.add(const NameValueModel(name: "alt", value: "sse")); + } else { + endpoint += 'generateContent'; + } + + return HttpRequestModel( + method: HTTPVerb.post, + url: endpoint, + authModel: aiRequestModel.apiKey == null + ? null + : AuthModel( + type: APIAuthType.apiKey, + apikey: AuthApiKeyModel( + key: aiRequestModel.apiKey!, + location: 'query', + name: 'key', + ), + ), + body: kJsonEncoder.convert({ + "contents": [ + { + "role": "user", + "parts": [ + {"text": aiRequestModel.userPrompt}, + ], + }, + ], + "systemInstruction": { + "role": "system", + "parts": [ + {"text": aiRequestModel.systemPrompt}, + ], + }, + "generationConfig": aiRequestModel.getModelConfigMap(), + }), + ); + } + + @override + String? outputFormatter(Map x) { + return x['candidates']?[0]?['content']?['parts']?[0]?['text']; + } + + @override + String? streamOutputFormatter(Map x) { + return x['candidates']?[0]?['content']?['parts']?[0]?['text']; + } +} diff --git a/packages/genai/lib/interface/model_providers/model_providers.dart b/packages/genai/lib/interface/model_providers/model_providers.dart new file mode 100644 index 00000000..b3fa202f --- /dev/null +++ b/packages/genai/lib/interface/model_providers/model_providers.dart @@ -0,0 +1,5 @@ +export 'anthropic.dart'; +export 'gemini.dart'; +export 'azureopenai.dart'; +export 'openai.dart'; +export 'ollama.dart'; diff --git a/packages/genai/lib/interface/model_providers/ollama.dart b/packages/genai/lib/interface/model_providers/ollama.dart new file mode 100644 index 00000000..6a6fc713 --- /dev/null +++ b/packages/genai/lib/interface/model_providers/ollama.dart @@ -0,0 +1,14 @@ +import '../../models/models.dart'; +import '../consts.dart'; +import 'openai.dart'; + +class OllamaModel extends OpenAIModel { + static final instance = OllamaModel(); + + @override + AIRequestModel get defaultAIRequestModel => kDefaultAiRequestModel.copyWith( + modelApiProvider: ModelAPIProvider.ollama, + url: kOllamaUrl, + modelConfigs: [kDefaultModelConfigTemperature, kDefaultModelConfigTopP], + ); +} diff --git a/packages/genai/lib/interface/model_providers/openai.dart b/packages/genai/lib/interface/model_providers/openai.dart new file mode 100644 index 00000000..a2b08f3d --- /dev/null +++ b/packages/genai/lib/interface/model_providers/openai.dart @@ -0,0 +1,53 @@ +import 'package:better_networking/better_networking.dart'; +import '../../models/models.dart'; +import '../consts.dart'; + +class OpenAIModel extends ModelProvider { + static final instance = OpenAIModel(); + + @override + AIRequestModel get defaultAIRequestModel => kDefaultAiRequestModel.copyWith( + modelApiProvider: ModelAPIProvider.openai, + url: kOpenAIUrl, + ); + + @override + HttpRequestModel? createRequest(AIRequestModel? aiRequestModel) { + if (aiRequestModel == null) { + return null; + } + return HttpRequestModel( + method: HTTPVerb.post, + url: aiRequestModel.url, + authModel: aiRequestModel.apiKey == null + ? null + : AuthModel( + type: APIAuthType.bearer, + bearer: AuthBearerModel(token: aiRequestModel.apiKey!), + ), + body: kJsonEncoder.convert({ + "model": aiRequestModel.model, + "messages": [ + {"role": "system", "content": aiRequestModel.systemPrompt}, + if (aiRequestModel.userPrompt.isNotEmpty) ...{ + {"role": "user", "content": aiRequestModel.userPrompt}, + } else ...{ + {"role": "user", "content": "Generate"}, + }, + ], + ...aiRequestModel.getModelConfigMap(), + if (aiRequestModel.stream ?? false) ...{'stream': true}, + }), + ); + } + + @override + String? outputFormatter(Map x) { + return x["choices"]?[0]["message"]?["content"]?.trim(); + } + + @override + String? streamOutputFormatter(Map x) { + return x["choices"]?[0]["delta"]?["content"]; + } +} diff --git a/packages/genai/lib/models/ai_request_model.dart b/packages/genai/lib/models/ai_request_model.dart new file mode 100644 index 00000000..e9a3a297 --- /dev/null +++ b/packages/genai/lib/models/ai_request_model.dart @@ -0,0 +1,54 @@ +import 'package:better_networking/better_networking.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +import '../interface/interface.dart'; +import 'model_config.dart'; +part 'ai_request_model.freezed.dart'; +part 'ai_request_model.g.dart'; + +@freezed +class AIRequestModel with _$AIRequestModel { + const AIRequestModel._(); + + @JsonSerializable(explicitToJson: true, anyMap: true) + const factory AIRequestModel({ + ModelAPIProvider? modelApiProvider, + @Default("") String url, + @Default(null) String? model, + @Default(null) String? apiKey, + @JsonKey(name: "system_prompt") @Default("") String systemPrompt, + @JsonKey(name: "user_prompt") @Default("") String userPrompt, + @JsonKey(name: "model_configs") + @Default([]) + List modelConfigs, + @Default(null) bool? stream, + }) = _AIRequestModel; + + factory AIRequestModel.fromJson(Map json) => + _$AIRequestModelFromJson(json); + + HttpRequestModel? get httpRequestModel => + kModelProvidersMap[modelApiProvider]?.createRequest(this); + + String? getFormattedOutput(Map x) => + kModelProvidersMap[modelApiProvider]?.outputFormatter(x); + + String? getFormattedStreamOutput(Map x) => + kModelProvidersMap[modelApiProvider]?.streamOutputFormatter(x); + + Map getModelConfigMap() { + Map m = {}; + for (var config in modelConfigs) { + m[config.id] = config.value.getPayloadValue(); + } + return m; + } + + int? getModelConfigIdx(String id) { + for (var idx = 0; idx < modelConfigs.length; idx++) { + if (modelConfigs[idx].id == id) { + return idx; + } + } + return null; + } +} diff --git a/packages/genai/lib/models/ai_request_model.freezed.dart b/packages/genai/lib/models/ai_request_model.freezed.dart new file mode 100644 index 00000000..7cec0388 --- /dev/null +++ b/packages/genai/lib/models/ai_request_model.freezed.dart @@ -0,0 +1,361 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'ai_request_model.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(T value) => value; + +final _privateConstructorUsedError = UnsupportedError( + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models', +); + +AIRequestModel _$AIRequestModelFromJson(Map json) { + return _AIRequestModel.fromJson(json); +} + +/// @nodoc +mixin _$AIRequestModel { + ModelAPIProvider? get modelApiProvider => throw _privateConstructorUsedError; + String get url => throw _privateConstructorUsedError; + String? get model => throw _privateConstructorUsedError; + String? get apiKey => throw _privateConstructorUsedError; + @JsonKey(name: "system_prompt") + String get systemPrompt => throw _privateConstructorUsedError; + @JsonKey(name: "user_prompt") + String get userPrompt => throw _privateConstructorUsedError; + @JsonKey(name: "model_configs") + List get modelConfigs => throw _privateConstructorUsedError; + bool? get stream => throw _privateConstructorUsedError; + + /// Serializes this AIRequestModel to a JSON map. + Map toJson() => throw _privateConstructorUsedError; + + /// Create a copy of AIRequestModel + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $AIRequestModelCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $AIRequestModelCopyWith<$Res> { + factory $AIRequestModelCopyWith( + AIRequestModel value, + $Res Function(AIRequestModel) then, + ) = _$AIRequestModelCopyWithImpl<$Res, AIRequestModel>; + @useResult + $Res call({ + ModelAPIProvider? modelApiProvider, + String url, + String? model, + String? apiKey, + @JsonKey(name: "system_prompt") String systemPrompt, + @JsonKey(name: "user_prompt") String userPrompt, + @JsonKey(name: "model_configs") List modelConfigs, + bool? stream, + }); +} + +/// @nodoc +class _$AIRequestModelCopyWithImpl<$Res, $Val extends AIRequestModel> + implements $AIRequestModelCopyWith<$Res> { + _$AIRequestModelCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of AIRequestModel + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? modelApiProvider = freezed, + Object? url = null, + Object? model = freezed, + Object? apiKey = freezed, + Object? systemPrompt = null, + Object? userPrompt = null, + Object? modelConfigs = null, + Object? stream = freezed, + }) { + return _then( + _value.copyWith( + modelApiProvider: freezed == modelApiProvider + ? _value.modelApiProvider + : modelApiProvider // ignore: cast_nullable_to_non_nullable + as ModelAPIProvider?, + url: null == url + ? _value.url + : url // ignore: cast_nullable_to_non_nullable + as String, + model: freezed == model + ? _value.model + : model // ignore: cast_nullable_to_non_nullable + as String?, + apiKey: freezed == apiKey + ? _value.apiKey + : apiKey // ignore: cast_nullable_to_non_nullable + as String?, + systemPrompt: null == systemPrompt + ? _value.systemPrompt + : systemPrompt // ignore: cast_nullable_to_non_nullable + as String, + userPrompt: null == userPrompt + ? _value.userPrompt + : userPrompt // ignore: cast_nullable_to_non_nullable + as String, + modelConfigs: null == modelConfigs + ? _value.modelConfigs + : modelConfigs // ignore: cast_nullable_to_non_nullable + as List, + stream: freezed == stream + ? _value.stream + : stream // ignore: cast_nullable_to_non_nullable + as bool?, + ) + as $Val, + ); + } +} + +/// @nodoc +abstract class _$$AIRequestModelImplCopyWith<$Res> + implements $AIRequestModelCopyWith<$Res> { + factory _$$AIRequestModelImplCopyWith( + _$AIRequestModelImpl value, + $Res Function(_$AIRequestModelImpl) then, + ) = __$$AIRequestModelImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({ + ModelAPIProvider? modelApiProvider, + String url, + String? model, + String? apiKey, + @JsonKey(name: "system_prompt") String systemPrompt, + @JsonKey(name: "user_prompt") String userPrompt, + @JsonKey(name: "model_configs") List modelConfigs, + bool? stream, + }); +} + +/// @nodoc +class __$$AIRequestModelImplCopyWithImpl<$Res> + extends _$AIRequestModelCopyWithImpl<$Res, _$AIRequestModelImpl> + implements _$$AIRequestModelImplCopyWith<$Res> { + __$$AIRequestModelImplCopyWithImpl( + _$AIRequestModelImpl _value, + $Res Function(_$AIRequestModelImpl) _then, + ) : super(_value, _then); + + /// Create a copy of AIRequestModel + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? modelApiProvider = freezed, + Object? url = null, + Object? model = freezed, + Object? apiKey = freezed, + Object? systemPrompt = null, + Object? userPrompt = null, + Object? modelConfigs = null, + Object? stream = freezed, + }) { + return _then( + _$AIRequestModelImpl( + modelApiProvider: freezed == modelApiProvider + ? _value.modelApiProvider + : modelApiProvider // ignore: cast_nullable_to_non_nullable + as ModelAPIProvider?, + url: null == url + ? _value.url + : url // ignore: cast_nullable_to_non_nullable + as String, + model: freezed == model + ? _value.model + : model // ignore: cast_nullable_to_non_nullable + as String?, + apiKey: freezed == apiKey + ? _value.apiKey + : apiKey // ignore: cast_nullable_to_non_nullable + as String?, + systemPrompt: null == systemPrompt + ? _value.systemPrompt + : systemPrompt // ignore: cast_nullable_to_non_nullable + as String, + userPrompt: null == userPrompt + ? _value.userPrompt + : userPrompt // ignore: cast_nullable_to_non_nullable + as String, + modelConfigs: null == modelConfigs + ? _value._modelConfigs + : modelConfigs // ignore: cast_nullable_to_non_nullable + as List, + stream: freezed == stream + ? _value.stream + : stream // ignore: cast_nullable_to_non_nullable + as bool?, + ), + ); + } +} + +/// @nodoc + +@JsonSerializable(explicitToJson: true, anyMap: true) +class _$AIRequestModelImpl extends _AIRequestModel { + const _$AIRequestModelImpl({ + this.modelApiProvider, + this.url = "", + this.model = null, + this.apiKey = null, + @JsonKey(name: "system_prompt") this.systemPrompt = "", + @JsonKey(name: "user_prompt") this.userPrompt = "", + @JsonKey(name: "model_configs") + final List modelConfigs = const [], + this.stream = null, + }) : _modelConfigs = modelConfigs, + super._(); + + factory _$AIRequestModelImpl.fromJson(Map json) => + _$$AIRequestModelImplFromJson(json); + + @override + final ModelAPIProvider? modelApiProvider; + @override + @JsonKey() + final String url; + @override + @JsonKey() + final String? model; + @override + @JsonKey() + final String? apiKey; + @override + @JsonKey(name: "system_prompt") + final String systemPrompt; + @override + @JsonKey(name: "user_prompt") + final String userPrompt; + final List _modelConfigs; + @override + @JsonKey(name: "model_configs") + List get modelConfigs { + if (_modelConfigs is EqualUnmodifiableListView) return _modelConfigs; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_modelConfigs); + } + + @override + @JsonKey() + final bool? stream; + + @override + String toString() { + return 'AIRequestModel(modelApiProvider: $modelApiProvider, url: $url, model: $model, apiKey: $apiKey, systemPrompt: $systemPrompt, userPrompt: $userPrompt, modelConfigs: $modelConfigs, stream: $stream)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$AIRequestModelImpl && + (identical(other.modelApiProvider, modelApiProvider) || + other.modelApiProvider == modelApiProvider) && + (identical(other.url, url) || other.url == url) && + (identical(other.model, model) || other.model == model) && + (identical(other.apiKey, apiKey) || other.apiKey == apiKey) && + (identical(other.systemPrompt, systemPrompt) || + other.systemPrompt == systemPrompt) && + (identical(other.userPrompt, userPrompt) || + other.userPrompt == userPrompt) && + const DeepCollectionEquality().equals( + other._modelConfigs, + _modelConfigs, + ) && + (identical(other.stream, stream) || other.stream == stream)); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => Object.hash( + runtimeType, + modelApiProvider, + url, + model, + apiKey, + systemPrompt, + userPrompt, + const DeepCollectionEquality().hash(_modelConfigs), + stream, + ); + + /// Create a copy of AIRequestModel + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$AIRequestModelImplCopyWith<_$AIRequestModelImpl> get copyWith => + __$$AIRequestModelImplCopyWithImpl<_$AIRequestModelImpl>( + this, + _$identity, + ); + + @override + Map toJson() { + return _$$AIRequestModelImplToJson(this); + } +} + +abstract class _AIRequestModel extends AIRequestModel { + const factory _AIRequestModel({ + final ModelAPIProvider? modelApiProvider, + final String url, + final String? model, + final String? apiKey, + @JsonKey(name: "system_prompt") final String systemPrompt, + @JsonKey(name: "user_prompt") final String userPrompt, + @JsonKey(name: "model_configs") final List modelConfigs, + final bool? stream, + }) = _$AIRequestModelImpl; + const _AIRequestModel._() : super._(); + + factory _AIRequestModel.fromJson(Map json) = + _$AIRequestModelImpl.fromJson; + + @override + ModelAPIProvider? get modelApiProvider; + @override + String get url; + @override + String? get model; + @override + String? get apiKey; + @override + @JsonKey(name: "system_prompt") + String get systemPrompt; + @override + @JsonKey(name: "user_prompt") + String get userPrompt; + @override + @JsonKey(name: "model_configs") + List get modelConfigs; + @override + bool? get stream; + + /// Create a copy of AIRequestModel + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$AIRequestModelImplCopyWith<_$AIRequestModelImpl> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/packages/genai/lib/models/ai_request_model.g.dart b/packages/genai/lib/models/ai_request_model.g.dart new file mode 100644 index 00000000..66db3eee --- /dev/null +++ b/packages/genai/lib/models/ai_request_model.g.dart @@ -0,0 +1,47 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'ai_request_model.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +_$AIRequestModelImpl _$$AIRequestModelImplFromJson(Map json) => + _$AIRequestModelImpl( + modelApiProvider: $enumDecodeNullable( + _$ModelAPIProviderEnumMap, + json['modelApiProvider'], + ), + url: json['url'] as String? ?? "", + model: json['model'] as String? ?? null, + apiKey: json['apiKey'] as String? ?? null, + systemPrompt: json['system_prompt'] as String? ?? "", + userPrompt: json['user_prompt'] as String? ?? "", + modelConfigs: + (json['model_configs'] as List?) + ?.map((e) => ModelConfig.fromJson(e as Map)) + .toList() ?? + const [], + stream: json['stream'] as bool? ?? null, + ); + +Map _$$AIRequestModelImplToJson( + _$AIRequestModelImpl instance, +) => { + 'modelApiProvider': _$ModelAPIProviderEnumMap[instance.modelApiProvider], + 'url': instance.url, + 'model': instance.model, + 'apiKey': instance.apiKey, + 'system_prompt': instance.systemPrompt, + 'user_prompt': instance.userPrompt, + 'model_configs': instance.modelConfigs.map((e) => e.toJson()).toList(), + 'stream': instance.stream, +}; + +const _$ModelAPIProviderEnumMap = { + ModelAPIProvider.openai: 'openai', + ModelAPIProvider.anthropic: 'anthropic', + ModelAPIProvider.gemini: 'gemini', + ModelAPIProvider.azureopenai: 'azureopenai', + ModelAPIProvider.ollama: 'ollama', +}; diff --git a/packages/genai/lib/models/available_models.dart b/packages/genai/lib/models/available_models.dart new file mode 100644 index 00000000..3b6d4c1d --- /dev/null +++ b/packages/genai/lib/models/available_models.dart @@ -0,0 +1,67 @@ +// To parse this JSON data, do +// +// final availableModels = availableModelsFromJson(jsonString); + +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'dart:convert'; +import '../interface/interface.dart'; +import 'ai_request_model.dart'; +part 'available_models.freezed.dart'; +part 'available_models.g.dart'; + +AvailableModels availableModelsFromJson(String str) => + AvailableModels.fromJson(json.decode(str)); + +String availableModelsToJson(AvailableModels data) => + json.encode(data.toJson()); + +@freezed +class AvailableModels with _$AvailableModels { + const AvailableModels._(); + const factory AvailableModels({ + @JsonKey(name: "version") required double version, + @JsonKey(name: "model_providers") + required List modelProviders, + }) = _AvailableModels; + + factory AvailableModels.fromJson(Map json) => + _$AvailableModelsFromJson(json); + + Map get map => + modelProviders.asMap().map( + (i, d) => MapEntry(d.providerId!, d), + ); +} + +@freezed +class AIModelProvider with _$AIModelProvider { + const AIModelProvider._(); + + const factory AIModelProvider({ + @JsonKey(name: "provider_id") ModelAPIProvider? providerId, + @JsonKey(name: "provider_name") String? providerName, + @JsonKey(name: "source_url") String? sourceUrl, + @JsonKey(name: "models") List? models, + }) = _AIModelProvider; + + factory AIModelProvider.fromJson(Map json) => + _$AIModelProviderFromJson(json); + + AIRequestModel? toAiRequestModel({Model? model}) { + var aiRequest = kModelProvidersMap[providerId]?.defaultAIRequestModel; + if (model != null) { + aiRequest = aiRequest?.copyWith(model: model.id); + } + return aiRequest; + } +} + +@freezed +class Model with _$Model { + const factory Model({ + @JsonKey(name: "id") String? id, + @JsonKey(name: "name") String? name, + }) = _Model; + + factory Model.fromJson(Map json) => _$ModelFromJson(json); +} diff --git a/packages/genai/lib/models/available_models.freezed.dart b/packages/genai/lib/models/available_models.freezed.dart new file mode 100644 index 00000000..32ce0905 --- /dev/null +++ b/packages/genai/lib/models/available_models.freezed.dart @@ -0,0 +1,653 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'available_models.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(T value) => value; + +final _privateConstructorUsedError = UnsupportedError( + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models', +); + +AvailableModels _$AvailableModelsFromJson(Map json) { + return _AvailableModels.fromJson(json); +} + +/// @nodoc +mixin _$AvailableModels { + @JsonKey(name: "version") + double get version => throw _privateConstructorUsedError; + @JsonKey(name: "model_providers") + List get modelProviders => + throw _privateConstructorUsedError; + + /// Serializes this AvailableModels to a JSON map. + Map toJson() => throw _privateConstructorUsedError; + + /// Create a copy of AvailableModels + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $AvailableModelsCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $AvailableModelsCopyWith<$Res> { + factory $AvailableModelsCopyWith( + AvailableModels value, + $Res Function(AvailableModels) then, + ) = _$AvailableModelsCopyWithImpl<$Res, AvailableModels>; + @useResult + $Res call({ + @JsonKey(name: "version") double version, + @JsonKey(name: "model_providers") List modelProviders, + }); +} + +/// @nodoc +class _$AvailableModelsCopyWithImpl<$Res, $Val extends AvailableModels> + implements $AvailableModelsCopyWith<$Res> { + _$AvailableModelsCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of AvailableModels + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({Object? version = null, Object? modelProviders = null}) { + return _then( + _value.copyWith( + version: null == version + ? _value.version + : version // ignore: cast_nullable_to_non_nullable + as double, + modelProviders: null == modelProviders + ? _value.modelProviders + : modelProviders // ignore: cast_nullable_to_non_nullable + as List, + ) + as $Val, + ); + } +} + +/// @nodoc +abstract class _$$AvailableModelsImplCopyWith<$Res> + implements $AvailableModelsCopyWith<$Res> { + factory _$$AvailableModelsImplCopyWith( + _$AvailableModelsImpl value, + $Res Function(_$AvailableModelsImpl) then, + ) = __$$AvailableModelsImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({ + @JsonKey(name: "version") double version, + @JsonKey(name: "model_providers") List modelProviders, + }); +} + +/// @nodoc +class __$$AvailableModelsImplCopyWithImpl<$Res> + extends _$AvailableModelsCopyWithImpl<$Res, _$AvailableModelsImpl> + implements _$$AvailableModelsImplCopyWith<$Res> { + __$$AvailableModelsImplCopyWithImpl( + _$AvailableModelsImpl _value, + $Res Function(_$AvailableModelsImpl) _then, + ) : super(_value, _then); + + /// Create a copy of AvailableModels + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({Object? version = null, Object? modelProviders = null}) { + return _then( + _$AvailableModelsImpl( + version: null == version + ? _value.version + : version // ignore: cast_nullable_to_non_nullable + as double, + modelProviders: null == modelProviders + ? _value._modelProviders + : modelProviders // ignore: cast_nullable_to_non_nullable + as List, + ), + ); + } +} + +/// @nodoc +@JsonSerializable() +class _$AvailableModelsImpl extends _AvailableModels { + const _$AvailableModelsImpl({ + @JsonKey(name: "version") required this.version, + @JsonKey(name: "model_providers") + required final List modelProviders, + }) : _modelProviders = modelProviders, + super._(); + + factory _$AvailableModelsImpl.fromJson(Map json) => + _$$AvailableModelsImplFromJson(json); + + @override + @JsonKey(name: "version") + final double version; + final List _modelProviders; + @override + @JsonKey(name: "model_providers") + List get modelProviders { + if (_modelProviders is EqualUnmodifiableListView) return _modelProviders; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_modelProviders); + } + + @override + String toString() { + return 'AvailableModels(version: $version, modelProviders: $modelProviders)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$AvailableModelsImpl && + (identical(other.version, version) || other.version == version) && + const DeepCollectionEquality().equals( + other._modelProviders, + _modelProviders, + )); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => Object.hash( + runtimeType, + version, + const DeepCollectionEquality().hash(_modelProviders), + ); + + /// Create a copy of AvailableModels + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$AvailableModelsImplCopyWith<_$AvailableModelsImpl> get copyWith => + __$$AvailableModelsImplCopyWithImpl<_$AvailableModelsImpl>( + this, + _$identity, + ); + + @override + Map toJson() { + return _$$AvailableModelsImplToJson(this); + } +} + +abstract class _AvailableModels extends AvailableModels { + const factory _AvailableModels({ + @JsonKey(name: "version") required final double version, + @JsonKey(name: "model_providers") + required final List modelProviders, + }) = _$AvailableModelsImpl; + const _AvailableModels._() : super._(); + + factory _AvailableModels.fromJson(Map json) = + _$AvailableModelsImpl.fromJson; + + @override + @JsonKey(name: "version") + double get version; + @override + @JsonKey(name: "model_providers") + List get modelProviders; + + /// Create a copy of AvailableModels + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$AvailableModelsImplCopyWith<_$AvailableModelsImpl> get copyWith => + throw _privateConstructorUsedError; +} + +AIModelProvider _$AIModelProviderFromJson(Map json) { + return _AIModelProvider.fromJson(json); +} + +/// @nodoc +mixin _$AIModelProvider { + @JsonKey(name: "provider_id") + ModelAPIProvider? get providerId => throw _privateConstructorUsedError; + @JsonKey(name: "provider_name") + String? get providerName => throw _privateConstructorUsedError; + @JsonKey(name: "source_url") + String? get sourceUrl => throw _privateConstructorUsedError; + @JsonKey(name: "models") + List? get models => throw _privateConstructorUsedError; + + /// Serializes this AIModelProvider to a JSON map. + Map toJson() => throw _privateConstructorUsedError; + + /// Create a copy of AIModelProvider + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $AIModelProviderCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $AIModelProviderCopyWith<$Res> { + factory $AIModelProviderCopyWith( + AIModelProvider value, + $Res Function(AIModelProvider) then, + ) = _$AIModelProviderCopyWithImpl<$Res, AIModelProvider>; + @useResult + $Res call({ + @JsonKey(name: "provider_id") ModelAPIProvider? providerId, + @JsonKey(name: "provider_name") String? providerName, + @JsonKey(name: "source_url") String? sourceUrl, + @JsonKey(name: "models") List? models, + }); +} + +/// @nodoc +class _$AIModelProviderCopyWithImpl<$Res, $Val extends AIModelProvider> + implements $AIModelProviderCopyWith<$Res> { + _$AIModelProviderCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of AIModelProvider + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? providerId = freezed, + Object? providerName = freezed, + Object? sourceUrl = freezed, + Object? models = freezed, + }) { + return _then( + _value.copyWith( + providerId: freezed == providerId + ? _value.providerId + : providerId // ignore: cast_nullable_to_non_nullable + as ModelAPIProvider?, + providerName: freezed == providerName + ? _value.providerName + : providerName // ignore: cast_nullable_to_non_nullable + as String?, + sourceUrl: freezed == sourceUrl + ? _value.sourceUrl + : sourceUrl // ignore: cast_nullable_to_non_nullable + as String?, + models: freezed == models + ? _value.models + : models // ignore: cast_nullable_to_non_nullable + as List?, + ) + as $Val, + ); + } +} + +/// @nodoc +abstract class _$$AIModelProviderImplCopyWith<$Res> + implements $AIModelProviderCopyWith<$Res> { + factory _$$AIModelProviderImplCopyWith( + _$AIModelProviderImpl value, + $Res Function(_$AIModelProviderImpl) then, + ) = __$$AIModelProviderImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({ + @JsonKey(name: "provider_id") ModelAPIProvider? providerId, + @JsonKey(name: "provider_name") String? providerName, + @JsonKey(name: "source_url") String? sourceUrl, + @JsonKey(name: "models") List? models, + }); +} + +/// @nodoc +class __$$AIModelProviderImplCopyWithImpl<$Res> + extends _$AIModelProviderCopyWithImpl<$Res, _$AIModelProviderImpl> + implements _$$AIModelProviderImplCopyWith<$Res> { + __$$AIModelProviderImplCopyWithImpl( + _$AIModelProviderImpl _value, + $Res Function(_$AIModelProviderImpl) _then, + ) : super(_value, _then); + + /// Create a copy of AIModelProvider + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? providerId = freezed, + Object? providerName = freezed, + Object? sourceUrl = freezed, + Object? models = freezed, + }) { + return _then( + _$AIModelProviderImpl( + providerId: freezed == providerId + ? _value.providerId + : providerId // ignore: cast_nullable_to_non_nullable + as ModelAPIProvider?, + providerName: freezed == providerName + ? _value.providerName + : providerName // ignore: cast_nullable_to_non_nullable + as String?, + sourceUrl: freezed == sourceUrl + ? _value.sourceUrl + : sourceUrl // ignore: cast_nullable_to_non_nullable + as String?, + models: freezed == models + ? _value._models + : models // ignore: cast_nullable_to_non_nullable + as List?, + ), + ); + } +} + +/// @nodoc +@JsonSerializable() +class _$AIModelProviderImpl extends _AIModelProvider { + const _$AIModelProviderImpl({ + @JsonKey(name: "provider_id") this.providerId, + @JsonKey(name: "provider_name") this.providerName, + @JsonKey(name: "source_url") this.sourceUrl, + @JsonKey(name: "models") final List? models, + }) : _models = models, + super._(); + + factory _$AIModelProviderImpl.fromJson(Map json) => + _$$AIModelProviderImplFromJson(json); + + @override + @JsonKey(name: "provider_id") + final ModelAPIProvider? providerId; + @override + @JsonKey(name: "provider_name") + final String? providerName; + @override + @JsonKey(name: "source_url") + final String? sourceUrl; + final List? _models; + @override + @JsonKey(name: "models") + List? get models { + final value = _models; + if (value == null) return null; + if (_models is EqualUnmodifiableListView) return _models; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(value); + } + + @override + String toString() { + return 'AIModelProvider(providerId: $providerId, providerName: $providerName, sourceUrl: $sourceUrl, models: $models)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$AIModelProviderImpl && + (identical(other.providerId, providerId) || + other.providerId == providerId) && + (identical(other.providerName, providerName) || + other.providerName == providerName) && + (identical(other.sourceUrl, sourceUrl) || + other.sourceUrl == sourceUrl) && + const DeepCollectionEquality().equals(other._models, _models)); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => Object.hash( + runtimeType, + providerId, + providerName, + sourceUrl, + const DeepCollectionEquality().hash(_models), + ); + + /// Create a copy of AIModelProvider + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$AIModelProviderImplCopyWith<_$AIModelProviderImpl> get copyWith => + __$$AIModelProviderImplCopyWithImpl<_$AIModelProviderImpl>( + this, + _$identity, + ); + + @override + Map toJson() { + return _$$AIModelProviderImplToJson(this); + } +} + +abstract class _AIModelProvider extends AIModelProvider { + const factory _AIModelProvider({ + @JsonKey(name: "provider_id") final ModelAPIProvider? providerId, + @JsonKey(name: "provider_name") final String? providerName, + @JsonKey(name: "source_url") final String? sourceUrl, + @JsonKey(name: "models") final List? models, + }) = _$AIModelProviderImpl; + const _AIModelProvider._() : super._(); + + factory _AIModelProvider.fromJson(Map json) = + _$AIModelProviderImpl.fromJson; + + @override + @JsonKey(name: "provider_id") + ModelAPIProvider? get providerId; + @override + @JsonKey(name: "provider_name") + String? get providerName; + @override + @JsonKey(name: "source_url") + String? get sourceUrl; + @override + @JsonKey(name: "models") + List? get models; + + /// Create a copy of AIModelProvider + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$AIModelProviderImplCopyWith<_$AIModelProviderImpl> get copyWith => + throw _privateConstructorUsedError; +} + +Model _$ModelFromJson(Map json) { + return _Model.fromJson(json); +} + +/// @nodoc +mixin _$Model { + @JsonKey(name: "id") + String? get id => throw _privateConstructorUsedError; + @JsonKey(name: "name") + String? get name => throw _privateConstructorUsedError; + + /// Serializes this Model to a JSON map. + Map toJson() => throw _privateConstructorUsedError; + + /// Create a copy of Model + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $ModelCopyWith get copyWith => throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $ModelCopyWith<$Res> { + factory $ModelCopyWith(Model value, $Res Function(Model) then) = + _$ModelCopyWithImpl<$Res, Model>; + @useResult + $Res call({ + @JsonKey(name: "id") String? id, + @JsonKey(name: "name") String? name, + }); +} + +/// @nodoc +class _$ModelCopyWithImpl<$Res, $Val extends Model> + implements $ModelCopyWith<$Res> { + _$ModelCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of Model + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({Object? id = freezed, Object? name = freezed}) { + return _then( + _value.copyWith( + id: freezed == id + ? _value.id + : id // ignore: cast_nullable_to_non_nullable + as String?, + name: freezed == name + ? _value.name + : name // ignore: cast_nullable_to_non_nullable + as String?, + ) + as $Val, + ); + } +} + +/// @nodoc +abstract class _$$ModelImplCopyWith<$Res> implements $ModelCopyWith<$Res> { + factory _$$ModelImplCopyWith( + _$ModelImpl value, + $Res Function(_$ModelImpl) then, + ) = __$$ModelImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({ + @JsonKey(name: "id") String? id, + @JsonKey(name: "name") String? name, + }); +} + +/// @nodoc +class __$$ModelImplCopyWithImpl<$Res> + extends _$ModelCopyWithImpl<$Res, _$ModelImpl> + implements _$$ModelImplCopyWith<$Res> { + __$$ModelImplCopyWithImpl( + _$ModelImpl _value, + $Res Function(_$ModelImpl) _then, + ) : super(_value, _then); + + /// Create a copy of Model + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({Object? id = freezed, Object? name = freezed}) { + return _then( + _$ModelImpl( + id: freezed == id + ? _value.id + : id // ignore: cast_nullable_to_non_nullable + as String?, + name: freezed == name + ? _value.name + : name // ignore: cast_nullable_to_non_nullable + as String?, + ), + ); + } +} + +/// @nodoc +@JsonSerializable() +class _$ModelImpl implements _Model { + const _$ModelImpl({ + @JsonKey(name: "id") this.id, + @JsonKey(name: "name") this.name, + }); + + factory _$ModelImpl.fromJson(Map json) => + _$$ModelImplFromJson(json); + + @override + @JsonKey(name: "id") + final String? id; + @override + @JsonKey(name: "name") + final String? name; + + @override + String toString() { + return 'Model(id: $id, name: $name)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$ModelImpl && + (identical(other.id, id) || other.id == id) && + (identical(other.name, name) || other.name == name)); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => Object.hash(runtimeType, id, name); + + /// Create a copy of Model + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$ModelImplCopyWith<_$ModelImpl> get copyWith => + __$$ModelImplCopyWithImpl<_$ModelImpl>(this, _$identity); + + @override + Map toJson() { + return _$$ModelImplToJson(this); + } +} + +abstract class _Model implements Model { + const factory _Model({ + @JsonKey(name: "id") final String? id, + @JsonKey(name: "name") final String? name, + }) = _$ModelImpl; + + factory _Model.fromJson(Map json) = _$ModelImpl.fromJson; + + @override + @JsonKey(name: "id") + String? get id; + @override + @JsonKey(name: "name") + String? get name; + + /// Create a copy of Model + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$ModelImplCopyWith<_$ModelImpl> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/packages/genai/lib/models/available_models.g.dart b/packages/genai/lib/models/available_models.g.dart new file mode 100644 index 00000000..a8bb7816 --- /dev/null +++ b/packages/genai/lib/models/available_models.g.dart @@ -0,0 +1,60 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'available_models.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +_$AvailableModelsImpl _$$AvailableModelsImplFromJson( + Map json, +) => _$AvailableModelsImpl( + version: (json['version'] as num).toDouble(), + modelProviders: (json['model_providers'] as List) + .map((e) => AIModelProvider.fromJson(e as Map)) + .toList(), +); + +Map _$$AvailableModelsImplToJson( + _$AvailableModelsImpl instance, +) => { + 'version': instance.version, + 'model_providers': instance.modelProviders, +}; + +_$AIModelProviderImpl _$$AIModelProviderImplFromJson( + Map json, +) => _$AIModelProviderImpl( + providerId: $enumDecodeNullable( + _$ModelAPIProviderEnumMap, + json['provider_id'], + ), + providerName: json['provider_name'] as String?, + sourceUrl: json['source_url'] as String?, + models: (json['models'] as List?) + ?.map((e) => Model.fromJson(e as Map)) + .toList(), +); + +Map _$$AIModelProviderImplToJson( + _$AIModelProviderImpl instance, +) => { + 'provider_id': _$ModelAPIProviderEnumMap[instance.providerId], + 'provider_name': instance.providerName, + 'source_url': instance.sourceUrl, + 'models': instance.models, +}; + +const _$ModelAPIProviderEnumMap = { + ModelAPIProvider.openai: 'openai', + ModelAPIProvider.anthropic: 'anthropic', + ModelAPIProvider.gemini: 'gemini', + ModelAPIProvider.azureopenai: 'azureopenai', + ModelAPIProvider.ollama: 'ollama', +}; + +_$ModelImpl _$$ModelImplFromJson(Map json) => + _$ModelImpl(id: json['id'] as String?, name: json['name'] as String?); + +Map _$$ModelImplToJson(_$ModelImpl instance) => + {'id': instance.id, 'name': instance.name}; diff --git a/packages/genai/lib/models/model_config.dart b/packages/genai/lib/models/model_config.dart new file mode 100644 index 00000000..15868d0c --- /dev/null +++ b/packages/genai/lib/models/model_config.dart @@ -0,0 +1,74 @@ +import 'model_config_value.dart'; + +class ModelConfig { + final String id; + final String name; + final String description; + final ConfigType type; + final ConfigValue value; + + ModelConfig({ + required this.id, + required this.name, + required this.description, + required this.type, + required this.value, + }) { + assert(checkTypeValue(type, value)); + } + + ModelConfig updateValue(ConfigValue value) { + return ModelConfig( + id: id, + name: name, + description: description, + type: type, + value: value, + ); + } + + factory ModelConfig.fromJson(Map x) { + final id = x['id'] as String?; + final name = x['name'] as String?; + final description = x['description'] as String?; + final type = x['type'] as String?; + final value = x['value'] as String?; + + final cT = getConfigTypeEnum(type); + final cV = deserilizeValue(cT, value); + + return ModelConfig( + id: id ?? "", + name: name ?? "", + description: description ?? "", + type: cT, + value: cV, + ); + } + + Map toJson() { + return { + 'id': id, + 'name': name, + 'description': description, + 'type': type.name.toString(), + 'value': value.serialize(), + }; + } + + ModelConfig copyWith({ + String? id, + String? name, + String? description, + ConfigType? type, + ConfigValue? value, + }) { + return ModelConfig( + id: id ?? this.id, + name: name ?? this.name, + description: description ?? this.description, + type: type ?? this.type, + value: value ?? this.value, + ); + } +} diff --git a/packages/genai/lib/models/model_config_value.dart b/packages/genai/lib/models/model_config_value.dart new file mode 100644 index 00000000..1aa27552 --- /dev/null +++ b/packages/genai/lib/models/model_config_value.dart @@ -0,0 +1,109 @@ +import 'dart:convert'; +import 'package:flutter/material.dart'; + +enum ConfigType { boolean, slider, numeric, text } + +ConfigType getConfigTypeEnum(String? t) { + try { + final val = ConfigType.values.byName(t ?? ""); + return val; + } catch (e) { + debugPrint("ConfigType <$t> not found."); + return ConfigType.text; + } +} + +bool checkTypeValue(ConfigType t, dynamic v) { + return switch (t) { + ConfigType.boolean => v is ConfigBooleanValue, + ConfigType.slider => v is ConfigSliderValue, + ConfigType.numeric => v is ConfigNumericValue, + ConfigType.text => v is ConfigTextValue, + }; +} + +dynamic deserilizeValue(ConfigType t, String? v) { + return switch (t) { + ConfigType.boolean => ConfigBooleanValue.deserialize(v ?? ""), + ConfigType.slider => ConfigSliderValue.deserialize(v ?? ""), + ConfigType.numeric => ConfigNumericValue.deserialize(v ?? ""), + ConfigType.text => ConfigTextValue.deserialize(v ?? ""), + }; +} + +abstract class ConfigValue { + ConfigValue(this.value); + + dynamic value; + + String serialize(); + + dynamic getPayloadValue() { + return value; + } +} + +class ConfigBooleanValue extends ConfigValue { + ConfigBooleanValue({required bool value}) : super(value); + + @override + String serialize() { + return value.toString(); + } + + static ConfigBooleanValue deserialize(String x) { + return ConfigBooleanValue(value: x == 'true'); + } +} + +class ConfigNumericValue extends ConfigValue { + ConfigNumericValue({required num? value}) : super(value); + + @override + String serialize() { + return value.toString(); + } + + static ConfigNumericValue deserialize(String x) { + return ConfigNumericValue(value: num.tryParse(x)); + } +} + +class ConfigSliderValue extends ConfigValue { + ConfigSliderValue({required (double, double, double) value}) : super(value); + + @override + String serialize() { + final v = value as (double, double, double); + return jsonEncode([v.$1, v.$2, v.$3]); + } + + @override + dynamic getPayloadValue() { + final v = value as (double, double, double); + return v.$2; + } + + static ConfigSliderValue deserialize(String x) { + final z = jsonDecode(x) as List; + final val = ( + double.parse(z[0].toString()), + double.parse(z[1].toString()), + double.parse(z[2].toString()), + ); + return ConfigSliderValue(value: val); + } +} + +class ConfigTextValue extends ConfigValue { + ConfigTextValue({required String value}) : super(value); + + @override + String serialize() { + return value.toString(); + } + + static ConfigTextValue deserialize(String x) { + return ConfigTextValue(value: x); + } +} diff --git a/packages/genai/lib/models/model_provider.dart b/packages/genai/lib/models/model_provider.dart new file mode 100644 index 00000000..c929645a --- /dev/null +++ b/packages/genai/lib/models/model_provider.dart @@ -0,0 +1,18 @@ +import 'package:better_networking/better_networking.dart'; +import '../models/models.dart'; + +abstract class ModelProvider { + AIRequestModel get defaultAIRequestModel => throw UnimplementedError(); + + HttpRequestModel? createRequest(AIRequestModel? aiRequestModel) { + throw UnimplementedError(); + } + + String? outputFormatter(Map x) { + throw UnimplementedError(); + } + + String? streamOutputFormatter(Map x) { + throw UnimplementedError(); + } +} diff --git a/packages/genai/lib/models/models.dart b/packages/genai/lib/models/models.dart new file mode 100644 index 00000000..5216053a --- /dev/null +++ b/packages/genai/lib/models/models.dart @@ -0,0 +1,6 @@ +export 'ai_request_model.dart'; +export 'available_models.dart'; +export 'model_config_value.dart'; +export 'model_config.dart'; +export 'model_provider.dart'; +export 'models_data.g.dart'; diff --git a/packages/genai/lib/models/models_data.g.dart b/packages/genai/lib/models/models_data.g.dart new file mode 100644 index 00000000..df125b5f --- /dev/null +++ b/packages/genai/lib/models/models_data.g.dart @@ -0,0 +1,2 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND +const kModelsData = {"version":1.0,"model_providers":[{"provider_id":"ollama","provider_name":"Ollama","source_url":null,"models":[{"id":"","name":"Custom"}]},{"provider_id":"openai","provider_name":"OpenAI","source_url":"https://platform.openai.com/docs/models","models":[{"id":"gpt-5","name":"GPT-5"},{"id":"gpt-5-mini","name":"GPT-5 mini"},{"id":"gpt-5-nano","name":"GPT-5 Nano"},{"id":"gpt-4.1","name":"GPT-4.1"},{"id":"gpt-oss-120b","name":"gpt-oss-120b"},{"id":"gpt-oss-20b","name":"gpt-oss-20b"},{"id":"o3-pro","name":"o3-pro"},{"id":"o3","name":"o3"},{"id":"o4-mini","name":"o4-mini"},{"id":"gpt-4o","name":"GPT-4o"},{"id":"gpt-4","name":"GPT-4"},{"id":"gpt-4o-mini","name":"GPT-4o Mini"},{"id":"","name":"Other"}]},{"provider_id":"anthropic","provider_name":"Anthropic","source_url":"https://docs.anthropic.com/en/docs/about-claude/models/overview","models":[{"id":"claude-opus-4-1","name":"Claude Opus 4.1"},{"id":"claude-opus-4-0","name":"Claude Opus 4"},{"id":"claude-sonnet-4-0","name":"Claude Sonnet 4"},{"id":"claude-3-7-sonnet-latest","name":"Claude Sonnet 3.7"},{"id":"claude-3-5-sonnet-latest","name":"Claude Sonnet 3.5"},{"id":"claude-3-5-haiku-latest","name":"Claude Haiku 3.5"},{"id":"","name":"Other"}]},{"provider_id":"gemini","provider_name":"Gemini","source_url":"https://ai.google.dev/gemini-api/docs/models","models":[{"id":"gemini-2.5-pro","name":"Gemini 2.5 Pro"},{"id":"gemini-2.5-flash","name":"Gemini 2.5 Flash"},{"id":"gemini-2.5-flash-lite","name":"Gemini 2.5 Flash-Lite"},{"id":"gemini-2.0-flash","name":"Gemini 2.0 Flash"},{"id":"gemini-2.0-flash-lite","name":"Gemini 2.0 Flash-Lite"},{"id":"","name":"Other"}]},{"provider_id":"azureopenai","provider_name":"Azure OpenAI","source_url":null,"models":[{"id":"","name":"Custom"}]}]}; diff --git a/packages/genai/lib/utils/ai_request_utils.dart b/packages/genai/lib/utils/ai_request_utils.dart new file mode 100644 index 00000000..a4b94481 --- /dev/null +++ b/packages/genai/lib/utils/ai_request_utils.dart @@ -0,0 +1,148 @@ +import 'dart:async'; +import 'dart:convert'; +import 'package:better_networking/better_networking.dart'; +import 'package:flutter/foundation.dart'; +import 'package:nanoid/nanoid.dart'; +import '../models/models.dart'; + +Future executeGenAIRequest(AIRequestModel? aiRequestModel) async { + final httpRequestModel = aiRequestModel?.httpRequestModel; + if (httpRequestModel == null) { + debugPrint("executeGenAIRequest -> httpRequestModel is null"); + return null; + } + final (response, _, _) = await sendHttpRequest( + nanoid(), + APIType.rest, + httpRequestModel, + ); + if (response == null) return null; + if (response.statusCode == 200) { + final data = jsonDecode(response.body); + return aiRequestModel?.getFormattedOutput(data); + } else { + debugPrint('LLM_EXCEPTION: ${response.statusCode}\n${response.body}'); + return null; + } +} + +Future> streamGenAIRequest( + AIRequestModel? aiRequestModel, +) async { + final httpRequestModel = aiRequestModel?.httpRequestModel; + final streamController = StreamController(); + if (httpRequestModel == null) { + debugPrint("streamGenAIRequest -> httpRequestModel is null"); + } else { + final httpStream = await streamHttpRequest( + nanoid(), + APIType.rest, + httpRequestModel, + ); + + final subscription = httpStream.listen( + (dat) { + if (dat == null) { + streamController.addError('STREAMING ERROR: NULL DATA'); + return; + } + + final chunk = dat.$2; + final error = dat.$4; + + if (chunk == null) { + streamController.addError(error ?? 'NULL ERROR'); + return; + } + + final ans = chunk.body; + + final lines = ans.split('\n'); + for (final line in lines) { + if (!line.startsWith('data: ') || line.contains('[DONE]')) continue; + final jsonStr = line.substring(6).trim(); + try { + final jsonData = jsonDecode(jsonStr); + final formattedOutput = aiRequestModel?.getFormattedStreamOutput( + jsonData, + ); + streamController.sink.add(formattedOutput); + } catch (e) { + debugPrint( + '⚠️ JSON decode error in SSE: $e\nSending as Regular Text', + ); + streamController.sink.add(jsonStr); + } + } + }, + onError: (error) { + streamController.addError('STREAM ERROR: $error'); + streamController.close(); + }, + onDone: () { + streamController.close(); + }, + cancelOnError: true, + ); + streamController.onCancel = () async { + await subscription.cancel(); + }; + } + return streamController.stream; +} + +Future callGenerativeModel( + AIRequestModel? aiRequestModel, { + required Function(String?) onAnswer, + required Function(dynamic) onError, +}) async { + if (aiRequestModel != null) { + try { + if (aiRequestModel.stream ?? false) { + final answerStream = await streamGenAIRequest(aiRequestModel); + processGenAIStreamOutput(answerStream, (w) { + onAnswer('$w '); + }, onError); + } else { + final answer = await executeGenAIRequest(aiRequestModel); + onAnswer(answer); + } + } catch (e) { + onError(e); + } + } +} + +void processGenAIStreamOutput( + Stream stream, + Function(String) onWord, + Function(dynamic) onError, +) { + String buffer = ''; + stream.listen( + (chunk) { + if (chunk == null || chunk.isEmpty) return; + buffer += chunk; + // Split on spaces but preserve last partial word + final parts = buffer.split(RegExp(r'\s+')); + if (parts.length > 1) { + // Keep the last part in buffer (it may be incomplete) + buffer = parts.removeLast(); + for (final word in parts) { + if (word.trim().isNotEmpty) { + onWord(word); + } + } + } + }, + onDone: () { + // Print any remaining word when stream is finished + if (buffer.trim().isNotEmpty) { + onWord(buffer); + } + }, + onError: (e) { + onError(e); + }, + ); +} diff --git a/packages/genai/lib/utils/model_manager.dart b/packages/genai/lib/utils/model_manager.dart new file mode 100644 index 00000000..a11518c2 --- /dev/null +++ b/packages/genai/lib/utils/model_manager.dart @@ -0,0 +1,97 @@ +import 'dart:convert'; +import 'package:better_networking/better_networking.dart'; +import 'package:flutter/foundation.dart'; +import '../consts.dart'; +import '../interface/interface.dart'; +import '../models/models.dart'; + +class ModelManager { + static Future fetchModelsFromRemote({ + String? remoteURL, + }) async { + try { + final (resp, _, _) = await sendHttpRequest( + 'FETCH_MODELS', + APIType.rest, + HttpRequestModel( + url: remoteURL ?? kModelRemoteUrl, + method: HTTPVerb.get, + ), + ); + if (resp == null) { + debugPrint('fetchModelsFromRemote -> resp == null'); + } else { + var remoteModels = availableModelsFromJson(resp.body); + return remoteModels; + } + } catch (e) { + debugPrint('fetchModelsFromRemote -> ${e.toString()}'); + } + return null; + } + + static Future fetchAvailableModels({ + String? ollamaUrl, + }) async { + try { + final oM = await fetchInstalledOllamaModels(ollamaUrl: ollamaUrl); + if (oM != null) { + List l = []; + for (var prov in kAvailableModels.modelProviders) { + if (prov.providerId == ModelAPIProvider.ollama) { + l.add( + prov.copyWith( + providerId: prov.providerId, + providerName: prov.providerName, + sourceUrl: prov.sourceUrl, + models: oM, + ), + ); + } else { + l.add(prov); + } + } + return kAvailableModels.copyWith( + version: kAvailableModels.version, + modelProviders: l, + ); + } + } catch (e) { + debugPrint('fetchAvailableModels -> ${e.toString()}'); + } + return kAvailableModels; + } + + static Future?> fetchInstalledOllamaModels({ + String? ollamaUrl, + }) async { + // All available models + // final url = "${ollamaUrl ?? kBaseOllamaUrl}/api/tags"; + // All loaded models + final url = "${ollamaUrl ?? kBaseOllamaUrl}/api/ps"; + + try { + final (resp, _, msg) = await sendHttpRequest( + 'OLLAMA_FETCH', + APIType.rest, + HttpRequestModel(url: url, method: HTTPVerb.get), + noSSL: true, + ); + // debugPrint("fetchInstalledOllamaModels -> $url -> ${resp?.body} -> $msg"); + if (resp == null) { + return null; + } + final output = jsonDecode(resp.body); + final models = output['models']; + if (models == null) return []; + List ollamaModels = []; + for (final m in models) { + ollamaModels.add(Model(id: m['model'], name: m['name'])); + } + return ollamaModels; + } catch (e) { + debugPrint('fetchInstalledOllamaModels -> ${e.toString()}'); + return null; + } + } +} diff --git a/packages/genai/lib/utils/utils.dart b/packages/genai/lib/utils/utils.dart new file mode 100644 index 00000000..e2ea0399 --- /dev/null +++ b/packages/genai/lib/utils/utils.dart @@ -0,0 +1,2 @@ +export 'ai_request_utils.dart'; +export 'model_manager.dart'; diff --git a/packages/genai/lib/widgets/ai_config_bool.dart b/packages/genai/lib/widgets/ai_config_bool.dart new file mode 100644 index 00000000..6d9f8d18 --- /dev/null +++ b/packages/genai/lib/widgets/ai_config_bool.dart @@ -0,0 +1,26 @@ +import 'package:flutter/material.dart'; +import '../models/models.dart'; + +class AIConfigBool extends StatelessWidget { + final ModelConfig configuration; + final Function(ModelConfig) onConfigUpdated; + final bool readonly; + const AIConfigBool({ + super.key, + required this.configuration, + required this.onConfigUpdated, + this.readonly = false, + }); + + @override + Widget build(BuildContext context) { + return Switch( + value: configuration.value.value as bool, + onChanged: (x) { + if (readonly) return; + configuration.value.value = x; + onConfigUpdated(configuration); + }, + ); + } +} diff --git a/packages/genai/lib/widgets/ai_config_field.dart b/packages/genai/lib/widgets/ai_config_field.dart new file mode 100644 index 00000000..ebae9384 --- /dev/null +++ b/packages/genai/lib/widgets/ai_config_field.dart @@ -0,0 +1,34 @@ +import 'package:flutter/material.dart'; +import '../models/models.dart'; + +class AIConfigField extends StatelessWidget { + final bool numeric; + final ModelConfig configuration; + final Function(ModelConfig) onConfigUpdated; + final bool readonly; + const AIConfigField({ + super.key, + this.numeric = false, + required this.configuration, + required this.onConfigUpdated, + this.readonly = false, + }); + + @override + Widget build(BuildContext context) { + return TextFormField( + initialValue: configuration.value.value.toString(), + onChanged: (x) { + if (readonly) return; + if (numeric) { + if (x.isEmpty) x = '0'; + if (num.tryParse(x) == null) return; + configuration.value.value = num.parse(x); + } else { + configuration.value.value = x; + } + onConfigUpdated(configuration); + }, + ); + } +} diff --git a/packages/genai/lib/widgets/ai_config_slider.dart b/packages/genai/lib/widgets/ai_config_slider.dart new file mode 100644 index 00000000..36b48774 --- /dev/null +++ b/packages/genai/lib/widgets/ai_config_slider.dart @@ -0,0 +1,36 @@ +import 'package:flutter/material.dart'; +import '../models/models.dart'; + +class AIConfigSlider extends StatelessWidget { + final ModelConfig configuration; + final Function(ModelConfig) onSliderUpdated; + final bool readonly; + const AIConfigSlider({ + super.key, + required this.configuration, + required this.onSliderUpdated, + this.readonly = false, + }); + + @override + Widget build(BuildContext context) { + final val = configuration.value.value as (double, double, double); + return Row( + children: [ + Expanded( + child: Slider( + min: val.$1, + value: val.$2, + max: val.$3, + onChanged: (x) { + if (readonly) return; + configuration.value.value = (val.$1, x, val.$3); + onSliderUpdated(configuration); + }, + ), + ), + Text(val.$2.toStringAsFixed(2)), + ], + ); + } +} diff --git a/packages/genai/lib/widgets/widgets.dart b/packages/genai/lib/widgets/widgets.dart new file mode 100644 index 00000000..15f9a10c --- /dev/null +++ b/packages/genai/lib/widgets/widgets.dart @@ -0,0 +1,3 @@ +export 'ai_config_bool.dart'; +export 'ai_config_field.dart'; +export 'ai_config_slider.dart'; diff --git a/packages/genai/models.json b/packages/genai/models.json new file mode 100644 index 00000000..5fcb9ad0 --- /dev/null +++ b/packages/genai/models.json @@ -0,0 +1,152 @@ +{ + "version": 1.0, + "model_providers": [ + { + "provider_id": "ollama", + "provider_name": "Ollama", + "source_url": null, + "models": [ + { + "id": "", + "name": "Custom" + } + ] + }, + { + "provider_id": "openai", + "provider_name": "OpenAI", + "source_url": "https://platform.openai.com/docs/models", + "models": [ + { + "id": "gpt-5", + "name": "GPT-5" + }, + { + "id": "gpt-5-mini", + "name": "GPT-5 mini" + }, + { + "id": "gpt-5-nano", + "name": "GPT-5 Nano" + }, + { + "id": "gpt-4.1", + "name": "GPT-4.1" + }, + { + "id": "gpt-oss-120b", + "name": "gpt-oss-120b" + }, + { + "id": "gpt-oss-20b", + "name": "gpt-oss-20b" + }, + { + "id": "o3-pro", + "name": "o3-pro" + }, + { + "id": "o3", + "name": "o3" + }, + { + "id": "o4-mini", + "name": "o4-mini" + }, + { + "id": "gpt-4o", + "name": "GPT-4o" + }, + { + "id": "gpt-4", + "name": "GPT-4" + }, + { + "id": "gpt-4o-mini", + "name": "GPT-4o Mini" + }, + { + "id": "", + "name": "Other" + } + ] + }, + { + "provider_id": "anthropic", + "provider_name": "Anthropic", + "source_url": "https://docs.anthropic.com/en/docs/about-claude/models/overview", + "models": [ + { + "id": "claude-opus-4-1", + "name": "Claude Opus 4.1" + }, + { + "id": "claude-opus-4-0", + "name": "Claude Opus 4" + }, + { + "id": "claude-sonnet-4-0", + "name": "Claude Sonnet 4" + }, + { + "id": "claude-3-7-sonnet-latest", + "name": "Claude Sonnet 3.7" + }, + { + "id": "claude-3-5-sonnet-latest", + "name": "Claude Sonnet 3.5" + }, + { + "id": "claude-3-5-haiku-latest", + "name": "Claude Haiku 3.5" + }, + { + "id": "", + "name": "Other" + } + ] + }, + { + "provider_id": "gemini", + "provider_name": "Gemini", + "source_url": "https://ai.google.dev/gemini-api/docs/models", + "models": [ + { + "id": "gemini-2.5-pro", + "name": "Gemini 2.5 Pro" + }, + { + "id": "gemini-2.5-flash", + "name": "Gemini 2.5 Flash" + }, + { + "id": "gemini-2.5-flash-lite", + "name": "Gemini 2.5 Flash-Lite" + }, + { + "id": "gemini-2.0-flash", + "name": "Gemini 2.0 Flash" + }, + { + "id": "gemini-2.0-flash-lite", + "name": "Gemini 2.0 Flash-Lite" + }, + { + "id": "", + "name": "Other" + } + ] + }, + { + "provider_id": "azureopenai", + "provider_name": "Azure OpenAI", + "source_url": null, + "models": [ + { + "id": "", + "name": "Custom" + } + ] + } + ] +} diff --git a/packages/genai/pubspec.yaml b/packages/genai/pubspec.yaml new file mode 100644 index 00000000..8cdb119a --- /dev/null +++ b/packages/genai/pubspec.yaml @@ -0,0 +1,32 @@ +name: genai +description: "A unified Dart/Flutter package for working with multiple Generative AI providers (Google Gemini, OpenAI, Anthropic, Azure OpenAI, Ollama, etc.) using a single request model." +version: 0.0.1 +homepage: https://github.com/foss42/apidash/tree/main/packages/genai + +topics: + - ai + - ollama + - gemini + - claude + - openai + +environment: + sdk: ^3.8.0 + flutter: ">=1.17.0" + +dependencies: + flutter: + sdk: flutter + better_networking: ^0.0.3 + freezed_annotation: ^2.4.1 + json_annotation: ^4.9.0 + nanoid: ^1.0.0 + +dev_dependencies: + flutter_test: + sdk: flutter + build_runner: ^2.4.12 + flutter_lints: ^4.0.0 + freezed: ^2.5.7 + json_serializable: ^6.7.1 + test: ^1.25.2 diff --git a/packages/genai/pubspec_overrides.yaml b/packages/genai/pubspec_overrides.yaml new file mode 100644 index 00000000..02df146a --- /dev/null +++ b/packages/genai/pubspec_overrides.yaml @@ -0,0 +1,6 @@ +# melos_managed_dependency_overrides: better_networking,seed +dependency_overrides: + better_networking: + path: ../better_networking + seed: + path: ../seed diff --git a/packages/genai/test/genai_test.dart b/packages/genai/test/genai_test.dart new file mode 100644 index 00000000..10046aca --- /dev/null +++ b/packages/genai/test/genai_test.dart @@ -0,0 +1,4 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:genai/genai.dart'; + +void main() {} diff --git a/packages/genai/test/interface/model_providers/gemini_test.dart b/packages/genai/test/interface/model_providers/gemini_test.dart new file mode 100644 index 00000000..04cd4cb9 --- /dev/null +++ b/packages/genai/test/interface/model_providers/gemini_test.dart @@ -0,0 +1,66 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:genai/interface/model_providers/gemini.dart'; +import 'package:genai/models/ai_request_model.dart'; +import 'package:genai/interface/consts.dart'; + +void main() { + group('GeminiModel', () { + test('should return default AIRequestModel with Gemini configs', () { + final defaultModel = GeminiModel.instance.defaultAIRequestModel; + + expect(defaultModel.modelApiProvider, equals(ModelAPIProvider.gemini)); + expect(defaultModel.url, equals(kGeminiUrl)); + expect(defaultModel.modelConfigs.length, greaterThan(0)); + }); + + test('should create correct HttpRequestModel for non-streaming', () { + const req = AIRequestModel( + modelApiProvider: ModelAPIProvider.gemini, + url: kGeminiUrl, + model: 'gemini-pro', + apiKey: '123', + userPrompt: 'Hello', + systemPrompt: 'Sys', + stream: false, + ); + + final httpReq = GeminiModel.instance.createRequest(req)!; + + expect(httpReq.url, contains('generateContent')); + expect(httpReq.method.name, equals('post')); + expect(httpReq.authModel?.apikey?.key, equals('123')); + }); + + test('should create correct HttpRequestModel for streaming', () { + const req = AIRequestModel( + modelApiProvider: ModelAPIProvider.gemini, + url: kGeminiUrl, + model: 'gemini-pro', + apiKey: '123', + userPrompt: 'Hello', + systemPrompt: 'Sys', + stream: true, + ); + + final httpReq = GeminiModel.instance.createRequest(req)!; + + expect(httpReq.url, contains('streamGenerateContent')); + }); + + test('should format output correctly', () { + final response = { + 'candidates': [ + { + 'content': { + 'parts': [ + {'text': 'Hello world'}, + ], + }, + }, + ], + }; + final output = GeminiModel.instance.outputFormatter(response); + expect(output, equals('Hello world')); + }); + }); +} diff --git a/packages/genai/test/models/ai_request_model_test.dart b/packages/genai/test/models/ai_request_model_test.dart new file mode 100644 index 00000000..f6749671 --- /dev/null +++ b/packages/genai/test/models/ai_request_model_test.dart @@ -0,0 +1,59 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:genai/models/ai_request_model.dart'; +import 'package:genai/interface/consts.dart'; +import 'package:genai/models/model_config_value.dart'; + +void main() { + group('AIRequestModel', () { + test('should serialize and deserialize from JSON', () { + final model = AIRequestModel( + modelApiProvider: ModelAPIProvider.gemini, + model: 'gemini-pro', + apiKey: '123', + systemPrompt: 'system', + userPrompt: 'user', + modelConfigs: [kDefaultModelConfigTemperature], + stream: true, + ); + + final json = model.toJson(); + final fromJson = AIRequestModel.fromJson(json); + + expect(fromJson.modelApiProvider, equals(ModelAPIProvider.gemini)); + expect(fromJson.model, equals('gemini-pro')); + expect(fromJson.apiKey, equals('123')); + expect(fromJson.systemPrompt, equals('system')); + expect(fromJson.userPrompt, equals('user')); + expect(fromJson.stream, isTrue); + }); + + test('should build config map correctly', () { + final model = AIRequestModel( + modelConfigs: [ + kDefaultModelConfigTemperature.copyWith( + value: ConfigSliderValue(value: (0, 0.8, 1)), + ), + kDefaultModelConfigMaxTokens.copyWith( + value: ConfigNumericValue(value: 200), + ), + ], + ); + + final configMap = model.getModelConfigMap(); + + expect(configMap['temperature'], equals(0.8)); + expect(configMap['max_tokens'], equals(200)); + }); + + test('should return correct config index', () { + final model = AIRequestModel( + modelConfigs: [ + kDefaultModelConfigTemperature, + kDefaultModelConfigMaxTokens, + ], + ); + expect(model.getModelConfigIdx('max_tokens'), equals(1)); + expect(model.getModelConfigIdx('foo'), isNull); + }); + }); +} diff --git a/packages/genai/test/models/model_config_test.dart b/packages/genai/test/models/model_config_test.dart new file mode 100644 index 00000000..3db741d9 --- /dev/null +++ b/packages/genai/test/models/model_config_test.dart @@ -0,0 +1,83 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:genai/models/model_config.dart'; +import 'package:genai/models/model_config_value.dart'; + +void main() { + group('ModelConfig', () { + test('constructor asserts correct type/value', () { + expect( + () => ModelConfig( + id: '1', + name: 'Temperature', + description: 'test', + type: ConfigType.boolean, + value: ConfigBooleanValue(value: true), + ), + returnsNormally, + ); + + expect( + () => ModelConfig( + id: '2', + name: 'Invalid', + description: 'wrong', + type: ConfigType.boolean, + value: ConfigNumericValue(value: 5), + ), + throwsA(isA()), + ); + }); + + test('updateValue returns new instance with updated value', () { + final config = ModelConfig( + id: '1', + name: 'Numeric', + description: 'test', + type: ConfigType.numeric, + value: ConfigNumericValue(value: 10), + ); + + final updated = config.updateValue(ConfigNumericValue(value: 20)); + expect(updated.value.value, 20); + expect(updated.id, config.id); + }); + + test('toJson and fromJson work correctly', () { + final config = ModelConfig( + id: 'temp', + name: 'Temperature', + description: 'test config', + type: ConfigType.numeric, + value: ConfigNumericValue(value: 5), + ); + + final json = config.toJson(); + expect(json['id'], 'temp'); + expect(json['type'], 'numeric'); + + final from = ModelConfig.fromJson(json); + expect(from.id, 'temp'); + expect(from.value is ConfigNumericValue, true); + expect((from.value as ConfigNumericValue).value, 5); + }); + + test('copyWith creates modified copy', () { + final config = ModelConfig( + id: 'slider', + name: 'Slider', + description: 'range', + type: ConfigType.slider, + value: ConfigSliderValue(value: (0, 0.3, 1)), + ); + + final copy = config.copyWith( + name: 'Updated Slider', + value: ConfigSliderValue(value: (0, 0.7, 1)), + ); + + expect(copy.name, 'Updated Slider'); + expect(copy.value.getPayloadValue(), 0.7); + expect(copy.id, 'slider'); // unchanged + }); + }); +} diff --git a/packages/genai/test/models/model_config_value_test.dart b/packages/genai/test/models/model_config_value_test.dart new file mode 100644 index 00000000..be2b2db0 --- /dev/null +++ b/packages/genai/test/models/model_config_value_test.dart @@ -0,0 +1,110 @@ +import 'dart:convert'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:genai/models/model_config_value.dart'; + +void main() { + group('ConfigType Enum', () { + test('getConfigTypeEnum returns correct enum', () { + expect(getConfigTypeEnum('boolean'), ConfigType.boolean); + expect(getConfigTypeEnum('slider'), ConfigType.slider); + expect(getConfigTypeEnum('numeric'), ConfigType.numeric); + expect(getConfigTypeEnum('text'), ConfigType.text); + }); + + test('getConfigTypeEnum falls back to text on invalid', () { + expect(getConfigTypeEnum('does_not_exist'), ConfigType.text); + expect(getConfigTypeEnum(null), ConfigType.text); + }); + }); + + group('ConfigBooleanValue', () { + test('serialize and deserialize works', () { + final value = ConfigBooleanValue(value: true); + final serialized = value.serialize(); + expect(serialized, 'true'); + + final deserialized = ConfigBooleanValue.deserialize(serialized); + expect(deserialized.value, true); + }); + }); + + group('ConfigNumericValue', () { + test('serialize and deserialize works', () { + final value = ConfigNumericValue(value: 42); + final serialized = value.serialize(); + expect(serialized, '42'); + + final deserialized = ConfigNumericValue.deserialize('42'); + expect(deserialized.value, 42); + + final nullValue = ConfigNumericValue.deserialize('not_a_number'); + expect(nullValue.value, null); + }); + }); + + group('ConfigSliderValue', () { + test('serialize and deserialize works', () { + final value = ConfigSliderValue(value: (0.0, 0.5, 1.0)); + final serialized = value.serialize(); + expect(serialized, jsonEncode([0.0, 0.5, 1.0])); + + final deserialized = ConfigSliderValue.deserialize(serialized); + expect(deserialized.value, (0.0, 0.5, 1.0)); + }); + + test('getPayloadValue returns middle element', () { + final slider = ConfigSliderValue(value: (0.0, 0.5, 1.0)); + expect(slider.getPayloadValue(), 0.5); + }); + }); + + group('ConfigTextValue', () { + test('serialize and deserialize works', () { + final value = ConfigTextValue(value: 'hello'); + final serialized = value.serialize(); + expect(serialized, 'hello'); + + final deserialized = ConfigTextValue.deserialize('world'); + expect(deserialized.value, 'world'); + }); + }); + + group('checkTypeValue', () { + test('validates correct type/value matches', () { + expect( + checkTypeValue(ConfigType.boolean, ConfigBooleanValue(value: true)), + true, + ); + expect( + checkTypeValue(ConfigType.numeric, ConfigNumericValue(value: 3)), + true, + ); + expect( + checkTypeValue( + ConfigType.slider, + ConfigSliderValue(value: (0, 0.5, 1)), + ), + true, + ); + expect( + checkTypeValue(ConfigType.text, ConfigTextValue(value: 'hi')), + true, + ); + }); + + test('returns false for mismatched type/value', () { + expect( + checkTypeValue(ConfigType.boolean, ConfigNumericValue(value: 1)), + false, + ); + expect( + checkTypeValue(ConfigType.numeric, ConfigTextValue(value: 'x')), + false, + ); + expect( + checkTypeValue(ConfigType.slider, ConfigBooleanValue(value: true)), + false, + ); + }); + }); +} diff --git a/packages/genai/test/utils.dart/ai_request_utils_test.dart b/packages/genai/test/utils.dart/ai_request_utils_test.dart new file mode 100644 index 00000000..1cf55e59 --- /dev/null +++ b/packages/genai/test/utils.dart/ai_request_utils_test.dart @@ -0,0 +1,36 @@ +import 'dart:io' show Platform; +import 'package:flutter_test/flutter_test.dart'; +import 'package:genai/models/ai_request_model.dart'; +import 'package:genai/interface/consts.dart'; +import 'package:genai/utils/ai_request_utils.dart'; + +const kGeminiApiKey = 'GEMINI_API_KEY'; + +void main() { + group('ai_request_utils', () { + test( + 'executeGenAIRequest should return formatted output on success', + () async { + Map envVars = Platform.environment; + String? kTestingAPIKey; + if (envVars.containsKey(kGeminiApiKey)) { + kTestingAPIKey = envVars[kGeminiApiKey]; + } else { + throw ArgumentError( + '$kGeminiApiKey should be available as an environment variable.', + ); + } + var model = AIRequestModel( + modelApiProvider: ModelAPIProvider.gemini, + model: 'gemini-2.0-flash', + url: kGeminiUrl, + userPrompt: 'Convert the Given Number into Binary', + systemPrompt: '1', + apiKey: kTestingAPIKey, + ); + final result = await executeGenAIRequest(model); + expect(result, isNotNull); + }, + ); + }); +} diff --git a/packages/genai/test/utils.dart/available_models_test.dart b/packages/genai/test/utils.dart/available_models_test.dart new file mode 100644 index 00000000..1a6b5d0b --- /dev/null +++ b/packages/genai/test/utils.dart/available_models_test.dart @@ -0,0 +1,74 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:genai/models/available_models.dart'; +import 'package:genai/interface/interface.dart'; + +void main() { + group('AvailableModels', () { + test('can parse from JSON and back', () { + const jsonString = ''' + { + "version": 1.0, + "model_providers": [ + { + "provider_id": "openai", + "provider_name": "OpenAI", + "source_url": "https://api.openai.com", + "models": [ + {"id": "gpt-4", "name": "GPT-4"} + ] + } + ] + } + '''; + + final models = availableModelsFromJson(jsonString); + expect(models.version, 1.0); + expect(models.modelProviders.length, 1); + expect(models.modelProviders.first.providerName, "OpenAI"); + + final backToJson = availableModelsToJson(models); + expect(backToJson.contains("GPT-4"), true); + }); + + test('map getter returns map of providers', () { + const provider = AIModelProvider( + providerId: ModelAPIProvider.openai, + providerName: "OpenAI", + models: [Model(id: "gpt-4", name: "GPT-4")], + ); + + const available = AvailableModels( + version: 1.0, + modelProviders: [provider], + ); + + expect(available.map.containsKey(ModelAPIProvider.openai), true); + expect(available.map[ModelAPIProvider.openai]?.providerName, "OpenAI"); + }); + }); + + group('AIModelProvider', () { + test( + 'toAiRequestModel returns default AIRequestModel with model override', + () { + const provider = AIModelProvider( + providerId: ModelAPIProvider.openai, + providerName: "OpenAI", + ); + + const model = Model(id: "gpt-4", name: "GPT-4"); + final req = provider.toAiRequestModel(model: model); + + expect(req?.model, "gpt-4"); + }, + ); + }); + + group('Model', () { + test('fromJson works', () { + final model = Model.fromJson({"id": "mistral", "name": "Mistral"}); + expect(model.id, "mistral"); + expect(model.name, "Mistral"); + }); + }); +} diff --git a/packages/genai/test/utils.dart/model_manager_test.dart b/packages/genai/test/utils.dart/model_manager_test.dart new file mode 100644 index 00000000..32939647 --- /dev/null +++ b/packages/genai/test/utils.dart/model_manager_test.dart @@ -0,0 +1,17 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:genai/models/available_models.dart'; +import 'package:genai/utils/model_manager.dart'; + +void main() { + group('ModelManager', () { + test('fetchModelsFromRemote returns parsed models', () async { + final result = await ModelManager.fetchModelsFromRemote(); + expect(result, isA()); + }); + + test('fetchInstalledOllamaModels parses response', () async { + final result = await ModelManager.fetchInstalledOllamaModels(); + expect(result, isNotNull); + }); + }); +} diff --git a/packages/genai/tool/json_to_dart.dart b/packages/genai/tool/json_to_dart.dart new file mode 100644 index 00000000..796252b4 --- /dev/null +++ b/packages/genai/tool/json_to_dart.dart @@ -0,0 +1,17 @@ +import 'dart:convert'; +import 'dart:io'; + +void main() { + final inputFile = File('models.json'); + final outputFile = File('lib/models/models_data.g.dart'); + + final jsonData = jsonDecode(inputFile.readAsStringSync()); + final dartCode = + ''' +// GENERATED CODE - DO NOT MODIFY BY HAND +const kModelsData = ${jsonEncode(jsonData)}; +'''; + + outputFile.writeAsStringSync(dartCode); + print('✅ Generated data.g.dart from data.json'); +} diff --git a/packages/genai/tool/pre_publish.sh b/packages/genai/tool/pre_publish.sh new file mode 100644 index 00000000..e51fd07a --- /dev/null +++ b/packages/genai/tool/pre_publish.sh @@ -0,0 +1,4 @@ +#!/bin/bash +echo "🔄 Running pre-publish steps..." +dart run tool/json_to_dart.dart +echo "✅ Pre-publish steps completed." diff --git a/pubspec.lock b/pubspec.lock index 3b1ea421..24514a53 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -106,10 +106,11 @@ packages: better_networking: dependency: transitive description: - path: "packages/better_networking" - relative: true - source: path - version: "0.0.1" + name: better_networking + sha256: a23f061e02eee363c4e80fd0b7df529d9353eeb55bacfe2acd3b1d5e09c4016c + url: "https://pub.dev" + source: hosted + version: "0.0.3" bidi: dependency: transitive description: @@ -397,6 +398,22 @@ packages: url: "https://pub.dev" source: hosted version: "0.5.0" + dio: + dependency: transitive + description: + name: dio + sha256: "253a18bbd4851fecba42f7343a1df3a9a4c1d31a2c1b37e221086b4fa8c8dbc9" + url: "https://pub.dev" + source: hosted + version: "5.8.0+1" + dio_web_adapter: + dependency: transitive + description: + name: dio_web_adapter + sha256: "7586e476d70caecaf1686d21eee7247ea43ef5c345eab9e0cc3583ff13378d78" + url: "https://pub.dev" + source: hosted + version: "2.1.1" desktop_webview_window: dependency: transitive description: @@ -698,7 +715,7 @@ packages: source: hosted version: "2.5.8" freezed_annotation: - dependency: transitive + dependency: "direct overridden" description: name: freezed_annotation sha256: c2e2d632dd9b8a2b7751117abcfc2b4888ecfe181bd9fca7170d9ef02e595fe2 @@ -726,6 +743,13 @@ packages: url: "https://pub.dev" source: hosted version: "0.32.1" + genai: + dependency: transitive + description: + path: "packages/genai" + relative: true + source: path + version: "0.0.1" glob: dependency: transitive description: @@ -1023,6 +1047,14 @@ packages: url: "https://pub.dev" source: hosted version: "5.1.1" + logger: + dependency: transitive + description: + name: logger + sha256: "55d6c23a6c15db14920e037fe7e0dc32e7cdaf3b64b4b25df2d541b5b6b81c0c" + url: "https://pub.dev" + source: hosted + version: "2.6.1" logging: dependency: transitive description: @@ -1134,6 +1166,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: @@ -1666,6 +1706,22 @@ packages: url: "https://pub.dev" source: hosted version: "7.0.0" + stac: + dependency: "direct main" + description: + name: stac + sha256: "9c24c0cb546ab04bf324c17451ad31181cb98e284a489f690a6f381b7a77e47a" + url: "https://pub.dev" + source: hosted + version: "0.11.0" + stac_framework: + dependency: transitive + description: + name: stac_framework + sha256: "54e1a14f01664443f3be3d158781c07e69ab0e04993495ddd0b6b1d68c84875b" + url: "https://pub.dev" + source: hosted + version: "0.3.0" stack_trace: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 0ef4fae7..0af972e2 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -64,6 +64,7 @@ dependencies: scrollable_positioned_list: ^0.3.8 share_plus: ^10.1.4 shared_preferences: ^2.5.2 + stac: ^0.11.0 url_launcher: ^6.2.5 uuid: ^4.5.0 vector_graphics_compiler: ^1.1.9+1 @@ -79,6 +80,7 @@ dependency_overrides: extended_text_field: ^16.0.0 pdf_widget_wrapper: ^1.0.4 web: ^1.1.1 + freezed_annotation: ^2.0.3 dev_dependencies: flutter_test: diff --git a/test/models/history_models.dart b/test/models/history_models.dart index d12af357..69d2fd06 100644 --- a/test/models/history_models.dart +++ b/test/models/history_models.dart @@ -57,6 +57,7 @@ final Map historyRequestModelJson1 = { "historyId": "historyId1", "metaData": historyMetaModelJson1, "httpRequestModel": httpRequestModelGet4Json, + 'aiRequestModel': null, "httpResponseModel": responseModelJson, 'preRequestScript': null, 'postRequestScript': null, diff --git a/test/models/request_models.dart b/test/models/request_models.dart index 1762706c..585cbc44 100644 --- a/test/models/request_models.dart +++ b/test/models/request_models.dart @@ -218,7 +218,8 @@ Map requestModelJson = { 'message': null, 'httpResponseModel': responseModelJson, 'preRequestScript': null, - 'postRequestScript': null + 'postRequestScript': null, + 'aiRequestModel': null }; /// Basic GET request model for apidash.dev diff --git a/test/models/settings_model_test.dart b/test/models/settings_model_test.dart index 3a7827d5..0ea7746a 100644 --- a/test/models/settings_model_test.dart +++ b/test/models/settings_model_test.dart @@ -19,6 +19,7 @@ void main() { workspaceFolderPath: null, isSSLDisabled: true, isDashBotEnabled: true, + defaultAIModel: {"model": "llama"}, ); test('Testing toJson()', () { @@ -38,6 +39,7 @@ void main() { "workspaceFolderPath": null, "isSSLDisabled": true, "isDashBotEnabled": true, + "defaultAIModel": {"model": "llama"} }; expect(sm.toJson(), expectedResult); }); @@ -59,6 +61,7 @@ void main() { "workspaceFolderPath": null, "isSSLDisabled": true, "isDashBotEnabled": true, + "defaultAIModel": {"model": "llama"} }; expect(SettingsModel.fromJson(input), sm); }); @@ -77,6 +80,7 @@ void main() { historyRetentionPeriod: HistoryRetentionPeriod.oneWeek, isSSLDisabled: false, isDashBotEnabled: false, + defaultAIModel: {"model": "llama"}, ); expect( sm.copyWith( @@ -104,7 +108,10 @@ void main() { "historyRetentionPeriod": "oneWeek", "workspaceFolderPath": null, "isSSLDisabled": true, - "isDashBotEnabled": true + "isDashBotEnabled": true, + "defaultAIModel": { + "model": "llama" + } }'''; expect(sm.toString(), expectedResult); }); diff --git a/test/screens/common_widgets/auth/api_key_auth_fields_test.dart b/test/screens/common_widgets/auth/api_key_auth_fields_test.dart index dc7e9134..2f319cf1 100644 --- a/test/screens/common_widgets/auth/api_key_auth_fields_test.dart +++ b/test/screens/common_widgets/auth/api_key_auth_fields_test.dart @@ -69,12 +69,16 @@ void main() { final authFields = find.byType(EnvAuthField); expect(authFields, findsNWidgets(2)); - // Find ExtendedTextField widgets within the EnvAuthField widgets - final textFields = find.byType(ExtendedTextField); - expect(textFields, findsAtLeastNWidgets(2)); + // Find the last EnvAuthField (API key value field) + final lastAuthField = authFields.last; + + // Find ExtendedTextField within the last EnvAuthField using find.descendant + final lastField = find.descendant( + of: lastAuthField, + matching: find.byType(ExtendedTextField), + ); + expect(lastField, findsOneWidget); - // Use testTextInput to directly input text - final lastField = textFields.last; await tester.tap(lastField); await tester.pumpAndSettle(); @@ -218,12 +222,22 @@ void main() { // Wait for the widget to settle await tester.pumpAndSettle(); - // Find ExtendedTextField widgets - final textFields = find.byType(ExtendedTextField); - expect(textFields, findsAtLeastNWidgets(2)); + // Find EnvAuthField widgets + final authFields = find.byType(EnvAuthField); + expect(authFields, findsNWidgets(2)); - // Tap and enter text in the name field (should be the first text field) - await tester.tap(textFields.first); + // Find the first EnvAuthField (API key name field) + final firstAuthField = authFields.first; + + // Find ExtendedTextField within the first EnvAuthField using find.descendant + final nameField = find.descendant( + of: firstAuthField, + matching: find.byType(ExtendedTextField), + ); + expect(nameField, findsOneWidget); + + // Tap and enter text in the name field + await tester.tap(nameField); await tester.pumpAndSettle(); // Use tester.testTextInput to enter text directly @@ -266,15 +280,21 @@ void main() { await tester.pumpAndSettle(); // Find EnvAuthField widgets - final textFields = find.byType(EnvAuthField); - expect(textFields, findsNWidgets(2)); + final authFields = find.byType(EnvAuthField); + expect(authFields, findsNWidgets(2)); - // Find the underlying ExtendedTextField widgets - final extendedTextFields = find.byType(ExtendedTextField); - expect(extendedTextFields, findsAtLeastNWidgets(2)); + // Find the last EnvAuthField (API key value field) + final lastAuthField = authFields.last; - // Tap and enter text in the key field (should be the last text field) - await tester.tap(extendedTextFields.last); + // Find ExtendedTextField within the last EnvAuthField using find.descendant + final keyField = find.descendant( + of: lastAuthField, + matching: find.byType(ExtendedTextField), + ); + expect(keyField, findsOneWidget); + + // Tap and enter text in the key field + await tester.tap(keyField); await tester.pumpAndSettle(); // Use tester.testTextInput to enter text directly diff --git a/test/screens/common_widgets/auth/basic_auth_fields_test.dart b/test/screens/common_widgets/auth/basic_auth_fields_test.dart index 6712a761..d4f60948 100644 --- a/test/screens/common_widgets/auth/basic_auth_fields_test.dart +++ b/test/screens/common_widgets/auth/basic_auth_fields_test.dart @@ -91,11 +91,20 @@ void main() { ), ); - // Find the username field (first ExtendedTextField) - final textFields = find.byType(ExtendedTextField); - expect(textFields, findsAtLeastNWidgets(2)); + // Find EnvAuthField widgets + final authFields = find.byType(EnvAuthField); + expect(authFields, findsNWidgets(2)); + + // Find the first EnvAuthField (username field) + final firstAuthField = authFields.first; + + // Find ExtendedTextField within the first EnvAuthField using find.descendant + final usernameField = find.descendant( + of: firstAuthField, + matching: find.byType(ExtendedTextField), + ); + expect(usernameField, findsOneWidget); - final usernameField = textFields.first; await tester.tap(usernameField); await tester.pumpAndSettle(); @@ -133,11 +142,20 @@ void main() { ), ); - // Find the password field (second ExtendedTextField) - final textFields = find.byType(ExtendedTextField); - expect(textFields, findsAtLeastNWidgets(2)); + // Find EnvAuthField widgets + final authFields = find.byType(EnvAuthField); + expect(authFields, findsNWidgets(2)); + + // Find the last EnvAuthField (password field) + final lastAuthField = authFields.last; + + // Find ExtendedTextField within the last EnvAuthField using find.descendant + final passwordField = find.descendant( + of: lastAuthField, + matching: find.byType(ExtendedTextField), + ); + expect(passwordField, findsOneWidget); - final passwordField = textFields.last; await tester.tap(passwordField); await tester.pumpAndSettle(); @@ -245,10 +263,19 @@ void main() { ); // Enter username - final textFields = find.byType(ExtendedTextField); - expect(textFields, findsAtLeastNWidgets(2)); + final authFields = find.byType(EnvAuthField); + expect(authFields, findsNWidgets(2)); + + // Find the first EnvAuthField (username field) + final firstAuthField = authFields.first; + + // Find ExtendedTextField within the first EnvAuthField using find.descendant + final usernameField = find.descendant( + of: firstAuthField, + matching: find.byType(ExtendedTextField), + ); + expect(usernameField, findsOneWidget); - final usernameField = textFields.first; await tester.tap(usernameField); await tester.pumpAndSettle(); diff --git a/test/screens/common_widgets/auth/bearer_auth_fields_test.dart b/test/screens/common_widgets/auth/bearer_auth_fields_test.dart index cd01c22b..9c1cce06 100644 --- a/test/screens/common_widgets/auth/bearer_auth_fields_test.dart +++ b/test/screens/common_widgets/auth/bearer_auth_fields_test.dart @@ -88,10 +88,16 @@ void main() { ); // Find the token field - final textFields = find.byType(ExtendedTextField); - expect(textFields, findsAtLeastNWidgets(1)); + final authField = find.byType(EnvAuthField); + expect(authField, findsOneWidget); + + // Find ExtendedTextField within the EnvAuthField using find.descendant + final tokenField = find.descendant( + of: authField, + matching: find.byType(ExtendedTextField), + ); + expect(tokenField, findsOneWidget); - final tokenField = textFields.first; await tester.tap(tokenField); await tester.pumpAndSettle(); @@ -197,10 +203,16 @@ void main() { ); // Enter token - final textFields = find.byType(ExtendedTextField); - expect(textFields, findsAtLeastNWidgets(1)); + final authField = find.byType(EnvAuthField); + expect(authField, findsOneWidget); + + // Find ExtendedTextField within the EnvAuthField using find.descendant + final tokenField = find.descendant( + of: authField, + matching: find.byType(ExtendedTextField), + ); + expect(tokenField, findsOneWidget); - final tokenField = textFields.first; await tester.tap(tokenField); await tester.pumpAndSettle(); @@ -254,10 +266,16 @@ void main() { ); // Enter token with whitespace - final textFields = find.byType(ExtendedTextField); - expect(textFields, findsAtLeastNWidgets(1)); + final authField = find.byType(EnvAuthField); + expect(authField, findsOneWidget); + + // Find ExtendedTextField within the EnvAuthField using find.descendant + final tokenField = find.descendant( + of: authField, + matching: find.byType(ExtendedTextField), + ); + expect(tokenField, findsOneWidget); - final tokenField = textFields.first; await tester.tap(tokenField); await tester.pumpAndSettle(); diff --git a/test/screens/common_widgets/auth/digest_auth_fields_test.dart b/test/screens/common_widgets/auth/digest_auth_fields_test.dart index 170fe075..12e1e327 100644 --- a/test/screens/common_widgets/auth/digest_auth_fields_test.dart +++ b/test/screens/common_widgets/auth/digest_auth_fields_test.dart @@ -109,11 +109,20 @@ void main() { ), ); - // Find the username field (first ExtendedTextField) - final textFields = find.byType(ExtendedTextField); - expect(textFields, findsAtLeastNWidgets(6)); + // Find EnvAuthField widgets + final authFields = find.byType(EnvAuthField); + expect(authFields, findsNWidgets(6)); + + // Find the first EnvAuthField (username field) + final firstAuthField = authFields.first; + + // Find ExtendedTextField within the first EnvAuthField using find.descendant + final usernameField = find.descendant( + of: firstAuthField, + matching: find.byType(ExtendedTextField), + ); + expect(usernameField, findsOneWidget); - final usernameField = textFields.first; await tester.tap(usernameField); await tester.pumpAndSettle(); @@ -156,11 +165,20 @@ void main() { ), ); - // Find the password field (second ExtendedTextField) - final textFields = find.byType(ExtendedTextField); - expect(textFields, findsAtLeastNWidgets(6)); + // Find EnvAuthField widgets + final authFields = find.byType(EnvAuthField); + expect(authFields, findsNWidgets(6)); + + // Find the second EnvAuthField (password field) + final secondAuthField = authFields.at(1); + + // Find ExtendedTextField within the second EnvAuthField using find.descendant + final passwordField = find.descendant( + of: secondAuthField, + matching: find.byType(ExtendedTextField), + ); + expect(passwordField, findsOneWidget); - final passwordField = textFields.at(1); await tester.tap(passwordField); await tester.pumpAndSettle(); @@ -245,11 +263,20 @@ void main() { ), ); - // Find the realm field (third ExtendedTextField) - final textFields = find.byType(ExtendedTextField); - expect(textFields, findsAtLeastNWidgets(6)); + // Find EnvAuthField widgets + final authFields = find.byType(EnvAuthField); + expect(authFields, findsNWidgets(6)); + + // Find the third EnvAuthField (realm field) + final thirdAuthField = authFields.at(2); + + // Find ExtendedTextField within the third EnvAuthField using find.descendant + final realmField = find.descendant( + of: thirdAuthField, + matching: find.byType(ExtendedTextField), + ); + expect(realmField, findsOneWidget); - final realmField = textFields.at(2); await tester.tap(realmField); await tester.pumpAndSettle(); @@ -370,7 +397,19 @@ void main() { ); // Enter username - final usernameField = find.byType(ExtendedTextField).first; + final authFields = find.byType(EnvAuthField); + expect(authFields, findsNWidgets(6)); + + // Find the first EnvAuthField (username field) + final firstAuthField = authFields.first; + + // Find ExtendedTextField within the first EnvAuthField using find.descendant + final usernameField = find.descendant( + of: firstAuthField, + matching: find.byType(ExtendedTextField), + ); + expect(usernameField, findsOneWidget); + await tester.tap(usernameField); tester.testTextInput.enterText('testuser'); await tester.pumpAndSettle(); @@ -435,10 +474,19 @@ void main() { ); // Enter username with whitespace - final textFields = find.byType(ExtendedTextField); - expect(textFields, findsAtLeastNWidgets(6)); + final authFields = find.byType(EnvAuthField); + expect(authFields, findsNWidgets(6)); + + // Find the first EnvAuthField (username field) + final firstAuthField = authFields.first; + + // Find ExtendedTextField within the first EnvAuthField using find.descendant + final usernameField = find.descendant( + of: firstAuthField, + matching: find.byType(ExtendedTextField), + ); + expect(usernameField, findsOneWidget); - final usernameField = textFields.first; await tester.tap(usernameField); await tester.pumpAndSettle(); diff --git a/test/screens/common_widgets/auth/jwt_auth_fields_test.dart b/test/screens/common_widgets/auth/jwt_auth_fields_test.dart index 206d0ba8..6165e7c9 100644 --- a/test/screens/common_widgets/auth/jwt_auth_fields_test.dart +++ b/test/screens/common_widgets/auth/jwt_auth_fields_test.dart @@ -270,7 +270,16 @@ void main() { ); // Find the secret field - final secretField = find.byType(ExtendedTextField).first; + final authField = find.byType(EnvAuthField); + expect(authField, findsOneWidget); + + // Find ExtendedTextField within the EnvAuthField using find.descendant + final secretField = find.descendant( + of: authField, + matching: find.byType(ExtendedTextField), + ); + expect(secretField, findsOneWidget); + await tester.tap(secretField); tester.testTextInput.enterText('new-secret'); await tester.pumpAndSettle(); 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); }); }); }