diff --git a/apps/mobile/packages/core/lib/core.dart b/apps/mobile/packages/core/lib/core.dart index 45c7da3f..f78a5d63 100644 --- a/apps/mobile/packages/core/lib/core.dart +++ b/apps/mobile/packages/core/lib/core.dart @@ -9,3 +9,14 @@ export 'src/presentation/observers/core_bloc_observer.dart'; export 'src/config/app_config.dart'; export 'src/routing/routing.dart'; export 'src/services/api_service/api_service.dart'; + +// Core API Services +export 'src/services/api_service/core_api_services/core_api_endpoints.dart'; +export 'src/services/api_service/core_api_services/file_upload/file_upload_service.dart'; +export 'src/services/api_service/core_api_services/file_upload/file_upload_response.dart'; +export 'src/services/api_service/core_api_services/signed_url/signed_url_service.dart'; +export 'src/services/api_service/core_api_services/signed_url/signed_url_response.dart'; +export 'src/services/api_service/core_api_services/llm/llm_service.dart'; +export 'src/services/api_service/core_api_services/llm/llm_response.dart'; +export 'src/services/api_service/core_api_services/verification/verification_service.dart'; +export 'src/services/api_service/core_api_services/verification/verification_response.dart'; diff --git a/apps/mobile/packages/core/lib/src/services/api_service/core_api_services/core_api_endpoints.dart b/apps/mobile/packages/core/lib/src/services/api_service/core_api_services/core_api_endpoints.dart new file mode 100644 index 00000000..500ff44a --- /dev/null +++ b/apps/mobile/packages/core/lib/src/services/api_service/core_api_services/core_api_endpoints.dart @@ -0,0 +1,29 @@ +/// Constants for Core API endpoints. +class CoreApiEndpoints { + CoreApiEndpoints._(); + + /// The base URL for the Core API. + static const String baseUrl = 'https://krow-core-api-e3g6witsvq-uc.a.run.app'; + + /// Upload a file. + static const String uploadFile = '/core/upload-file'; + + /// Create a signed URL for a file. + static const String createSignedUrl = '/core/create-signed-url'; + + /// Invoke a Large Language Model. + static const String invokeLlm = '/core/invoke-llm'; + + /// Root for verification operations. + static const String verifications = '/core/verifications'; + + /// Get status of a verification job. + static String verificationStatus(String id) => '/core/verifications/$id'; + + /// Review a verification decision. + static String verificationReview(String id) => + '/core/verifications/$id/review'; + + /// Retry a verification job. + static String verificationRetry(String id) => '/core/verifications/$id/retry'; +} diff --git a/apps/mobile/packages/core/lib/src/services/api_service/core_api_services/file_upload/file_upload_response.dart b/apps/mobile/packages/core/lib/src/services/api_service/core_api_services/file_upload/file_upload_response.dart new file mode 100644 index 00000000..941fe01d --- /dev/null +++ b/apps/mobile/packages/core/lib/src/services/api_service/core_api_services/file_upload/file_upload_response.dart @@ -0,0 +1,54 @@ +/// Response model for file upload operation. +class FileUploadResponse { + /// Creates a [FileUploadResponse]. + const FileUploadResponse({ + required this.fileUri, + required this.contentType, + required this.size, + required this.bucket, + required this.path, + this.requestId, + }); + + /// Factory to create [FileUploadResponse] from JSON. + factory FileUploadResponse.fromJson(Map json) { + return FileUploadResponse( + fileUri: json['fileUri'] as String, + contentType: json['contentType'] as String, + size: json['size'] as int, + bucket: json['bucket'] as String, + path: json['path'] as String, + requestId: json['requestId'] as String?, + ); + } + + /// The Cloud Storage URI of the uploaded file. + final String fileUri; + + /// The MIME type of the file. + final String contentType; + + /// The size of the file in bytes. + final int size; + + /// The bucket where the file was uploaded. + final String bucket; + + /// The path within the bucket. + final String path; + + /// The unique request ID from the server. + final String? requestId; + + /// Converts the response to a JSON map. + Map toJson() { + return { + 'fileUri': fileUri, + 'contentType': contentType, + 'size': size, + 'bucket': bucket, + 'path': path, + 'requestId': requestId, + }; + } +} diff --git a/apps/mobile/packages/core/lib/src/services/api_service/core_api_services/file_upload/file_upload_service.dart b/apps/mobile/packages/core/lib/src/services/api_service/core_api_services/file_upload/file_upload_service.dart new file mode 100644 index 00000000..d5e090b0 --- /dev/null +++ b/apps/mobile/packages/core/lib/src/services/api_service/core_api_services/file_upload/file_upload_service.dart @@ -0,0 +1,31 @@ +import 'package:dio/dio.dart'; +import 'package:krow_domain/krow_domain.dart'; +import '../core_api_endpoints.dart'; + +/// Service for uploading files to the Core API. +class FileUploadService extends BaseCoreService { + /// Creates a [FileUploadService]. + FileUploadService(super.api); + + /// Uploads a file with optional visibility and category. + /// + /// [filePath] is the local path to the file. + /// [visibility] can be 'public' or 'private'. + /// [category] is an optional metadata field. + Future uploadFile({ + required String filePath, + required String fileName, + String visibility = 'private', + String? category, + }) async { + return action(() async { + final FormData formData = FormData.fromMap({ + 'file': await MultipartFile.fromFile(filePath, filename: fileName), + 'visibility': visibility, + if (category != null) 'category': category, + }); + + return api.post(CoreApiEndpoints.uploadFile, data: formData); + }); + } +} diff --git a/apps/mobile/packages/core/lib/src/services/api_service/core_api_services/llm/llm_response.dart b/apps/mobile/packages/core/lib/src/services/api_service/core_api_services/llm/llm_response.dart new file mode 100644 index 00000000..add3c331 --- /dev/null +++ b/apps/mobile/packages/core/lib/src/services/api_service/core_api_services/llm/llm_response.dart @@ -0,0 +1,42 @@ +/// Response model for LLM invocation. +class LlmResponse { + /// Creates an [LlmResponse]. + const LlmResponse({ + required this.result, + required this.model, + required this.latencyMs, + this.requestId, + }); + + /// Factory to create [LlmResponse] from JSON. + factory LlmResponse.fromJson(Map json) { + return LlmResponse( + result: json['result'] as Map, + model: json['model'] as String, + latencyMs: json['latencyMs'] as int, + requestId: json['requestId'] as String?, + ); + } + + /// The JSON result returned by the model. + final Map result; + + /// The model name used for invocation. + final String model; + + /// Time taken for the request in milliseconds. + final int latencyMs; + + /// The unique request ID from the server. + final String? requestId; + + /// Converts the response to a JSON map. + Map toJson() { + return { + 'result': result, + 'model': model, + 'latencyMs': latencyMs, + 'requestId': requestId, + }; + } +} diff --git a/apps/mobile/packages/core/lib/src/services/api_service/core_api_services/llm/llm_service.dart b/apps/mobile/packages/core/lib/src/services/api_service/core_api_services/llm/llm_service.dart new file mode 100644 index 00000000..0681dd1b --- /dev/null +++ b/apps/mobile/packages/core/lib/src/services/api_service/core_api_services/llm/llm_service.dart @@ -0,0 +1,31 @@ +import 'package:krow_domain/krow_domain.dart'; +import '../core_api_endpoints.dart'; + +/// Service for invoking Large Language Models (LLM). +class LlmService extends BaseCoreService { + /// Creates an [LlmService]. + LlmService(super.api); + + /// Invokes the LLM with a [prompt] and optional [schema]. + /// + /// [prompt] is the text instruction for the model. + /// [responseJsonSchema] is an optional JSON schema to enforce structure. + /// [fileUrls] are optional URLs of files (images/PDFs) to include in context. + Future invokeLlm({ + required String prompt, + Map? responseJsonSchema, + List? fileUrls, + }) async { + return action(() async { + return api.post( + CoreApiEndpoints.invokeLlm, + data: { + 'prompt': prompt, + if (responseJsonSchema != null) + 'responseJsonSchema': responseJsonSchema, + if (fileUrls != null) 'fileUrls': fileUrls, + }, + ); + }); + } +} diff --git a/apps/mobile/packages/core/lib/src/services/api_service/core_api_services/signed_url/signed_url_response.dart b/apps/mobile/packages/core/lib/src/services/api_service/core_api_services/signed_url/signed_url_response.dart new file mode 100644 index 00000000..bf286f07 --- /dev/null +++ b/apps/mobile/packages/core/lib/src/services/api_service/core_api_services/signed_url/signed_url_response.dart @@ -0,0 +1,36 @@ +/// Response model for creating a signed URL. +class SignedUrlResponse { + /// Creates a [SignedUrlResponse]. + const SignedUrlResponse({ + required this.signedUrl, + required this.expiresAt, + this.requestId, + }); + + /// Factory to create [SignedUrlResponse] from JSON. + factory SignedUrlResponse.fromJson(Map json) { + return SignedUrlResponse( + signedUrl: json['signedUrl'] as String, + expiresAt: DateTime.parse(json['expiresAt'] as String), + requestId: json['requestId'] as String?, + ); + } + + /// The generated signed URL. + final String signedUrl; + + /// The timestamp when the URL expires. + final DateTime expiresAt; + + /// The unique request ID from the server. + final String? requestId; + + /// Converts the response to a JSON map. + Map toJson() { + return { + 'signedUrl': signedUrl, + 'expiresAt': expiresAt.toIso8601String(), + 'requestId': requestId, + }; + } +} diff --git a/apps/mobile/packages/core/lib/src/services/api_service/core_api_services/signed_url/signed_url_service.dart b/apps/mobile/packages/core/lib/src/services/api_service/core_api_services/signed_url/signed_url_service.dart new file mode 100644 index 00000000..31ca5948 --- /dev/null +++ b/apps/mobile/packages/core/lib/src/services/api_service/core_api_services/signed_url/signed_url_service.dart @@ -0,0 +1,27 @@ +import 'package:krow_domain/krow_domain.dart'; +import '../core_api_endpoints.dart'; + +/// Service for creating signed URLs for Cloud Storage objects. +class SignedUrlService extends BaseCoreService { + /// Creates a [SignedUrlService]. + SignedUrlService(super.api); + + /// Creates a signed URL for a specific [fileUri]. + /// + /// [fileUri] should be in gs:// format. + /// [expiresInSeconds] must be <= 900. + Future createSignedUrl({ + required String fileUri, + int expiresInSeconds = 300, + }) async { + return action(() async { + return api.post( + CoreApiEndpoints.createSignedUrl, + data: { + 'fileUri': fileUri, + 'expiresInSeconds': expiresInSeconds, + }, + ); + }); + } +} diff --git a/apps/mobile/packages/core/lib/src/services/api_service/core_api_services/verification/verification_response.dart b/apps/mobile/packages/core/lib/src/services/api_service/core_api_services/verification/verification_response.dart new file mode 100644 index 00000000..b59072c6 --- /dev/null +++ b/apps/mobile/packages/core/lib/src/services/api_service/core_api_services/verification/verification_response.dart @@ -0,0 +1,50 @@ +/// Response model for verification operations. +class VerificationResponse { + /// Creates a [VerificationResponse]. + const VerificationResponse({ + required this.verificationId, + required this.status, + this.type, + this.review, + this.requestId, + }); + + /// Factory to create [VerificationResponse] from JSON. + factory VerificationResponse.fromJson(Map json) { + return VerificationResponse( + verificationId: json['verificationId'] as String, + status: json['status'] as String, + type: json['type'] as String?, + review: json['review'] != null + ? json['review'] as Map + : null, + requestId: json['requestId'] as String?, + ); + } + + /// The unique ID of the verification job. + final String verificationId; + + /// Current status (e.g., PENDING, PROCESSING, SUCCESS, FAILED, NEEDS_REVIEW). + final String status; + + /// The type of verification (e.g., attire, government_id). + final String? type; + + /// Optional human review details. + final Map? review; + + /// The unique request ID from the server. + final String? requestId; + + /// Converts the response to a JSON map. + Map toJson() { + return { + 'verificationId': verificationId, + 'status': status, + 'type': type, + 'review': review, + 'requestId': requestId, + }; + } +} diff --git a/apps/mobile/packages/core/lib/src/services/api_service/core_api_services/verification/verification_service.dart b/apps/mobile/packages/core/lib/src/services/api_service/core_api_services/verification/verification_service.dart new file mode 100644 index 00000000..1446bddc --- /dev/null +++ b/apps/mobile/packages/core/lib/src/services/api_service/core_api_services/verification/verification_service.dart @@ -0,0 +1,69 @@ +import 'package:krow_domain/krow_domain.dart'; +import '../core_api_endpoints.dart'; + +/// Service for handling async verification jobs. +class VerificationService extends BaseCoreService { + /// Creates a [VerificationService]. + VerificationService(super.api); + + /// Enqueues a new verification job. + /// + /// [type] can be 'attire', 'government_id', etc. + /// [subjectType] is usually 'worker'. + /// [fileUri] is the gs:// path of the uploaded file. + Future createVerification({ + required String type, + required String subjectType, + required String subjectId, + required String fileUri, + Map? rules, + }) async { + return action(() async { + return api.post( + CoreApiEndpoints.verifications, + data: { + 'type': type, + 'subjectType': subjectType, + 'subjectId': subjectId, + 'fileUri': fileUri, + if (rules != null) 'rules': rules, + }, + ); + }); + } + + /// Polls the status of a specific verification. + Future getStatus(String verificationId) async { + return action(() async { + return api.get(CoreApiEndpoints.verificationStatus(verificationId)); + }); + } + + /// Submits a manual review decision. + /// + /// [decision] should be 'APPROVED' or 'REJECTED'. + Future reviewVerification({ + required String verificationId, + required String decision, + String? note, + String? reasonCode, + }) async { + return action(() async { + return api.post( + CoreApiEndpoints.verificationReview(verificationId), + data: { + 'decision': decision, + if (note != null) 'note': note, + if (reasonCode != null) 'reasonCode': reasonCode, + }, + ); + }); + } + + /// Retries a verification job that failed or needs re-processing. + Future retryVerification(String verificationId) async { + return action(() async { + return api.post(CoreApiEndpoints.verificationRetry(verificationId)); + }); + } +}