feat: AI translation in Database (#5515)

* chore: add tranlate field type

* chore: integrate ai translate

* chore: integrate client api

* chore: implement UI
This commit is contained in:
Nathan.fooo
2024-06-12 16:32:28 +08:00
committed by GitHub
parent 815c99710e
commit 3d7a500550
74 changed files with 1833 additions and 148 deletions

View File

@ -449,6 +449,7 @@ pub enum FieldType {
CreatedTime = 9,
Relation = 10,
Summary = 11,
Translate = 12,
}
impl Display for FieldType {
@ -489,6 +490,7 @@ impl FieldType {
FieldType::CreatedTime => "Created time",
FieldType::Relation => "Relation",
FieldType::Summary => "Summarize",
FieldType::Translate => "Translate",
};
s.to_string()
}

View File

@ -109,6 +109,10 @@ impl From<&Filter> for FilterPB {
.cloned::<TextFilterPB>()
.unwrap()
.try_into(),
FieldType::Translate => condition_and_content
.cloned::<TextFilterPB>()
.unwrap()
.try_into(),
};
Self {
@ -156,6 +160,9 @@ impl TryFrom<FilterDataPB> for FilterInner {
FieldType::Summary => {
BoxAny::new(TextFilterPB::try_from(bytes).map_err(|_| ErrorCode::ProtobufSerde)?)
},
FieldType::Translate => {
BoxAny::new(TextFilterPB::try_from(bytes).map_err(|_| ErrorCode::ProtobufSerde)?)
},
};
Ok(Self::Data {

View File

@ -16,6 +16,7 @@ macro_rules! impl_into_field_type {
9 => FieldType::CreatedTime,
10 => FieldType::Relation,
11 => FieldType::Summary,
12 => FieldType::Translate,
_ => {
tracing::error!("🔴Can't parse FieldType from value: {}", ty);
FieldType::RichText

View File

@ -380,3 +380,18 @@ pub struct SummaryRowPB {
#[pb(index = 3)]
pub field_id: String,
}
#[derive(Debug, Default, Clone, ProtoBuf, Validate)]
pub struct TranslateRowPB {
#[pb(index = 1)]
#[validate(custom = "required_not_empty_str")]
pub view_id: String,
#[pb(index = 2)]
#[validate(custom = "required_not_empty_str")]
pub row_id: String,
#[pb(index = 3)]
#[validate(custom = "required_not_empty_str")]
pub field_id: String,
}

View File

@ -7,6 +7,7 @@ mod select_option_entities;
mod summary_entities;
mod text_entities;
mod timestamp_entities;
mod translate_entities;
mod url_entities;
pub use checkbox_entities::*;
@ -18,4 +19,5 @@ pub use select_option_entities::*;
pub use summary_entities::*;
pub use text_entities::*;
pub use timestamp_entities::*;
pub use translate_entities::*;
pub use url_entities::*;

View File

@ -0,0 +1,50 @@
use crate::services::field::translate_type_option::translate::TranslateTypeOption;
use flowy_derive::{ProtoBuf, ProtoBuf_Enum};
#[derive(Debug, Clone, Default, ProtoBuf)]
pub struct TranslateTypeOptionPB {
#[pb(index = 1)]
pub auto_fill: bool,
#[pb(index = 2)]
pub language: TranslateLanguagePB,
}
impl From<TranslateTypeOption> for TranslateTypeOptionPB {
fn from(value: TranslateTypeOption) -> Self {
TranslateTypeOptionPB {
auto_fill: value.auto_fill,
language: value.language_type.into(),
}
}
}
impl From<TranslateTypeOptionPB> for TranslateTypeOption {
fn from(value: TranslateTypeOptionPB) -> Self {
TranslateTypeOption {
auto_fill: value.auto_fill,
language_type: value.language as i64,
}
}
}
#[derive(Clone, Debug, Copy, ProtoBuf_Enum, Default)]
#[repr(i64)]
pub enum TranslateLanguagePB {
Chinese = 0,
#[default]
English = 1,
French = 2,
German = 3,
}
impl From<i64> for TranslateLanguagePB {
fn from(data: i64) -> Self {
match data {
0 => TranslateLanguagePB::Chinese,
1 => TranslateLanguagePB::English,
2 => TranslateLanguagePB::French,
3 => TranslateLanguagePB::German,
_ => TranslateLanguagePB::English,
}
}
}

View File

@ -1104,3 +1104,16 @@ pub(crate) async fn summarize_row_handler(
.await?;
Ok(())
}
pub(crate) async fn translate_row_handler(
data: AFPluginData<TranslateRowPB>,
manager: AFPluginState<Weak<DatabaseManager>>,
) -> Result<(), FlowyError> {
let manager = upgrade_manager(manager)?;
let data = data.try_into_inner()?;
let row_id = RowId::from(data.row_id);
manager
.translate_row(data.view_id, row_id, data.field_id)
.await?;
Ok(())
}

View File

@ -91,6 +91,7 @@ pub fn init(database_manager: Weak<DatabaseManager>) -> AFPlugin {
.event(DatabaseEvent::GetRelatedDatabaseRows, get_related_database_rows_handler)
// AI
.event(DatabaseEvent::SummarizeRow, summarize_row_handler)
.event(DatabaseEvent::TranslateRow, translate_row_handler)
}
/// [DatabaseEvent] defines events that are used to interact with the Grid. You could check [this](https://appflowy.gitbook.io/docs/essential-documentation/contribute-to-appflowy/architecture/backend/protobuf)
@ -373,4 +374,7 @@ pub enum DatabaseEvent {
#[event(input = "SummaryRowPB")]
SummarizeRow = 174,
#[event(input = "TranslateRowPB")]
TranslateRow = 175,
}

View File

@ -17,15 +17,19 @@ use tracing::{event, instrument, trace};
use collab_integrate::collab_builder::{AppFlowyCollabBuilder, CollabBuilderConfig};
use collab_integrate::{CollabKVAction, CollabKVDB, CollabPersistenceConfig};
use flowy_database_pub::cloud::{DatabaseCloudService, SummaryRowContent};
use flowy_database_pub::cloud::{
DatabaseCloudService, SummaryRowContent, TranslateItem, TranslateRowContent,
};
use flowy_error::{internal_error, FlowyError, FlowyResult};
use lib_infra::box_any::BoxAny;
use lib_infra::priority_task::TaskDispatcher;
use crate::entities::{DatabaseLayoutPB, DatabaseSnapshotPB};
use crate::entities::{DatabaseLayoutPB, DatabaseSnapshotPB, FieldType};
use crate::services::cell::stringify_cell;
use crate::services::database::DatabaseEditor;
use crate::services::database_view::DatabaseLayoutDepsResolver;
use crate::services::field::translate_type_option::translate::TranslateTypeOption;
use crate::services::field_settings::default_field_settings_by_layout_map;
use crate::services::share::csv::{CSVFormat, CSVImporter, ImportResult};
@ -459,6 +463,77 @@ impl DatabaseManager {
Ok(())
}
#[instrument(level = "debug", skip_all)]
pub async fn translate_row(
&self,
view_id: String,
row_id: RowId,
field_id: String,
) -> FlowyResult<()> {
let database = self.get_database_with_view_id(&view_id).await?;
let mut translate_row_content = TranslateRowContent::new();
let mut language = "english".to_string();
if let Some(row) = database.get_row(&view_id, &row_id) {
let fields = database.get_fields(&view_id, None);
for field in fields {
// When translate a row, skip the content in the "AI Translate" cell; it does not need to
// be translated.
if field.id != field_id {
if let Some(cell) = row.cells.get(&field.id) {
translate_row_content.push(TranslateItem {
title: field.name.clone(),
content: stringify_cell(cell, &field),
})
}
} else {
language = TranslateTypeOption::language_from_type(
field
.type_options
.get(&FieldType::Translate.to_string())
.cloned()
.map(TranslateTypeOption::from)
.unwrap_or_default()
.language_type,
)
.to_string();
}
}
}
// Call the cloud service to summarize the row.
trace!(
"[AI]:translate to {}, content:{:?}",
language,
translate_row_content
);
let response = self
.cloud_service
.translate_database_row(&self.user.workspace_id()?, translate_row_content, &language)
.await?;
// Format the response items into a single string
let content = response
.items
.into_iter()
.map(|value| {
value
.into_iter()
.map(|(_k, v)| v.to_string())
.collect::<Vec<String>>()
.join(", ")
})
.collect::<Vec<String>>()
.join(",");
trace!("[AI]:translate row response: {}", content);
// Update the cell with the response from the cloud service.
database
.update_cell_with_changeset(&view_id, &row_id, &field_id, BoxAny::new(content))
.await?;
Ok(())
}
/// Only expose this method for testing
#[cfg(debug_assertions)]
pub fn get_cloud_service(&self) -> &Arc<dyn DatabaseCloudService> {

View File

@ -262,6 +262,9 @@ impl<'a> CellBuilder<'a> {
FieldType::Summary => {
cells.insert(field_id, insert_text_cell(cell_str, field));
},
FieldType::Translate => {
cells.insert(field_id, insert_text_cell(cell_str, field));
},
}
}
}

View File

@ -1703,8 +1703,9 @@ pub async fn update_field_type_option_fn(
update.update_type_options(|type_options_update| {
event!(
tracing::Level::TRACE,
"insert type option to field type: {:?}",
field_type
"insert type option to field type: {:?}, {:?}",
field_type,
type_option_data
);
type_options_update.insert(&field_type.to_string(), type_option_data);
});

View File

@ -7,6 +7,7 @@ pub mod selection_type_option;
pub mod summary_type_option;
pub mod text_type_option;
pub mod timestamp_type_option;
pub mod translate_type_option;
mod type_option;
mod type_option_cell;
mod url_type_option;

View File

@ -85,6 +85,7 @@ impl CellDataDecoder for RichTextTypeOption {
| FieldType::CreatedTime
| FieldType::Relation => None,
FieldType::Summary => Some(StringCellData::from(stringify_cell(cell, field))),
FieldType::Translate => Some(StringCellData::from(stringify_cell(cell, field))),
}
}

View File

@ -0,0 +1,2 @@
pub mod translate;
pub mod translate_entities;

View File

@ -0,0 +1,137 @@
use crate::entities::TextFilterPB;
use crate::services::cell::{CellDataChangeset, CellDataDecoder};
use crate::services::field::type_options::translate_type_option::translate_entities::TranslateCellData;
use crate::services::field::type_options::util::ProtobufStr;
use crate::services::field::{
TypeOption, TypeOptionCellData, TypeOptionCellDataCompare, TypeOptionCellDataFilter,
TypeOptionCellDataSerde, TypeOptionTransform,
};
use crate::services::sort::SortCondition;
use collab::core::any_map::AnyMapExtension;
use collab_database::fields::{TypeOptionData, TypeOptionDataBuilder};
use collab_database::rows::Cell;
use flowy_error::FlowyResult;
use std::cmp::Ordering;
#[derive(Debug, Clone)]
pub struct TranslateTypeOption {
pub auto_fill: bool,
/// Use [TranslateTypeOption::language_from_type] to get the language name
pub language_type: i64,
}
impl TranslateTypeOption {
pub fn language_from_type(language_type: i64) -> &'static str {
match language_type {
0 => "Chinese",
1 => "English",
2 => "French",
3 => "German",
_ => "English",
}
}
}
impl Default for TranslateTypeOption {
fn default() -> Self {
Self {
auto_fill: false,
language_type: 1,
}
}
}
impl From<TypeOptionData> for TranslateTypeOption {
fn from(value: TypeOptionData) -> Self {
let auto_fill = value.get_bool_value("auto_fill").unwrap_or_default();
let language = value.get_i64_value("language").unwrap_or_default();
Self {
auto_fill,
language_type: language,
}
}
}
impl From<TranslateTypeOption> for TypeOptionData {
fn from(value: TranslateTypeOption) -> Self {
TypeOptionDataBuilder::new()
.insert_bool_value("auto_fill", value.auto_fill)
.insert_i64_value("language", value.language_type)
.build()
}
}
impl TypeOption for TranslateTypeOption {
type CellData = TranslateCellData;
type CellChangeset = String;
type CellProtobufType = ProtobufStr;
type CellFilter = TextFilterPB;
}
impl CellDataChangeset for TranslateTypeOption {
fn apply_changeset(
&self,
changeset: String,
_cell: Option<Cell>,
) -> FlowyResult<(Cell, TranslateCellData)> {
let cell_data = TranslateCellData(changeset);
Ok((cell_data.clone().into(), cell_data))
}
}
impl TypeOptionCellDataFilter for TranslateTypeOption {
fn apply_filter(
&self,
filter: &<Self as TypeOption>::CellFilter,
cell_data: &<Self as TypeOption>::CellData,
) -> bool {
filter.is_visible(cell_data)
}
}
impl TypeOptionCellDataCompare for TranslateTypeOption {
fn apply_cmp(
&self,
cell_data: &<Self as TypeOption>::CellData,
other_cell_data: &<Self as TypeOption>::CellData,
sort_condition: SortCondition,
) -> Ordering {
match (cell_data.is_cell_empty(), other_cell_data.is_cell_empty()) {
(true, true) => Ordering::Equal,
(true, false) => Ordering::Greater,
(false, true) => Ordering::Less,
(false, false) => {
let order = cell_data.0.cmp(&other_cell_data.0);
sort_condition.evaluate_order(order)
},
}
}
}
impl CellDataDecoder for TranslateTypeOption {
fn decode_cell(&self, cell: &Cell) -> FlowyResult<TranslateCellData> {
Ok(TranslateCellData::from(cell))
}
fn stringify_cell_data(&self, cell_data: TranslateCellData) -> String {
cell_data.to_string()
}
fn numeric_cell(&self, _cell: &Cell) -> Option<f64> {
None
}
}
impl TypeOptionTransform for TranslateTypeOption {}
impl TypeOptionCellDataSerde for TranslateTypeOption {
fn protobuf_encode(
&self,
cell_data: <Self as TypeOption>::CellData,
) -> <Self as TypeOption>::CellProtobufType {
ProtobufStr::from(cell_data.0)
}
fn parse_cell(&self, cell: &Cell) -> FlowyResult<<Self as TypeOption>::CellData> {
Ok(TranslateCellData::from(cell))
}
}

View File

@ -0,0 +1,46 @@
use crate::entities::FieldType;
use crate::services::field::{TypeOptionCellData, CELL_DATA};
use collab::core::any_map::AnyMapExtension;
use collab_database::rows::{new_cell_builder, Cell};
#[derive(Default, Debug, Clone)]
pub struct TranslateCellData(pub String);
impl std::ops::Deref for TranslateCellData {
type Target = String;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl TypeOptionCellData for TranslateCellData {
fn is_cell_empty(&self) -> bool {
self.0.is_empty()
}
}
impl From<&Cell> for TranslateCellData {
fn from(cell: &Cell) -> Self {
Self(cell.get_str_value(CELL_DATA).unwrap_or_default())
}
}
impl From<TranslateCellData> for Cell {
fn from(data: TranslateCellData) -> Self {
new_cell_builder(FieldType::Translate)
.insert_str_value(CELL_DATA, data.0)
.build()
}
}
impl ToString for TranslateCellData {
fn to_string(&self) -> String {
self.0.clone()
}
}
impl AsRef<str> for TranslateCellData {
fn as_ref(&self) -> &str {
&self.0
}
}

View File

@ -11,11 +11,13 @@ use flowy_error::FlowyResult;
use crate::entities::{
CheckboxTypeOptionPB, ChecklistTypeOptionPB, DateTypeOptionPB, FieldType,
MultiSelectTypeOptionPB, NumberTypeOptionPB, RelationTypeOptionPB, RichTextTypeOptionPB,
SingleSelectTypeOptionPB, SummarizationTypeOptionPB, TimestampTypeOptionPB, URLTypeOptionPB,
SingleSelectTypeOptionPB, SummarizationTypeOptionPB, TimestampTypeOptionPB,
TranslateTypeOptionPB, URLTypeOptionPB,
};
use crate::services::cell::CellDataDecoder;
use crate::services::field::checklist_type_option::ChecklistTypeOption;
use crate::services::field::summary_type_option::summary::SummarizationTypeOption;
use crate::services::field::translate_type_option::translate::TranslateTypeOption;
use crate::services::field::{
CheckboxTypeOption, DateTypeOption, MultiSelectTypeOption, NumberTypeOption, RelationTypeOption,
RichTextTypeOption, SingleSelectTypeOption, TimestampTypeOption, URLTypeOption,
@ -185,6 +187,9 @@ pub fn type_option_data_from_pb<T: Into<Bytes>>(
FieldType::Summary => {
SummarizationTypeOptionPB::try_from(bytes).map(|pb| SummarizationTypeOption::from(pb).into())
},
FieldType::Translate => {
TranslateTypeOptionPB::try_from(bytes).map(|pb| TranslateTypeOption::from(pb).into())
},
}
}
@ -252,6 +257,12 @@ pub fn type_option_to_pb(type_option: TypeOptionData, field_type: &FieldType) ->
.try_into()
.unwrap()
},
FieldType::Translate => {
let translate_type_option: TranslateTypeOption = type_option.into();
TranslateTypeOptionPB::from(translate_type_option)
.try_into()
.unwrap()
},
}
}
@ -272,5 +283,6 @@ pub fn default_type_option_data_from_type(field_type: FieldType) -> TypeOptionDa
FieldType::Checklist => ChecklistTypeOption.into(),
FieldType::Relation => RelationTypeOption::default().into(),
FieldType::Summary => SummarizationTypeOption::default().into(),
FieldType::Translate => TranslateTypeOption::default().into(),
}
}

