diff --git a/pkgs/dart_services/Dockerfile b/pkgs/dart_services/Dockerfile index 63cb78c06..54fe8cca7 100644 --- a/pkgs/dart_services/Dockerfile +++ b/pkgs/dart_services/Dockerfile @@ -1,6 +1,7 @@ ARG PROJECT_ID ARG FLUTTER_CHANNEL ARG BUILD_SHA +ARG GOOGLE_API_KEY FROM gcr.io/$PROJECT_ID/flutter:$FLUTTER_CHANNEL @@ -16,6 +17,7 @@ RUN dart compile exe bin/server.dart -o bin/server RUN dart run grinder build-project-templates ENV BUILD_SHA=$BUILD_SHA +ENV GOOGLE_API_KEY=$GOOGLE_API_KEY EXPOSE 8080 CMD ["/app/bin/server"] diff --git a/pkgs/dart_services/README.md b/pkgs/dart_services/README.md index bfe71036d..49298128d 100644 --- a/pkgs/dart_services/README.md +++ b/pkgs/dart_services/README.md @@ -43,12 +43,6 @@ To rebuild the shelf router, run: dart run build_runner build --delete-conflicting-outputs ``` -And to update the shared code from dartpad_shared, run: - -``` -dart tool/grind.dart copy-shared-source -``` - ### Modifying supported packages Package dependencies are pinned using the `pub_dependencies_.yaml` diff --git a/pkgs/dart_services/analysis_options.yaml b/pkgs/dart_services/analysis_options.yaml index 90258da1c..d059c7b96 100644 --- a/pkgs/dart_services/analysis_options.yaml +++ b/pkgs/dart_services/analysis_options.yaml @@ -17,3 +17,4 @@ linter: rules: - prefer_final_in_for_each - prefer_final_locals + - sort_pub_dependencies diff --git a/pkgs/dart_services/lib/src/common_server.dart b/pkgs/dart_services/lib/src/common_server.dart index 51582d175..fbfe1f97a 100644 --- a/pkgs/dart_services/lib/src/common_server.dart +++ b/pkgs/dart_services/lib/src/common_server.dart @@ -7,6 +7,8 @@ import 'dart:convert'; import 'dart:io'; import 'package:dartpad_shared/model.dart' as api; +import 'package:google_generative_ai/google_generative_ai.dart' as google_ai; +import 'package:http/http.dart' as http; import 'package:logging/logging.dart'; import 'package:shelf/shelf.dart'; import 'package:shelf_router/shelf_router.dart'; @@ -200,6 +202,50 @@ class CommonServerApi { return ok(version().toJson()); } + static final String? googleApiKey = Platform.environment['GOOGLE_API_KEY']; + http.Client? geminiHttpClient; + + @Route.post('$apiPrefix/_gemini') + Future gemini(Request request, String apiVersion) async { + if (apiVersion != api3) return unhandledVersion(apiVersion); + + // Read the api key from env variables (populated on the server). + final apiKey = googleApiKey; + if (apiKey == null) { + return Response.internalServerError( + body: 'gemini key not configured on server'); + } + + // Only allow the call from dartpad.dev. + final origin = request.origin; + if (origin != 'https://dartpad.dev') { + return Response.badRequest( + body: 'Gemini calls only allowed from the DartPad front-end'); + } + + final sourceRequest = + api.SourceRequest.fromJson(await request.readAsJson()); + + geminiHttpClient ??= http.Client(); + + final model = google_ai.GenerativeModel( + model: 'models/gemini-1.5-flash-latest', + apiKey: apiKey, + httpClient: geminiHttpClient, + ); + + final result = await serialize(() async { + // call gemini + final result = await model.generateContent([ + google_ai.Content.text(sourceRequest.source), + ]); + + return api.GeminiResponse(response: result.text!); + }); + + return ok(result.toJson()); + } + Response ok(Map json) { return Response.ok( _jsonEncoder.convert(json), @@ -327,3 +373,7 @@ String _formatMessage( return message; } + +extension on Request { + String? get origin => headers['origin']; +} diff --git a/pkgs/dart_services/lib/src/common_server.g.dart b/pkgs/dart_services/lib/src/common_server.g.dart index f548d8b29..fc1bf9250 100644 --- a/pkgs/dart_services/lib/src/common_server.g.dart +++ b/pkgs/dart_services/lib/src/common_server.g.dart @@ -48,5 +48,10 @@ Router _$CommonServerApiRouter(CommonServerApi service) { r'/api//version', service.versionGet, ); + router.add( + 'POST', + r'/api//_gemini', + service.gemini, + ); return router; } diff --git a/pkgs/dart_services/pubspec.yaml b/pkgs/dart_services/pubspec.yaml index 36eaec92c..4a4e0c14c 100644 --- a/pkgs/dart_services/pubspec.yaml +++ b/pkgs/dart_services/pubspec.yaml @@ -12,6 +12,7 @@ dependencies: bazel_worker: ^1.1.1 dartpad_shared: any encrypt: ^5.0.3 + google_generative_ai: ^0.4.0 http: ^1.2.1 json_annotation: any logging: ^1.2.0 diff --git a/pkgs/dartpad_shared/lib/model.dart b/pkgs/dartpad_shared/lib/model.dart index 72b37f77f..107a5bb50 100644 --- a/pkgs/dartpad_shared/lib/model.dart +++ b/pkgs/dartpad_shared/lib/model.dart @@ -375,6 +375,23 @@ class VersionResponse { Map toJson() => _$VersionResponseToJson(this); } +@JsonSerializable() +class GeminiResponse { + final String response; + + GeminiResponse({ + required this.response, + }); + + factory GeminiResponse.fromJson(Map json) => + _$GeminiResponseFromJson(json); + + Map toJson() => _$GeminiResponseToJson(this); + + @override + String toString() => 'GeminiResponse[response=$response]'; +} + @JsonSerializable() class PackageInfo { final String name; diff --git a/pkgs/dartpad_shared/lib/model.g.dart b/pkgs/dartpad_shared/lib/model.g.dart index ab1123e67..ece3f3dcb 100644 --- a/pkgs/dartpad_shared/lib/model.g.dart +++ b/pkgs/dartpad_shared/lib/model.g.dart @@ -296,6 +296,16 @@ Map _$VersionResponseToJson(VersionResponse instance) => 'packages': instance.packages, }; +GeminiResponse _$GeminiResponseFromJson(Map json) => + GeminiResponse( + response: json['response'] as String, + ); + +Map _$GeminiResponseToJson(GeminiResponse instance) => + { + 'response': instance.response, + }; + PackageInfo _$PackageInfoFromJson(Map json) => PackageInfo( name: json['name'] as String, version: json['version'] as String, diff --git a/pkgs/dartpad_shared/lib/services.dart b/pkgs/dartpad_shared/lib/services.dart index 3016f7e0a..71c024a50 100644 --- a/pkgs/dartpad_shared/lib/services.dart +++ b/pkgs/dartpad_shared/lib/services.dart @@ -5,6 +5,7 @@ import 'dart:convert'; import 'package:http/http.dart'; +import 'package:meta/meta.dart'; import 'model.dart'; @@ -40,6 +41,11 @@ class ServicesClient { Future compileDDC(CompileRequest request) => _requestPost('compileDDC', request.toJson(), CompileDDCResponse.fromJson); + /// Note: this API is experimental and could change or be removed at any time. + @experimental + Future gemini(SourceRequest request) => + _requestPost('_gemini', request.toJson(), GeminiResponse.fromJson); + void dispose() => client.close(); Future _requestGet( diff --git a/pkgs/dartpad_shared/pubspec.yaml b/pkgs/dartpad_shared/pubspec.yaml index 9bed6f1a6..b7668d164 100644 --- a/pkgs/dartpad_shared/pubspec.yaml +++ b/pkgs/dartpad_shared/pubspec.yaml @@ -8,6 +8,7 @@ environment: dependencies: http: ^1.1.0 json_annotation: ^4.9.0 + meta: ^1.14.0 dev_dependencies: build_runner: ^2.4.5 diff --git a/pkgs/dartpad_ui/lib/model.dart b/pkgs/dartpad_ui/lib/model.dart index 2dec6f9c5..6c65faf25 100644 --- a/pkgs/dartpad_ui/lib/model.dart +++ b/pkgs/dartpad_ui/lib/model.dart @@ -317,6 +317,10 @@ class AppServices { return await services.document(request); } + Future gemini(SourceRequest request) async { + return await services.gemini(request); + } + Future compile(CompileRequest request) async { try { appModel.compilingBusy.value = true;