Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: parse chat response #6843

Merged
merged 3 commits into from
Nov 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 48 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,10 +42,12 @@ AppFlowy is the AI workspace where you achieve more without losing control of yo
## User Installation

- [Download AppFlowy Desktop (macOS, Windows, and Linux)](https://github.com/AppFlowy-IO/AppFlowy/releases)
- Other channels: [FlatHub](https://flathub.org/apps/io.appflowy.AppFlowy), [Snapcraft](https://snapcraft.io/appflowy), [Sourceforge](https://sourceforge.net/projects/appflowy/)
- Other
channels: [FlatHub](https://flathub.org/apps/io.appflowy.AppFlowy), [Snapcraft](https://snapcraft.io/appflowy), [Sourceforge](https://sourceforge.net/projects/appflowy/)
- Available on
- [App Store](https://apps.apple.com/app/appflowy/id6457261352): iPhone
- [Play Store](https://play.google.com/store/apps/details?id=io.appflowy.appflowy): Android 10 or above; ARMv7 is not supported
- [App Store](https://apps.apple.com/app/appflowy/id6457261352): iPhone
- [Play Store](https://play.google.com/store/apps/details?id=io.appflowy.appflowy): Android 10 or above; ARMv7 is
not supported
- [Self-hosting AppFlowy](https://docs.appflowy.io/docs/guides/appflowy/self-hosting-appflowy)
- [Source](https://docs.appflowy.io/docs/documentation/appflowy/from-source)

Expand All @@ -61,32 +63,43 @@ AppFlowy is the AI workspace where you achieve more without losing control of yo

## Getting Started with development

Please view the [documentation](https://docs.appflowy.io/docs/documentation/appflowy/from-source) for OS specific development instructions
Please view the [documentation](https://docs.appflowy.io/docs/documentation/appflowy/from-source) for OS specific
development instructions

## Roadmap

- [AppFlowy Roadmap ReadMe](https://docs.appflowy.io/docs/appflowy/roadmap)
- [AppFlowy Public Roadmap](https://github.com/orgs/AppFlowy-IO/projects/5/views/12)

If you'd like to propose a feature, submit a feature request [here](https://github.com/AppFlowy-IO/AppFlowy/issues/new?assignees=&labels=&template=feature_request.yaml&title=%5BFR%5D+) <br/>
If you'd like to report a bug, submit a bug report [here](https://github.com/AppFlowy-IO/AppFlowy/issues/new?assignees=&labels=&template=bug_report.yaml&title=%5BBug%5D+)
If you'd like to propose a feature, submit a feature
request [here](https://github.com/AppFlowy-IO/AppFlowy/issues/new?assignees=&labels=&template=feature_request.yaml&title=%5BFR%5D+) <br/>
If you'd like to report a bug, submit a bug
report [here](https://github.com/AppFlowy-IO/AppFlowy/issues/new?assignees=&labels=&template=bug_report.yaml&title=%5BBug%5D+)

## **Releases**

Please see the [changelog](https://www.appflowy.io/whatsnew) for more details about a given release.

## Contributing

Contributions make the open-source community a fantastic place to learn, inspire, and create. Any contributions you make are **greatly appreciated**. Please look at [Contributing to AppFlowy](https://docs.appflowy.io/docs/documentation/software-contributions/contributing-to-appflowy) for details.
Contributions make the open-source community a fantastic place to learn, inspire, and create. Any contributions you make
are **greatly appreciated**. Please look
at [Contributing to AppFlowy](https://docs.appflowy.io/docs/documentation/software-contributions/contributing-to-appflowy)
for details.

If your Pull Request is accepted as it fixes a bug, adds functionality, or makes AppFlowy's codebase significantly easier to use or understand, **Congratulations!** If your administrative and managerial work behind the scenes sustains the community, **Congratulations!** You are now an official contributor to AppFlowy. Get in touch with us ([link](https://tally.so/r/mKP5z3)) to receive the very special Contributor T-shirt!
If your Pull Request is accepted as it fixes a bug, adds functionality, or makes AppFlowy's codebase significantly
easier to use or understand, **Congratulations!** If your administrative and managerial work behind the scenes sustains
the community, **Congratulations!** You are now an official contributor to AppFlowy. Get in touch with
us ([link](https://tally.so/r/mKP5z3)) to receive the very special Contributor T-shirt!
Proudly wear your T-shirt and show it to us by tagging [@appflowy](https://twitter.com/appflowy) on Twitter.

## Translations 🌎🗺

[![translation badge](https://inlang.com/badge?url=github.com/AppFlowy-IO/AppFlowy)](https://inlang.com/editor/github.com/AppFlowy-IO/AppFlowy?ref=badge)

To add translations, you can manually edit the JSON translation files in `/frontend/resources/translations`, use the [inlang online editor](https://inlang.com/editor/github.com/AppFlowy-IO/AppFlowy), or run `npx inlang machine translate` to add missing translations.
To add translations, you can manually edit the JSON translation files in `/frontend/resources/translations`, use
the [inlang online editor](https://inlang.com/editor/github.com/AppFlowy-IO/AppFlowy), or
run `npx inlang machine translate` to add missing translations.

## Join the community to build AppFlowy together

Expand All @@ -96,33 +109,51 @@ To add translations, you can manually edit the JSON translation files in `/front

## Why Are We Building This?

Notion has been our favourite project and knowledge management tool in recent years because of its aesthetic appeal and functionality. Our team uses it daily, and we are on its paid plan. However, as we all know, Notion has its limitations. These include weak data security and poor compatibility with mobile devices. Likewise, alternative collaborative workplace management tools also have their constraints.
Notion has been our favourite project and knowledge management tool in recent years because of its aesthetic appeal and
functionality. Our team uses it daily, and we are on its paid plan. However, as we all know, Notion has its limitations.
These include weak data security and poor compatibility with mobile devices. Likewise, alternative collaborative
workplace management tools also have their constraints.

The limitations we encountered using these tools and our past work experience with collaborative productivity tools have led to our firm belief that there is a glass ceiling on what's possible for these tools in the future. This emanates from the fact that these tools will probably struggle to scale horizontally at some point and be forced to prioritize a proportion of customers whose needs differ from the rest. While decision-makers want a workplace OS, it is impossible to come up with a one-size fits all solution in such a fragmented market.
The limitations we encountered using these tools and our past work experience with collaborative productivity tools have
led to our firm belief that there is a glass ceiling on what's possible for these tools in the future. This emanates
from the fact that these tools will probably struggle to scale horizontally at some point and be forced to prioritize a
proportion of customers whose needs differ from the rest. While decision-makers want a workplace OS, it is impossible to
come up with a one-size fits all solution in such a fragmented market.

When a customer's evolving core needs are not satisfied, they either switch to another or build one from the ground up, in-house. Consequently, they either go under another ceiling or buy an expensive ticket to learn a hard lesson. This is a requirement for many resources and expertise, building a reliable and easy-to-use collaborative tool, not to mention the speed and native experience. The same may apply to individual users as well.
When a customer's evolving core needs are not satisfied, they either switch to another or build one from the ground up,
in-house. Consequently, they either go under another ceiling or buy an expensive ticket to learn a hard lesson. This is
a requirement for many resources and expertise, building a reliable and easy-to-use collaborative tool, not to mention
the speed and native experience. The same may apply to individual users as well.

All these restrictions necessitate our mission - to make it possible for anyone to create apps that suit their needs well.
All these restrictions necessitate our mission - to make it possible for anyone to create apps that suit their needs
well.

- To individuals, we would like to offer Notion's functionality, data security, and cross-platform native experience.
- To enterprises and hackers, AppFlowy is dedicated to offering building blocks and collaboration infra services to enable you to make apps on your own. Moreover, you have 100% control of your data. You can design and modify AppFlowy your way, with a single codebase written in Flutter and Rust supporting multiple platforms armed with long-term maintainability.
- To enterprises and hackers, AppFlowy is dedicated to offering building blocks and collaboration infra services to
enable you to make apps on your own. Moreover, you have 100% control of your data. You can design and modify AppFlowy
your way, with a single codebase written in Flutter and Rust supporting multiple platforms armed with long-term
maintainability.

We decided to achieve this mission by upholding the three most fundamental values:

- Data privacy first
- Reliable native experience
- Community-driven extensibility

We do not claim to outperform Notion in terms of functionality and design, at least for now. Besides, our priority doesn't lie in more functionality at the moment. Instead, we would like to cultivate a community to democratize the knowledge and wheels of making complex workplace management tools while enabling people and businesses to create beautiful things on their own by equipping them with a versatile toolbox of building blocks.
We do not claim to outperform Notion in terms of functionality and design, at least for now. Besides, our priority
doesn't lie in more functionality at the moment. Instead, we would like to cultivate a community to democratize the
knowledge and wheels of making complex workplace management tools while enabling people and businesses to create
beautiful things on their own by equipping them with a versatile toolbox of building blocks.

## License

Distributed under the AGPLv3 License. See [`LICENSE.md`](https://github.com/AppFlowy-IO/AppFlowy/blob/main/LICENSE) for more information.
Distributed under the AGPLv3 License. See [`LICENSE.md`](https://github.com/AppFlowy-IO/AppFlowy/blob/main/LICENSE) for
more information.

## Acknowledgments

Special thanks to these amazing projects which help power AppFlowy.IO:

- [flutter-quill](https://github.com/singerdmx/flutter-quill)
- [cargo-make](https://github.com/sagiegurari/cargo-make)
- [contrib.rocks](https://contrib.rocks)
- [flutter_chat_ui](https://pub.dev/packages/flutter_chat_ui)
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ class ChatAIMessageBloc extends Bloc<ChatAIMessageEvent, ChatAIMessageState> {
}) : super(
ChatAIMessageState.initial(
message,
messageReferenceSource(refSourceJsonString),
parseMetadata(refSourceJsonString),
),
) {
if (state.stream != null) {
Expand All @@ -42,9 +42,9 @@ class ChatAIMessageBloc extends Bloc<ChatAIMessageEvent, ChatAIMessageState> {
add(const ChatAIMessageEvent.onAIResponseLimit());
}
},
onMetadata: (sources) {
onMetadata: (metadata) {
if (!isClosed) {
add(ChatAIMessageEvent.receiveSources(sources));
add(ChatAIMessageEvent.receiveMetadata(metadata));
}
},
);
Expand Down Expand Up @@ -116,10 +116,12 @@ class ChatAIMessageBloc extends Bloc<ChatAIMessageEvent, ChatAIMessageState> {
),
);
},
receiveSources: (List<ChatMessageRefSource> sources) {
receiveMetadata: (metadata) {
Log.debug("AI Steps: ${metadata.progress?.step}");
emit(
state.copyWith(
sources: sources,
sources: metadata.sources,
progress: metadata.progress,
),
);
},
Expand All @@ -139,8 +141,8 @@ class ChatAIMessageEvent with _$ChatAIMessageEvent {
const factory ChatAIMessageEvent.retry() = _Retry;
const factory ChatAIMessageEvent.retryResult(String text) = _RetryResult;
const factory ChatAIMessageEvent.onAIResponseLimit() = _OnAIResponseLimit;
const factory ChatAIMessageEvent.receiveSources(
List<ChatMessageRefSource> sources,
const factory ChatAIMessageEvent.receiveMetadata(
MetadataCollection metadata,
) = _ReceiveMetadata;
}

Expand All @@ -151,17 +153,19 @@ class ChatAIMessageState with _$ChatAIMessageState {
required String text,
required MessageState messageState,
required List<ChatMessageRefSource> sources,
required AIChatProgress? progress,
}) = _ChatAIMessageState;

factory ChatAIMessageState.initial(
dynamic text,
List<ChatMessageRefSource> sources,
MetadataCollection metadata,
) {
return ChatAIMessageState(
text: text is String ? text : "",
stream: text is AnswerStream ? text : null,
messageState: const MessageState.ready(),
sources: sources,
sources: metadata.sources,
progress: metadata.progress,
);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ class ChatBloc extends Bloc<ChatEvent, ChatState> {
bool isLoadingPreviousMessages = false;
bool hasMorePreviousMessages = true;
AnswerStream? answerStream;
int numSendMessage = 0;

@override
Future<void> close() async {
Expand Down Expand Up @@ -144,6 +145,8 @@ class ChatBloc extends Bloc<ChatEvent, ChatState> {
String message,
Map<String, dynamic>? metadata,
) {
numSendMessage += 1;

final relatedQuestionMessages = chatController.messages.where(
(message) {
return onetimeMessageTypeFromMeta(message.metadata) ==
Expand Down Expand Up @@ -290,9 +293,13 @@ class ChatBloc extends Bloc<ChatEvent, ChatState> {
chatId: chatId,
messageId: lastSentMessage!.messageId,
);

// when previous numSendMessage is not equal to current numSendMessage, it means that the user
// has sent a new message. So we don't need to get related questions.
final preNumSendMessage = numSendMessage;
await AIEventGetRelatedQuestion(payload).send().fold(
(list) {
if (!isClosed) {
if (!isClosed && preNumSendMessage == numSendMessage) {
add(
ChatEvent.didReceiveRelatedQuestions(
list.items.map((e) => e.content).toList(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,20 @@ class ChatMessageRefSource {
Map<String, dynamic> toJson() => _$ChatMessageRefSourceToJson(this);
}

@JsonSerializable()
class AIChatProgress {
AIChatProgress({
required this.step,
});

factory AIChatProgress.fromJson(Map<String, dynamic> json) =>
_$AIChatProgressFromJson(json);

final String step;

Map<String, dynamic> toJson() => _$AIChatProgressToJson(this);
}

enum PromptResponseState {
ready,
sendingQuestion,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,38 +66,58 @@ ChatFile? chatFileFromMap(Map<String, dynamic>? map) {
return ChatFile.fromFilePath(filePath);
}

List<ChatMessageRefSource> messageReferenceSource(String? s) {
if (s == null || s.isEmpty || s == "null") {
return [];
class MetadataCollection {
MetadataCollection({
required this.sources,
this.progress,
});
final List<ChatMessageRefSource> sources;
final AIChatProgress? progress;
}

MetadataCollection parseMetadata(String? s) {
if (s == null || s.trim().isEmpty || s.toLowerCase() == "null") {
return MetadataCollection(sources: []);
}

final List<ChatMessageRefSource> metadata = [];
AIChatProgress? progress;

try {
final metadataJson = jsonDecode(s);
if (metadataJson == null) {
Log.warn("metadata is null");
return [];
final dynamic decodedJson = jsonDecode(s);
if (decodedJson == null) {
return MetadataCollection(sources: []);
}
// [{"id":null,"name":"The Five Dysfunctions of a Team.pdf","source":"/Users/weidongfu/Desktop/The Five Dysfunctions of a Team.pdf"}]

if (metadataJson is Map<String, dynamic>) {
if (metadataJson.isNotEmpty) {
metadata.add(ChatMessageRefSource.fromJson(metadataJson));
void processMap(Map<String, dynamic> map) {
if (map.containsKey("step") && map["step"] != null) {
progress = AIChatProgress.fromJson(map);
} else if (map.containsKey("id") && map["id"] != null) {
metadata.add(ChatMessageRefSource.fromJson(map));
} else {
Log.info("Unsupported metadata format: $map");
}
}

if (decodedJson is Map<String, dynamic>) {
processMap(decodedJson);
} else if (decodedJson is List) {
for (final element in decodedJson) {
if (element is Map<String, dynamic>) {
processMap(element);
} else {
Log.error("Invalid metadata element: $element");
}
}
} else if (metadataJson is List) {
metadata.addAll(
metadataJson.map(
(e) => ChatMessageRefSource.fromJson(e as Map<String, dynamic>),
),
);
} else {
Log.error("Invalid metadata: $metadataJson");
Log.error("Invalid metadata format: $decodedJson");
}
} catch (e) {
Log.error("Failed to parse metadata: $e");
} catch (e, stacktrace) {
Log.error("Failed to parse metadata: $e, input: $s");
Log.debug(stacktrace.toString());
}

return metadata;
return MetadataCollection(sources: metadata, progress: progress);
}

Future<List<ChatMessageMetaPB>> metadataPBFromMetadata(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import 'dart:async';
import 'dart:ffi';
import 'dart:isolate';

import 'package:appflowy/plugins/ai_chat/application/chat_entity.dart';
import 'package:appflowy/plugins/ai_chat/application/chat_message_service.dart';

class AnswerStream {
Expand All @@ -25,7 +24,7 @@ class AnswerStream {
} else if (event.startsWith("metadata:")) {
if (_onMetadata != null) {
final s = event.substring(9);
_onMetadata!(messageReferenceSource(s));
_onMetadata!(parseMetadata(s));
}
} else if (event == "AI_RESPONSE_LIMIT") {
if (_onAIResponseLimit != null) {
Expand Down Expand Up @@ -59,7 +58,7 @@ class AnswerStream {
void Function()? _onEnd;
void Function(String error)? _onError;
void Function()? _onAIResponseLimit;
void Function(List<ChatMessageRefSource> metadata)? _onMetadata;
void Function(MetadataCollection metadataCollection)? _onMetadata;

int get nativePort => _port.sendPort.nativePort;
bool get hasStarted => _hasStarted;
Expand All @@ -78,7 +77,7 @@ class AnswerStream {
void Function()? onEnd,
void Function(String error)? onError,
void Function()? onAIResponseLimit,
void Function(List<ChatMessageRefSource> metadata)? onMetadata,
void Function(MetadataCollection metadata)? onMetadata,
}) {
_onData = onData;
_onStart = onStart;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/ai_chat/application/chat_ai_message_bloc.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter_animate/flutter_animate.dart';
import 'package:flutter_bloc/flutter_bloc.dart';

/// An animated generating indicator for an AI response
class ChatAILoading extends StatelessWidget {
Expand All @@ -23,9 +25,14 @@ class ChatAILoading extends StatelessWidget {
children: [
Padding(
padding: const EdgeInsetsDirectional.only(end: 4.0),
child: FlowyText(
LocaleKeys.chat_generatingResponse.tr(),
color: Theme.of(context).hintColor,
child: BlocBuilder<ChatAIMessageBloc, ChatAIMessageState>(
builder: (context, state) {
return FlowyText(
state.progress?.step ??
LocaleKeys.chat_generatingResponse.tr(),
color: Theme.of(context).hintColor,
);
},
),
),
buildDot(const Color(0xFF9327FF))
Expand Down
Loading
Loading