View File

@ -11,6 +11,7 @@ use lib_infra::box_any::BoxAny;
use crate::entities::FieldType;
use crate::services::cell::{CellCache, CellDataChangeset, CellDataDecoder, CellProtobufBlob};
use crate::services::field::summary_type_option::summary::SummarizationTypeOption;
use crate::services::field::translate_type_option::translate::TranslateTypeOption;
use crate::services::field::{
CheckboxTypeOption, ChecklistTypeOption, DateTypeOption, MultiSelectTypeOption, NumberTypeOption,
RelationTypeOption, RichTextTypeOption, SingleSelectTypeOption, TimestampTypeOption, TypeOption,
@ -449,6 +450,16 @@ impl<'a> TypeOptionCellExt<'a> {
self.cell_data_cache.clone(),
)
}),
FieldType::Translate => self
.field
.get_type_option::<TranslateTypeOption>(field_type)
.map(|type_option| {
TypeOptionCellDataHandlerImpl::new_with_boxed(
type_option,
field_type,
self.cell_data_cache.clone(),
)
}),
}
}
@ -552,6 +563,9 @@ fn get_type_option_transform_handler(
},
FieldType::Summary => Box::new(SummarizationTypeOption::from(type_option_data))
as Box<dyn TypeOptionTransformHandler>,
FieldType::Translate => {
Box::new(TranslateTypeOption::from(type_option_data)) as Box<dyn TypeOptionTransformHandler>
},
}
}

View File

@ -281,6 +281,7 @@ impl FilterInner {
FieldType::Checkbox => BoxAny::new(CheckboxFilterPB::parse(condition as u8, content)),
FieldType::Relation => BoxAny::new(RelationFilterPB::parse(condition as u8, content)),
FieldType::Summary => BoxAny::new(TextFilterPB::parse(condition as u8, content)),
FieldType::Translate => BoxAny::new(TextFilterPB::parse(condition as u8, content)),
};
FilterInner::Data {
@ -367,6 +368,10 @@ impl<'a> From<&'a Filter> for FilterMap {
let filter = condition_and_content.cloned::<TextFilterPB>()?;
(filter.condition as u8, filter.content)
},
FieldType::Translate => {
let filter = condition_and_content.cloned::<TextFilterPB>()?;
(filter.condition as u8, filter.content)
},
};
Some((condition, content))
};