From f5d2a860f41344b847d4b3ae9576ed07c648d858 Mon Sep 17 00:00:00 2001 From: code-schreiber Date: Mon, 27 Apr 2026 12:07:43 +0200 Subject: [PATCH 01/16] Add some tests --- .../api/models/return_chat_event_test.dart | 61 +++++++++++++++++++ test/dummy_test.dart | 7 --- 2 files changed, 61 insertions(+), 7 deletions(-) create mode 100644 test/data/api/models/return_chat_event_test.dart delete mode 100644 test/dummy_test.dart diff --git a/test/data/api/models/return_chat_event_test.dart b/test/data/api/models/return_chat_event_test.dart new file mode 100644 index 0000000..3f529fe --- /dev/null +++ b/test/data/api/models/return_chat_event_test.dart @@ -0,0 +1,61 @@ +import 'package:evi_example/data/api/models/generated/lib/api.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'dart:convert'; + +void main() { + group('ReturnChatEvent Tests', () { + test('should parse a valid JSON string for return_chat_event', () { + final jsonString = ''' + { + "chat_id": "550e8400-e29b-41d4-a716-446655440000", + "emotion_features": "{\\"happiness\\": 0.8, \\"sadness\\": 0.1}", + "id": "660e8400-e29b-41d4-a716-446655440000", + "message_text": "Hello world", + "metadata": null, + "related_event_id": null, + "role": "USER", + "timestamp": 1672531200, + "type": "USER_MESSAGE" + } + '''; + + final Map jsonMap = jsonDecode(jsonString); + final event = ReturnChatEvent.fromJson(jsonMap); + + expect(event, isNotNull); + expect(event!.chatId, '550e8400-e29b-41d4-a716-446655440000'); + expect(event.id, '660e8400-e29b-41d4-a716-446655440000'); + expect(event.role, ReturnChatEventRole.USER); + expect(event.type, ReturnChatEventType.USER_MESSAGE); + expect(event.messageText, 'Hello world'); + expect(event.timestamp, 1672531200); + // Double-parsing check for emotion_features + expect(event.emotionFeatures, isNotNull); + final emotionMap = jsonDecode(event.emotionFeatures!); + expect(emotionMap['happiness'], 0.8); + expect(emotionMap['sadness'], 0.1); + }); + + test('should handle null emotion_features', () { + final jsonString = ''' + { + "chat_id": "550e8400-e29b-41d4-a716-446655440000", + "emotion_features": null, + "id": "660e8400-e29b-41d4-a716-446655440000", + "message_text": "Hello world", + "metadata": null, + "related_event_id": null, + "role": "USER", + "timestamp": 1672531200, + "type": "USER_MESSAGE" + } + '''; + + final Map jsonMap = jsonDecode(jsonString); + final event = ReturnChatEvent.fromJson(jsonMap); + + expect(event, isNotNull); + expect(event!.emotionFeatures, isNull); + }); + }); +} diff --git a/test/dummy_test.dart b/test/dummy_test.dart deleted file mode 100644 index cdc3313..0000000 --- a/test/dummy_test.dart +++ /dev/null @@ -1,7 +0,0 @@ -import 'package:flutter_test/flutter_test.dart'; - -void main() { - test('dummy test', () { - expect(true, isTrue); - }); -} From d513ac58bf455003e086b8d7e3ba9a75cc59ec65 Mon Sep 17 00:00:00 2001 From: code-schreiber Date: Mon, 27 Apr 2026 12:53:20 +0200 Subject: [PATCH 02/16] Add characterization tests for chat_provider.dart to be able to refactor without breaking existing functionality --- lib/provider/chat_provider.dart | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/lib/provider/chat_provider.dart b/lib/provider/chat_provider.dart index 98ea7b8..253bd45 100644 --- a/lib/provider/chat_provider.dart +++ b/lib/provider/chat_provider.dart @@ -87,7 +87,11 @@ class ChatProvider extends ChangeNotifier { throw Exception('Failed to fetch messages: ${response.statusCode}'); } - final data = json.decode(response.body); + return filterMessages(response.body); + } + + filterMessages(String body) { + final data = json.decode(body); final allMessages = data['events_page'] .map((message) => { 'role': message['role'], @@ -103,11 +107,10 @@ class ChatProvider extends ChangeNotifier { allMessages.first['text'].length > 200) { allMessages.removeAt(0); } - return allMessages; } - Map _processEmotions(List> messages) { + Map processEmotions(List> messages) { Map allEmotions = {}; int messageCount = 0; @@ -253,7 +256,7 @@ class ChatProvider extends ChangeNotifier { return false; } - emotions = _processEmotions(userMessages); + emotions = processEmotions(userMessages); whatWentWell = await _processWhatWentWell(userMessages); challenges = await _processChallenges(userMessages); From b43a589f80289ae7d6bb1cdca8e32b3a1e738f4c Mon Sep 17 00:00:00 2001 From: code-schreiber Date: Mon, 27 Apr 2026 13:20:49 +0200 Subject: [PATCH 03/16] Add openapi_generator and its strictly necessary generated files --- lib/data/api/chat_event_models.dart | 8 + lib/data/api/chat_events_spec.yaml | 283 ++++++++++++++++++ .../models/generated/doc/ReturnChatEvent.md | 23 ++ .../generated/doc/ReturnChatEventRole.md | 14 + .../generated/doc/ReturnChatEventType.md | 14 + lib/data/api/models/generated/lib/api.dart | 32 ++ .../api/models/generated/lib/api_helper.dart | 120 ++++++++ .../lib/model/return_chat_event.dart | 205 +++++++++++++ .../lib/model/return_chat_event_role.dart | 98 ++++++ .../lib/model/return_chat_event_type.dart | 137 +++++++++ pubspec.yaml | 2 + 11 files changed, 936 insertions(+) create mode 100644 lib/data/api/chat_event_models.dart create mode 100644 lib/data/api/chat_events_spec.yaml create mode 100644 lib/data/api/models/generated/doc/ReturnChatEvent.md create mode 100644 lib/data/api/models/generated/doc/ReturnChatEventRole.md create mode 100644 lib/data/api/models/generated/doc/ReturnChatEventType.md create mode 100644 lib/data/api/models/generated/lib/api.dart create mode 100644 lib/data/api/models/generated/lib/api_helper.dart create mode 100644 lib/data/api/models/generated/lib/model/return_chat_event.dart create mode 100644 lib/data/api/models/generated/lib/model/return_chat_event_role.dart create mode 100644 lib/data/api/models/generated/lib/model/return_chat_event_type.dart diff --git a/lib/data/api/chat_event_models.dart b/lib/data/api/chat_event_models.dart new file mode 100644 index 0000000..ec2a012 --- /dev/null +++ b/lib/data/api/chat_event_models.dart @@ -0,0 +1,8 @@ +// Openapi Generator last run: : 2026-04-27T13:21:51.864161 +import 'package:openapi_generator_annotations/openapi_generator_annotations.dart'; + +@Openapi( + inputSpec: InputSpec(path: 'lib/data/api/chat_events_spec.yaml'), + outputDirectory: 'lib/data/api/models/generated', + generatorName: Generator.dart) +class ChatEventModels {} \ No newline at end of file diff --git a/lib/data/api/chat_events_spec.yaml b/lib/data/api/chat_events_spec.yaml new file mode 100644 index 0000000..bbdb40f --- /dev/null +++ b/lib/data/api/chat_events_spec.yaml @@ -0,0 +1,283 @@ +openapi: 3.1.0 +info: + title: empathic-voice-interface + version: 1.0.0 +paths: + /v0/evi/chats/{id}: + get: + operationId: list-chat-events + summary: List chat events + description: Fetches a paginated list of **Chat** events. + tags: + - subpackage_chats + parameters: + - name: id + in: path + description: Identifier for a Chat. Formatted as a UUID. + required: true + schema: + type: string + format: uuid + - name: page_size + in: query + description: >- + Specifies the maximum number of results to include per page, + enabling pagination. The value must be between 1 and 100, inclusive. + + + For example, if `page_size` is set to 10, each page will include `up + to 10 items. Defaults to 10. + required: false + schema: + type: integer + - name: page_number + in: query + description: >- + Specifies the page number to retrieve, enabling pagination. + + + This parameter uses zero-based indexing. For example, setting + `page_number` to 0 retrieves the first page of results (items 0-9 if + `page_size` is 10), setting `page_number` to 1 retrieves the second + page (items 10-19), and so on. Defaults to 0, which retrieves the first + page. + required: false + schema: + type: integer + default: 0 + - name: ascending_order + in: query + description: >- + Specifies the sorting order of the results based on their creation + date. Set to true for ascending order (chronological, with the + oldest records first) and false for descending order + (reverse-chronological, with the newest records first). Defaults to + true. + required: false + schema: + type: boolean + - name: X-Hume-Api-Key + in: header + required: true + schema: + type: string + responses: + '200': + description: Success + content: + application/json: + schema: + $ref: '#/components/schemas/return_chat_paged_events' +servers: + - url: https://api.hume.ai +components: + schemas: + return_config_spec: + type: object + properties: + id: + type: string + description: Identifier for a Config. Formatted as a UUID. + version: + type: + - integer + - 'null' + description: >- + Version number for a Config. + + + Configs, Prompts, Custom Voices, and Tools are versioned. This + versioning system supports iterative development, allowing you to + progressively refine configurations and revert to previous versions + if needed. + + + Version numbers are integer values representing different iterations + of the Config. Each update to the Config increments its version + number. + required: + - id + description: The Config associated with this Chat. + title: return_config_spec + ReturnChatEventRole: + type: string + enum: + - USER + - AGENT + - SYSTEM + - TOOL + title: ReturnChatEventRole + ReturnChatEventType: + type: string + enum: + - AGENT_MESSAGE + - ASSISTANT_PROSODY + - CHAT_START_MESSAGE + - CHAT_END_MESSAGE + - FUNCTION_CALL + - FUNCTION_CALL_RESPONSE + - PAUSE_ONSET + - RESUME_ONSET + - SESSION_SETTINGS + - SYSTEM_PROMPT + - USER_INTERRUPTION + - USER_MESSAGE + - USER_RECORDING_START_MESSAGE + title: ReturnChatEventType + return_chat_event: + type: object + properties: + chat_id: + type: string + description: Identifier for the Chat this event occurred in. Formatted as a UUID. + emotion_features: + type: + - string + - 'null' + description: >- + Stringified JSON containing the prosody model inference results. + + + EVI uses the prosody model to measure 48 expressions related to + speech and vocal characteristics. These results contain a detailed + emotional and tonal analysis of the audio. Scores typically range + from 0 to 1, with higher values indicating a stronger confidence + level in the measured attribute. + id: + type: string + description: Identifier for a Chat Event. Formatted as a UUID. + message_text: + type: + - string + - 'null' + description: >- + The text of the Chat Event. This field contains the message content + for each event type listed in the `type` field. + metadata: + type: + - string + - 'null' + description: Stringified JSON with additional metadata about the chat event. + related_event_id: + type: + - string + - 'null' + description: >- + Identifier for a related chat event. Currently only seen on + ASSISTANT_PROSODY events, to point back to the ASSISTANT_MESSAGE + that generated these prosody scores + role: + $ref: '#/components/schemas/ReturnChatEventRole' + timestamp: + type: integer + format: int64 + description: >- + Time at which the Chat Event occurred. Measured in seconds since the + Unix epoch. + type: + $ref: '#/components/schemas/ReturnChatEventType' + required: + - chat_id + - id + - role + - timestamp + - type + description: A description of a single event in a chat returned from the server + title: return_chat_event + ReturnChatPagedEventsPaginationDirection: + type: string + enum: + - ASC + - DESC + title: ReturnChatPagedEventsPaginationDirection + ReturnChatPagedEventsStatus: + type: string + enum: + - ACTIVE + - USER_ENDED + - USER_TIMEOUT + - MAX_DURATION_TIMEOUT + - INACTIVITY_TIMEOUT + - ERROR + title: ReturnChatPagedEventsStatus + return_chat_paged_events: + type: object + properties: + chat_group_id: + type: string + description: >- + Identifier for the Chat Group. Any chat resumed from this Chat will + have the same `chat_group_id`. Formatted as a UUID. + config: + $ref: '#/components/schemas/return_config_spec' + end_timestamp: + type: + - integer + - 'null' + format: int64 + description: >- + Time at which the Chat ended. Measured in seconds since the Unix + epoch. + events_page: + type: array + items: + $ref: '#/components/schemas/return_chat_event' + description: List of Chat Events for the specified `page_number` and `page_size`. + id: + type: string + description: Identifier for a Chat. Formatted as a UUID. + metadata: + type: + - string + - 'null' + description: Stringified JSON with additional metadata about the chat. + page_number: + type: integer + description: >- + The page number of the returned list. + + + This value corresponds to the `page_number` parameter specified in + the request. Pagination uses zero-based indexing. + page_size: + type: integer + description: >- + The maximum number of items returned per page. + + + This value corresponds to the `page_size` parameter specified in + the request. + pagination_direction: + $ref: '#/components/schemas/ReturnChatPagedEventsPaginationDirection' + start_timestamp: + type: + - integer + - 'null' + format: int64 + description: >- + Time at which the Chat started. Measured in seconds since the Unix + epoch. + status: + $ref: '#/components/schemas/ReturnChatPagedEventsStatus' + total_pages: + type: integer + description: The total number of pages in the collection. + required: + - chat_group_id + - events_page + - id + - page_number + - page_size + - pagination_direction + - start_timestamp + - status + - total_pages + description: >- + A description of chat status with a paginated list of chat events + returned from the server + title: return_chat_paged_events + securitySchemes: + bearerAuth: + type: apiKey + in: header + name: X-Hume-Api-Key diff --git a/lib/data/api/models/generated/doc/ReturnChatEvent.md b/lib/data/api/models/generated/doc/ReturnChatEvent.md new file mode 100644 index 0000000..da2825a --- /dev/null +++ b/lib/data/api/models/generated/doc/ReturnChatEvent.md @@ -0,0 +1,23 @@ +# openapi.model.ReturnChatEvent + +## Load the model package +```dart +import 'package:openapi/api.dart'; +``` + +## Properties +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**chatId** | **String** | Identifier for the Chat this event occurred in. Formatted as a UUID. | +**emotionFeatures** | **String** | Stringified JSON containing the prosody model inference results. EVI uses the prosody model to measure 48 expressions related to speech and vocal characteristics. These results contain a detailed emotional and tonal analysis of the audio. Scores typically range from 0 to 1, with higher values indicating a stronger confidence level in the measured attribute. | [optional] +**id** | **String** | Identifier for a Chat Event. Formatted as a UUID. | +**messageText** | **String** | The text of the Chat Event. This field contains the message content for each event type listed in the `type` field. | [optional] +**metadata** | **String** | Stringified JSON with additional metadata about the chat event. | [optional] +**relatedEventId** | **String** | Identifier for a related chat event. Currently only seen on ASSISTANT_PROSODY events, to point back to the ASSISTANT_MESSAGE that generated these prosody scores | [optional] +**role** | [**ReturnChatEventRole**](ReturnChatEventRole.md) | | +**timestamp** | **int** | Time at which the Chat Event occurred. Measured in seconds since the Unix epoch. | +**type** | [**ReturnChatEventType**](ReturnChatEventType.md) | | + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/lib/data/api/models/generated/doc/ReturnChatEventRole.md b/lib/data/api/models/generated/doc/ReturnChatEventRole.md new file mode 100644 index 0000000..ca21c0e --- /dev/null +++ b/lib/data/api/models/generated/doc/ReturnChatEventRole.md @@ -0,0 +1,14 @@ +# openapi.model.ReturnChatEventRole + +## Load the model package +```dart +import 'package:openapi/api.dart'; +``` + +## Properties +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/lib/data/api/models/generated/doc/ReturnChatEventType.md b/lib/data/api/models/generated/doc/ReturnChatEventType.md new file mode 100644 index 0000000..51b147f --- /dev/null +++ b/lib/data/api/models/generated/doc/ReturnChatEventType.md @@ -0,0 +1,14 @@ +# openapi.model.ReturnChatEventType + +## Load the model package +```dart +import 'package:openapi/api.dart'; +``` + +## Properties +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/lib/data/api/models/generated/lib/api.dart b/lib/data/api/models/generated/lib/api.dart new file mode 100644 index 0000000..1ea5111 --- /dev/null +++ b/lib/data/api/models/generated/lib/api.dart @@ -0,0 +1,32 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.18 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +library openapi.api; + +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; + +import 'package:collection/collection.dart'; +import 'package:http/http.dart'; +import 'package:intl/intl.dart'; +import 'package:meta/meta.dart'; + +part 'api_helper.dart'; + +part 'model/return_chat_event.dart'; +part 'model/return_chat_event_role.dart'; +part 'model/return_chat_event_type.dart'; + +const _delimiters = {'csv': ',', 'ssv': ' ', 'tsv': '\t', 'pipes': '|'}; +const _dateEpochMarker = 'epoch'; + +bool _isEpochMarker(String? pattern) => + pattern == _dateEpochMarker || pattern == '/$_dateEpochMarker/'; diff --git a/lib/data/api/models/generated/lib/api_helper.dart b/lib/data/api/models/generated/lib/api_helper.dart new file mode 100644 index 0000000..0dcdf11 --- /dev/null +++ b/lib/data/api/models/generated/lib/api_helper.dart @@ -0,0 +1,120 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.18 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +part of openapi.api; + +class QueryParam { + const QueryParam(this.name, this.value); + + final String name; + final String value; + + @override + String toString() => + '${Uri.encodeQueryComponent(name)}=${Uri.encodeQueryComponent(value)}'; +} + +// Ported from the Java version. +Iterable _queryParams( + String collectionFormat, + String name, + dynamic value, +) { + // Assertions to run in debug mode only. + assert(name.isNotEmpty, 'Parameter cannot be an empty string.'); + + final params = []; + + if (value is List) { + if (collectionFormat == 'multi') { + return value.map( + (dynamic v) => QueryParam(name, parameterToString(v)), + ); + } + + // Default collection format is 'csv'. + if (collectionFormat.isEmpty) { + collectionFormat = 'csv'; // ignore: parameter_assignments + } + + final delimiter = _delimiters[collectionFormat] ?? ','; + + params.add(QueryParam( + name, + value.map(parameterToString).join(delimiter), + )); + } else if (value != null) { + params.add(QueryParam(name, parameterToString(value))); + } + + return params; +} + +/// Format the given parameter object into a [String]. +String parameterToString(dynamic value) { + if (value == null) { + return ''; + } + if (value is DateTime) { + return value.toUtc().toIso8601String(); + } + if (value is ReturnChatEventRole) { + return ReturnChatEventRoleTypeTransformer().encode(value).toString(); + } + if (value is ReturnChatEventType) { + return ReturnChatEventTypeTypeTransformer().encode(value).toString(); + } + return value.toString(); +} + +/// Returns the decoded body as UTF-8 if the given headers indicate an 'application/json' +/// content type. Otherwise, returns the decoded body as decoded by dart:http package. +Future _decodeBodyBytes(Response response) async { + final contentType = response.headers['content-type']; + return contentType != null && + contentType.toLowerCase().startsWith('application/json') + ? response.bodyBytes.isEmpty + ? '' + : utf8.decode(response.bodyBytes) + : response.body; +} + +/// Returns a valid [T] value found at the specified Map [key], null otherwise. +T? mapValueOfType(dynamic map, String key) { + final dynamic value = map is Map ? map[key] : null; + return value is T ? value : null; +} + +/// Returns a valid Map found at the specified Map [key], null otherwise. +Map? mapCastOfType(dynamic map, String key) { + final dynamic value = map is Map ? map[key] : null; + return value is Map ? value.cast() : null; +} + +/// Returns a valid [DateTime] found at the specified Map [key], null otherwise. +DateTime? mapDateTime(dynamic map, String key, [String? pattern]) { + final dynamic value = map is Map ? map[key] : null; + if (value != null) { + int? millis; + if (value is int) { + millis = value; + } else if (value is String) { + if (_isEpochMarker(pattern)) { + millis = int.tryParse(value); + } else { + return DateTime.tryParse(value); + } + } + if (millis != null) { + return DateTime.fromMillisecondsSinceEpoch(millis, isUtc: true); + } + } + return null; +} diff --git a/lib/data/api/models/generated/lib/model/return_chat_event.dart b/lib/data/api/models/generated/lib/model/return_chat_event.dart new file mode 100644 index 0000000..699c800 --- /dev/null +++ b/lib/data/api/models/generated/lib/model/return_chat_event.dart @@ -0,0 +1,205 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.18 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +part of openapi.api; + +class ReturnChatEvent { + /// Returns a new [ReturnChatEvent] instance. + ReturnChatEvent({ + required this.chatId, + this.emotionFeatures, + required this.id, + this.messageText, + this.metadata, + this.relatedEventId, + required this.role, + required this.timestamp, + required this.type, + }); + + /// Identifier for the Chat this event occurred in. Formatted as a UUID. + String chatId; + + /// Stringified JSON containing the prosody model inference results. EVI uses the prosody model to measure 48 expressions related to speech and vocal characteristics. These results contain a detailed emotional and tonal analysis of the audio. Scores typically range from 0 to 1, with higher values indicating a stronger confidence level in the measured attribute. + String? emotionFeatures; + + /// Identifier for a Chat Event. Formatted as a UUID. + String id; + + /// The text of the Chat Event. This field contains the message content for each event type listed in the `type` field. + String? messageText; + + /// Stringified JSON with additional metadata about the chat event. + String? metadata; + + /// Identifier for a related chat event. Currently only seen on ASSISTANT_PROSODY events, to point back to the ASSISTANT_MESSAGE that generated these prosody scores + String? relatedEventId; + + ReturnChatEventRole role; + + /// Time at which the Chat Event occurred. Measured in seconds since the Unix epoch. + int timestamp; + + ReturnChatEventType type; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is ReturnChatEvent && + other.chatId == chatId && + other.emotionFeatures == emotionFeatures && + other.id == id && + other.messageText == messageText && + other.metadata == metadata && + other.relatedEventId == relatedEventId && + other.role == role && + other.timestamp == timestamp && + other.type == type; + + @override + int get hashCode => + // ignore: unnecessary_parenthesis + (chatId.hashCode) + + (emotionFeatures == null ? 0 : emotionFeatures!.hashCode) + + (id.hashCode) + + (messageText == null ? 0 : messageText!.hashCode) + + (metadata == null ? 0 : metadata!.hashCode) + + (relatedEventId == null ? 0 : relatedEventId!.hashCode) + + (role.hashCode) + + (timestamp.hashCode) + + (type.hashCode); + + @override + String toString() => + 'ReturnChatEvent[chatId=$chatId, emotionFeatures=$emotionFeatures, id=$id, messageText=$messageText, metadata=$metadata, relatedEventId=$relatedEventId, role=$role, timestamp=$timestamp, type=$type]'; + + Map toJson() { + final json = {}; + json[r'chat_id'] = this.chatId; + if (this.emotionFeatures != null) { + json[r'emotion_features'] = this.emotionFeatures; + } else { + json[r'emotion_features'] = null; + } + json[r'id'] = this.id; + if (this.messageText != null) { + json[r'message_text'] = this.messageText; + } else { + json[r'message_text'] = null; + } + if (this.metadata != null) { + json[r'metadata'] = this.metadata; + } else { + json[r'metadata'] = null; + } + if (this.relatedEventId != null) { + json[r'related_event_id'] = this.relatedEventId; + } else { + json[r'related_event_id'] = null; + } + json[r'role'] = this.role; + json[r'timestamp'] = this.timestamp; + json[r'type'] = this.type; + return json; + } + + /// Returns a new [ReturnChatEvent] instance and imports its values from + /// [value] if it's a [Map], null otherwise. + // ignore: prefer_constructors_over_static_methods + static ReturnChatEvent? fromJson(dynamic value) { + if (value is Map) { + final json = value.cast(); + + // Ensure that the map contains the required keys. + // Note 1: the values aren't checked for validity beyond being non-null. + // Note 2: this code is stripped in release mode! + assert(() { + requiredKeys.forEach((key) { + assert(json.containsKey(key), + 'Required key "ReturnChatEvent[$key]" is missing from JSON.'); + assert(json[key] != null, + 'Required key "ReturnChatEvent[$key]" has a null value in JSON.'); + }); + return true; + }()); + + return ReturnChatEvent( + chatId: mapValueOfType(json, r'chat_id')!, + emotionFeatures: mapValueOfType(json, r'emotion_features'), + id: mapValueOfType(json, r'id')!, + messageText: mapValueOfType(json, r'message_text'), + metadata: mapValueOfType(json, r'metadata'), + relatedEventId: mapValueOfType(json, r'related_event_id'), + role: ReturnChatEventRole.fromJson(json[r'role'])!, + timestamp: mapValueOfType(json, r'timestamp')!, + type: ReturnChatEventType.fromJson(json[r'type'])!, + ); + } + return null; + } + + static List listFromJson( + dynamic json, { + bool growable = false, + }) { + final result = []; + if (json is List && json.isNotEmpty) { + for (final row in json) { + final value = ReturnChatEvent.fromJson(row); + if (value != null) { + result.add(value); + } + } + } + return result.toList(growable: growable); + } + + static Map mapFromJson(dynamic json) { + final map = {}; + if (json is Map && json.isNotEmpty) { + json = json.cast(); // ignore: parameter_assignments + for (final entry in json.entries) { + final value = ReturnChatEvent.fromJson(entry.value); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + // maps a json object with a list of ReturnChatEvent-objects as value to a dart map + static Map> mapListFromJson( + dynamic json, { + bool growable = false, + }) { + final map = >{}; + if (json is Map && json.isNotEmpty) { + // ignore: parameter_assignments + json = json.cast(); + for (final entry in json.entries) { + map[entry.key] = ReturnChatEvent.listFromJson( + entry.value, + growable: growable, + ); + } + } + return map; + } + + /// The list of required keys that must be present in a JSON. + static const requiredKeys = { + 'chat_id', + 'id', + 'role', + 'timestamp', + 'type', + }; +} diff --git a/lib/data/api/models/generated/lib/model/return_chat_event_role.dart b/lib/data/api/models/generated/lib/model/return_chat_event_role.dart new file mode 100644 index 0000000..32847aa --- /dev/null +++ b/lib/data/api/models/generated/lib/model/return_chat_event_role.dart @@ -0,0 +1,98 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.18 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +part of openapi.api; + +class ReturnChatEventRole { + /// Instantiate a new enum with the provided [value]. + const ReturnChatEventRole._(this.value); + + /// The underlying value of this enum member. + final String value; + + @override + String toString() => value; + + String toJson() => value; + + static const USER = ReturnChatEventRole._(r'USER'); + static const AGENT = ReturnChatEventRole._(r'AGENT'); + static const SYSTEM = ReturnChatEventRole._(r'SYSTEM'); + static const TOOL = ReturnChatEventRole._(r'TOOL'); + + /// List of all possible values in this [enum][ReturnChatEventRole]. + static const values = [ + USER, + AGENT, + SYSTEM, + TOOL, + ]; + + static ReturnChatEventRole? fromJson(dynamic value) => + ReturnChatEventRoleTypeTransformer().decode(value); + + static List listFromJson( + dynamic json, { + bool growable = false, + }) { + final result = []; + if (json is List && json.isNotEmpty) { + for (final row in json) { + final value = ReturnChatEventRole.fromJson(row); + if (value != null) { + result.add(value); + } + } + } + return result.toList(growable: growable); + } +} + +/// Transformation class that can [encode] an instance of [ReturnChatEventRole] to String, +/// and [decode] dynamic data back to [ReturnChatEventRole]. +class ReturnChatEventRoleTypeTransformer { + factory ReturnChatEventRoleTypeTransformer() => + _instance ??= const ReturnChatEventRoleTypeTransformer._(); + + const ReturnChatEventRoleTypeTransformer._(); + + String encode(ReturnChatEventRole data) => data.value; + + /// Decodes a [dynamic value][data] to a ReturnChatEventRole. + /// + /// If [allowNull] is true and the [dynamic value][data] cannot be decoded successfully, + /// then null is returned. However, if [allowNull] is false and the [dynamic value][data] + /// cannot be decoded successfully, then an [UnimplementedError] is thrown. + /// + /// The [allowNull] is very handy when an API changes and a new enum value is added or removed, + /// and users are still using an old app with the old code. + ReturnChatEventRole? decode(dynamic data, {bool allowNull = true}) { + if (data != null) { + switch (data) { + case r'USER': + return ReturnChatEventRole.USER; + case r'AGENT': + return ReturnChatEventRole.AGENT; + case r'SYSTEM': + return ReturnChatEventRole.SYSTEM; + case r'TOOL': + return ReturnChatEventRole.TOOL; + default: + if (!allowNull) { + throw ArgumentError('Unknown enum value to decode: $data'); + } + } + } + return null; + } + + /// Singleton [ReturnChatEventRoleTypeTransformer] instance. + static ReturnChatEventRoleTypeTransformer? _instance; +} diff --git a/lib/data/api/models/generated/lib/model/return_chat_event_type.dart b/lib/data/api/models/generated/lib/model/return_chat_event_type.dart new file mode 100644 index 0000000..4bd94ff --- /dev/null +++ b/lib/data/api/models/generated/lib/model/return_chat_event_type.dart @@ -0,0 +1,137 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.18 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +part of openapi.api; + +class ReturnChatEventType { + /// Instantiate a new enum with the provided [value]. + const ReturnChatEventType._(this.value); + + /// The underlying value of this enum member. + final String value; + + @override + String toString() => value; + + String toJson() => value; + + static const AGENT_MESSAGE = ReturnChatEventType._(r'AGENT_MESSAGE'); + static const ASSISTANT_PROSODY = ReturnChatEventType._(r'ASSISTANT_PROSODY'); + static const CHAT_START_MESSAGE = + ReturnChatEventType._(r'CHAT_START_MESSAGE'); + static const CHAT_END_MESSAGE = ReturnChatEventType._(r'CHAT_END_MESSAGE'); + static const FUNCTION_CALL = ReturnChatEventType._(r'FUNCTION_CALL'); + static const FUNCTION_CALL_RESPONSE = + ReturnChatEventType._(r'FUNCTION_CALL_RESPONSE'); + static const PAUSE_ONSET = ReturnChatEventType._(r'PAUSE_ONSET'); + static const RESUME_ONSET = ReturnChatEventType._(r'RESUME_ONSET'); + static const SESSION_SETTINGS = ReturnChatEventType._(r'SESSION_SETTINGS'); + static const SYSTEM_PROMPT = ReturnChatEventType._(r'SYSTEM_PROMPT'); + static const USER_INTERRUPTION = ReturnChatEventType._(r'USER_INTERRUPTION'); + static const USER_MESSAGE = ReturnChatEventType._(r'USER_MESSAGE'); + static const USER_RECORDING_START_MESSAGE = + ReturnChatEventType._(r'USER_RECORDING_START_MESSAGE'); + + /// List of all possible values in this [enum][ReturnChatEventType]. + static const values = [ + AGENT_MESSAGE, + ASSISTANT_PROSODY, + CHAT_START_MESSAGE, + CHAT_END_MESSAGE, + FUNCTION_CALL, + FUNCTION_CALL_RESPONSE, + PAUSE_ONSET, + RESUME_ONSET, + SESSION_SETTINGS, + SYSTEM_PROMPT, + USER_INTERRUPTION, + USER_MESSAGE, + USER_RECORDING_START_MESSAGE, + ]; + + static ReturnChatEventType? fromJson(dynamic value) => + ReturnChatEventTypeTypeTransformer().decode(value); + + static List listFromJson( + dynamic json, { + bool growable = false, + }) { + final result = []; + if (json is List && json.isNotEmpty) { + for (final row in json) { + final value = ReturnChatEventType.fromJson(row); + if (value != null) { + result.add(value); + } + } + } + return result.toList(growable: growable); + } +} + +/// Transformation class that can [encode] an instance of [ReturnChatEventType] to String, +/// and [decode] dynamic data back to [ReturnChatEventType]. +class ReturnChatEventTypeTypeTransformer { + factory ReturnChatEventTypeTypeTransformer() => + _instance ??= const ReturnChatEventTypeTypeTransformer._(); + + const ReturnChatEventTypeTypeTransformer._(); + + String encode(ReturnChatEventType data) => data.value; + + /// Decodes a [dynamic value][data] to a ReturnChatEventType. + /// + /// If [allowNull] is true and the [dynamic value][data] cannot be decoded successfully, + /// then null is returned. However, if [allowNull] is false and the [dynamic value][data] + /// cannot be decoded successfully, then an [UnimplementedError] is thrown. + /// + /// The [allowNull] is very handy when an API changes and a new enum value is added or removed, + /// and users are still using an old app with the old code. + ReturnChatEventType? decode(dynamic data, {bool allowNull = true}) { + if (data != null) { + switch (data) { + case r'AGENT_MESSAGE': + return ReturnChatEventType.AGENT_MESSAGE; + case r'ASSISTANT_PROSODY': + return ReturnChatEventType.ASSISTANT_PROSODY; + case r'CHAT_START_MESSAGE': + return ReturnChatEventType.CHAT_START_MESSAGE; + case r'CHAT_END_MESSAGE': + return ReturnChatEventType.CHAT_END_MESSAGE; + case r'FUNCTION_CALL': + return ReturnChatEventType.FUNCTION_CALL; + case r'FUNCTION_CALL_RESPONSE': + return ReturnChatEventType.FUNCTION_CALL_RESPONSE; + case r'PAUSE_ONSET': + return ReturnChatEventType.PAUSE_ONSET; + case r'RESUME_ONSET': + return ReturnChatEventType.RESUME_ONSET; + case r'SESSION_SETTINGS': + return ReturnChatEventType.SESSION_SETTINGS; + case r'SYSTEM_PROMPT': + return ReturnChatEventType.SYSTEM_PROMPT; + case r'USER_INTERRUPTION': + return ReturnChatEventType.USER_INTERRUPTION; + case r'USER_MESSAGE': + return ReturnChatEventType.USER_MESSAGE; + case r'USER_RECORDING_START_MESSAGE': + return ReturnChatEventType.USER_RECORDING_START_MESSAGE; + default: + if (!allowNull) { + throw ArgumentError('Unknown enum value to decode: $data'); + } + } + } + return null; + } + + /// Singleton [ReturnChatEventTypeTypeTransformer] instance. + static ReturnChatEventTypeTypeTransformer? _instance; +} diff --git a/pubspec.yaml b/pubspec.yaml index c8aaa07..2825129 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -43,6 +43,7 @@ dependencies: http: ^1.2.2 freezed_annotation: ^2.4.4 json_annotation: ^4.9.0 + openapi_generator_annotations: ^6.1.0 provider: ^6.1.1 flutter_svg: ^2.0.0 fl_chart: ^0.69.2 @@ -66,6 +67,7 @@ dev_dependencies: # rules and activating additional ones. flutter_lints: ^5.0.0 build_runner: ^2.4.13 + openapi_generator: ^6.1.0 freezed: ^2.5.7 json_serializable: ^6.9.0 From 9ce98604db2ebc4ba0fa179fc4a7d8059f56e7f1 Mon Sep 17 00:00:00 2001 From: code-schreiber Date: Wed, 29 Apr 2026 14:34:12 +0200 Subject: [PATCH 04/16] Add characterization tests to be able to refactor without breaking existing functionality --- lib/provider/chat_provider.dart | 4 +- test/provider/chat_provider_test.dart | 235 ++++++++++++++++++++++++++ 2 files changed, 237 insertions(+), 2 deletions(-) create mode 100644 test/provider/chat_provider_test.dart diff --git a/lib/provider/chat_provider.dart b/lib/provider/chat_provider.dart index 253bd45..1de600b 100644 --- a/lib/provider/chat_provider.dart +++ b/lib/provider/chat_provider.dart @@ -90,7 +90,7 @@ class ChatProvider extends ChangeNotifier { return filterMessages(response.body); } - filterMessages(String body) { + static List> filterMessages(String body) { final data = json.decode(body); final allMessages = data['events_page'] .map((message) => { @@ -110,7 +110,7 @@ class ChatProvider extends ChangeNotifier { return allMessages; } - Map processEmotions(List> messages) { + static Map processEmotions(List> messages) { Map allEmotions = {}; int messageCount = 0; diff --git a/test/provider/chat_provider_test.dart b/test/provider/chat_provider_test.dart new file mode 100644 index 0000000..795aba7 --- /dev/null +++ b/test/provider/chat_provider_test.dart @@ -0,0 +1,235 @@ +import 'package:evi_example/provider/chat_provider.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + group('ChatProvider Tests', () { + group('filterMessages', () { + test( + 'removes messages from the start until finding one less than 200 characters', + () { + final json = ''' + { + "events_page": [ + { + "role": "SYSTEM", + "message_text": "this is more than 200 characters long this is more than 200 characters long this is more than 200 characters long this is more than 200 characters long this is more than 200 characters long this is long", + "emotion_features": null + }, + { + "role": "SYSTEM", + "message_text": "this is less than 200 characters", + "emotion_features": null + } + ] + } + '''; + + final result = ChatProvider.filterMessages(json); + + expect(result[0]['text'], "this is less than 200 characters"); + expect(result.length, 1); + }); + + test('handles null message_text gracefully', () { + final json = ''' + { + "events_page": [ + { + "role": "SYSTEM", + "message_text": null, + "emotion_features": null + }, + { + "role": "USER", + "message_text": null + }, + { + "role": "SYSTEM", + "message_text": "this is more than 200 characters long this is more than 200 characters long this is more than 200 characters long this is more than 200 characters long this is more than 200 characters long this is long", + "emotion_features": null + }, + { + "role": "SYSTEM", + "message_text": "this is less than 200 characters", + "emotion_features": null + } + ] + } + '''; + + final result = ChatProvider.filterMessages(json); + + expect(result[0]['text'], '1x'); + expect(result[1]['text'], '2x'); + expect(result.length, 2); + }); + + test('handles empty events_page list', () { + final jsonInput = '{"events_page": []}'; + + final result = ChatProvider.filterMessages(jsonInput); + + expect(result, isEmpty); + }); + + test('no trimming when first message is USER', () { + final jsonInput = ''' + { + "events_page": [ + {"role": "USER", "message_text": "Hello!", "emotion_features": "{}"} + ] + } + '''; + + final result = ChatProvider.filterMessages(jsonInput); + + expect(result.length, 1); + expect(result.first['role'], 'USER'); + }); + + test('stops trimming when non-USER message is short (<= 200 chars)', () { + final jsonInput = ''' + { + "events_page": [ + {"role": "ASSISTANT", "message_text": "${"a" * 250}", "emotion_features": "{}"}, + {"role": "ASSISTANT", "message_text": "Short message", "emotion_features": "{}"}, + {"role": "USER", "message_text": "User hello", "emotion_features": "{}"} + ] + } + '''; + + final result = ChatProvider.filterMessages(jsonInput); + + // It should stop at the "Short message" because it's <= 200 chars, even though it's not USER. + expect(result[0]['text'], 'Short message'); + expect(result[1]['text'], 'User hello'); + expect(result.length, 2); + }); + + test('trims multiple long non-USER messages', () { + final jsonInput = ''' + { + "events_page": [ + {"role": "ASSISTANT", "message_text": "${"a" * 250}", "emotion_features": "{}"}, + {"role": "ASSISTANT", "message_text": "${"b" * 250}", "emotion_features": "{}"}, + {"role": "USER", "message_text": "User text", "emotion_features": "{}"} + ] + } + '''; + + final result = ChatProvider.filterMessages(jsonInput); + + expect(result.length, 1); + expect(result.first['role'], 'USER'); + }); + + test('trims until specific condition is met in mixed sequence', () { + final jsonInput = ''' + { + "events_page": [ + {"role": "ASSISTANT", "message_text": "${"a" * 250}", "emotion_features": "{}"}, + {"role": "ASSISTANT", "message_text": "${"b" * 250}", "emotion_features": "{}"}, + {"role": "ASSISTANT", "message_text": "Short one", "emotion_features": "{}"}, + {"role": "USER", "message_text": "User text", "emotion_features": "{}"} + ] + } + '''; + + final result = ChatProvider.filterMessages(jsonInput); + + expect(result.length, 2); + expect(result.first['text'], 'Short one'); + }); + + test('stops trimming if role changes to USER', () { + final jsonInput = ''' + { + "events_page": [ + {"role": "ASSISTANT", "message_text": "${"a" * 250}", "emotion_features": "{}"}, + {"role": "USER", "message_text": "${"b" * 250}", "emotion_features": "{}"} + ] + } + '''; + + final result = ChatProvider.filterMessages(jsonInput); + + // Even if the USER message is long, it should stop because role == 'USER'. + expect(result.first['role'], 'USER'); + expect(result.length, 1); + }); + + test('handles malformed JSON input', () { + final jsonInput = '{ invalid }'; + expect(() => ChatProvider.filterMessages(jsonInput), + throwsA(isA())); + }); + + test('handles missing events_page key in JSON', () { + final jsonInput = '{"not_events": []}'; + + expect(() => ChatProvider.filterMessages(jsonInput), + throwsNoSuchMethodError); + }); + }); + + group('processEmotions', () { + test('calculates averages', () { + final messages = [ + {'emotion_features': '{"Admiration": 0.4, "Adoration": 0.2}'}, + { + 'emotion_features': + '{"Admiration": 0.2, "Adoration": 0.6, "Awkwardness": 1.0}' + }, + {'emotion_features': null} + ]; + + final result = ChatProvider.processEmotions(messages); + + expect(result['Awkwardness'], 0.5); + expect(result['Adoration'], 0.4); + expect(result['Admiration'], (0.4 + 0.2) / 2); + }); + + test('sorts averages', () { + final messages = [ + {'emotion_features': '{"Admiration": 0.4, "Adoration": 0.2}'}, + { + 'emotion_features': + '{"Admiration": 0.2, "Adoration": 0.6, "Awkwardness": 1.0}' + }, + {'emotion_features': null} + ]; + + final result = ChatProvider.processEmotions(messages); + + expect(result.keys.toString(), "(Awkwardness, Adoration, Admiration)"); + expect(result.values.toString(), "(0.5, 0.4, ${(0.4 + 0.2) / 2})"); + }); + + test('ignores empty emotion_features', () { + final messages = [ + {'emotion_features': '{"Admiration": 0.8}'}, + {'emotion_features': null} + ]; + + final result = ChatProvider.processEmotions(messages); + + expect(result['Admiration'], 0.8); + expect(result.length, 1); + }); + + test('ignores empty emotion_features', () { + final messages = [ + {'emotion_features': '{"Admiration": 0.8}'}, + {'emotion_features': '{}'} + ]; + + final result = ChatProvider.processEmotions(messages); + + final expected = {}; + expected['Admiration'] = 0.4; + expect(result, expected); + }); + }); + }); +} From 8aad67f2da07d37c4994aa9ee222a251cac2df3f Mon Sep 17 00:00:00 2001 From: code-schreiber Date: Wed, 29 Apr 2026 14:43:44 +0200 Subject: [PATCH 05/16] Improve existing functionality with tests safety net --- lib/provider/chat_provider.dart | 15 ++++++--------- test/provider/chat_provider_test.dart | 8 +++++--- 2 files changed, 11 insertions(+), 12 deletions(-) diff --git a/lib/provider/chat_provider.dart b/lib/provider/chat_provider.dart index 1de600b..b84b957 100644 --- a/lib/provider/chat_provider.dart +++ b/lib/provider/chat_provider.dart @@ -68,7 +68,7 @@ class ChatProvider extends ChangeNotifier { _context = context; } - Future>> _fetchChatMessages(String chatId) async { + Future _fetchChatMessages(String chatId) async { final url = Uri.parse('https://api.hume.ai/v0/evi/chats/$chatId') .replace(queryParameters: { 'page_number': '0', @@ -86,8 +86,7 @@ class ChatProvider extends ChangeNotifier { if (response.statusCode != 200) { throw Exception('Failed to fetch messages: ${response.statusCode}'); } - - return filterMessages(response.body); + return response.body; } static List> filterMessages(String body) { @@ -104,6 +103,7 @@ class ChatProvider extends ChangeNotifier { // Remove messages from the start until finding one less than 200 characters while (allMessages.isNotEmpty && allMessages.first['role'] != 'USER' && + allMessages.first['text'] != null && allMessages.first['text'].length > 200) { allMessages.removeAt(0); } @@ -243,14 +243,11 @@ class ChatProvider extends ChangeNotifier { if (_chats.isEmpty) return false; try { - final chatId = _chats.last['chat_id']; - - // Fetch messages - final messages = await _fetchChatMessages(chatId); - _chatMessages = messages; + final body = await _fetchChatMessages(_chats.last['chat_id']); + _chatMessages = filterMessages(body); // Process emotions for user messages - final userMessages = messages.where((m) => m['role'] == 'USER').toList(); + final userMessages = _chatMessages.where((m) => m['role'] == 'USER').toList(); if (userMessages.isEmpty) { print('No user messages found.'); return false; diff --git a/test/provider/chat_provider_test.dart b/test/provider/chat_provider_test.dart index 795aba7..49d4a59 100644 --- a/test/provider/chat_provider_test.dart +++ b/test/provider/chat_provider_test.dart @@ -59,9 +59,11 @@ void main() { final result = ChatProvider.filterMessages(json); - expect(result[0]['text'], '1x'); - expect(result[1]['text'], '2x'); - expect(result.length, 2); + expect(result[0]["role"], "SYSTEM"); + expect(result[1]["role"], "USER"); + expect(result[2]["role"], "SYSTEM"); + expect(result[3]["role"], "SYSTEM"); + expect(result.length, 4); }); test('handles empty events_page list', () { From 984e374e9e98a5aff487769f638c12a81f7ab224 Mon Sep 17 00:00:00 2001 From: code-schreiber Date: Mon, 4 May 2026 12:42:45 +0200 Subject: [PATCH 06/16] Adapt JSON structure in tests to reflect reality --- .../api/models/generated/lib/api_helper.dart | 10 + .../lib/model/return_chat_paged_events.dart | 240 +++++++++++++ ...hat_paged_events_pagination_direction.dart | 92 +++++ .../return_chat_paged_events_status.dart | 108 ++++++ .../models/return_chat_paged_events_test.dart | 59 ++++ test/provider/chat_provider_test.dart | 320 ++++++++++++++---- 6 files changed, 770 insertions(+), 59 deletions(-) create mode 100644 lib/data/api/models/generated/lib/model/return_chat_paged_events.dart create mode 100644 lib/data/api/models/generated/lib/model/return_chat_paged_events_pagination_direction.dart create mode 100644 lib/data/api/models/generated/lib/model/return_chat_paged_events_status.dart create mode 100644 test/data/api/models/return_chat_paged_events_test.dart diff --git a/lib/data/api/models/generated/lib/api_helper.dart b/lib/data/api/models/generated/lib/api_helper.dart index 0dcdf11..1fa0174 100644 --- a/lib/data/api/models/generated/lib/api_helper.dart +++ b/lib/data/api/models/generated/lib/api_helper.dart @@ -71,6 +71,16 @@ String parameterToString(dynamic value) { if (value is ReturnChatEventType) { return ReturnChatEventTypeTypeTransformer().encode(value).toString(); } + if (value is ReturnChatPagedEventsPaginationDirection) { + return ReturnChatPagedEventsPaginationDirectionTypeTransformer() + .encode(value) + .toString(); + } + if (value is ReturnChatPagedEventsStatus) { + return ReturnChatPagedEventsStatusTypeTransformer() + .encode(value) + .toString(); + } return value.toString(); } diff --git a/lib/data/api/models/generated/lib/model/return_chat_paged_events.dart b/lib/data/api/models/generated/lib/model/return_chat_paged_events.dart new file mode 100644 index 0000000..114a8da --- /dev/null +++ b/lib/data/api/models/generated/lib/model/return_chat_paged_events.dart @@ -0,0 +1,240 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.18 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +part of openapi.api; + +class ReturnChatPagedEvents { + /// Returns a new [ReturnChatPagedEvents] instance. + ReturnChatPagedEvents({ + required this.chatGroupId, + this.config, + this.endTimestamp, + this.eventsPage = const [], + required this.id, + this.metadata, + required this.pageNumber, + required this.pageSize, + required this.paginationDirection, + required this.startTimestamp, + required this.status, + required this.totalPages, + }); + + /// Identifier for the Chat Group. Any chat resumed from this Chat will have the same `chat_group_id`. Formatted as a UUID. + String chatGroupId; + + /// + /// Please note: This property should have been non-nullable! Since the specification file + /// does not include a default value (using the "default:" property), however, the generated + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// + ReturnConfigSpec? config; + + /// Time at which the Chat ended. Measured in seconds since the Unix epoch. + int? endTimestamp; + + /// List of Chat Events for the specified `page_number` and `page_size`. + List eventsPage; + + /// Identifier for a Chat. Formatted as a UUID. + String id; + + /// Stringified JSON with additional metadata about the chat. + String? metadata; + + /// The page number of the returned list. This value corresponds to the `page_number` parameter specified in the request. Pagination uses zero-based indexing. + int pageNumber; + + /// The maximum number of items returned per page. This value corresponds to the `page_size` parameter specified in the request. + int pageSize; + + ReturnChatPagedEventsPaginationDirection paginationDirection; + + /// Time at which the Chat started. Measured in seconds since the Unix epoch. + int? startTimestamp; + + ReturnChatPagedEventsStatus status; + + /// The total number of pages in the collection. + int totalPages; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is ReturnChatPagedEvents && + other.chatGroupId == chatGroupId && + other.config == config && + other.endTimestamp == endTimestamp && + _deepEquality.equals(other.eventsPage, eventsPage) && + other.id == id && + other.metadata == metadata && + other.pageNumber == pageNumber && + other.pageSize == pageSize && + other.paginationDirection == paginationDirection && + other.startTimestamp == startTimestamp && + other.status == status && + other.totalPages == totalPages; + + @override + int get hashCode => + // ignore: unnecessary_parenthesis + (chatGroupId.hashCode) + + (config == null ? 0 : config!.hashCode) + + (endTimestamp == null ? 0 : endTimestamp!.hashCode) + + (eventsPage.hashCode) + + (id.hashCode) + + (metadata == null ? 0 : metadata!.hashCode) + + (pageNumber.hashCode) + + (pageSize.hashCode) + + (paginationDirection.hashCode) + + (startTimestamp == null ? 0 : startTimestamp!.hashCode) + + (status.hashCode) + + (totalPages.hashCode); + + @override + String toString() => + 'ReturnChatPagedEvents[chatGroupId=$chatGroupId, config=$config, endTimestamp=$endTimestamp, eventsPage=$eventsPage, id=$id, metadata=$metadata, pageNumber=$pageNumber, pageSize=$pageSize, paginationDirection=$paginationDirection, startTimestamp=$startTimestamp, status=$status, totalPages=$totalPages]'; + + Map toJson() { + final json = {}; + json[r'chat_group_id'] = this.chatGroupId; + if (this.config != null) { + json[r'config'] = this.config; + } else { + json[r'config'] = null; + } + if (this.endTimestamp != null) { + json[r'end_timestamp'] = this.endTimestamp; + } else { + json[r'end_timestamp'] = null; + } + json[r'events_page'] = this.eventsPage; + json[r'id'] = this.id; + if (this.metadata != null) { + json[r'metadata'] = this.metadata; + } else { + json[r'metadata'] = null; + } + json[r'page_number'] = this.pageNumber; + json[r'page_size'] = this.pageSize; + json[r'pagination_direction'] = this.paginationDirection; + if (this.startTimestamp != null) { + json[r'start_timestamp'] = this.startTimestamp; + } else { + json[r'start_timestamp'] = null; + } + json[r'status'] = this.status; + json[r'total_pages'] = this.totalPages; + return json; + } + + /// Returns a new [ReturnChatPagedEvents] instance and imports its values from + /// [value] if it's a [Map], null otherwise. + // ignore: prefer_constructors_over_static_methods + static ReturnChatPagedEvents? fromJson(String value) { + final data = jsonDecode(value); + if (data is Map) { + final json = data.cast(); + + // Ensure that the map contains the required keys. + // Note 1: the values aren't checked for validity beyond being non-null. + // Note 2: this code is stripped in release mode! + assert(() { + requiredKeys.forEach((key) { + assert(json.containsKey(key), + 'Required key "ReturnChatPagedEvents[$key]" is missing from JSON.'); + assert(json[key] != null, + 'Required key "ReturnChatPagedEvents[$key]" has a null value in JSON.'); + }); + return true; + }()); + + return ReturnChatPagedEvents( + chatGroupId: mapValueOfType(json, r'chat_group_id')!, + config: ReturnConfigSpec.fromJson(json[r'config']), + endTimestamp: mapValueOfType(json, r'end_timestamp'), + eventsPage: ReturnChatEvent.listFromJson(json[r'events_page']), + id: mapValueOfType(json, r'id')!, + metadata: mapValueOfType(json, r'metadata'), + pageNumber: mapValueOfType(json, r'page_number')!, + pageSize: mapValueOfType(json, r'page_size')!, + paginationDirection: ReturnChatPagedEventsPaginationDirection.fromJson( + json[r'pagination_direction'])!, + startTimestamp: mapValueOfType(json, r'start_timestamp'), + status: ReturnChatPagedEventsStatus.fromJson(json[r'status'])!, + totalPages: mapValueOfType(json, r'total_pages')!, + ); + } + return null; + } + + static List listFromJson( + dynamic json, { + bool growable = false, + }) { + final result = []; + if (json is List && json.isNotEmpty) { + for (final row in json) { + final value = ReturnChatPagedEvents.fromJson(row); + if (value != null) { + result.add(value); + } + } + } + return result.toList(growable: growable); + } + + static Map mapFromJson(dynamic json) { + final map = {}; + if (json is Map && json.isNotEmpty) { + json = json.cast(); // ignore: parameter_assignments + for (final entry in json.entries) { + final value = ReturnChatPagedEvents.fromJson(entry.value); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + // maps a json object with a list of ReturnChatPagedEvents-objects as value to a dart map + static Map> mapListFromJson( + dynamic json, { + bool growable = false, + }) { + final map = >{}; + if (json is Map && json.isNotEmpty) { + // ignore: parameter_assignments + json = json.cast(); + for (final entry in json.entries) { + map[entry.key] = ReturnChatPagedEvents.listFromJson( + entry.value, + growable: growable, + ); + } + } + return map; + } + + /// The list of required keys that must be present in a JSON. + static const requiredKeys = { + 'chat_group_id', + 'events_page', + 'id', + 'page_number', + 'page_size', + 'pagination_direction', + 'start_timestamp', + 'status', + 'total_pages', + }; +} diff --git a/lib/data/api/models/generated/lib/model/return_chat_paged_events_pagination_direction.dart b/lib/data/api/models/generated/lib/model/return_chat_paged_events_pagination_direction.dart new file mode 100644 index 0000000..84a5b36 --- /dev/null +++ b/lib/data/api/models/generated/lib/model/return_chat_paged_events_pagination_direction.dart @@ -0,0 +1,92 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.18 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +part of openapi.api; + +class ReturnChatPagedEventsPaginationDirection { + /// Instantiate a new enum with the provided [value]. + const ReturnChatPagedEventsPaginationDirection._(this.value); + + /// The underlying value of this enum member. + final String value; + + @override + String toString() => value; + + String toJson() => value; + + static const ASC = ReturnChatPagedEventsPaginationDirection._(r'ASC'); + static const DESC = ReturnChatPagedEventsPaginationDirection._(r'DESC'); + + /// List of all possible values in this [enum][ReturnChatPagedEventsPaginationDirection]. + static const values = [ + ASC, + DESC, + ]; + + static ReturnChatPagedEventsPaginationDirection? fromJson(dynamic value) => + ReturnChatPagedEventsPaginationDirectionTypeTransformer().decode(value); + + static List listFromJson( + dynamic json, { + bool growable = false, + }) { + final result = []; + if (json is List && json.isNotEmpty) { + for (final row in json) { + final value = ReturnChatPagedEventsPaginationDirection.fromJson(row); + if (value != null) { + result.add(value); + } + } + } + return result.toList(growable: growable); + } +} + +/// Transformation class that can [encode] an instance of [ReturnChatPagedEventsPaginationDirection] to String, +/// and [decode] dynamic data back to [ReturnChatPagedEventsPaginationDirection]. +class ReturnChatPagedEventsPaginationDirectionTypeTransformer { + factory ReturnChatPagedEventsPaginationDirectionTypeTransformer() => + _instance ??= + const ReturnChatPagedEventsPaginationDirectionTypeTransformer._(); + + const ReturnChatPagedEventsPaginationDirectionTypeTransformer._(); + + String encode(ReturnChatPagedEventsPaginationDirection data) => data.value; + + /// Decodes a [dynamic value][data] to a ReturnChatPagedEventsPaginationDirection. + /// + /// If [allowNull] is true and the [dynamic value][data] cannot be decoded successfully, + /// then null is returned. However, if [allowNull] is false and the [dynamic value][data] + /// cannot be decoded successfully, then an [UnimplementedError] is thrown. + /// + /// The [allowNull] is very handy when an API changes and a new enum value is added or removed, + /// and users are still using an old app with the old code. + ReturnChatPagedEventsPaginationDirection? decode(dynamic data, + {bool allowNull = true}) { + if (data != null) { + switch (data) { + case r'ASC': + return ReturnChatPagedEventsPaginationDirection.ASC; + case r'DESC': + return ReturnChatPagedEventsPaginationDirection.DESC; + default: + if (!allowNull) { + throw ArgumentError('Unknown enum value to decode: $data'); + } + } + } + return null; + } + + /// Singleton [ReturnChatPagedEventsPaginationDirectionTypeTransformer] instance. + static ReturnChatPagedEventsPaginationDirectionTypeTransformer? _instance; +} diff --git a/lib/data/api/models/generated/lib/model/return_chat_paged_events_status.dart b/lib/data/api/models/generated/lib/model/return_chat_paged_events_status.dart new file mode 100644 index 0000000..322a671 --- /dev/null +++ b/lib/data/api/models/generated/lib/model/return_chat_paged_events_status.dart @@ -0,0 +1,108 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.18 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +part of openapi.api; + +class ReturnChatPagedEventsStatus { + /// Instantiate a new enum with the provided [value]. + const ReturnChatPagedEventsStatus._(this.value); + + /// The underlying value of this enum member. + final String value; + + @override + String toString() => value; + + String toJson() => value; + + static const ACTIVE = ReturnChatPagedEventsStatus._(r'ACTIVE'); + static const USER_ENDED = ReturnChatPagedEventsStatus._(r'USER_ENDED'); + static const USER_TIMEOUT = ReturnChatPagedEventsStatus._(r'USER_TIMEOUT'); + static const MAX_DURATION_TIMEOUT = + ReturnChatPagedEventsStatus._(r'MAX_DURATION_TIMEOUT'); + static const INACTIVITY_TIMEOUT = + ReturnChatPagedEventsStatus._(r'INACTIVITY_TIMEOUT'); + static const ERROR = ReturnChatPagedEventsStatus._(r'ERROR'); + + /// List of all possible values in this [enum][ReturnChatPagedEventsStatus]. + static const values = [ + ACTIVE, + USER_ENDED, + USER_TIMEOUT, + MAX_DURATION_TIMEOUT, + INACTIVITY_TIMEOUT, + ERROR, + ]; + + static ReturnChatPagedEventsStatus? fromJson(dynamic value) => + ReturnChatPagedEventsStatusTypeTransformer().decode(value); + + static List listFromJson( + dynamic json, { + bool growable = false, + }) { + final result = []; + if (json is List && json.isNotEmpty) { + for (final row in json) { + final value = ReturnChatPagedEventsStatus.fromJson(row); + if (value != null) { + result.add(value); + } + } + } + return result.toList(growable: growable); + } +} + +/// Transformation class that can [encode] an instance of [ReturnChatPagedEventsStatus] to String, +/// and [decode] dynamic data back to [ReturnChatPagedEventsStatus]. +class ReturnChatPagedEventsStatusTypeTransformer { + factory ReturnChatPagedEventsStatusTypeTransformer() => + _instance ??= const ReturnChatPagedEventsStatusTypeTransformer._(); + + const ReturnChatPagedEventsStatusTypeTransformer._(); + + String encode(ReturnChatPagedEventsStatus data) => data.value; + + /// Decodes a [dynamic value][data] to a ReturnChatPagedEventsStatus. + /// + /// If [allowNull] is true and the [dynamic value][data] cannot be decoded successfully, + /// then null is returned. However, if [allowNull] is false and the [dynamic value][data] + /// cannot be decoded successfully, then an [UnimplementedError] is thrown. + /// + /// The [allowNull] is very handy when an API changes and a new enum value is added or removed, + /// and users are still using an old app with the old code. + ReturnChatPagedEventsStatus? decode(dynamic data, {bool allowNull = true}) { + if (data != null) { + switch (data) { + case r'ACTIVE': + return ReturnChatPagedEventsStatus.ACTIVE; + case r'USER_ENDED': + return ReturnChatPagedEventsStatus.USER_ENDED; + case r'USER_TIMEOUT': + return ReturnChatPagedEventsStatus.USER_TIMEOUT; + case r'MAX_DURATION_TIMEOUT': + return ReturnChatPagedEventsStatus.MAX_DURATION_TIMEOUT; + case r'INACTIVITY_TIMEOUT': + return ReturnChatPagedEventsStatus.INACTIVITY_TIMEOUT; + case r'ERROR': + return ReturnChatPagedEventsStatus.ERROR; + default: + if (!allowNull) { + throw ArgumentError('Unknown enum value to decode: $data'); + } + } + } + return null; + } + + /// Singleton [ReturnChatPagedEventsStatusTypeTransformer] instance. + static ReturnChatPagedEventsStatusTypeTransformer? _instance; +} diff --git a/test/data/api/models/return_chat_paged_events_test.dart b/test/data/api/models/return_chat_paged_events_test.dart new file mode 100644 index 0000000..52fa49f --- /dev/null +++ b/test/data/api/models/return_chat_paged_events_test.dart @@ -0,0 +1,59 @@ +import 'package:evi_example/data/api/models/generated/lib/api.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + group('ReturnChatPagedEvents', () { + test('should parse return_chat_paged_events correctly', () {}); + + test('to throw error for empty JSON`', () async { + expect(() => ReturnChatPagedEvents.fromJson("{}"), throwsAssertionError); + }); + + test('to do basic mapping of required keys', () async { + final jsonString = ''' + { + "chat_group_id": "770e8400-e29b-41d4-a716-446655440000", + "events_page": [ + { + "chat_id": "550e8400-e29b-41d4-a716", + "emotion_features": "{\\"happiness\\": 0.5}", + "id": "660e8400-e29b-41d4-a716-446655440000", + "message_text": "Event 1", + "role": "USER", + "timestamp": 1672531200, + "type": "USER_MESSAGE" + } + ], + "id": "990e8400-e29b-41d4-a716-446655440000", + "page_number": 0, + "page_size": 10, + "pagination_direction": "ASC", + "start_timestamp": 1672531200, + "status": "ACTIVE", + "total_pages": 1 + } + '''; + + final result = ReturnChatPagedEvents.fromJson(jsonString); + + expect(result, isNotNull); + expect(result!.chatGroupId, "770e8400-e29b-41d4-a716-446655440000"); + expect(result.eventsPage[0].chatId, "550e8400-e29b-41d4-a716"); + expect(result.eventsPage[0].emotionFeatures, "{\"happiness\": 0.5}"); + expect(result.eventsPage[0].id, "660e8400-e29b-41d4-a716-446655440000"); + expect(result.eventsPage[0].messageText, "Event 1"); + expect(result.eventsPage[0].role, ReturnChatEventRole.USER); + expect(result.eventsPage[0].timestamp, 1672531200); + expect(result.eventsPage[0].type, ReturnChatEventType.USER_MESSAGE); + expect(result.eventsPage.length, 1); + expect(result.id, "990e8400-e29b-41d4-a716-446655440000"); + expect(result.pageNumber, 0); + expect(result.pageSize, 10); + expect(result.paginationDirection, + ReturnChatPagedEventsPaginationDirection.ASC); + expect(result.startTimestamp, 1672531200); + expect(result.status, ReturnChatPagedEventsStatus.ACTIVE); + expect(result.totalPages, 1); + }); + }); +} diff --git a/test/provider/chat_provider_test.dart b/test/provider/chat_provider_test.dart index 49d4a59..1937d75 100644 --- a/test/provider/chat_provider_test.dart +++ b/test/provider/chat_provider_test.dart @@ -1,3 +1,4 @@ +import 'package:evi_example/data/api/models/generated/lib/api.dart'; import 'package:evi_example/provider/chat_provider.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -7,170 +8,371 @@ void main() { test( 'removes messages from the start until finding one less than 200 characters', () { - final json = ''' + final body = ''' { + "chat_group_id": "", "events_page": [ { "role": "SYSTEM", "message_text": "this is more than 200 characters long this is more than 200 characters long this is more than 200 characters long this is more than 200 characters long this is more than 200 characters long this is long", - "emotion_features": null + "emotion_features": null, + "chat_id": "", + "id": "", + "timestamp": 0, + "type": "USER_MESSAGE" }, { "role": "SYSTEM", "message_text": "this is less than 200 characters", - "emotion_features": null + "emotion_features": null, + "chat_id": "", + "id": "", + "timestamp": 0, + "type": "USER_MESSAGE" } - ] + ], + "id": "", + "page_number": 0, + "page_size": 0, + "pagination_direction": "ASC", + "start_timestamp": 0, + "status": "ACTIVE", + "total_pages": 0 } '''; - final result = ChatProvider.filterMessages(json); + final result = ChatProvider.filterMessages(body); expect(result[0]['text'], "this is less than 200 characters"); expect(result.length, 1); }); test('handles null message_text gracefully', () { - final json = ''' + final body = ''' { "events_page": [ { "role": "SYSTEM", "message_text": null, - "emotion_features": null + "emotion_features": null, + "chat_id": "", + "id": "", + "timestamp": 0, + "type": "USER_MESSAGE" }, { "role": "USER", - "message_text": null + "message_text": null, + "emotion_features": null, + "chat_id": "", + "id": "", + "timestamp": 0, + "type": "USER_MESSAGE" }, { "role": "SYSTEM", "message_text": "this is more than 200 characters long this is more than 200 characters long this is more than 200 characters long this is more than 200 characters long this is more than 200 characters long this is long", - "emotion_features": null + "emotion_features": null, + "chat_id": "", + "id": "", + "timestamp": 0, + "type": "USER_MESSAGE" }, { "role": "SYSTEM", "message_text": "this is less than 200 characters", - "emotion_features": null + "emotion_features": null, + "chat_id": "", + "id": "", + "timestamp": 0, + "type": "USER_MESSAGE" } - ] + ], + "chat_group_id": "", + "id": "", + "page_number": 0, + "page_size": 0, + "pagination_direction": "ASC", + "start_timestamp": 0, + "status": "ACTIVE", + "total_pages": 0 } '''; - final result = ChatProvider.filterMessages(json); + final result = ChatProvider.filterMessages(body); - expect(result[0]["role"], "SYSTEM"); - expect(result[1]["role"], "USER"); - expect(result[2]["role"], "SYSTEM"); - expect(result[3]["role"], "SYSTEM"); + expect(result[0]["role"], ReturnChatEventRole.SYSTEM); + expect(result[1]["role"], ReturnChatEventRole.USER); + expect(result[2]["role"], ReturnChatEventRole.SYSTEM); + expect(result[3]["role"], ReturnChatEventRole.SYSTEM); expect(result.length, 4); }); test('handles empty events_page list', () { - final jsonInput = '{"events_page": []}'; + final body = ''' + { + "chat_group_id": "", + "events_page": [ + ], + "id": "", + "page_number": 0, + "page_size": 0, + "pagination_direction": "ASC", + "start_timestamp": 0, + "status": "ACTIVE", + "total_pages": 0 + } + '''; - final result = ChatProvider.filterMessages(jsonInput); + final result = ChatProvider.filterMessages(body); expect(result, isEmpty); }); test('no trimming when first message is USER', () { - final jsonInput = ''' - { - "events_page": [ - {"role": "USER", "message_text": "Hello!", "emotion_features": "{}"} - ] - } + final body = ''' + { + "chat_group_id": "", + "events_page": [ + { + "role": "USER", + "message_text": "Hello!", + "emotion_features": "{}", + "chat_id": "", + "id": "", + "timestamp": 0, + "type": "USER_MESSAGE" + } + ], + "id": "", + "page_number": 0, + "page_size": 0, + "pagination_direction": "ASC", + "start_timestamp": 0, + "status": "ACTIVE", + "total_pages": 0 + } '''; - final result = ChatProvider.filterMessages(jsonInput); + final result = ChatProvider.filterMessages(body); expect(result.length, 1); - expect(result.first['role'], 'USER'); + expect(result.first['role'], ReturnChatEventRole.USER); }); test('stops trimming when non-USER message is short (<= 200 chars)', () { - final jsonInput = ''' + final body = ''' { "events_page": [ - {"role": "ASSISTANT", "message_text": "${"a" * 250}", "emotion_features": "{}"}, - {"role": "ASSISTANT", "message_text": "Short message", "emotion_features": "{}"}, - {"role": "USER", "message_text": "User hello", "emotion_features": "{}"} - ] + { + "role": "SYSTEM", + "message_text": "${"a" * 250}", + "emotion_features": "{}", + "chat_id": "", + "id": "", + "timestamp": 0, + "type": "USER_MESSAGE" + }, + { + "role": "SYSTEM", + "message_text": "Short message", + "emotion_features": "{}", + "chat_id": "", + "id": "", + "timestamp": 0, + "type": "USER_MESSAGE" + }, + { + "role": "USER", + "message_text": "User hello", + "emotion_features": "{}", + "chat_id": "", + "id": "", + "timestamp": 0, + "type": "USER_MESSAGE" + } + ], + "chat_group_id": "", + "id": "", + "page_number": 0, + "page_size": 0, + "pagination_direction": "ASC", + "start_timestamp": 0, + "status": "ACTIVE", + "total_pages": 0 } '''; - final result = ChatProvider.filterMessages(jsonInput); + final result = ChatProvider.filterMessages(body); // It should stop at the "Short message" because it's <= 200 chars, even though it's not USER. + expect(result, isNotEmpty); expect(result[0]['text'], 'Short message'); expect(result[1]['text'], 'User hello'); expect(result.length, 2); }); test('trims multiple long non-USER messages', () { - final jsonInput = ''' + final body = ''' { "events_page": [ - {"role": "ASSISTANT", "message_text": "${"a" * 250}", "emotion_features": "{}"}, - {"role": "ASSISTANT", "message_text": "${"b" * 250}", "emotion_features": "{}"}, - {"role": "USER", "message_text": "User text", "emotion_features": "{}"} - ] + { + "role": "SYSTEM", + "message_text": "${"a" * 250}", + "emotion_features": "{}", + "chat_id": "", + "id": "", + "timestamp": 0, + "type": "USER_MESSAGE" + }, + { + "role": "SYSTEM", + "message_text": "${"b" * 250}", + "emotion_features": "{}", + "chat_id": "", + "id": "", + "timestamp": 0, + "type": "USER_MESSAGE" + }, + { + "role": "USER", + "message_text": "User text", + "emotion_features": "{}", + "chat_id": "", + "id": "", + "timestamp": 0, + "type": "USER_MESSAGE" + } + ], + "chat_group_id": "", + "id": "", + "page_number": 0, + "page_size": 0, + "pagination_direction": "ASC", + "start_timestamp": 0, + "status": "ACTIVE", + "total_pages": 0 } '''; - final result = ChatProvider.filterMessages(jsonInput); + final result = ChatProvider.filterMessages(body); + expect(result, isNotEmpty); + expect(result.first['role'], ReturnChatEventRole.USER); expect(result.length, 1); - expect(result.first['role'], 'USER'); }); test('trims until specific condition is met in mixed sequence', () { - final jsonInput = ''' + final body = ''' { "events_page": [ - {"role": "ASSISTANT", "message_text": "${"a" * 250}", "emotion_features": "{}"}, - {"role": "ASSISTANT", "message_text": "${"b" * 250}", "emotion_features": "{}"}, - {"role": "ASSISTANT", "message_text": "Short one", "emotion_features": "{}"}, - {"role": "USER", "message_text": "User text", "emotion_features": "{}"} - ] + { + "role": "SYSTEM", + "message_text": "${"a" * 250}", + "emotion_features": "{}", + "chat_id": "", + "id": "", + "timestamp": 0, + "type": "USER_MESSAGE" + }, + { + "role": "SYSTEM", + "message_text": "${"b" * 250}", + "emotion_features": "{}", + "chat_id": "", + "id": "", + "timestamp": 0, + "type": "USER_MESSAGE" + }, + { + "role": "SYSTEM", + "message_text": "Short one", + "emotion_features": "{}", + "chat_id": "", + "id": "", + "timestamp": 0, + "type": "USER_MESSAGE" + }, + { + "role": "USER", + "message_text": "User text", + "emotion_features": "{}", + "chat_id": "", + "id": "", + "timestamp": 0, + "type": "USER_MESSAGE" + } + ], + "chat_group_id": "", + "id": "", + "page_number": 0, + "page_size": 0, + "pagination_direction": "ASC", + "start_timestamp": 0, + "status": "ACTIVE", + "total_pages": 0 } '''; - final result = ChatProvider.filterMessages(jsonInput); + final result = ChatProvider.filterMessages(body); - expect(result.length, 2); + expect(result, isNotEmpty); expect(result.first['text'], 'Short one'); + expect(result.length, 2); }); test('stops trimming if role changes to USER', () { - final jsonInput = ''' + final body = ''' { "events_page": [ - {"role": "ASSISTANT", "message_text": "${"a" * 250}", "emotion_features": "{}"}, - {"role": "USER", "message_text": "${"b" * 250}", "emotion_features": "{}"} - ] + { + "role": "SYSTEM", + "message_text": "${"a" * 250}", + "emotion_features": "{}", + "chat_id": "", + "id": "", + "timestamp": 0, + "type": "USER_MESSAGE" + }, + { + "role": "USER", + "message_text": "${"b" * 250}", + "emotion_features": "{}", + "chat_id": "", + "id": "", + "timestamp": 0, + "type": "USER_MESSAGE" + } + ], + "chat_group_id": "", + "id": "", + "page_number": 0, + "page_size": 0, + "pagination_direction": "ASC", + "start_timestamp": 0, + "status": "ACTIVE", + "total_pages": 0 } '''; - final result = ChatProvider.filterMessages(jsonInput); + final result = ChatProvider.filterMessages(body); // Even if the USER message is long, it should stop because role == 'USER'. - expect(result.first['role'], 'USER'); + expect(result, isNotEmpty); + expect(result.first['role'], ReturnChatEventRole.USER); expect(result.length, 1); }); test('handles malformed JSON input', () { - final jsonInput = '{ invalid }'; - expect(() => ChatProvider.filterMessages(jsonInput), + final body = '{ invalid }'; + expect(() => ChatProvider.filterMessages(body), throwsA(isA())); }); - test('handles missing events_page key in JSON', () { - final jsonInput = '{"not_events": []}'; + test('throws error when missing events_page in JSON', () { + final body = '{"not_events": []}'; - expect(() => ChatProvider.filterMessages(jsonInput), - throwsNoSuchMethodError); + expect(() => ChatProvider.filterMessages(body), throwsAssertionError); }); }); From 4190413149686e17d0a5a1b3ac1b271ece0a2d59 Mon Sep 17 00:00:00 2001 From: code-schreiber Date: Mon, 4 May 2026 13:52:27 +0200 Subject: [PATCH 07/16] Use generated HTPP call instead of custom GET request. Also fix parsing --- .../lib/model/return_chat_paged_events.dart | 7 ++- lib/evi_message.dart | 2 +- lib/pages/my_home_page.dart | 2 +- lib/provider/chat_provider.dart | 49 ++++++++--------- .../models/return_chat_paged_events_test.dart | 10 ++-- test/evi_message_test.dart | 45 ++++++++++++++++ test/provider/chat_provider_test.dart | 54 +++++++++---------- 7 files changed, 106 insertions(+), 63 deletions(-) create mode 100644 test/evi_message_test.dart diff --git a/lib/data/api/models/generated/lib/model/return_chat_paged_events.dart b/lib/data/api/models/generated/lib/model/return_chat_paged_events.dart index 114a8da..0886630 100644 --- a/lib/data/api/models/generated/lib/model/return_chat_paged_events.dart +++ b/lib/data/api/models/generated/lib/model/return_chat_paged_events.dart @@ -139,10 +139,9 @@ class ReturnChatPagedEvents { /// Returns a new [ReturnChatPagedEvents] instance and imports its values from /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods - static ReturnChatPagedEvents? fromJson(String value) { - final data = jsonDecode(value); - if (data is Map) { - final json = data.cast(); + static ReturnChatPagedEvents? fromJson(dynamic value) { + if (value is Map) { + final json = value.cast(); // Ensure that the map contains the required keys. // Note 1: the values aren't checked for validity beyond being non-null. diff --git a/lib/evi_message.dart b/lib/evi_message.dart index 44c5571..c830ebc 100644 --- a/lib/evi_message.dart +++ b/lib/evi_message.dart @@ -73,7 +73,7 @@ class ProsodyInference { class Inference { final ProsodyInference? prosody; - Inference(json) : prosody = ProsodyInference(json['prosody']); + Inference(json) : prosody = json['prosody'] != null ? ProsodyInference(json['prosody']) : null; } class AssistantMessage extends EviMessage { diff --git a/lib/pages/my_home_page.dart b/lib/pages/my_home_page.dart index dc25fd9..960a931 100644 --- a/lib/pages/my_home_page.dart +++ b/lib/pages/my_home_page.dart @@ -174,7 +174,7 @@ class _MyHomePageState extends State { uri += '?access_token=${ConfigManager.instance.humeAccessToken}'; } else if (ConfigManager.instance.humeApiKey.isNotEmpty) { uri += - '?api_key=REPLACE_THIS_WITH_ACTUAL_HUME_API_KEY&config_id=REPLACE_THIS_WITH_ACTUAL_HUME_CONFIG_ID'; + '?api_key=${ConfigManager.instance.humeApiKey}'; } else { throw Exception('Please set your Hume API credentials in main.dart'); } diff --git a/lib/provider/chat_provider.dart b/lib/provider/chat_provider.dart index b84b957..8605d95 100644 --- a/lib/provider/chat_provider.dart +++ b/lib/provider/chat_provider.dart @@ -1,10 +1,12 @@ import 'dart:convert'; +import 'package:evi_example/utils.dart'; import 'package:flutter/material.dart'; import 'package:http/http.dart' as http; import 'package:shared_preferences/shared_preferences.dart'; +import '../data/api/models/generated/lib/api.dart'; import '../pages/emotion_page.dart'; import './initial_sessions.dart'; // Add this import @@ -68,41 +70,35 @@ class ChatProvider extends ChangeNotifier { _context = context; } - Future _fetchChatMessages(String chatId) async { - final url = Uri.parse('https://api.hume.ai/v0/evi/chats/$chatId') - .replace(queryParameters: { - 'page_number': '0', - 'page_size': '100', - 'ascending_order': 'false', - }); - - final response = await http.get( - url, - headers: { - 'X-Hume-Api-Key': 'REPLACE_THIS_WITH_ACTUAL_HUME_API_KEY', - }, - ); - - if (response.statusCode != 200) { - throw Exception('Failed to fetch messages: ${response.statusCode}'); + Future _fetchChatMessages(String chatId) async { + try { + final events = await SubpackageChatsApi().listChatEvents( + chatId, + ConfigManager.instance.humeApiKey, + pageNumber: 0, + pageSize: 100, + ascendingOrder: false, + ); + if (events == null) throw Exception('Failed to fetch messages: null events'); + return events; + } catch (e) { + throw Exception('Failed to fetch messages: $e'); } - return response.body; } - static List> filterMessages(String body) { - final data = json.decode(body); - final allMessages = data['events_page'] + static List> filterMessages(ReturnChatPagedEvents events) { + final allMessages = events.eventsPage .map((message) => { - 'role': message['role'], - 'text': message['message_text'], - 'emotion_features': message['emotion_features'], + 'role': message.role, + 'text': message.messageText, + 'emotion_features': message.emotionFeatures, }) .toList() .cast>(); print(allMessages); // Remove messages from the start until finding one less than 200 characters while (allMessages.isNotEmpty && - allMessages.first['role'] != 'USER' && + allMessages.first['role'] != ReturnChatEventRole.USER && allMessages.first['text'] != null && allMessages.first['text'].length > 200) { allMessages.removeAt(0); @@ -243,7 +239,8 @@ class ChatProvider extends ChangeNotifier { if (_chats.isEmpty) return false; try { - final body = await _fetchChatMessages(_chats.last['chat_id']); + var chatId = _chats.last['chat_id']; + final body = await _fetchChatMessages(chatId); _chatMessages = filterMessages(body); // Process emotions for user messages diff --git a/test/data/api/models/return_chat_paged_events_test.dart b/test/data/api/models/return_chat_paged_events_test.dart index 52fa49f..ca791d6 100644 --- a/test/data/api/models/return_chat_paged_events_test.dart +++ b/test/data/api/models/return_chat_paged_events_test.dart @@ -1,3 +1,5 @@ +import 'dart:convert'; + import 'package:evi_example/data/api/models/generated/lib/api.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -5,8 +7,10 @@ void main() { group('ReturnChatPagedEvents', () { test('should parse return_chat_paged_events correctly', () {}); - test('to throw error for empty JSON`', () async { - expect(() => ReturnChatPagedEvents.fromJson("{}"), throwsAssertionError); + test('to return null for empty JSON`', () async { + var result = ReturnChatPagedEvents.fromJson("{}"); + + expect(result, isNull); }); test('to do basic mapping of required keys', () async { @@ -34,7 +38,7 @@ void main() { } '''; - final result = ReturnChatPagedEvents.fromJson(jsonString); + final result = ReturnChatPagedEvents.fromJson(jsonDecode(jsonString)); expect(result, isNotNull); expect(result!.chatGroupId, "770e8400-e29b-41d4-a716-446655440000"); diff --git a/test/evi_message_test.dart b/test/evi_message_test.dart new file mode 100644 index 0000000..8052ad9 --- /dev/null +++ b/test/evi_message_test.dart @@ -0,0 +1,45 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:evi_example/evi_message.dart'; + +void main() { + test('AssistantMessage with empty models has null prosody', () { + final jsonString = ''' + { + "type": "assistant_message", + "message": {"role": "user", "content": "hello"}, + "models": {} + } + '''; + + final message = EviMessage.decode(jsonString) as AssistantMessage; + + expect(message.models.prosody, isNull); + }); + + test('AssistantMessage with prosody in models parses scores', () { + var emotion1 = 'Admiration'; + var emotion1score = 0.00838470458984375; + var emotion2 = 'Amusement'; + var emotion2score = 0.04827880859375; + final jsonString = ''' + { + "type": "assistant_message", + "message": {"role": "user", "content": "hello"}, + "models": { + "prosody": { + "scores": { + "$emotion1": $emotion1score, + "$emotion2": $emotion2score + } + } + } + } + '''; + + final msg = EviMessage.decode(jsonString) as AssistantMessage; + + expect(msg.models.prosody, isNotNull); + expect(msg.models.prosody!.scores[emotion1], emotion1score); + expect(msg.models.prosody!.scores[emotion2], emotion2score); + }); +} \ No newline at end of file diff --git a/test/provider/chat_provider_test.dart b/test/provider/chat_provider_test.dart index 1937d75..33a2d19 100644 --- a/test/provider/chat_provider_test.dart +++ b/test/provider/chat_provider_test.dart @@ -1,3 +1,5 @@ +import 'dart:convert'; + import 'package:evi_example/data/api/models/generated/lib/api.dart'; import 'package:evi_example/provider/chat_provider.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -8,7 +10,7 @@ void main() { test( 'removes messages from the start until finding one less than 200 characters', () { - final body = ''' + final jsonString = ''' { "chat_group_id": "", "events_page": [ @@ -40,15 +42,16 @@ void main() { "total_pages": 0 } '''; + final events = ReturnChatPagedEvents.fromJson(jsonDecode(jsonString))!; - final result = ChatProvider.filterMessages(body); + final result = ChatProvider.filterMessages(events); expect(result[0]['text'], "this is less than 200 characters"); expect(result.length, 1); }); test('handles null message_text gracefully', () { - final body = ''' + final jsonString = ''' { "events_page": [ { @@ -98,8 +101,9 @@ void main() { "total_pages": 0 } '''; + final events = ReturnChatPagedEvents.fromJson(jsonDecode(jsonString))!; - final result = ChatProvider.filterMessages(body); + final result = ChatProvider.filterMessages(events); expect(result[0]["role"], ReturnChatEventRole.SYSTEM); expect(result[1]["role"], ReturnChatEventRole.USER); @@ -109,7 +113,7 @@ void main() { }); test('handles empty events_page list', () { - final body = ''' + final jsonString = ''' { "chat_group_id": "", "events_page": [ @@ -123,14 +127,15 @@ void main() { "total_pages": 0 } '''; + final events = ReturnChatPagedEvents.fromJson(jsonDecode(jsonString))!; - final result = ChatProvider.filterMessages(body); + final result = ChatProvider.filterMessages(events); expect(result, isEmpty); }); test('no trimming when first message is USER', () { - final body = ''' + final jsonString = ''' { "chat_group_id": "", "events_page": [ @@ -153,15 +158,16 @@ void main() { "total_pages": 0 } '''; + final events = ReturnChatPagedEvents.fromJson(jsonDecode(jsonString))!; - final result = ChatProvider.filterMessages(body); + final result = ChatProvider.filterMessages(events); expect(result.length, 1); expect(result.first['role'], ReturnChatEventRole.USER); }); test('stops trimming when non-USER message is short (<= 200 chars)', () { - final body = ''' + final jsonString = ''' { "events_page": [ { @@ -202,8 +208,9 @@ void main() { "total_pages": 0 } '''; + final events = ReturnChatPagedEvents.fromJson(jsonDecode(jsonString))!; - final result = ChatProvider.filterMessages(body); + final result = ChatProvider.filterMessages(events); // It should stop at the "Short message" because it's <= 200 chars, even though it's not USER. expect(result, isNotEmpty); @@ -213,7 +220,7 @@ void main() { }); test('trims multiple long non-USER messages', () { - final body = ''' + final jsonString = ''' { "events_page": [ { @@ -254,8 +261,9 @@ void main() { "total_pages": 0 } '''; + final events = ReturnChatPagedEvents.fromJson(jsonDecode(jsonString))!; - final result = ChatProvider.filterMessages(body); + final result = ChatProvider.filterMessages(events); expect(result, isNotEmpty); expect(result.first['role'], ReturnChatEventRole.USER); @@ -263,7 +271,7 @@ void main() { }); test('trims until specific condition is met in mixed sequence', () { - final body = ''' + final jsonString = ''' { "events_page": [ { @@ -313,8 +321,9 @@ void main() { "total_pages": 0 } '''; + final events = ReturnChatPagedEvents.fromJson(jsonDecode(jsonString))!; - final result = ChatProvider.filterMessages(body); + final result = ChatProvider.filterMessages(events); expect(result, isNotEmpty); expect(result.first['text'], 'Short one'); @@ -322,7 +331,7 @@ void main() { }); test('stops trimming if role changes to USER', () { - final body = ''' + final jsonString = ''' { "events_page": [ { @@ -354,26 +363,15 @@ void main() { "total_pages": 0 } '''; + final events = ReturnChatPagedEvents.fromJson(jsonDecode(jsonString))!; - final result = ChatProvider.filterMessages(body); + final result = ChatProvider.filterMessages(events); // Even if the USER message is long, it should stop because role == 'USER'. expect(result, isNotEmpty); expect(result.first['role'], ReturnChatEventRole.USER); expect(result.length, 1); }); - - test('handles malformed JSON input', () { - final body = '{ invalid }'; - expect(() => ChatProvider.filterMessages(body), - throwsA(isA())); - }); - - test('throws error when missing events_page in JSON', () { - final body = '{"not_events": []}'; - - expect(() => ChatProvider.filterMessages(body), throwsAssertionError); - }); }); group('processEmotions', () { From 952a64e9bb17ee74ca4e58a63ca26ed97b349b77 Mon Sep 17 00:00:00 2001 From: code-schreiber Date: Wed, 6 May 2026 12:56:45 +0200 Subject: [PATCH 08/16] Use retrofit to generated HTPP instead of openapi_generator_annotations, appropiate for this project since it is not a library --- lib/data/api/chat_event_models.dart | 8 - lib/data/api/generated/export.dart | 17 ++ .../generated/models/return_chat_event.dart | 59 +++++ .../generated/models/return_chat_event.g.dart | 58 +++++ .../models/return_chat_event_role.dart | 28 ++ .../models/return_chat_event_type.dart | 46 ++++ .../models/return_chat_paged_events.dart | 77 ++++++ .../models/return_chat_paged_events.g.dart | 61 +++++ ...hat_paged_events_pagination_direction.dart | 24 ++ .../return_chat_paged_events_status.dart | 32 +++ .../generated/models/return_config_spec.dart | 30 +++ .../models/return_config_spec.g.dart | 19 ++ lib/data/api/generated/rest_client.dart | 25 ++ .../subpackage_chats_client.dart | 39 +++ .../subpackage_chats_client.g.dart | 98 +++++++ .../models/generated/doc/ReturnChatEvent.md | 23 -- .../generated/doc/ReturnChatEventRole.md | 14 - .../generated/doc/ReturnChatEventType.md | 14 - lib/data/api/models/generated/lib/api.dart | 32 --- .../api/models/generated/lib/api_helper.dart | 130 ---------- .../lib/model/return_chat_event.dart | 205 --------------- .../lib/model/return_chat_event_role.dart | 98 ------- .../lib/model/return_chat_event_type.dart | 137 ---------- .../lib/model/return_chat_paged_events.dart | 239 ------------------ ...hat_paged_events_pagination_direction.dart | 92 ------- .../return_chat_paged_events_status.dart | 108 -------- lib/provider/chat_provider.dart | 23 +- pubspec.yaml | 6 +- swagger_parser.yaml | 5 + .../api/models/return_chat_event_test.dart | 10 +- .../models/return_chat_paged_events_test.dart | 24 +- test/provider/chat_provider_test.dart | 81 ++++-- 32 files changed, 707 insertions(+), 1155 deletions(-) delete mode 100644 lib/data/api/chat_event_models.dart create mode 100644 lib/data/api/generated/export.dart create mode 100644 lib/data/api/generated/models/return_chat_event.dart create mode 100644 lib/data/api/generated/models/return_chat_event.g.dart create mode 100644 lib/data/api/generated/models/return_chat_event_role.dart create mode 100644 lib/data/api/generated/models/return_chat_event_type.dart create mode 100644 lib/data/api/generated/models/return_chat_paged_events.dart create mode 100644 lib/data/api/generated/models/return_chat_paged_events.g.dart create mode 100644 lib/data/api/generated/models/return_chat_paged_events_pagination_direction.dart create mode 100644 lib/data/api/generated/models/return_chat_paged_events_status.dart create mode 100644 lib/data/api/generated/models/return_config_spec.dart create mode 100644 lib/data/api/generated/models/return_config_spec.g.dart create mode 100644 lib/data/api/generated/rest_client.dart create mode 100644 lib/data/api/generated/subpackage_chats/subpackage_chats_client.dart create mode 100644 lib/data/api/generated/subpackage_chats/subpackage_chats_client.g.dart delete mode 100644 lib/data/api/models/generated/doc/ReturnChatEvent.md delete mode 100644 lib/data/api/models/generated/doc/ReturnChatEventRole.md delete mode 100644 lib/data/api/models/generated/doc/ReturnChatEventType.md delete mode 100644 lib/data/api/models/generated/lib/api.dart delete mode 100644 lib/data/api/models/generated/lib/api_helper.dart delete mode 100644 lib/data/api/models/generated/lib/model/return_chat_event.dart delete mode 100644 lib/data/api/models/generated/lib/model/return_chat_event_role.dart delete mode 100644 lib/data/api/models/generated/lib/model/return_chat_event_type.dart delete mode 100644 lib/data/api/models/generated/lib/model/return_chat_paged_events.dart delete mode 100644 lib/data/api/models/generated/lib/model/return_chat_paged_events_pagination_direction.dart delete mode 100644 lib/data/api/models/generated/lib/model/return_chat_paged_events_status.dart create mode 100644 swagger_parser.yaml diff --git a/lib/data/api/chat_event_models.dart b/lib/data/api/chat_event_models.dart deleted file mode 100644 index ec2a012..0000000 --- a/lib/data/api/chat_event_models.dart +++ /dev/null @@ -1,8 +0,0 @@ -// Openapi Generator last run: : 2026-04-27T13:21:51.864161 -import 'package:openapi_generator_annotations/openapi_generator_annotations.dart'; - -@Openapi( - inputSpec: InputSpec(path: 'lib/data/api/chat_events_spec.yaml'), - outputDirectory: 'lib/data/api/models/generated', - generatorName: Generator.dart) -class ChatEventModels {} \ No newline at end of file diff --git a/lib/data/api/generated/export.dart b/lib/data/api/generated/export.dart new file mode 100644 index 0000000..c282a83 --- /dev/null +++ b/lib/data/api/generated/export.dart @@ -0,0 +1,17 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint, unused_import + +// Clients +export 'subpackage_chats/subpackage_chats_client.dart'; +// Data classes +export 'models/return_config_spec.dart'; +export 'models/return_chat_event_role.dart'; +export 'models/return_chat_event_type.dart'; +export 'models/return_chat_event.dart'; +export 'models/return_chat_paged_events_pagination_direction.dart'; +export 'models/return_chat_paged_events_status.dart'; +export 'models/return_chat_paged_events.dart'; +// Root client +export 'rest_client.dart'; + diff --git a/lib/data/api/generated/models/return_chat_event.dart b/lib/data/api/generated/models/return_chat_event.dart new file mode 100644 index 0000000..726de18 --- /dev/null +++ b/lib/data/api/generated/models/return_chat_event.dart @@ -0,0 +1,59 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint, unused_import + +import 'package:json_annotation/json_annotation.dart'; + +import 'return_chat_event_role.dart'; +import 'return_chat_event_type.dart'; + +part 'return_chat_event.g.dart'; + +/// A description of a single event in a chat returned from the server +@JsonSerializable() +class ReturnChatEvent { + const ReturnChatEvent({ + required this.chatId, + required this.emotionFeatures, + required this.id, + required this.messageText, + required this.metadata, + required this.relatedEventId, + required this.role, + required this.timestamp, + required this.type, + }); + + factory ReturnChatEvent.fromJson(Map json) => _$ReturnChatEventFromJson(json); + + /// Identifier for the Chat this event occurred in. Formatted as a UUID. + @JsonKey(name: 'chat_id') + final String chatId; + + /// Stringified JSON containing the prosody model inference results. + /// + /// EVI uses the prosody model to measure 48 expressions related to speech and vocal characteristics. These results contain a detailed emotional and tonal analysis of the audio. Scores typically range from 0 to 1, with higher values indicating a stronger confidence level in the measured attribute. + @JsonKey(name: 'emotion_features') + final String? emotionFeatures; + + /// Identifier for a Chat Event. Formatted as a UUID. + final String id; + + /// The text of the Chat Event. This field contains the message content for each event type listed in the `type` field. + @JsonKey(name: 'message_text') + final String? messageText; + + /// Stringified JSON with additional metadata about the chat event. + final String? metadata; + + /// Identifier for a related chat event. Currently only seen on ASSISTANT_PROSODY events, to point back to the ASSISTANT_MESSAGE that generated these prosody scores + @JsonKey(name: 'related_event_id') + final String? relatedEventId; + final ReturnChatEventRole role; + + /// Time at which the Chat Event occurred. Measured in seconds since the Unix epoch. + final int timestamp; + final ReturnChatEventType type; + + Map toJson() => _$ReturnChatEventToJson(this); +} diff --git a/lib/data/api/generated/models/return_chat_event.g.dart b/lib/data/api/generated/models/return_chat_event.g.dart new file mode 100644 index 0000000..2139a60 --- /dev/null +++ b/lib/data/api/generated/models/return_chat_event.g.dart @@ -0,0 +1,58 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'return_chat_event.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +ReturnChatEvent _$ReturnChatEventFromJson(Map json) => + ReturnChatEvent( + chatId: json['chat_id'] as String, + emotionFeatures: json['emotion_features'] as String?, + id: json['id'] as String, + messageText: json['message_text'] as String?, + metadata: json['metadata'] as String?, + relatedEventId: json['related_event_id'] as String?, + role: ReturnChatEventRole.fromJson(json['role'] as String), + timestamp: (json['timestamp'] as num).toInt(), + type: ReturnChatEventType.fromJson(json['type'] as String), + ); + +Map _$ReturnChatEventToJson(ReturnChatEvent instance) => + { + 'chat_id': instance.chatId, + 'emotion_features': instance.emotionFeatures, + 'id': instance.id, + 'message_text': instance.messageText, + 'metadata': instance.metadata, + 'related_event_id': instance.relatedEventId, + 'role': _$ReturnChatEventRoleEnumMap[instance.role]!, + 'timestamp': instance.timestamp, + 'type': _$ReturnChatEventTypeEnumMap[instance.type]!, + }; + +const _$ReturnChatEventRoleEnumMap = { + ReturnChatEventRole.user: 'USER', + ReturnChatEventRole.agent: 'AGENT', + ReturnChatEventRole.system: 'SYSTEM', + ReturnChatEventRole.tool: 'TOOL', + ReturnChatEventRole.$unknown: r'$unknown', +}; + +const _$ReturnChatEventTypeEnumMap = { + ReturnChatEventType.agentMessage: 'AGENT_MESSAGE', + ReturnChatEventType.assistantProsody: 'ASSISTANT_PROSODY', + ReturnChatEventType.chatStartMessage: 'CHAT_START_MESSAGE', + ReturnChatEventType.chatEndMessage: 'CHAT_END_MESSAGE', + ReturnChatEventType.functionCall: 'FUNCTION_CALL', + ReturnChatEventType.functionCallResponse: 'FUNCTION_CALL_RESPONSE', + ReturnChatEventType.pauseOnset: 'PAUSE_ONSET', + ReturnChatEventType.resumeOnset: 'RESUME_ONSET', + ReturnChatEventType.sessionSettings: 'SESSION_SETTINGS', + ReturnChatEventType.systemPrompt: 'SYSTEM_PROMPT', + ReturnChatEventType.userInterruption: 'USER_INTERRUPTION', + ReturnChatEventType.userMessage: 'USER_MESSAGE', + ReturnChatEventType.userRecordingStartMessage: 'USER_RECORDING_START_MESSAGE', + ReturnChatEventType.$unknown: r'$unknown', +}; diff --git a/lib/data/api/generated/models/return_chat_event_role.dart b/lib/data/api/generated/models/return_chat_event_role.dart new file mode 100644 index 0000000..63468be --- /dev/null +++ b/lib/data/api/generated/models/return_chat_event_role.dart @@ -0,0 +1,28 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint, unused_import + +import 'package:json_annotation/json_annotation.dart'; + +@JsonEnum() +enum ReturnChatEventRole { + @JsonValue('USER') + user('USER'), + @JsonValue('AGENT') + agent('AGENT'), + @JsonValue('SYSTEM') + system('SYSTEM'), + @JsonValue('TOOL') + tool('TOOL'), + /// Default value for all unparsed values, allows backward compatibility when adding new values on the backend. + $unknown(null); + + const ReturnChatEventRole(this.json); + + factory ReturnChatEventRole.fromJson(String json) => values.firstWhere( + (e) => e.json == json, + orElse: () => $unknown, + ); + + final String? json; +} diff --git a/lib/data/api/generated/models/return_chat_event_type.dart b/lib/data/api/generated/models/return_chat_event_type.dart new file mode 100644 index 0000000..327dbf0 --- /dev/null +++ b/lib/data/api/generated/models/return_chat_event_type.dart @@ -0,0 +1,46 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint, unused_import + +import 'package:json_annotation/json_annotation.dart'; + +@JsonEnum() +enum ReturnChatEventType { + @JsonValue('AGENT_MESSAGE') + agentMessage('AGENT_MESSAGE'), + @JsonValue('ASSISTANT_PROSODY') + assistantProsody('ASSISTANT_PROSODY'), + @JsonValue('CHAT_START_MESSAGE') + chatStartMessage('CHAT_START_MESSAGE'), + @JsonValue('CHAT_END_MESSAGE') + chatEndMessage('CHAT_END_MESSAGE'), + @JsonValue('FUNCTION_CALL') + functionCall('FUNCTION_CALL'), + @JsonValue('FUNCTION_CALL_RESPONSE') + functionCallResponse('FUNCTION_CALL_RESPONSE'), + @JsonValue('PAUSE_ONSET') + pauseOnset('PAUSE_ONSET'), + @JsonValue('RESUME_ONSET') + resumeOnset('RESUME_ONSET'), + @JsonValue('SESSION_SETTINGS') + sessionSettings('SESSION_SETTINGS'), + @JsonValue('SYSTEM_PROMPT') + systemPrompt('SYSTEM_PROMPT'), + @JsonValue('USER_INTERRUPTION') + userInterruption('USER_INTERRUPTION'), + @JsonValue('USER_MESSAGE') + userMessage('USER_MESSAGE'), + @JsonValue('USER_RECORDING_START_MESSAGE') + userRecordingStartMessage('USER_RECORDING_START_MESSAGE'), + /// Default value for all unparsed values, allows backward compatibility when adding new values on the backend. + $unknown(null); + + const ReturnChatEventType(this.json); + + factory ReturnChatEventType.fromJson(String json) => values.firstWhere( + (e) => e.json == json, + orElse: () => $unknown, + ); + + final String? json; +} diff --git a/lib/data/api/generated/models/return_chat_paged_events.dart b/lib/data/api/generated/models/return_chat_paged_events.dart new file mode 100644 index 0000000..e3e6bc7 --- /dev/null +++ b/lib/data/api/generated/models/return_chat_paged_events.dart @@ -0,0 +1,77 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint, unused_import + +import 'package:json_annotation/json_annotation.dart'; + +import 'return_chat_event.dart'; +import 'return_chat_paged_events_pagination_direction.dart'; +import 'return_chat_paged_events_status.dart'; +import 'return_config_spec.dart'; + +part 'return_chat_paged_events.g.dart'; + +/// A description of chat status with a paginated list of chat events returned from the server +@JsonSerializable() +class ReturnChatPagedEvents { + const ReturnChatPagedEvents({ + required this.chatGroupId, + required this.config, + required this.endTimestamp, + required this.eventsPage, + required this.id, + required this.metadata, + required this.pageNumber, + required this.pageSize, + required this.paginationDirection, + required this.startTimestamp, + required this.status, + required this.totalPages, + }); + + factory ReturnChatPagedEvents.fromJson(Map json) => _$ReturnChatPagedEventsFromJson(json); + + /// Identifier for the Chat Group. Any chat resumed from this Chat will have the same `chat_group_id`. Formatted as a UUID. + @JsonKey(name: 'chat_group_id') + final String chatGroupId; + final ReturnConfigSpec config; + + /// Time at which the Chat ended. Measured in seconds since the Unix epoch. + @JsonKey(name: 'end_timestamp') + final int? endTimestamp; + + /// List of Chat Events for the specified `page_number` and `page_size`. + @JsonKey(name: 'events_page') + final List eventsPage; + + /// Identifier for a Chat. Formatted as a UUID. + final String id; + + /// Stringified JSON with additional metadata about the chat. + final String? metadata; + + /// The page number of the returned list. + /// + /// This value corresponds to the `page_number` parameter specified in the request. Pagination uses zero-based indexing. + @JsonKey(name: 'page_number') + final int pageNumber; + + /// The maximum number of items returned per page. + /// + /// This value corresponds to the `page_size` parameter specified in the request. + @JsonKey(name: 'page_size') + final int pageSize; + @JsonKey(name: 'pagination_direction') + final ReturnChatPagedEventsPaginationDirection paginationDirection; + + /// Time at which the Chat started. Measured in seconds since the Unix epoch. + @JsonKey(name: 'start_timestamp') + final int? startTimestamp; + final ReturnChatPagedEventsStatus status; + + /// The total number of pages in the collection. + @JsonKey(name: 'total_pages') + final int totalPages; + + Map toJson() => _$ReturnChatPagedEventsToJson(this); +} diff --git a/lib/data/api/generated/models/return_chat_paged_events.g.dart b/lib/data/api/generated/models/return_chat_paged_events.g.dart new file mode 100644 index 0000000..59bc565 --- /dev/null +++ b/lib/data/api/generated/models/return_chat_paged_events.g.dart @@ -0,0 +1,61 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'return_chat_paged_events.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +ReturnChatPagedEvents _$ReturnChatPagedEventsFromJson( + Map json) => + ReturnChatPagedEvents( + chatGroupId: json['chat_group_id'] as String, + config: ReturnConfigSpec.fromJson(json['config'] as Map), + endTimestamp: (json['end_timestamp'] as num?)?.toInt(), + eventsPage: (json['events_page'] as List) + .map((e) => ReturnChatEvent.fromJson(e as Map)) + .toList(), + id: json['id'] as String, + metadata: json['metadata'] as String?, + pageNumber: (json['page_number'] as num).toInt(), + pageSize: (json['page_size'] as num).toInt(), + paginationDirection: ReturnChatPagedEventsPaginationDirection.fromJson( + json['pagination_direction'] as String), + startTimestamp: (json['start_timestamp'] as num?)?.toInt(), + status: ReturnChatPagedEventsStatus.fromJson(json['status'] as String), + totalPages: (json['total_pages'] as num).toInt(), + ); + +Map _$ReturnChatPagedEventsToJson( + ReturnChatPagedEvents instance) => + { + 'chat_group_id': instance.chatGroupId, + 'config': instance.config, + 'end_timestamp': instance.endTimestamp, + 'events_page': instance.eventsPage, + 'id': instance.id, + 'metadata': instance.metadata, + 'page_number': instance.pageNumber, + 'page_size': instance.pageSize, + 'pagination_direction': _$ReturnChatPagedEventsPaginationDirectionEnumMap[ + instance.paginationDirection]!, + 'start_timestamp': instance.startTimestamp, + 'status': _$ReturnChatPagedEventsStatusEnumMap[instance.status]!, + 'total_pages': instance.totalPages, + }; + +const _$ReturnChatPagedEventsPaginationDirectionEnumMap = { + ReturnChatPagedEventsPaginationDirection.asc: 'ASC', + ReturnChatPagedEventsPaginationDirection.desc: 'DESC', + ReturnChatPagedEventsPaginationDirection.$unknown: r'$unknown', +}; + +const _$ReturnChatPagedEventsStatusEnumMap = { + ReturnChatPagedEventsStatus.active: 'ACTIVE', + ReturnChatPagedEventsStatus.userEnded: 'USER_ENDED', + ReturnChatPagedEventsStatus.userTimeout: 'USER_TIMEOUT', + ReturnChatPagedEventsStatus.maxDurationTimeout: 'MAX_DURATION_TIMEOUT', + ReturnChatPagedEventsStatus.inactivityTimeout: 'INACTIVITY_TIMEOUT', + ReturnChatPagedEventsStatus.error: 'ERROR', + ReturnChatPagedEventsStatus.$unknown: r'$unknown', +}; diff --git a/lib/data/api/generated/models/return_chat_paged_events_pagination_direction.dart b/lib/data/api/generated/models/return_chat_paged_events_pagination_direction.dart new file mode 100644 index 0000000..890f53d --- /dev/null +++ b/lib/data/api/generated/models/return_chat_paged_events_pagination_direction.dart @@ -0,0 +1,24 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint, unused_import + +import 'package:json_annotation/json_annotation.dart'; + +@JsonEnum() +enum ReturnChatPagedEventsPaginationDirection { + @JsonValue('ASC') + asc('ASC'), + @JsonValue('DESC') + desc('DESC'), + /// Default value for all unparsed values, allows backward compatibility when adding new values on the backend. + $unknown(null); + + const ReturnChatPagedEventsPaginationDirection(this.json); + + factory ReturnChatPagedEventsPaginationDirection.fromJson(String json) => values.firstWhere( + (e) => e.json == json, + orElse: () => $unknown, + ); + + final String? json; +} diff --git a/lib/data/api/generated/models/return_chat_paged_events_status.dart b/lib/data/api/generated/models/return_chat_paged_events_status.dart new file mode 100644 index 0000000..4413eeb --- /dev/null +++ b/lib/data/api/generated/models/return_chat_paged_events_status.dart @@ -0,0 +1,32 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint, unused_import + +import 'package:json_annotation/json_annotation.dart'; + +@JsonEnum() +enum ReturnChatPagedEventsStatus { + @JsonValue('ACTIVE') + active('ACTIVE'), + @JsonValue('USER_ENDED') + userEnded('USER_ENDED'), + @JsonValue('USER_TIMEOUT') + userTimeout('USER_TIMEOUT'), + @JsonValue('MAX_DURATION_TIMEOUT') + maxDurationTimeout('MAX_DURATION_TIMEOUT'), + @JsonValue('INACTIVITY_TIMEOUT') + inactivityTimeout('INACTIVITY_TIMEOUT'), + @JsonValue('ERROR') + error('ERROR'), + /// Default value for all unparsed values, allows backward compatibility when adding new values on the backend. + $unknown(null); + + const ReturnChatPagedEventsStatus(this.json); + + factory ReturnChatPagedEventsStatus.fromJson(String json) => values.firstWhere( + (e) => e.json == json, + orElse: () => $unknown, + ); + + final String? json; +} diff --git a/lib/data/api/generated/models/return_config_spec.dart b/lib/data/api/generated/models/return_config_spec.dart new file mode 100644 index 0000000..1913e2e --- /dev/null +++ b/lib/data/api/generated/models/return_config_spec.dart @@ -0,0 +1,30 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint, unused_import + +import 'package:json_annotation/json_annotation.dart'; + +part 'return_config_spec.g.dart'; + +/// The Config associated with this Chat. +@JsonSerializable() +class ReturnConfigSpec { + const ReturnConfigSpec({ + required this.id, + required this.version, + }); + + factory ReturnConfigSpec.fromJson(Map json) => _$ReturnConfigSpecFromJson(json); + + /// Identifier for a Config. Formatted as a UUID. + final String id; + + /// Version number for a Config. + /// + /// Configs, Prompts, Custom Voices, and Tools are versioned. This versioning system supports iterative development, allowing you to progressively refine configurations and revert to previous versions if needed. + /// + /// Version numbers are integer values representing different iterations of the Config. Each update to the Config increments its version number. + final int? version; + + Map toJson() => _$ReturnConfigSpecToJson(this); +} diff --git a/lib/data/api/generated/models/return_config_spec.g.dart b/lib/data/api/generated/models/return_config_spec.g.dart new file mode 100644 index 0000000..c3f4870 --- /dev/null +++ b/lib/data/api/generated/models/return_config_spec.g.dart @@ -0,0 +1,19 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'return_config_spec.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +ReturnConfigSpec _$ReturnConfigSpecFromJson(Map json) => + ReturnConfigSpec( + id: json['id'] as String, + version: (json['version'] as num?)?.toInt(), + ); + +Map _$ReturnConfigSpecToJson(ReturnConfigSpec instance) => + { + 'id': instance.id, + 'version': instance.version, + }; diff --git a/lib/data/api/generated/rest_client.dart b/lib/data/api/generated/rest_client.dart new file mode 100644 index 0000000..6ef5b31 --- /dev/null +++ b/lib/data/api/generated/rest_client.dart @@ -0,0 +1,25 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint, unused_import + +import 'package:dio/dio.dart'; + +import 'subpackage_chats/subpackage_chats_client.dart'; + +/// empathic-voice-interface `v1.0.0` +class RestClient { + RestClient( + Dio dio, { + String? baseUrl, + }) : _dio = dio, + _baseUrl = baseUrl; + + final Dio _dio; + final String? _baseUrl; + + static String get version => '1.0.0'; + + SubpackageChatsClient? _subpackageChats; + + SubpackageChatsClient get subpackageChats => _subpackageChats ??= SubpackageChatsClient(_dio, baseUrl: _baseUrl); +} diff --git a/lib/data/api/generated/subpackage_chats/subpackage_chats_client.dart b/lib/data/api/generated/subpackage_chats/subpackage_chats_client.dart new file mode 100644 index 0000000..66d7716 --- /dev/null +++ b/lib/data/api/generated/subpackage_chats/subpackage_chats_client.dart @@ -0,0 +1,39 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint, unused_import + +import 'package:dio/dio.dart'; +import 'package:retrofit/retrofit.dart'; + +import '../models/return_chat_paged_events.dart'; + +part 'subpackage_chats_client.g.dart'; + +@RestApi() +abstract class SubpackageChatsClient { + factory SubpackageChatsClient(Dio dio, {String? baseUrl}) = _SubpackageChatsClient; + + /// List chat events. + /// + /// Fetches a paginated list of **Chat** events. + /// + /// [id] - Identifier for a Chat. Formatted as a UUID. + /// + /// [pageSize] - Specifies the maximum number of results to include per page, enabling pagination. The value must be between 1 and 100, inclusive. + /// + /// For example, if `page_size` is set to 10, each page will include `up to 10 items. Defaults to 10. + /// + /// [pageNumber] - Specifies the page number to retrieve, enabling pagination. + /// + /// This parameter uses zero-based indexing. For example, setting `page_number` to 0 retrieves the first page of results (items 0-9 if `page_size` is 10), setting `page_number` to 1 retrieves the second page (items 10-19), and so on. Defaults to 0, which retrieves the first page. + /// + /// [ascendingOrder] - Specifies the sorting order of the results based on their creation date. Set to true for ascending order (chronological, with the oldest records first) and false for descending order (reverse-chronological, with the newest records first). Defaults to true. + @GET('/v0/evi/chats/{id}') + Future listChatEvents({ + @Path('id') required String id, + @Header('X-Hume-Api-Key') required String xHumeApiKey, + @Query('page_number') int pageNumber = 0, + @Query('page_size') int? pageSize, + @Query('ascending_order') bool? ascendingOrder, + }); +} diff --git a/lib/data/api/generated/subpackage_chats/subpackage_chats_client.g.dart b/lib/data/api/generated/subpackage_chats/subpackage_chats_client.g.dart new file mode 100644 index 0000000..2e27703 --- /dev/null +++ b/lib/data/api/generated/subpackage_chats/subpackage_chats_client.g.dart @@ -0,0 +1,98 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'subpackage_chats_client.dart'; + +// ************************************************************************** +// RetrofitGenerator +// ************************************************************************** + +// ignore_for_file: unnecessary_brace_in_string_interps,no_leading_underscores_for_local_identifiers,unused_element,unnecessary_string_interpolations + +class _SubpackageChatsClient implements SubpackageChatsClient { + _SubpackageChatsClient( + this._dio, { + this.baseUrl, + this.errorLogger, + }); + + final Dio _dio; + + String? baseUrl; + + final ParseErrorLogger? errorLogger; + + @override + Future listChatEvents({ + required String id, + required String xHumeApiKey, + int pageNumber = 0, + int? pageSize, + bool? ascendingOrder, + }) async { + final _extra = {}; + final queryParameters = { + r'page_number': pageNumber, + r'page_size': pageSize, + r'ascending_order': ascendingOrder, + }; + queryParameters.removeWhere((k, v) => v == null); + final _headers = {r'X-Hume-Api-Key': xHumeApiKey}; + _headers.removeWhere((k, v) => v == null); + const Map? _data = null; + final _options = _setStreamType(Options( + method: 'GET', + headers: _headers, + extra: _extra, + ) + .compose( + _dio.options, + '/v0/evi/chats/${id}', + queryParameters: queryParameters, + data: _data, + ) + .copyWith( + baseUrl: _combineBaseUrls( + _dio.options.baseUrl, + baseUrl, + ))); + final _result = await _dio.fetch>(_options); + late ReturnChatPagedEvents _value; + try { + _value = ReturnChatPagedEvents.fromJson(_result.data!); + } on Object catch (e, s) { + errorLogger?.logError(e, s, _options); + rethrow; + } + return _value; + } + + RequestOptions _setStreamType(RequestOptions requestOptions) { + if (T != dynamic && + !(requestOptions.responseType == ResponseType.bytes || + requestOptions.responseType == ResponseType.stream)) { + if (T == String) { + requestOptions.responseType = ResponseType.plain; + } else { + requestOptions.responseType = ResponseType.json; + } + } + return requestOptions; + } + + String _combineBaseUrls( + String dioBaseUrl, + String? baseUrl, + ) { + if (baseUrl == null || baseUrl.trim().isEmpty) { + return dioBaseUrl; + } + + final url = Uri.parse(baseUrl); + + if (url.isAbsolute) { + return url.toString(); + } + + return Uri.parse(dioBaseUrl).resolveUri(url).toString(); + } +} diff --git a/lib/data/api/models/generated/doc/ReturnChatEvent.md b/lib/data/api/models/generated/doc/ReturnChatEvent.md deleted file mode 100644 index da2825a..0000000 --- a/lib/data/api/models/generated/doc/ReturnChatEvent.md +++ /dev/null @@ -1,23 +0,0 @@ -# openapi.model.ReturnChatEvent - -## Load the model package -```dart -import 'package:openapi/api.dart'; -``` - -## Properties -Name | Type | Description | Notes ------------- | ------------- | ------------- | ------------- -**chatId** | **String** | Identifier for the Chat this event occurred in. Formatted as a UUID. | -**emotionFeatures** | **String** | Stringified JSON containing the prosody model inference results. EVI uses the prosody model to measure 48 expressions related to speech and vocal characteristics. These results contain a detailed emotional and tonal analysis of the audio. Scores typically range from 0 to 1, with higher values indicating a stronger confidence level in the measured attribute. | [optional] -**id** | **String** | Identifier for a Chat Event. Formatted as a UUID. | -**messageText** | **String** | The text of the Chat Event. This field contains the message content for each event type listed in the `type` field. | [optional] -**metadata** | **String** | Stringified JSON with additional metadata about the chat event. | [optional] -**relatedEventId** | **String** | Identifier for a related chat event. Currently only seen on ASSISTANT_PROSODY events, to point back to the ASSISTANT_MESSAGE that generated these prosody scores | [optional] -**role** | [**ReturnChatEventRole**](ReturnChatEventRole.md) | | -**timestamp** | **int** | Time at which the Chat Event occurred. Measured in seconds since the Unix epoch. | -**type** | [**ReturnChatEventType**](ReturnChatEventType.md) | | - -[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) - - diff --git a/lib/data/api/models/generated/doc/ReturnChatEventRole.md b/lib/data/api/models/generated/doc/ReturnChatEventRole.md deleted file mode 100644 index ca21c0e..0000000 --- a/lib/data/api/models/generated/doc/ReturnChatEventRole.md +++ /dev/null @@ -1,14 +0,0 @@ -# openapi.model.ReturnChatEventRole - -## Load the model package -```dart -import 'package:openapi/api.dart'; -``` - -## Properties -Name | Type | Description | Notes ------------- | ------------- | ------------- | ------------- - -[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) - - diff --git a/lib/data/api/models/generated/doc/ReturnChatEventType.md b/lib/data/api/models/generated/doc/ReturnChatEventType.md deleted file mode 100644 index 51b147f..0000000 --- a/lib/data/api/models/generated/doc/ReturnChatEventType.md +++ /dev/null @@ -1,14 +0,0 @@ -# openapi.model.ReturnChatEventType - -## Load the model package -```dart -import 'package:openapi/api.dart'; -``` - -## Properties -Name | Type | Description | Notes ------------- | ------------- | ------------- | ------------- - -[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) - - diff --git a/lib/data/api/models/generated/lib/api.dart b/lib/data/api/models/generated/lib/api.dart deleted file mode 100644 index 1ea5111..0000000 --- a/lib/data/api/models/generated/lib/api.dart +++ /dev/null @@ -1,32 +0,0 @@ -// -// AUTO-GENERATED FILE, DO NOT MODIFY! -// -// @dart=2.18 - -// ignore_for_file: unused_element, unused_import -// ignore_for_file: always_put_required_named_parameters_first -// ignore_for_file: constant_identifier_names -// ignore_for_file: lines_longer_than_80_chars - -library openapi.api; - -import 'dart:async'; -import 'dart:convert'; -import 'dart:io'; - -import 'package:collection/collection.dart'; -import 'package:http/http.dart'; -import 'package:intl/intl.dart'; -import 'package:meta/meta.dart'; - -part 'api_helper.dart'; - -part 'model/return_chat_event.dart'; -part 'model/return_chat_event_role.dart'; -part 'model/return_chat_event_type.dart'; - -const _delimiters = {'csv': ',', 'ssv': ' ', 'tsv': '\t', 'pipes': '|'}; -const _dateEpochMarker = 'epoch'; - -bool _isEpochMarker(String? pattern) => - pattern == _dateEpochMarker || pattern == '/$_dateEpochMarker/'; diff --git a/lib/data/api/models/generated/lib/api_helper.dart b/lib/data/api/models/generated/lib/api_helper.dart deleted file mode 100644 index 1fa0174..0000000 --- a/lib/data/api/models/generated/lib/api_helper.dart +++ /dev/null @@ -1,130 +0,0 @@ -// -// AUTO-GENERATED FILE, DO NOT MODIFY! -// -// @dart=2.18 - -// ignore_for_file: unused_element, unused_import -// ignore_for_file: always_put_required_named_parameters_first -// ignore_for_file: constant_identifier_names -// ignore_for_file: lines_longer_than_80_chars - -part of openapi.api; - -class QueryParam { - const QueryParam(this.name, this.value); - - final String name; - final String value; - - @override - String toString() => - '${Uri.encodeQueryComponent(name)}=${Uri.encodeQueryComponent(value)}'; -} - -// Ported from the Java version. -Iterable _queryParams( - String collectionFormat, - String name, - dynamic value, -) { - // Assertions to run in debug mode only. - assert(name.isNotEmpty, 'Parameter cannot be an empty string.'); - - final params = []; - - if (value is List) { - if (collectionFormat == 'multi') { - return value.map( - (dynamic v) => QueryParam(name, parameterToString(v)), - ); - } - - // Default collection format is 'csv'. - if (collectionFormat.isEmpty) { - collectionFormat = 'csv'; // ignore: parameter_assignments - } - - final delimiter = _delimiters[collectionFormat] ?? ','; - - params.add(QueryParam( - name, - value.map(parameterToString).join(delimiter), - )); - } else if (value != null) { - params.add(QueryParam(name, parameterToString(value))); - } - - return params; -} - -/// Format the given parameter object into a [String]. -String parameterToString(dynamic value) { - if (value == null) { - return ''; - } - if (value is DateTime) { - return value.toUtc().toIso8601String(); - } - if (value is ReturnChatEventRole) { - return ReturnChatEventRoleTypeTransformer().encode(value).toString(); - } - if (value is ReturnChatEventType) { - return ReturnChatEventTypeTypeTransformer().encode(value).toString(); - } - if (value is ReturnChatPagedEventsPaginationDirection) { - return ReturnChatPagedEventsPaginationDirectionTypeTransformer() - .encode(value) - .toString(); - } - if (value is ReturnChatPagedEventsStatus) { - return ReturnChatPagedEventsStatusTypeTransformer() - .encode(value) - .toString(); - } - return value.toString(); -} - -/// Returns the decoded body as UTF-8 if the given headers indicate an 'application/json' -/// content type. Otherwise, returns the decoded body as decoded by dart:http package. -Future _decodeBodyBytes(Response response) async { - final contentType = response.headers['content-type']; - return contentType != null && - contentType.toLowerCase().startsWith('application/json') - ? response.bodyBytes.isEmpty - ? '' - : utf8.decode(response.bodyBytes) - : response.body; -} - -/// Returns a valid [T] value found at the specified Map [key], null otherwise. -T? mapValueOfType(dynamic map, String key) { - final dynamic value = map is Map ? map[key] : null; - return value is T ? value : null; -} - -/// Returns a valid Map found at the specified Map [key], null otherwise. -Map? mapCastOfType(dynamic map, String key) { - final dynamic value = map is Map ? map[key] : null; - return value is Map ? value.cast() : null; -} - -/// Returns a valid [DateTime] found at the specified Map [key], null otherwise. -DateTime? mapDateTime(dynamic map, String key, [String? pattern]) { - final dynamic value = map is Map ? map[key] : null; - if (value != null) { - int? millis; - if (value is int) { - millis = value; - } else if (value is String) { - if (_isEpochMarker(pattern)) { - millis = int.tryParse(value); - } else { - return DateTime.tryParse(value); - } - } - if (millis != null) { - return DateTime.fromMillisecondsSinceEpoch(millis, isUtc: true); - } - } - return null; -} diff --git a/lib/data/api/models/generated/lib/model/return_chat_event.dart b/lib/data/api/models/generated/lib/model/return_chat_event.dart deleted file mode 100644 index 699c800..0000000 --- a/lib/data/api/models/generated/lib/model/return_chat_event.dart +++ /dev/null @@ -1,205 +0,0 @@ -// -// AUTO-GENERATED FILE, DO NOT MODIFY! -// -// @dart=2.18 - -// ignore_for_file: unused_element, unused_import -// ignore_for_file: always_put_required_named_parameters_first -// ignore_for_file: constant_identifier_names -// ignore_for_file: lines_longer_than_80_chars - -part of openapi.api; - -class ReturnChatEvent { - /// Returns a new [ReturnChatEvent] instance. - ReturnChatEvent({ - required this.chatId, - this.emotionFeatures, - required this.id, - this.messageText, - this.metadata, - this.relatedEventId, - required this.role, - required this.timestamp, - required this.type, - }); - - /// Identifier for the Chat this event occurred in. Formatted as a UUID. - String chatId; - - /// Stringified JSON containing the prosody model inference results. EVI uses the prosody model to measure 48 expressions related to speech and vocal characteristics. These results contain a detailed emotional and tonal analysis of the audio. Scores typically range from 0 to 1, with higher values indicating a stronger confidence level in the measured attribute. - String? emotionFeatures; - - /// Identifier for a Chat Event. Formatted as a UUID. - String id; - - /// The text of the Chat Event. This field contains the message content for each event type listed in the `type` field. - String? messageText; - - /// Stringified JSON with additional metadata about the chat event. - String? metadata; - - /// Identifier for a related chat event. Currently only seen on ASSISTANT_PROSODY events, to point back to the ASSISTANT_MESSAGE that generated these prosody scores - String? relatedEventId; - - ReturnChatEventRole role; - - /// Time at which the Chat Event occurred. Measured in seconds since the Unix epoch. - int timestamp; - - ReturnChatEventType type; - - @override - bool operator ==(Object other) => - identical(this, other) || - other is ReturnChatEvent && - other.chatId == chatId && - other.emotionFeatures == emotionFeatures && - other.id == id && - other.messageText == messageText && - other.metadata == metadata && - other.relatedEventId == relatedEventId && - other.role == role && - other.timestamp == timestamp && - other.type == type; - - @override - int get hashCode => - // ignore: unnecessary_parenthesis - (chatId.hashCode) + - (emotionFeatures == null ? 0 : emotionFeatures!.hashCode) + - (id.hashCode) + - (messageText == null ? 0 : messageText!.hashCode) + - (metadata == null ? 0 : metadata!.hashCode) + - (relatedEventId == null ? 0 : relatedEventId!.hashCode) + - (role.hashCode) + - (timestamp.hashCode) + - (type.hashCode); - - @override - String toString() => - 'ReturnChatEvent[chatId=$chatId, emotionFeatures=$emotionFeatures, id=$id, messageText=$messageText, metadata=$metadata, relatedEventId=$relatedEventId, role=$role, timestamp=$timestamp, type=$type]'; - - Map toJson() { - final json = {}; - json[r'chat_id'] = this.chatId; - if (this.emotionFeatures != null) { - json[r'emotion_features'] = this.emotionFeatures; - } else { - json[r'emotion_features'] = null; - } - json[r'id'] = this.id; - if (this.messageText != null) { - json[r'message_text'] = this.messageText; - } else { - json[r'message_text'] = null; - } - if (this.metadata != null) { - json[r'metadata'] = this.metadata; - } else { - json[r'metadata'] = null; - } - if (this.relatedEventId != null) { - json[r'related_event_id'] = this.relatedEventId; - } else { - json[r'related_event_id'] = null; - } - json[r'role'] = this.role; - json[r'timestamp'] = this.timestamp; - json[r'type'] = this.type; - return json; - } - - /// Returns a new [ReturnChatEvent] instance and imports its values from - /// [value] if it's a [Map], null otherwise. - // ignore: prefer_constructors_over_static_methods - static ReturnChatEvent? fromJson(dynamic value) { - if (value is Map) { - final json = value.cast(); - - // Ensure that the map contains the required keys. - // Note 1: the values aren't checked for validity beyond being non-null. - // Note 2: this code is stripped in release mode! - assert(() { - requiredKeys.forEach((key) { - assert(json.containsKey(key), - 'Required key "ReturnChatEvent[$key]" is missing from JSON.'); - assert(json[key] != null, - 'Required key "ReturnChatEvent[$key]" has a null value in JSON.'); - }); - return true; - }()); - - return ReturnChatEvent( - chatId: mapValueOfType(json, r'chat_id')!, - emotionFeatures: mapValueOfType(json, r'emotion_features'), - id: mapValueOfType(json, r'id')!, - messageText: mapValueOfType(json, r'message_text'), - metadata: mapValueOfType(json, r'metadata'), - relatedEventId: mapValueOfType(json, r'related_event_id'), - role: ReturnChatEventRole.fromJson(json[r'role'])!, - timestamp: mapValueOfType(json, r'timestamp')!, - type: ReturnChatEventType.fromJson(json[r'type'])!, - ); - } - return null; - } - - static List listFromJson( - dynamic json, { - bool growable = false, - }) { - final result = []; - if (json is List && json.isNotEmpty) { - for (final row in json) { - final value = ReturnChatEvent.fromJson(row); - if (value != null) { - result.add(value); - } - } - } - return result.toList(growable: growable); - } - - static Map mapFromJson(dynamic json) { - final map = {}; - if (json is Map && json.isNotEmpty) { - json = json.cast(); // ignore: parameter_assignments - for (final entry in json.entries) { - final value = ReturnChatEvent.fromJson(entry.value); - if (value != null) { - map[entry.key] = value; - } - } - } - return map; - } - - // maps a json object with a list of ReturnChatEvent-objects as value to a dart map - static Map> mapListFromJson( - dynamic json, { - bool growable = false, - }) { - final map = >{}; - if (json is Map && json.isNotEmpty) { - // ignore: parameter_assignments - json = json.cast(); - for (final entry in json.entries) { - map[entry.key] = ReturnChatEvent.listFromJson( - entry.value, - growable: growable, - ); - } - } - return map; - } - - /// The list of required keys that must be present in a JSON. - static const requiredKeys = { - 'chat_id', - 'id', - 'role', - 'timestamp', - 'type', - }; -} diff --git a/lib/data/api/models/generated/lib/model/return_chat_event_role.dart b/lib/data/api/models/generated/lib/model/return_chat_event_role.dart deleted file mode 100644 index 32847aa..0000000 --- a/lib/data/api/models/generated/lib/model/return_chat_event_role.dart +++ /dev/null @@ -1,98 +0,0 @@ -// -// AUTO-GENERATED FILE, DO NOT MODIFY! -// -// @dart=2.18 - -// ignore_for_file: unused_element, unused_import -// ignore_for_file: always_put_required_named_parameters_first -// ignore_for_file: constant_identifier_names -// ignore_for_file: lines_longer_than_80_chars - -part of openapi.api; - -class ReturnChatEventRole { - /// Instantiate a new enum with the provided [value]. - const ReturnChatEventRole._(this.value); - - /// The underlying value of this enum member. - final String value; - - @override - String toString() => value; - - String toJson() => value; - - static const USER = ReturnChatEventRole._(r'USER'); - static const AGENT = ReturnChatEventRole._(r'AGENT'); - static const SYSTEM = ReturnChatEventRole._(r'SYSTEM'); - static const TOOL = ReturnChatEventRole._(r'TOOL'); - - /// List of all possible values in this [enum][ReturnChatEventRole]. - static const values = [ - USER, - AGENT, - SYSTEM, - TOOL, - ]; - - static ReturnChatEventRole? fromJson(dynamic value) => - ReturnChatEventRoleTypeTransformer().decode(value); - - static List listFromJson( - dynamic json, { - bool growable = false, - }) { - final result = []; - if (json is List && json.isNotEmpty) { - for (final row in json) { - final value = ReturnChatEventRole.fromJson(row); - if (value != null) { - result.add(value); - } - } - } - return result.toList(growable: growable); - } -} - -/// Transformation class that can [encode] an instance of [ReturnChatEventRole] to String, -/// and [decode] dynamic data back to [ReturnChatEventRole]. -class ReturnChatEventRoleTypeTransformer { - factory ReturnChatEventRoleTypeTransformer() => - _instance ??= const ReturnChatEventRoleTypeTransformer._(); - - const ReturnChatEventRoleTypeTransformer._(); - - String encode(ReturnChatEventRole data) => data.value; - - /// Decodes a [dynamic value][data] to a ReturnChatEventRole. - /// - /// If [allowNull] is true and the [dynamic value][data] cannot be decoded successfully, - /// then null is returned. However, if [allowNull] is false and the [dynamic value][data] - /// cannot be decoded successfully, then an [UnimplementedError] is thrown. - /// - /// The [allowNull] is very handy when an API changes and a new enum value is added or removed, - /// and users are still using an old app with the old code. - ReturnChatEventRole? decode(dynamic data, {bool allowNull = true}) { - if (data != null) { - switch (data) { - case r'USER': - return ReturnChatEventRole.USER; - case r'AGENT': - return ReturnChatEventRole.AGENT; - case r'SYSTEM': - return ReturnChatEventRole.SYSTEM; - case r'TOOL': - return ReturnChatEventRole.TOOL; - default: - if (!allowNull) { - throw ArgumentError('Unknown enum value to decode: $data'); - } - } - } - return null; - } - - /// Singleton [ReturnChatEventRoleTypeTransformer] instance. - static ReturnChatEventRoleTypeTransformer? _instance; -} diff --git a/lib/data/api/models/generated/lib/model/return_chat_event_type.dart b/lib/data/api/models/generated/lib/model/return_chat_event_type.dart deleted file mode 100644 index 4bd94ff..0000000 --- a/lib/data/api/models/generated/lib/model/return_chat_event_type.dart +++ /dev/null @@ -1,137 +0,0 @@ -// -// AUTO-GENERATED FILE, DO NOT MODIFY! -// -// @dart=2.18 - -// ignore_for_file: unused_element, unused_import -// ignore_for_file: always_put_required_named_parameters_first -// ignore_for_file: constant_identifier_names -// ignore_for_file: lines_longer_than_80_chars - -part of openapi.api; - -class ReturnChatEventType { - /// Instantiate a new enum with the provided [value]. - const ReturnChatEventType._(this.value); - - /// The underlying value of this enum member. - final String value; - - @override - String toString() => value; - - String toJson() => value; - - static const AGENT_MESSAGE = ReturnChatEventType._(r'AGENT_MESSAGE'); - static const ASSISTANT_PROSODY = ReturnChatEventType._(r'ASSISTANT_PROSODY'); - static const CHAT_START_MESSAGE = - ReturnChatEventType._(r'CHAT_START_MESSAGE'); - static const CHAT_END_MESSAGE = ReturnChatEventType._(r'CHAT_END_MESSAGE'); - static const FUNCTION_CALL = ReturnChatEventType._(r'FUNCTION_CALL'); - static const FUNCTION_CALL_RESPONSE = - ReturnChatEventType._(r'FUNCTION_CALL_RESPONSE'); - static const PAUSE_ONSET = ReturnChatEventType._(r'PAUSE_ONSET'); - static const RESUME_ONSET = ReturnChatEventType._(r'RESUME_ONSET'); - static const SESSION_SETTINGS = ReturnChatEventType._(r'SESSION_SETTINGS'); - static const SYSTEM_PROMPT = ReturnChatEventType._(r'SYSTEM_PROMPT'); - static const USER_INTERRUPTION = ReturnChatEventType._(r'USER_INTERRUPTION'); - static const USER_MESSAGE = ReturnChatEventType._(r'USER_MESSAGE'); - static const USER_RECORDING_START_MESSAGE = - ReturnChatEventType._(r'USER_RECORDING_START_MESSAGE'); - - /// List of all possible values in this [enum][ReturnChatEventType]. - static const values = [ - AGENT_MESSAGE, - ASSISTANT_PROSODY, - CHAT_START_MESSAGE, - CHAT_END_MESSAGE, - FUNCTION_CALL, - FUNCTION_CALL_RESPONSE, - PAUSE_ONSET, - RESUME_ONSET, - SESSION_SETTINGS, - SYSTEM_PROMPT, - USER_INTERRUPTION, - USER_MESSAGE, - USER_RECORDING_START_MESSAGE, - ]; - - static ReturnChatEventType? fromJson(dynamic value) => - ReturnChatEventTypeTypeTransformer().decode(value); - - static List listFromJson( - dynamic json, { - bool growable = false, - }) { - final result = []; - if (json is List && json.isNotEmpty) { - for (final row in json) { - final value = ReturnChatEventType.fromJson(row); - if (value != null) { - result.add(value); - } - } - } - return result.toList(growable: growable); - } -} - -/// Transformation class that can [encode] an instance of [ReturnChatEventType] to String, -/// and [decode] dynamic data back to [ReturnChatEventType]. -class ReturnChatEventTypeTypeTransformer { - factory ReturnChatEventTypeTypeTransformer() => - _instance ??= const ReturnChatEventTypeTypeTransformer._(); - - const ReturnChatEventTypeTypeTransformer._(); - - String encode(ReturnChatEventType data) => data.value; - - /// Decodes a [dynamic value][data] to a ReturnChatEventType. - /// - /// If [allowNull] is true and the [dynamic value][data] cannot be decoded successfully, - /// then null is returned. However, if [allowNull] is false and the [dynamic value][data] - /// cannot be decoded successfully, then an [UnimplementedError] is thrown. - /// - /// The [allowNull] is very handy when an API changes and a new enum value is added or removed, - /// and users are still using an old app with the old code. - ReturnChatEventType? decode(dynamic data, {bool allowNull = true}) { - if (data != null) { - switch (data) { - case r'AGENT_MESSAGE': - return ReturnChatEventType.AGENT_MESSAGE; - case r'ASSISTANT_PROSODY': - return ReturnChatEventType.ASSISTANT_PROSODY; - case r'CHAT_START_MESSAGE': - return ReturnChatEventType.CHAT_START_MESSAGE; - case r'CHAT_END_MESSAGE': - return ReturnChatEventType.CHAT_END_MESSAGE; - case r'FUNCTION_CALL': - return ReturnChatEventType.FUNCTION_CALL; - case r'FUNCTION_CALL_RESPONSE': - return ReturnChatEventType.FUNCTION_CALL_RESPONSE; - case r'PAUSE_ONSET': - return ReturnChatEventType.PAUSE_ONSET; - case r'RESUME_ONSET': - return ReturnChatEventType.RESUME_ONSET; - case r'SESSION_SETTINGS': - return ReturnChatEventType.SESSION_SETTINGS; - case r'SYSTEM_PROMPT': - return ReturnChatEventType.SYSTEM_PROMPT; - case r'USER_INTERRUPTION': - return ReturnChatEventType.USER_INTERRUPTION; - case r'USER_MESSAGE': - return ReturnChatEventType.USER_MESSAGE; - case r'USER_RECORDING_START_MESSAGE': - return ReturnChatEventType.USER_RECORDING_START_MESSAGE; - default: - if (!allowNull) { - throw ArgumentError('Unknown enum value to decode: $data'); - } - } - } - return null; - } - - /// Singleton [ReturnChatEventTypeTypeTransformer] instance. - static ReturnChatEventTypeTypeTransformer? _instance; -} diff --git a/lib/data/api/models/generated/lib/model/return_chat_paged_events.dart b/lib/data/api/models/generated/lib/model/return_chat_paged_events.dart deleted file mode 100644 index 0886630..0000000 --- a/lib/data/api/models/generated/lib/model/return_chat_paged_events.dart +++ /dev/null @@ -1,239 +0,0 @@ -// -// AUTO-GENERATED FILE, DO NOT MODIFY! -// -// @dart=2.18 - -// ignore_for_file: unused_element, unused_import -// ignore_for_file: always_put_required_named_parameters_first -// ignore_for_file: constant_identifier_names -// ignore_for_file: lines_longer_than_80_chars - -part of openapi.api; - -class ReturnChatPagedEvents { - /// Returns a new [ReturnChatPagedEvents] instance. - ReturnChatPagedEvents({ - required this.chatGroupId, - this.config, - this.endTimestamp, - this.eventsPage = const [], - required this.id, - this.metadata, - required this.pageNumber, - required this.pageSize, - required this.paginationDirection, - required this.startTimestamp, - required this.status, - required this.totalPages, - }); - - /// Identifier for the Chat Group. Any chat resumed from this Chat will have the same `chat_group_id`. Formatted as a UUID. - String chatGroupId; - - /// - /// Please note: This property should have been non-nullable! Since the specification file - /// does not include a default value (using the "default:" property), however, the generated - /// source code must fall back to having a nullable type. - /// Consider adding a "default:" property in the specification file to hide this note. - /// - ReturnConfigSpec? config; - - /// Time at which the Chat ended. Measured in seconds since the Unix epoch. - int? endTimestamp; - - /// List of Chat Events for the specified `page_number` and `page_size`. - List eventsPage; - - /// Identifier for a Chat. Formatted as a UUID. - String id; - - /// Stringified JSON with additional metadata about the chat. - String? metadata; - - /// The page number of the returned list. This value corresponds to the `page_number` parameter specified in the request. Pagination uses zero-based indexing. - int pageNumber; - - /// The maximum number of items returned per page. This value corresponds to the `page_size` parameter specified in the request. - int pageSize; - - ReturnChatPagedEventsPaginationDirection paginationDirection; - - /// Time at which the Chat started. Measured in seconds since the Unix epoch. - int? startTimestamp; - - ReturnChatPagedEventsStatus status; - - /// The total number of pages in the collection. - int totalPages; - - @override - bool operator ==(Object other) => - identical(this, other) || - other is ReturnChatPagedEvents && - other.chatGroupId == chatGroupId && - other.config == config && - other.endTimestamp == endTimestamp && - _deepEquality.equals(other.eventsPage, eventsPage) && - other.id == id && - other.metadata == metadata && - other.pageNumber == pageNumber && - other.pageSize == pageSize && - other.paginationDirection == paginationDirection && - other.startTimestamp == startTimestamp && - other.status == status && - other.totalPages == totalPages; - - @override - int get hashCode => - // ignore: unnecessary_parenthesis - (chatGroupId.hashCode) + - (config == null ? 0 : config!.hashCode) + - (endTimestamp == null ? 0 : endTimestamp!.hashCode) + - (eventsPage.hashCode) + - (id.hashCode) + - (metadata == null ? 0 : metadata!.hashCode) + - (pageNumber.hashCode) + - (pageSize.hashCode) + - (paginationDirection.hashCode) + - (startTimestamp == null ? 0 : startTimestamp!.hashCode) + - (status.hashCode) + - (totalPages.hashCode); - - @override - String toString() => - 'ReturnChatPagedEvents[chatGroupId=$chatGroupId, config=$config, endTimestamp=$endTimestamp, eventsPage=$eventsPage, id=$id, metadata=$metadata, pageNumber=$pageNumber, pageSize=$pageSize, paginationDirection=$paginationDirection, startTimestamp=$startTimestamp, status=$status, totalPages=$totalPages]'; - - Map toJson() { - final json = {}; - json[r'chat_group_id'] = this.chatGroupId; - if (this.config != null) { - json[r'config'] = this.config; - } else { - json[r'config'] = null; - } - if (this.endTimestamp != null) { - json[r'end_timestamp'] = this.endTimestamp; - } else { - json[r'end_timestamp'] = null; - } - json[r'events_page'] = this.eventsPage; - json[r'id'] = this.id; - if (this.metadata != null) { - json[r'metadata'] = this.metadata; - } else { - json[r'metadata'] = null; - } - json[r'page_number'] = this.pageNumber; - json[r'page_size'] = this.pageSize; - json[r'pagination_direction'] = this.paginationDirection; - if (this.startTimestamp != null) { - json[r'start_timestamp'] = this.startTimestamp; - } else { - json[r'start_timestamp'] = null; - } - json[r'status'] = this.status; - json[r'total_pages'] = this.totalPages; - return json; - } - - /// Returns a new [ReturnChatPagedEvents] instance and imports its values from - /// [value] if it's a [Map], null otherwise. - // ignore: prefer_constructors_over_static_methods - static ReturnChatPagedEvents? fromJson(dynamic value) { - if (value is Map) { - final json = value.cast(); - - // Ensure that the map contains the required keys. - // Note 1: the values aren't checked for validity beyond being non-null. - // Note 2: this code is stripped in release mode! - assert(() { - requiredKeys.forEach((key) { - assert(json.containsKey(key), - 'Required key "ReturnChatPagedEvents[$key]" is missing from JSON.'); - assert(json[key] != null, - 'Required key "ReturnChatPagedEvents[$key]" has a null value in JSON.'); - }); - return true; - }()); - - return ReturnChatPagedEvents( - chatGroupId: mapValueOfType(json, r'chat_group_id')!, - config: ReturnConfigSpec.fromJson(json[r'config']), - endTimestamp: mapValueOfType(json, r'end_timestamp'), - eventsPage: ReturnChatEvent.listFromJson(json[r'events_page']), - id: mapValueOfType(json, r'id')!, - metadata: mapValueOfType(json, r'metadata'), - pageNumber: mapValueOfType(json, r'page_number')!, - pageSize: mapValueOfType(json, r'page_size')!, - paginationDirection: ReturnChatPagedEventsPaginationDirection.fromJson( - json[r'pagination_direction'])!, - startTimestamp: mapValueOfType(json, r'start_timestamp'), - status: ReturnChatPagedEventsStatus.fromJson(json[r'status'])!, - totalPages: mapValueOfType(json, r'total_pages')!, - ); - } - return null; - } - - static List listFromJson( - dynamic json, { - bool growable = false, - }) { - final result = []; - if (json is List && json.isNotEmpty) { - for (final row in json) { - final value = ReturnChatPagedEvents.fromJson(row); - if (value != null) { - result.add(value); - } - } - } - return result.toList(growable: growable); - } - - static Map mapFromJson(dynamic json) { - final map = {}; - if (json is Map && json.isNotEmpty) { - json = json.cast(); // ignore: parameter_assignments - for (final entry in json.entries) { - final value = ReturnChatPagedEvents.fromJson(entry.value); - if (value != null) { - map[entry.key] = value; - } - } - } - return map; - } - - // maps a json object with a list of ReturnChatPagedEvents-objects as value to a dart map - static Map> mapListFromJson( - dynamic json, { - bool growable = false, - }) { - final map = >{}; - if (json is Map && json.isNotEmpty) { - // ignore: parameter_assignments - json = json.cast(); - for (final entry in json.entries) { - map[entry.key] = ReturnChatPagedEvents.listFromJson( - entry.value, - growable: growable, - ); - } - } - return map; - } - - /// The list of required keys that must be present in a JSON. - static const requiredKeys = { - 'chat_group_id', - 'events_page', - 'id', - 'page_number', - 'page_size', - 'pagination_direction', - 'start_timestamp', - 'status', - 'total_pages', - }; -} diff --git a/lib/data/api/models/generated/lib/model/return_chat_paged_events_pagination_direction.dart b/lib/data/api/models/generated/lib/model/return_chat_paged_events_pagination_direction.dart deleted file mode 100644 index 84a5b36..0000000 --- a/lib/data/api/models/generated/lib/model/return_chat_paged_events_pagination_direction.dart +++ /dev/null @@ -1,92 +0,0 @@ -// -// AUTO-GENERATED FILE, DO NOT MODIFY! -// -// @dart=2.18 - -// ignore_for_file: unused_element, unused_import -// ignore_for_file: always_put_required_named_parameters_first -// ignore_for_file: constant_identifier_names -// ignore_for_file: lines_longer_than_80_chars - -part of openapi.api; - -class ReturnChatPagedEventsPaginationDirection { - /// Instantiate a new enum with the provided [value]. - const ReturnChatPagedEventsPaginationDirection._(this.value); - - /// The underlying value of this enum member. - final String value; - - @override - String toString() => value; - - String toJson() => value; - - static const ASC = ReturnChatPagedEventsPaginationDirection._(r'ASC'); - static const DESC = ReturnChatPagedEventsPaginationDirection._(r'DESC'); - - /// List of all possible values in this [enum][ReturnChatPagedEventsPaginationDirection]. - static const values = [ - ASC, - DESC, - ]; - - static ReturnChatPagedEventsPaginationDirection? fromJson(dynamic value) => - ReturnChatPagedEventsPaginationDirectionTypeTransformer().decode(value); - - static List listFromJson( - dynamic json, { - bool growable = false, - }) { - final result = []; - if (json is List && json.isNotEmpty) { - for (final row in json) { - final value = ReturnChatPagedEventsPaginationDirection.fromJson(row); - if (value != null) { - result.add(value); - } - } - } - return result.toList(growable: growable); - } -} - -/// Transformation class that can [encode] an instance of [ReturnChatPagedEventsPaginationDirection] to String, -/// and [decode] dynamic data back to [ReturnChatPagedEventsPaginationDirection]. -class ReturnChatPagedEventsPaginationDirectionTypeTransformer { - factory ReturnChatPagedEventsPaginationDirectionTypeTransformer() => - _instance ??= - const ReturnChatPagedEventsPaginationDirectionTypeTransformer._(); - - const ReturnChatPagedEventsPaginationDirectionTypeTransformer._(); - - String encode(ReturnChatPagedEventsPaginationDirection data) => data.value; - - /// Decodes a [dynamic value][data] to a ReturnChatPagedEventsPaginationDirection. - /// - /// If [allowNull] is true and the [dynamic value][data] cannot be decoded successfully, - /// then null is returned. However, if [allowNull] is false and the [dynamic value][data] - /// cannot be decoded successfully, then an [UnimplementedError] is thrown. - /// - /// The [allowNull] is very handy when an API changes and a new enum value is added or removed, - /// and users are still using an old app with the old code. - ReturnChatPagedEventsPaginationDirection? decode(dynamic data, - {bool allowNull = true}) { - if (data != null) { - switch (data) { - case r'ASC': - return ReturnChatPagedEventsPaginationDirection.ASC; - case r'DESC': - return ReturnChatPagedEventsPaginationDirection.DESC; - default: - if (!allowNull) { - throw ArgumentError('Unknown enum value to decode: $data'); - } - } - } - return null; - } - - /// Singleton [ReturnChatPagedEventsPaginationDirectionTypeTransformer] instance. - static ReturnChatPagedEventsPaginationDirectionTypeTransformer? _instance; -} diff --git a/lib/data/api/models/generated/lib/model/return_chat_paged_events_status.dart b/lib/data/api/models/generated/lib/model/return_chat_paged_events_status.dart deleted file mode 100644 index 322a671..0000000 --- a/lib/data/api/models/generated/lib/model/return_chat_paged_events_status.dart +++ /dev/null @@ -1,108 +0,0 @@ -// -// AUTO-GENERATED FILE, DO NOT MODIFY! -// -// @dart=2.18 - -// ignore_for_file: unused_element, unused_import -// ignore_for_file: always_put_required_named_parameters_first -// ignore_for_file: constant_identifier_names -// ignore_for_file: lines_longer_than_80_chars - -part of openapi.api; - -class ReturnChatPagedEventsStatus { - /// Instantiate a new enum with the provided [value]. - const ReturnChatPagedEventsStatus._(this.value); - - /// The underlying value of this enum member. - final String value; - - @override - String toString() => value; - - String toJson() => value; - - static const ACTIVE = ReturnChatPagedEventsStatus._(r'ACTIVE'); - static const USER_ENDED = ReturnChatPagedEventsStatus._(r'USER_ENDED'); - static const USER_TIMEOUT = ReturnChatPagedEventsStatus._(r'USER_TIMEOUT'); - static const MAX_DURATION_TIMEOUT = - ReturnChatPagedEventsStatus._(r'MAX_DURATION_TIMEOUT'); - static const INACTIVITY_TIMEOUT = - ReturnChatPagedEventsStatus._(r'INACTIVITY_TIMEOUT'); - static const ERROR = ReturnChatPagedEventsStatus._(r'ERROR'); - - /// List of all possible values in this [enum][ReturnChatPagedEventsStatus]. - static const values = [ - ACTIVE, - USER_ENDED, - USER_TIMEOUT, - MAX_DURATION_TIMEOUT, - INACTIVITY_TIMEOUT, - ERROR, - ]; - - static ReturnChatPagedEventsStatus? fromJson(dynamic value) => - ReturnChatPagedEventsStatusTypeTransformer().decode(value); - - static List listFromJson( - dynamic json, { - bool growable = false, - }) { - final result = []; - if (json is List && json.isNotEmpty) { - for (final row in json) { - final value = ReturnChatPagedEventsStatus.fromJson(row); - if (value != null) { - result.add(value); - } - } - } - return result.toList(growable: growable); - } -} - -/// Transformation class that can [encode] an instance of [ReturnChatPagedEventsStatus] to String, -/// and [decode] dynamic data back to [ReturnChatPagedEventsStatus]. -class ReturnChatPagedEventsStatusTypeTransformer { - factory ReturnChatPagedEventsStatusTypeTransformer() => - _instance ??= const ReturnChatPagedEventsStatusTypeTransformer._(); - - const ReturnChatPagedEventsStatusTypeTransformer._(); - - String encode(ReturnChatPagedEventsStatus data) => data.value; - - /// Decodes a [dynamic value][data] to a ReturnChatPagedEventsStatus. - /// - /// If [allowNull] is true and the [dynamic value][data] cannot be decoded successfully, - /// then null is returned. However, if [allowNull] is false and the [dynamic value][data] - /// cannot be decoded successfully, then an [UnimplementedError] is thrown. - /// - /// The [allowNull] is very handy when an API changes and a new enum value is added or removed, - /// and users are still using an old app with the old code. - ReturnChatPagedEventsStatus? decode(dynamic data, {bool allowNull = true}) { - if (data != null) { - switch (data) { - case r'ACTIVE': - return ReturnChatPagedEventsStatus.ACTIVE; - case r'USER_ENDED': - return ReturnChatPagedEventsStatus.USER_ENDED; - case r'USER_TIMEOUT': - return ReturnChatPagedEventsStatus.USER_TIMEOUT; - case r'MAX_DURATION_TIMEOUT': - return ReturnChatPagedEventsStatus.MAX_DURATION_TIMEOUT; - case r'INACTIVITY_TIMEOUT': - return ReturnChatPagedEventsStatus.INACTIVITY_TIMEOUT; - case r'ERROR': - return ReturnChatPagedEventsStatus.ERROR; - default: - if (!allowNull) { - throw ArgumentError('Unknown enum value to decode: $data'); - } - } - } - return null; - } - - /// Singleton [ReturnChatPagedEventsStatusTypeTransformer] instance. - static ReturnChatPagedEventsStatusTypeTransformer? _instance; -} diff --git a/lib/provider/chat_provider.dart b/lib/provider/chat_provider.dart index 8605d95..e12cd19 100644 --- a/lib/provider/chat_provider.dart +++ b/lib/provider/chat_provider.dart @@ -1,12 +1,11 @@ import 'dart:convert'; +import 'package:dio/dio.dart'; +import 'package:evi_example/data/api/generated/export.dart'; import 'package:evi_example/utils.dart'; import 'package:flutter/material.dart'; import 'package:http/http.dart' as http; - import 'package:shared_preferences/shared_preferences.dart'; - -import '../data/api/models/generated/lib/api.dart'; import '../pages/emotion_page.dart'; import './initial_sessions.dart'; // Add this import @@ -72,14 +71,16 @@ class ChatProvider extends ChangeNotifier { Future _fetchChatMessages(String chatId) async { try { - final events = await SubpackageChatsApi().listChatEvents( - chatId, - ConfigManager.instance.humeApiKey, - pageNumber: 0, - pageSize: 100, - ascendingOrder: false, + var baseUrl = 'https://api.hume.ai/'; // TODO extract duplicate + final dio = Dio(BaseOptions(baseUrl: baseUrl)); + final client = RestClient(dio); + final events = await client.subpackageChats.listChatEvents( + id: chatId, + xHumeApiKey: ConfigManager.instance.humeApiKey, + pageNumber: 0, + pageSize: 100, + ascendingOrder: false, ); - if (events == null) throw Exception('Failed to fetch messages: null events'); return events; } catch (e) { throw Exception('Failed to fetch messages: $e'); @@ -98,7 +99,7 @@ class ChatProvider extends ChangeNotifier { print(allMessages); // Remove messages from the start until finding one less than 200 characters while (allMessages.isNotEmpty && - allMessages.first['role'] != ReturnChatEventRole.USER && + allMessages.first['role'] != ReturnChatEventRole.user && allMessages.first['text'] != null && allMessages.first['text'].length > 200) { allMessages.removeAt(0); diff --git a/pubspec.yaml b/pubspec.yaml index 2825129..0006edf 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -43,7 +43,8 @@ dependencies: http: ^1.2.2 freezed_annotation: ^2.4.4 json_annotation: ^4.9.0 - openapi_generator_annotations: ^6.1.0 + dio: ^5.7.0 + retrofit: ^4.4.1 provider: ^6.1.1 flutter_svg: ^2.0.0 fl_chart: ^0.69.2 @@ -67,7 +68,8 @@ dev_dependencies: # rules and activating additional ones. flutter_lints: ^5.0.0 build_runner: ^2.4.13 - openapi_generator: ^6.1.0 + swagger_parser: ^1.23.2 + retrofit_generator: ^9.1.2 freezed: ^2.5.7 json_serializable: ^6.9.0 diff --git a/swagger_parser.yaml b/swagger_parser.yaml new file mode 100644 index 0000000..523af5d --- /dev/null +++ b/swagger_parser.yaml @@ -0,0 +1,5 @@ +swagger_parser: + schema_path: lib/data/api/chat_events_spec.yaml + output_directory: lib/data/api/generated + json_serializer: json_serializable + root_client: true diff --git a/test/data/api/models/return_chat_event_test.dart b/test/data/api/models/return_chat_event_test.dart index 3f529fe..73b0131 100644 --- a/test/data/api/models/return_chat_event_test.dart +++ b/test/data/api/models/return_chat_event_test.dart @@ -1,4 +1,4 @@ -import 'package:evi_example/data/api/models/generated/lib/api.dart'; +import 'package:evi_example/data/api/generated/export.dart'; import 'package:flutter_test/flutter_test.dart'; import 'dart:convert'; @@ -23,10 +23,10 @@ void main() { final event = ReturnChatEvent.fromJson(jsonMap); expect(event, isNotNull); - expect(event!.chatId, '550e8400-e29b-41d4-a716-446655440000'); + expect(event.chatId, '550e8400-e29b-41d4-a716-446655440000'); expect(event.id, '660e8400-e29b-41d4-a716-446655440000'); - expect(event.role, ReturnChatEventRole.USER); - expect(event.type, ReturnChatEventType.USER_MESSAGE); + expect(event.role, ReturnChatEventRole.user); + expect(event.type, ReturnChatEventType.userMessage); expect(event.messageText, 'Hello world'); expect(event.timestamp, 1672531200); // Double-parsing check for emotion_features @@ -55,7 +55,7 @@ void main() { final event = ReturnChatEvent.fromJson(jsonMap); expect(event, isNotNull); - expect(event!.emotionFeatures, isNull); + expect(event.emotionFeatures, isNull); }); }); } diff --git a/test/data/api/models/return_chat_paged_events_test.dart b/test/data/api/models/return_chat_paged_events_test.dart index ca791d6..4f243c1 100644 --- a/test/data/api/models/return_chat_paged_events_test.dart +++ b/test/data/api/models/return_chat_paged_events_test.dart @@ -1,6 +1,5 @@ import 'dart:convert'; - -import 'package:evi_example/data/api/models/generated/lib/api.dart'; +import 'package:evi_example/data/api/generated/export.dart'; import 'package:flutter_test/flutter_test.dart'; void main() { @@ -8,9 +7,7 @@ void main() { test('should parse return_chat_paged_events correctly', () {}); test('to return null for empty JSON`', () async { - var result = ReturnChatPagedEvents.fromJson("{}"); - - expect(result, isNull); + expect(ReturnChatPagedEvents.fromJson({}), throwsAssertionError); }); test('to do basic mapping of required keys', () async { @@ -34,29 +31,32 @@ void main() { "pagination_direction": "ASC", "start_timestamp": 1672531200, "status": "ACTIVE", - "total_pages": 1 + "total_pages": 1, + "config": { + "id": "config1d", + "version": 0 + } } '''; final result = ReturnChatPagedEvents.fromJson(jsonDecode(jsonString)); expect(result, isNotNull); - expect(result!.chatGroupId, "770e8400-e29b-41d4-a716-446655440000"); + expect(result.chatGroupId, "770e8400-e29b-41d4-a716-446655440000"); expect(result.eventsPage[0].chatId, "550e8400-e29b-41d4-a716"); expect(result.eventsPage[0].emotionFeatures, "{\"happiness\": 0.5}"); expect(result.eventsPage[0].id, "660e8400-e29b-41d4-a716-446655440000"); expect(result.eventsPage[0].messageText, "Event 1"); - expect(result.eventsPage[0].role, ReturnChatEventRole.USER); + expect(result.eventsPage[0].role, ReturnChatEventRole.user); expect(result.eventsPage[0].timestamp, 1672531200); - expect(result.eventsPage[0].type, ReturnChatEventType.USER_MESSAGE); + expect(result.eventsPage[0].type, ReturnChatEventType.userMessage); expect(result.eventsPage.length, 1); expect(result.id, "990e8400-e29b-41d4-a716-446655440000"); expect(result.pageNumber, 0); expect(result.pageSize, 10); - expect(result.paginationDirection, - ReturnChatPagedEventsPaginationDirection.ASC); + expect(result.paginationDirection, ReturnChatPagedEventsPaginationDirection.asc); expect(result.startTimestamp, 1672531200); - expect(result.status, ReturnChatPagedEventsStatus.ACTIVE); + expect(result.status, ReturnChatPagedEventsStatus.active); expect(result.totalPages, 1); }); }); diff --git a/test/provider/chat_provider_test.dart b/test/provider/chat_provider_test.dart index 33a2d19..a1b2f8a 100644 --- a/test/provider/chat_provider_test.dart +++ b/test/provider/chat_provider_test.dart @@ -1,6 +1,5 @@ import 'dart:convert'; - -import 'package:evi_example/data/api/models/generated/lib/api.dart'; +import 'package:evi_example/data/api/generated/export.dart'; import 'package:evi_example/provider/chat_provider.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -39,10 +38,14 @@ void main() { "pagination_direction": "ASC", "start_timestamp": 0, "status": "ACTIVE", - "total_pages": 0 + "total_pages": 0, + "config": { + "id": "config1d", + "version": 0 + } } '''; - final events = ReturnChatPagedEvents.fromJson(jsonDecode(jsonString))!; + final events = ReturnChatPagedEvents.fromJson(jsonDecode(jsonString)); final result = ChatProvider.filterMessages(events); @@ -98,17 +101,21 @@ void main() { "pagination_direction": "ASC", "start_timestamp": 0, "status": "ACTIVE", - "total_pages": 0 + "total_pages": 0, + "config": { + "id": "config1d", + "version": 0 + } } '''; - final events = ReturnChatPagedEvents.fromJson(jsonDecode(jsonString))!; + final events = ReturnChatPagedEvents.fromJson(jsonDecode(jsonString)); final result = ChatProvider.filterMessages(events); - expect(result[0]["role"], ReturnChatEventRole.SYSTEM); - expect(result[1]["role"], ReturnChatEventRole.USER); - expect(result[2]["role"], ReturnChatEventRole.SYSTEM); - expect(result[3]["role"], ReturnChatEventRole.SYSTEM); + expect(result[0]["role"], ReturnChatEventRole.system); + expect(result[1]["role"], ReturnChatEventRole.user); + expect(result[2]["role"], ReturnChatEventRole.system); + expect(result[3]["role"], ReturnChatEventRole.system); expect(result.length, 4); }); @@ -124,10 +131,14 @@ void main() { "pagination_direction": "ASC", "start_timestamp": 0, "status": "ACTIVE", - "total_pages": 0 + "total_pages": 0, + "config": { + "id": "config1d", + "version": 0 + } } '''; - final events = ReturnChatPagedEvents.fromJson(jsonDecode(jsonString))!; + final events = ReturnChatPagedEvents.fromJson(jsonDecode(jsonString)); final result = ChatProvider.filterMessages(events); @@ -155,15 +166,19 @@ void main() { "pagination_direction": "ASC", "start_timestamp": 0, "status": "ACTIVE", - "total_pages": 0 + "total_pages": 0, + "config": { + "id": "config1d", + "version": 0 + } } '''; - final events = ReturnChatPagedEvents.fromJson(jsonDecode(jsonString))!; + final events = ReturnChatPagedEvents.fromJson(jsonDecode(jsonString)); final result = ChatProvider.filterMessages(events); expect(result.length, 1); - expect(result.first['role'], ReturnChatEventRole.USER); + expect(result.first['role'], ReturnChatEventRole.user); }); test('stops trimming when non-USER message is short (<= 200 chars)', () { @@ -205,10 +220,14 @@ void main() { "pagination_direction": "ASC", "start_timestamp": 0, "status": "ACTIVE", - "total_pages": 0 + "total_pages": 0, + "config": { + "id": "config1d", + "version": 0 + } } '''; - final events = ReturnChatPagedEvents.fromJson(jsonDecode(jsonString))!; + final events = ReturnChatPagedEvents.fromJson(jsonDecode(jsonString)); final result = ChatProvider.filterMessages(events); @@ -258,15 +277,19 @@ void main() { "pagination_direction": "ASC", "start_timestamp": 0, "status": "ACTIVE", - "total_pages": 0 + "total_pages": 0, + "config": { + "id": "config1d", + "version": 0 + } } '''; - final events = ReturnChatPagedEvents.fromJson(jsonDecode(jsonString))!; + final events = ReturnChatPagedEvents.fromJson(jsonDecode(jsonString)); final result = ChatProvider.filterMessages(events); expect(result, isNotEmpty); - expect(result.first['role'], ReturnChatEventRole.USER); + expect(result.first['role'], ReturnChatEventRole.user); expect(result.length, 1); }); @@ -318,10 +341,14 @@ void main() { "pagination_direction": "ASC", "start_timestamp": 0, "status": "ACTIVE", - "total_pages": 0 + "total_pages": 0, + "config": { + "id": "config1d", + "version": 0 + } } '''; - final events = ReturnChatPagedEvents.fromJson(jsonDecode(jsonString))!; + final events = ReturnChatPagedEvents.fromJson(jsonDecode(jsonString)); final result = ChatProvider.filterMessages(events); @@ -360,16 +387,20 @@ void main() { "pagination_direction": "ASC", "start_timestamp": 0, "status": "ACTIVE", - "total_pages": 0 + "total_pages": 0, + "config": { + "id": "config1d", + "version": 0 + } } '''; - final events = ReturnChatPagedEvents.fromJson(jsonDecode(jsonString))!; + final events = ReturnChatPagedEvents.fromJson(jsonDecode(jsonString)); final result = ChatProvider.filterMessages(events); // Even if the USER message is long, it should stop because role == 'USER'. expect(result, isNotEmpty); - expect(result.first['role'], ReturnChatEventRole.USER); + expect(result.first['role'], ReturnChatEventRole.user); expect(result.length, 1); }); }); From eec700c9704ddde909787dc4e84d33c65eb1a469 Mon Sep 17 00:00:00 2001 From: code-schreiber Date: Mon, 11 May 2026 12:27:05 +0200 Subject: [PATCH 09/16] Fix test --- test/data/api/models/return_chat_paged_events_test.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/data/api/models/return_chat_paged_events_test.dart b/test/data/api/models/return_chat_paged_events_test.dart index 4f243c1..0b3b518 100644 --- a/test/data/api/models/return_chat_paged_events_test.dart +++ b/test/data/api/models/return_chat_paged_events_test.dart @@ -6,8 +6,8 @@ void main() { group('ReturnChatPagedEvents', () { test('should parse return_chat_paged_events correctly', () {}); - test('to return null for empty JSON`', () async { - expect(ReturnChatPagedEvents.fromJson({}), throwsAssertionError); + test('errors for empty JSON`', () async { + expect(() => ReturnChatPagedEvents.fromJson({}), throwsA(anything)); }); test('to do basic mapping of required keys', () async { From 32f921239322e498b315f6d073f73a8d41bc8cf6 Mon Sep 17 00:00:00 2001 From: code-schreiber Date: Mon, 11 May 2026 12:51:59 +0200 Subject: [PATCH 10/16] Add opencode to review PR --- .github/workflows/opencode.yml | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 .github/workflows/opencode.yml diff --git a/.github/workflows/opencode.yml b/.github/workflows/opencode.yml new file mode 100644 index 0000000..7b9a05b --- /dev/null +++ b/.github/workflows/opencode.yml @@ -0,0 +1,25 @@ +name: opencode + +on: + pull_request_review_comment: + types: [created] + +jobs: + opencode: + if: | + contains(github.event.comment.body, '/oc') || + contains(github.event.comment.body, '/opencode') + runs-on: ubuntu-latest + permissions: + id-token: write + steps: + - name: Checkout repository + uses: actions/checkout@v6 + with: + fetch-depth: 1 + persist-credentials: false + + - name: Run OpenCode + uses: anomalyco/opencode/github@latest + with: + model: Big Pickle From 010594c72758ad43cc009bbdea5f9f9851148a9c Mon Sep 17 00:00:00 2001 From: code-schreiber Date: Mon, 11 May 2026 13:03:19 +0200 Subject: [PATCH 11/16] Add opencode to review PR --- .github/workflows/opencode.yml | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/.github/workflows/opencode.yml b/.github/workflows/opencode.yml index 7b9a05b..59cf9db 100644 --- a/.github/workflows/opencode.yml +++ b/.github/workflows/opencode.yml @@ -1,25 +1,25 @@ -name: opencode +name: opencode-review on: - pull_request_review_comment: - types: [created] + pull_request: + types: [opened, synchronize, reopened, ready_for_review] jobs: - opencode: - if: | - contains(github.event.comment.body, '/oc') || - contains(github.event.comment.body, '/opencode') + review: runs-on: ubuntu-latest permissions: - id-token: write + contents: read + pull-requests: read steps: - - name: Checkout repository - uses: actions/checkout@v6 + - uses: actions/checkout@v6 with: - fetch-depth: 1 persist-credentials: false - - - name: Run OpenCode - uses: anomalyco/opencode/github@latest + - uses: anomalyco/opencode/github@latest with: model: Big Pickle + share: false + prompt: | + Review this pull request: + - Check for code quality issues + - Look for potential bugs + - Suggest improvements \ No newline at end of file From e8c382b2b7a8e1a5c8792a345e2964f6e31ea717 Mon Sep 17 00:00:00 2001 From: code-schreiber Date: Mon, 11 May 2026 13:09:43 +0200 Subject: [PATCH 12/16] Add opencode model --- .github/workflows/opencode.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/opencode.yml b/.github/workflows/opencode.yml index 59cf9db..ba0d626 100644 --- a/.github/workflows/opencode.yml +++ b/.github/workflows/opencode.yml @@ -16,7 +16,7 @@ jobs: persist-credentials: false - uses: anomalyco/opencode/github@latest with: - model: Big Pickle + model: opencode/big-pickle share: false prompt: | Review this pull request: From 8008e0c3926633b5647fb337f0ee8dd5aa4e4fed Mon Sep 17 00:00:00 2001 From: code-schreiber Date: Mon, 11 May 2026 13:13:10 +0200 Subject: [PATCH 13/16] Add id-token permission --- .github/workflows/opencode.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/opencode.yml b/.github/workflows/opencode.yml index ba0d626..8c045d4 100644 --- a/.github/workflows/opencode.yml +++ b/.github/workflows/opencode.yml @@ -8,6 +8,7 @@ jobs: review: runs-on: ubuntu-latest permissions: + id-token: read contents: read pull-requests: read steps: From 82a052c3613e1adb9ef4dc850c8fbdfd7ace7720 Mon Sep 17 00:00:00 2001 From: code-schreiber Date: Mon, 11 May 2026 13:16:18 +0200 Subject: [PATCH 14/16] Speed up APK CI check --- .github/workflows/flutter.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/flutter.yml b/.github/workflows/flutter.yml index 6c62959..e464f94 100644 --- a/.github/workflows/flutter.yml +++ b/.github/workflows/flutter.yml @@ -57,8 +57,7 @@ jobs: - name: Build APK run: | - flutter build apk --release \ - --dart-define=HUME_API_KEY=${{ secrets.HUME_API_KEY }} + flutter build apk --debug --target-platform android-arm64 - name: Build iOS if: runner.os == 'macOS' From 290ac862d1e5fd6a0c5b799cb527152cd7741919 Mon Sep 17 00:00:00 2001 From: code-schreiber Date: Mon, 11 May 2026 13:18:02 +0200 Subject: [PATCH 15/16] Change id-token permission --- .github/workflows/opencode.yml | 2 +- android/app/build.gradle | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/opencode.yml b/.github/workflows/opencode.yml index 8c045d4..89f73bc 100644 --- a/.github/workflows/opencode.yml +++ b/.github/workflows/opencode.yml @@ -8,7 +8,7 @@ jobs: review: runs-on: ubuntu-latest permissions: - id-token: read + id-token: write contents: read pull-requests: read steps: diff --git a/android/app/build.gradle b/android/app/build.gradle index dc78650..c0442ed 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -24,7 +24,7 @@ android { applicationId = "com.example.evi_example" // You can update the following values to match your application needs. // For more information, see: https://flutter.dev/to/review-gradle-config. - minSdk = 23 + minSdkVersion = flutter.minSdkVersion targetSdk = flutter.targetSdkVersion versionCode = flutter.versionCode versionName = flutter.versionName From aefffd0e87479ac92d995087bd9a523e862e1322 Mon Sep 17 00:00:00 2001 From: code-schreiber Date: Mon, 11 May 2026 13:41:29 +0200 Subject: [PATCH 16/16] Improve tests --- test/provider/chat_provider_test.dart | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/test/provider/chat_provider_test.dart b/test/provider/chat_provider_test.dart index a1b2f8a..7d330d5 100644 --- a/test/provider/chat_provider_test.dart +++ b/test/provider/chat_provider_test.dart @@ -451,7 +451,7 @@ void main() { expect(result.length, 1); }); - test('ignores empty emotion_features', () { + test('handles empty emotion_features object {}', () { final messages = [ {'emotion_features': '{"Admiration": 0.8}'}, {'emotion_features': '{}'} @@ -463,6 +463,19 @@ void main() { expected['Admiration'] = 0.4; expect(result, expected); }); + + test('skips emotion_features that decodes to non-Map type (e.g. JSON array)', () { + final messages = [ + {'emotion_features': '[]'}, + {'emotion_features': '{"Admiration": 0.8}'}, + {'emotion_features': '"just_a_string"'} + ]; + + final result = ChatProvider.processEmotions(messages); + + expect(result['Admiration'], 0.8); + expect(result.length, 1); + }); }); }); }