| // Copyright 2016 The Fuchsia Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "peridot/bin/suggestion_engine/suggestion_engine_impl.h" |
| |
| #include <string> |
| |
| #include <fuchsia/modular/cpp/fidl.h> |
| #include <lib/context/cpp/context_helper.h> |
| #include <lib/fidl/cpp/optional.h> |
| #include <lib/fsl/vmo/strings.h> |
| #include <lib/fxl/functional/make_copyable.h> |
| #include <lib/fxl/time/time_delta.h> |
| #include <lib/fxl/time/time_point.h> |
| #include "lib/fidl/cpp/clone.h" |
| #include "lib/fxl/random/uuid.h" |
| |
| #include "peridot/bin/suggestion_engine/decision_policies/rank_over_threshold_decision_policy.h" |
| #include "peridot/bin/suggestion_engine/filters/conjugate_ranked_passive_filter.h" |
| #include "peridot/bin/suggestion_engine/filters/ranked_active_filter.h" |
| #include "peridot/bin/suggestion_engine/filters/ranked_passive_filter.h" |
| #include "peridot/bin/suggestion_engine/rankers/linear_ranker.h" |
| #include "peridot/bin/suggestion_engine/ranking_features/affinity_ranking_feature.h" |
| #include "peridot/bin/suggestion_engine/ranking_features/annoyance_ranking_feature.h" |
| #include "peridot/bin/suggestion_engine/ranking_features/dead_story_ranking_feature.h" |
| #include "peridot/bin/suggestion_engine/ranking_features/interrupting_ranking_feature.h" |
| #include "peridot/bin/suggestion_engine/ranking_features/kronk_ranking_feature.h" |
| #include "peridot/bin/suggestion_engine/ranking_features/mod_pair_ranking_feature.h" |
| #include "peridot/bin/suggestion_engine/ranking_features/proposal_hint_ranking_feature.h" |
| #include "peridot/bin/suggestion_engine/ranking_features/query_match_ranking_feature.h" |
| #include "peridot/bin/suggestion_engine/ranking_features/ranking_feature.h" |
| #include "peridot/lib/fidl/json_xdr.h" |
| |
| namespace modular { |
| |
| SuggestionEngineImpl::SuggestionEngineImpl( |
| fuchsia::modular::ContextReaderPtr context_reader, |
| fuchsia::modular::PuppetMasterPtr puppet_master) |
| : debug_(std::make_shared<SuggestionDebugImpl>()), |
| next_processor_(debug_), |
| query_processor_(debug_), |
| context_reader_(std::move(context_reader)), |
| puppet_master_(std::move(puppet_master)), |
| context_listener_binding_(this) { |
| RegisterRankingFeatures(); |
| } |
| |
| SuggestionEngineImpl::~SuggestionEngineImpl() = default; |
| |
| fxl::WeakPtr<SuggestionDebugImpl> SuggestionEngineImpl::debug() { |
| return debug_->GetWeakPtr(); |
| } |
| |
| void SuggestionEngineImpl::AddNextProposal( |
| ProposalPublisherImpl* source, fuchsia::modular::Proposal proposal) { |
| if (proposal.wants_rich_suggestion) { |
| if (ComponentCanUseRichSuggestions(source->component_url())) { |
| AddProposalWithRichSuggestion(source, std::move(proposal)); |
| return; |
| } |
| FXL_LOG(INFO) << "Attempt to add rich suggestion for unallowed component " |
| << source->component_url(); |
| } |
| next_processor_.AddProposal(source->component_url(), std::move(proposal)); |
| } |
| |
| void SuggestionEngineImpl::ProposeNavigation( |
| const fuchsia::modular::NavigationAction navigation) { |
| navigation_processor_.Navigate(navigation); |
| } |
| |
| void SuggestionEngineImpl::AddProposalWithRichSuggestion( |
| ProposalPublisherImpl* source, fuchsia::modular::Proposal proposal) { |
| if (!proposal.story_name) { |
| // Puppet master will generate a story name on execution of the |
| // proposal actions. |
| proposal.story_name = ""; |
| } |
| |
| SuggestionPrototype* suggestion = |
| next_processor_.GetSuggestion(source->component_url(), proposal.id); |
| |
| // We keep track of the previous story since a new one will be created for a |
| // existing proposal. |
| // TODO(miguelfrde): this logic should probably belong in NextProcessor. We |
| // should also allow clients to reuse the story_name and mod_name to update |
| // the mod in the suggestion directly rather than creating a new one, however |
| // this is not working yet. |
| std::string existing_story; |
| if (suggestion && !suggestion->preloaded_story_id.empty()) { |
| existing_story = suggestion->preloaded_story_id; |
| } |
| |
| fuchsia::modular::StoryPuppetMasterPtr story_puppet_master; |
| puppet_master_->ControlStory(proposal.story_name, |
| story_puppet_master.NewRequest()); |
| fuchsia::modular::StoryOptions story_options; |
| story_options.kind_of_proto_story = true; |
| story_puppet_master->SetCreateOptions(std::move(story_options)); |
| |
| auto fut = modular::Future<fuchsia::modular::ExecuteResult>::Create( |
| "SuggestionEngine::AddProposalWithRichSuggestion.fut"); |
| story_puppet_master->Enqueue(std::move(proposal.on_selected)); |
| story_puppet_master->Execute(fut->Completer()); |
| fut->Then(fxl::MakeCopyable( |
| [this, fut, source_url = source->component_url(), |
| proposal = std::move(proposal), sp = std::move(story_puppet_master), |
| existing_story](fuchsia::modular::ExecuteResult result) mutable { |
| if (result.status != fuchsia::modular::ExecuteStatus::OK) { |
| FXL_LOG(WARNING) << "Preloading of rich suggestion actions resulted " |
| << "non successful status=" |
| << (uint32_t)result.status |
| << " message=" << result.error_message; |
| } |
| if (proposal.story_name->empty()) { |
| proposal.story_name = result.story_id; |
| } |
| |
| if (existing_story.empty()) { |
| next_processor_.AddProposal(source_url, result.story_id, |
| std::move(proposal)); |
| } |
| })); |
| } |
| |
| void SuggestionEngineImpl::RemoveNextProposal(const std::string& component_url, |
| const std::string& proposal_id) { |
| SuggestionPrototype* suggestion = |
| next_processor_.GetSuggestion(component_url, proposal_id); |
| if (suggestion && !suggestion->preloaded_story_id.empty()) { |
| auto story_name = suggestion->proposal.story_name; |
| puppet_master_->DeleteStory( |
| story_name, [this, story_name, component_url, proposal_id] { |
| next_processor_.RemoveProposal(component_url, proposal_id); |
| }); |
| } else { |
| next_processor_.RemoveProposal(component_url, proposal_id); |
| } |
| } |
| |
| void SuggestionEngineImpl::Connect( |
| fidl::InterfaceRequest<fuchsia::modular::SuggestionEngine> request) { |
| bindings_.AddBinding(this, std::move(request)); |
| } |
| |
| void SuggestionEngineImpl::Connect( |
| fidl::InterfaceRequest<fuchsia::modular::SuggestionProvider> request) { |
| suggestion_provider_bindings_.AddBinding(this, std::move(request)); |
| } |
| |
| void SuggestionEngineImpl::Connect( |
| fidl::InterfaceRequest<fuchsia::modular::SuggestionDebug> request) { |
| debug_bindings_.AddBinding(debug_.get(), std::move(request)); |
| } |
| |
| // |fuchsia::modular::SuggestionProvider| |
| void SuggestionEngineImpl::Query( |
| fidl::InterfaceHandle<fuchsia::modular::QueryListener> listener, |
| fuchsia::modular::UserInput input, int count) { |
| query_processor_.ExecuteQuery(std::move(input), count, std::move(listener)); |
| } |
| |
| // |fuchsia::modular::SuggestionProvider| |
| void SuggestionEngineImpl::SubscribeToInterruptions( |
| fidl::InterfaceHandle<fuchsia::modular::InterruptionListener> listener) { |
| next_processor_.RegisterInterruptionListener(std::move(listener)); |
| } |
| |
| // |fuchsia::modular::SuggestionProvider| |
| void SuggestionEngineImpl::SubscribeToNavigation( |
| fidl::InterfaceHandle<fuchsia::modular::NavigationListener> listener) { |
| navigation_processor_.RegisterListener(std::move(listener)); |
| } |
| |
| // |fuchsia::modular::SuggestionProvider| |
| void SuggestionEngineImpl::SubscribeToNext( |
| fidl::InterfaceHandle<fuchsia::modular::NextListener> listener, int count) { |
| next_processor_.RegisterListener(std::move(listener), count); |
| } |
| |
| // |fuchsia::modular::SuggestionProvider| |
| void SuggestionEngineImpl::NotifyInteraction( |
| fidl::StringPtr suggestion_uuid, |
| fuchsia::modular::Interaction interaction) { |
| // Find the suggestion |
| bool suggestion_in_ask = false; |
| RankedSuggestion* suggestion = next_processor_.GetSuggestion(suggestion_uuid); |
| if (!suggestion) { |
| suggestion = query_processor_.GetSuggestion(suggestion_uuid); |
| suggestion_in_ask = true; |
| } |
| |
| if (!suggestion) { |
| FXL_LOG(WARNING) << "Requested suggestion in notify interaction not found. " |
| << "UUID: " << suggestion_uuid; |
| return; |
| } |
| |
| // If it exists (and it should), perform the action and clean up |
| auto component_url = suggestion->prototype->source_url; |
| std::string log_detail = suggestion->prototype |
| ? short_proposal_str(*suggestion->prototype) |
| : "invalid"; |
| |
| FXL_LOG(INFO) << (interaction.type == |
| fuchsia::modular::InteractionType::SELECTED |
| ? "Accepted" |
| : "Dismissed") |
| << " suggestion " << suggestion_uuid << " (" << log_detail |
| << ")"; |
| |
| debug_->OnSuggestionSelected(suggestion->prototype); |
| |
| auto& proposal = suggestion->prototype->proposal; |
| auto proposal_id = proposal.id; |
| auto preloaded_story_id = suggestion->prototype->preloaded_story_id; |
| suggestion->interrupting = false; |
| |
| switch (interaction.type) { |
| case fuchsia::modular::InteractionType::SELECTED: { |
| HandleSelectedInteraction( |
| component_url, preloaded_story_id, proposal, |
| std::move(suggestion->prototype->bound_listener), suggestion_in_ask); |
| break; |
| } |
| case fuchsia::modular::InteractionType::DISMISSED: { |
| if (suggestion_in_ask) { |
| query_processor_.CleanUpPreviousQuery(); |
| } else { |
| RemoveNextProposal(component_url, proposal_id); |
| } |
| break; |
| } |
| case fuchsia::modular::InteractionType::EXPIRED: |
| case fuchsia::modular::InteractionType::SNOOZED: { |
| // No need to remove since it was either expired by a timeout in |
| // session shell or snoozed by the user, however we should still refresh |
| // the next processor (if not in ask) given that `interrupting=false` set |
| // above. |
| if (!suggestion_in_ask) { |
| next_processor_.UpdateRanking(); |
| } |
| break; |
| } |
| } |
| } |
| |
| // |fuchsia::modular::SuggestionEngine| |
| void SuggestionEngineImpl::RegisterProposalPublisher( |
| fidl::StringPtr url, |
| fidl::InterfaceRequest<fuchsia::modular::ProposalPublisher> publisher) { |
| // Check to see if a fuchsia::modular::ProposalPublisher has already been |
| // created for the component with this url. If not, create one. |
| std::unique_ptr<ProposalPublisherImpl>& source = proposal_publishers_[url]; |
| if (!source) { // create if it didn't already exist |
| source = std::make_unique<ProposalPublisherImpl>(this, url); |
| } |
| |
| source->AddBinding(std::move(publisher)); |
| } |
| |
| // |fuchsia::modular::SuggestionEngine| |
| void SuggestionEngineImpl::RegisterQueryHandler( |
| fidl::StringPtr url, fidl::InterfaceHandle<fuchsia::modular::QueryHandler> |
| query_handler_handle) { |
| query_processor_.RegisterQueryHandler(url, std::move(query_handler_handle)); |
| } |
| |
| // end fuchsia::modular::SuggestionEngine |
| |
| void SuggestionEngineImpl::RegisterRankingFeatures() { |
| // Create common ranking features |
| ranking_features["proposal_hint_rf"] = |
| std::make_shared<ProposalHintRankingFeature>(); |
| ranking_features["kronk_rf"] = std::make_shared<KronkRankingFeature>(); |
| ranking_features["mod_pairs_rf"] = std::make_shared<ModPairRankingFeature>(); |
| ranking_features["query_match_rf"] = |
| std::make_shared<QueryMatchRankingFeature>(); |
| ranking_features["affinity_rf"] = std::make_shared<AffinityRankingFeature>(); |
| ranking_features["annoyance_rf"] = |
| std::make_shared<AnnoyanceRankingFeature>(); |
| ranking_features["dead_story_rf"] = |
| std::make_shared<DeadStoryRankingFeature>(); |
| ranking_features["is_interrupting_rf"] = |
| std::make_shared<InterruptingRankingFeature>(); |
| |
| // Get context updates every time a story is focused to rerank suggestions |
| // based on the story that is focused at the moment. |
| fuchsia::modular::ContextQuery query; |
| for (auto const& it : ranking_features) { |
| fuchsia::modular::ContextSelectorPtr selector = |
| it.second->CreateContextSelector(); |
| if (selector) { |
| AddToContextQuery(&query, it.first, std::move(*selector)); |
| } |
| } |
| context_reader_->Subscribe(std::move(query), |
| context_listener_binding_.NewBinding()); |
| |
| // TODO(jwnichols): Replace the code configuration of the ranking features |
| // with a configuration file |
| |
| // Set up the next ranking features |
| auto next_ranker = std::make_unique<LinearRanker>(); |
| next_ranker->AddRankingFeature(1.0, ranking_features["proposal_hint_rf"]); |
| next_ranker->AddRankingFeature(-0.1, ranking_features["kronk_rf"]); |
| next_ranker->AddRankingFeature(0, ranking_features["mod_pairs_rf"]); |
| next_ranker->AddRankingFeature(1.0, ranking_features["affinity_rf"]); |
| next_processor_.SetRanker(std::move(next_ranker)); |
| |
| // Set up the query ranking features |
| auto query_ranker = std::make_unique<LinearRanker>(); |
| query_ranker->AddRankingFeature(1.0, ranking_features["proposal_hint_rf"]); |
| query_ranker->AddRankingFeature(-0.1, ranking_features["kronk_rf"]); |
| query_ranker->AddRankingFeature(0, ranking_features["mod_pairs_rf"]); |
| query_ranker->AddRankingFeature(0, ranking_features["query_match_rf"]); |
| query_processor_.SetRanker(std::move(query_ranker)); |
| |
| // Set up the interrupt ranking features |
| auto interrupt_ranker = std::make_unique<LinearRanker>(); |
| interrupt_ranker->AddRankingFeature(1.0, ranking_features["annoyance_rf"]); |
| auto decision_policy = std::make_unique<RankOverThresholdDecisionPolicy>( |
| std::move(interrupt_ranker)); |
| next_processor_.SetInterruptionDecisionPolicy(std::move(decision_policy)); |
| |
| // Set up passive filters |
| std::vector<std::unique_ptr<SuggestionPassiveFilter>> passive_filters; |
| passive_filters.push_back(std::make_unique<ConjugateRankedPassiveFilter>( |
| ranking_features["affinity_rf"])); |
| passive_filters.push_back(std::make_unique<RankedPassiveFilter>( |
| ranking_features["is_interrupting_rf"])); |
| next_processor_.SetPassiveFilters(std::move(passive_filters)); |
| } |
| |
| void SuggestionEngineImpl::OnContextUpdate( |
| fuchsia::modular::ContextUpdate update) { |
| for (auto& entry : update.values.take()) { |
| for (const auto& rf_it : ranking_features) { |
| if (entry.key == rf_it.first) { // Update key == rf key |
| rf_it.second->UpdateContext(entry.value); |
| } |
| } |
| } |
| next_processor_.UpdateRanking(); |
| } |
| |
| bool SuggestionEngineImpl::ComponentCanUseRichSuggestions( |
| const std::string& component_url) { |
| // Only kronk is allowed to preload stories in suggestions to make |
| // rich suggestions. |
| // Proposinator is used for testing. |
| return component_url.find("kronk") != std::string::npos || |
| component_url.find("krohnkite") != std::string::npos || |
| component_url.find("Proposinator") != std::string::npos; |
| } |
| |
| void SuggestionEngineImpl::HandleSelectedInteraction( |
| const std::string& component_url, const std::string& preloaded_story_id, |
| fuchsia::modular::Proposal& proposal, |
| fuchsia::modular::ProposalListenerPtr listener, bool suggestion_in_ask) { |
| // Rich suggestions are only in Next, so we don't check suggestion_in_ask. |
| if (!preloaded_story_id.empty()) { |
| if (listener) { |
| listener->OnProposalAccepted(proposal.id, preloaded_story_id); |
| } |
| // TODO(miguelfrde): eventually we should promote stories here. For now rich |
| // suggestions aren't removed or promoted. |
| return; |
| } |
| |
| if (!proposal.story_name) { |
| // Puppet master will generate a story name. |
| proposal.story_name = ""; |
| } |
| fuchsia::modular::StoryPuppetMasterPtr story_puppet_master; |
| puppet_master_->ControlStory(proposal.story_name, |
| story_puppet_master.NewRequest()); |
| auto fut = modular::Future<fuchsia::modular::ExecuteResult>::Create( |
| "SuggestionEngine::HandleSelectedInteraction.fut"); |
| // TODO(miguelfred): break up |commands| if it is too large of a list for one |
| // FIDL message. |
| story_puppet_master->Enqueue(std::move(proposal.on_selected)); |
| story_puppet_master->Execute(fut->Completer()); |
| fut->Then(fxl::MakeCopyable( |
| [this, proposal_id = proposal.id, suggestion_in_ask, component_url, |
| listener = std::move(listener), sp = std::move(story_puppet_master), |
| fut](fuchsia::modular::ExecuteResult result) mutable { |
| // TODO(miguelfrde): check status. |
| if (listener) { |
| listener->OnProposalAccepted(proposal_id, result.story_id); |
| } |
| if (suggestion_in_ask) { |
| query_processor_.CleanUpPreviousQuery(); |
| } else { |
| next_processor_.RemoveProposal(component_url, proposal_id); |
| } |
| })); |
| } |
| |
| } // namespace modular |