1 /*
2 * Copyright (C) 2018 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17 #include <jni.h>
18
19 #include <memory>
20 #include <vector>
21
22 #include "utils/flatbuffers/mutable.h"
23 #include "utils/intents/intent-generator.h"
24 #include "utils/intents/remote-action-template.h"
25 #include "utils/java/jni-helper.h"
26 #include "utils/jvm-test-utils.h"
27 #include "utils/resources_generated.h"
28 #include "utils/testing/logging_event_listener.h"
29 #include "utils/variant.h"
30 #include "gmock/gmock.h"
31 #include "gtest/gtest.h"
32 #include "flatbuffers/reflection.h"
33
34 namespace libtextclassifier3 {
35 namespace {
36
37 using ::testing::ElementsAre;
38 using ::testing::IsEmpty;
39 using ::testing::SizeIs;
40
BuildTestIntentFactoryModel(const std::string & entity_type,const std::string & generator_code)41 flatbuffers::DetachedBuffer BuildTestIntentFactoryModel(
42 const std::string& entity_type, const std::string& generator_code) {
43 // Test intent generation options.
44 IntentFactoryModelT options;
45 options.generator.emplace_back(new IntentFactoryModel_::IntentGeneratorT());
46 options.generator.back()->type = entity_type;
47 options.generator.back()->lua_template_generator = std::vector<unsigned char>(
48 generator_code.data(), generator_code.data() + generator_code.size());
49 flatbuffers::FlatBufferBuilder builder;
50 builder.Finish(IntentFactoryModel::Pack(builder, &options));
51 return builder.Release();
52 }
53
BuildTestResources()54 flatbuffers::DetachedBuffer BuildTestResources() {
55 // Custom string resources.
56 ResourcePoolT test_resources;
57 test_resources.locale.emplace_back(new LanguageTagT);
58 test_resources.locale.back()->language = "en";
59 test_resources.locale.emplace_back(new LanguageTagT);
60 test_resources.locale.back()->language = "de";
61
62 // Add `add_calendar_event`
63 test_resources.resource_entry.emplace_back(new ResourceEntryT);
64 test_resources.resource_entry.back()->name = "add_calendar_event";
65
66 // en
67 test_resources.resource_entry.back()->resource.emplace_back(new ResourceT);
68 test_resources.resource_entry.back()->resource.back()->content = "Schedule";
69 test_resources.resource_entry.back()->resource.back()->locale.push_back(0);
70
71 // Add `add_calendar_event_desc`
72 test_resources.resource_entry.emplace_back(new ResourceEntryT);
73 test_resources.resource_entry.back()->name = "add_calendar_event_desc";
74
75 // en
76 test_resources.resource_entry.back()->resource.emplace_back(new ResourceT);
77 test_resources.resource_entry.back()->resource.back()->content =
78 "Schedule event for selected time";
79 test_resources.resource_entry.back()->resource.back()->locale.push_back(0);
80
81 // Add `map`.
82 test_resources.resource_entry.emplace_back(new ResourceEntryT);
83 test_resources.resource_entry.back()->name = "map";
84
85 // en
86 test_resources.resource_entry.back()->resource.emplace_back(new ResourceT);
87 test_resources.resource_entry.back()->resource.back()->content = "Map";
88 test_resources.resource_entry.back()->resource.back()->locale.push_back(0);
89
90 // de
91 test_resources.resource_entry.back()->resource.emplace_back(new ResourceT);
92 test_resources.resource_entry.back()->resource.back()->content = "Karte";
93 test_resources.resource_entry.back()->resource.back()->locale.push_back(1);
94
95 // Add `map_desc`.
96 test_resources.resource_entry.emplace_back(new ResourceEntryT);
97 test_resources.resource_entry.back()->name = "map_desc";
98
99 // en
100 test_resources.resource_entry.back()->resource.emplace_back(new ResourceT);
101 test_resources.resource_entry.back()->resource.back()->content =
102 "Locate selected address";
103 test_resources.resource_entry.back()->resource.back()->locale.push_back(0);
104
105 // de
106 test_resources.resource_entry.back()->resource.emplace_back(new ResourceT);
107 test_resources.resource_entry.back()->resource.back()->content =
108 "Ausgewählte Adresse finden";
109 test_resources.resource_entry.back()->resource.back()->locale.push_back(1);
110
111 flatbuffers::FlatBufferBuilder builder;
112 builder.Finish(ResourcePool::Pack(builder, &test_resources));
113 return builder.Release();
114 }
115
116 // Common methods for intent generator tests.
117 class IntentGeneratorTest : public testing::Test {
118 protected:
IntentGeneratorTest()119 explicit IntentGeneratorTest()
120 : jni_cache_(JniCache::Create(GetJenv())),
121 resource_buffer_(BuildTestResources()),
122 resources_(
123 flatbuffers::GetRoot<ResourcePool>(resource_buffer_.data())) {}
124
125 const std::shared_ptr<JniCache> jni_cache_;
126 const flatbuffers::DetachedBuffer resource_buffer_;
127 const ResourcePool* resources_;
128 };
129
TEST_F(IntentGeneratorTest,HandlesDefaultClassification)130 TEST_F(IntentGeneratorTest, HandlesDefaultClassification) {
131 flatbuffers::DetachedBuffer intent_factory_model =
132 BuildTestIntentFactoryModel("unused", "");
133 std::unique_ptr<IntentGenerator> generator = IntentGenerator::Create(
134 /*options=*/flatbuffers::GetRoot<IntentFactoryModel>(
135 intent_factory_model.data()),
136 /*resources=*/resources_,
137 /*jni_cache=*/jni_cache_);
138 ClassificationResult classification;
139 std::vector<RemoteActionTemplate> intents;
140 EXPECT_TRUE(generator->GenerateIntents(
141 /*device_locales=*/nullptr, classification, /*reference_time_ms_utc=*/0,
142 /*text=*/"", /*selection_indices=*/{kInvalidIndex, kInvalidIndex},
143 /*context=*/nullptr,
144 /*annotations_entity_data_schema=*/nullptr,
145 /*enable_add_contact_intent=*/false, /*enable_search_intent=*/false,
146 &intents));
147 EXPECT_THAT(intents, IsEmpty());
148 }
149
TEST_F(IntentGeneratorTest,FailsGracefully)150 TEST_F(IntentGeneratorTest, FailsGracefully) {
151 flatbuffers::DetachedBuffer intent_factory_model =
152 BuildTestIntentFactoryModel("test", R"lua(
153 return {
154 {
155 -- Should fail, as no app GetAndroidContext() is provided.
156 data = external.android.package_name,
157 }
158 })lua");
159 std::unique_ptr<IntentGenerator> generator = IntentGenerator::Create(
160 flatbuffers::GetRoot<IntentFactoryModel>(intent_factory_model.data()),
161 /*resources=*/resources_, jni_cache_);
162 ClassificationResult classification = {"test", 1.0};
163 std::vector<RemoteActionTemplate> intents;
164 EXPECT_FALSE(generator->GenerateIntents(
165 JniHelper::NewStringUTF(GetJenv(), "en-US").ValueOrDie().get(),
166 classification,
167 /*reference_time_ms_utc=*/0, "test", {0, 4}, /*context=*/nullptr,
168 /*annotations_entity_data_schema=*/nullptr,
169 /*enable_add_contact_intent=*/false, /*enable_search_intent=*/false,
170 &intents));
171 EXPECT_THAT(intents, IsEmpty());
172 }
173
TEST_F(IntentGeneratorTest,HandlesEntityIntentGeneration)174 TEST_F(IntentGeneratorTest, HandlesEntityIntentGeneration) {
175 flatbuffers::DetachedBuffer intent_factory_model =
176 BuildTestIntentFactoryModel("address", R"lua(
177 return {
178 {
179 title_without_entity = external.android.R.map,
180 title_with_entity = external.entity.text,
181 description = external.android.R.map_desc,
182 action = "android.intent.action.VIEW",
183 data = "geo:0,0?q=" ..
184 external.android.urlencode(external.entity.text),
185 }
186 })lua");
187 std::unique_ptr<IntentGenerator> generator = IntentGenerator::Create(
188 flatbuffers::GetRoot<IntentFactoryModel>(intent_factory_model.data()),
189 /*resources=*/resources_, jni_cache_);
190 ClassificationResult classification = {"address", 1.0};
191 std::vector<RemoteActionTemplate> intents;
192 EXPECT_TRUE(generator->GenerateIntents(
193 JniHelper::NewStringUTF(GetJenv(), "en-US").ValueOrDie().get(),
194 classification,
195 /*reference_time_ms_utc=*/0, "333 E Wonderview Ave", {0, 20},
196 GetAndroidContext(),
197 /*annotations_entity_data_schema=*/nullptr,
198 /*enable_add_contact_intent=*/false, /*enable_search_intent=*/false,
199 &intents));
200 EXPECT_THAT(intents, SizeIs(1));
201 EXPECT_EQ(intents[0].title_without_entity.value(), "Map");
202 EXPECT_EQ(intents[0].title_with_entity.value(), "333 E Wonderview Ave");
203 EXPECT_EQ(intents[0].description.value(), "Locate selected address");
204 EXPECT_EQ(intents[0].action.value(), "android.intent.action.VIEW");
205 EXPECT_EQ(intents[0].data.value(), "geo:0,0?q=333%20E%20Wonderview%20Ave");
206 }
207
TEST_F(IntentGeneratorTest,HandlesAddContactIntentEnabledGeneration)208 TEST_F(IntentGeneratorTest, HandlesAddContactIntentEnabledGeneration) {
209 flatbuffers::DetachedBuffer intent_factory_model =
210 BuildTestIntentFactoryModel("address", R"lua(
211 if external.enable_add_contact_intent then return {
212 {
213 title_without_entity = external.android.R.map,
214 title_with_entity = external.entity.text,
215 description = external.android.R.map_desc,
216 action = "android.intent.action.VIEW",
217 data = "geo:0,0?q=" ..
218 external.android.urlencode(external.entity.text),
219 }
220 } else return {} end)lua");
221 std::unique_ptr<IntentGenerator> generator = IntentGenerator::Create(
222 flatbuffers::GetRoot<IntentFactoryModel>(intent_factory_model.data()),
223 /*resources=*/resources_, jni_cache_);
224 ClassificationResult classification = {"address", 1.0};
225 std::vector<RemoteActionTemplate> intents;
226 EXPECT_TRUE(generator->GenerateIntents(
227 JniHelper::NewStringUTF(GetJenv(), "en-US").ValueOrDie().get(),
228 classification,
229 /*reference_time_ms_utc=*/0, "333 E Wonderview Ave", {0, 20},
230 GetAndroidContext(),
231 /*annotations_entity_data_schema=*/nullptr,
232 /*enable_add_contact_intent=*/true, /*enable_search_intent=*/false,
233 &intents));
234 EXPECT_THAT(intents, SizeIs(1));
235 EXPECT_EQ(intents[0].title_without_entity.value(), "Map");
236 }
237
TEST_F(IntentGeneratorTest,HandlesAddContactIntentDisabledGeneration)238 TEST_F(IntentGeneratorTest, HandlesAddContactIntentDisabledGeneration) {
239 flatbuffers::DetachedBuffer intent_factory_model =
240 BuildTestIntentFactoryModel("address", R"lua(
241 if external.enable_add_contact_intent then return {
242 {
243 title_without_entity = external.android.R.map,
244 title_with_entity = external.entity.text,
245 description = external.android.R.map_desc,
246 action = "android.intent.action.VIEW",
247 data = "geo:0,0?q=" ..
248 external.android.urlencode(external.entity.text),
249 }
250 } else return {} end)lua");
251 std::unique_ptr<IntentGenerator> generator = IntentGenerator::Create(
252 flatbuffers::GetRoot<IntentFactoryModel>(intent_factory_model.data()),
253 /*resources=*/resources_, jni_cache_);
254 ClassificationResult classification = {"address", 1.0};
255 std::vector<RemoteActionTemplate> intents;
256 EXPECT_TRUE(generator->GenerateIntents(
257 JniHelper::NewStringUTF(GetJenv(), "en-US").ValueOrDie().get(),
258 classification,
259 /*reference_time_ms_utc=*/0, "333 E Wonderview Ave", {0, 20},
260 GetAndroidContext(),
261 /*annotations_entity_data_schema=*/nullptr,
262 /*enable_add_contact_intent=*/false, /*enable_search_intent=*/false,
263 &intents));
264 EXPECT_THAT(intents, SizeIs(0));
265 }
266
TEST_F(IntentGeneratorTest,HandlesAddSearchIntentEnabledGeneration)267 TEST_F(IntentGeneratorTest, HandlesAddSearchIntentEnabledGeneration) {
268 flatbuffers::DetachedBuffer intent_factory_model =
269 BuildTestIntentFactoryModel("address", R"lua(
270 if external.enable_search_intent then return {
271 {
272 title_without_entity = external.android.R.map,
273 title_with_entity = external.entity.text,
274 description = external.android.R.map_desc,
275 action = "android.intent.action.VIEW",
276 data = "geo:0,0?q=" ..
277 external.android.urlencode(external.entity.text),
278 }
279 } else return {} end)lua");
280 std::unique_ptr<IntentGenerator> generator = IntentGenerator::Create(
281 flatbuffers::GetRoot<IntentFactoryModel>(intent_factory_model.data()),
282 /*resources=*/resources_, jni_cache_);
283 ClassificationResult classification = {"address", 1.0};
284 std::vector<RemoteActionTemplate> intents;
285 EXPECT_TRUE(generator->GenerateIntents(
286 JniHelper::NewStringUTF(GetJenv(), "en-US").ValueOrDie().get(),
287 classification,
288 /*reference_time_ms_utc=*/0, "333 E Wonderview Ave", {0, 20},
289 GetAndroidContext(),
290 /*annotations_entity_data_schema=*/nullptr,
291 /*enable_add_contact_intent=*/false, /*enable_search_intent=*/true,
292 &intents));
293 EXPECT_THAT(intents, SizeIs(1));
294 EXPECT_EQ(intents[0].title_without_entity.value(), "Map");
295 }
296
TEST_F(IntentGeneratorTest,HandlesSearchIntentDisabledGeneration)297 TEST_F(IntentGeneratorTest, HandlesSearchIntentDisabledGeneration) {
298 flatbuffers::DetachedBuffer intent_factory_model =
299 BuildTestIntentFactoryModel("address", R"lua(
300 if external.enable_search_intent then return {
301 {
302 title_without_entity = external.android.R.map,
303 title_with_entity = external.entity.text,
304 description = external.android.R.map_desc,
305 action = "android.intent.action.VIEW",
306 data = "geo:0,0?q=" ..
307 external.android.urlencode(external.entity.text),
308 }
309 } else return {} end)lua");
310 std::unique_ptr<IntentGenerator> generator = IntentGenerator::Create(
311 flatbuffers::GetRoot<IntentFactoryModel>(intent_factory_model.data()),
312 /*resources=*/resources_, jni_cache_);
313 ClassificationResult classification = {"address", 1.0};
314 std::vector<RemoteActionTemplate> intents;
315 EXPECT_TRUE(generator->GenerateIntents(
316 JniHelper::NewStringUTF(GetJenv(), "en-US").ValueOrDie().get(),
317 classification,
318 /*reference_time_ms_utc=*/0, "333 E Wonderview Ave", {0, 20},
319 GetAndroidContext(),
320 /*annotations_entity_data_schema=*/nullptr,
321 /*enable_add_contact_intent=*/false, /*enable_search_intent=*/false,
322 &intents));
323 EXPECT_THAT(intents, SizeIs(0));
324 }
325
TEST_F(IntentGeneratorTest,HandlesCallbacks)326 TEST_F(IntentGeneratorTest, HandlesCallbacks) {
327 flatbuffers::DetachedBuffer intent_factory_model =
328 BuildTestIntentFactoryModel("test", R"lua(
329 local test = external.entity["text"]
330 return {
331 {
332 data = "encoded=" .. external.android.urlencode(test),
333 category = { "test_category" },
334 extra = {
335 { name = "package", string_value = external.android.package_name},
336 { name = "scheme",
337 string_value = external.android.url_schema("https://google.com")},
338 { name = "host",
339 string_value = external.android.url_host("https://google.com/search")},
340 { name = "permission",
341 bool_value = external.android.user_restrictions["no_sms"] },
342 { name = "language",
343 string_value = external.android.device_locales[1].language },
344 { name = "description",
345 string_value = external.format("$1 $0", "hello", "world") },
346 },
347 request_code = external.hash(test)
348 }
349 })lua");
350 std::unique_ptr<IntentGenerator> generator = IntentGenerator::Create(
351 flatbuffers::GetRoot<IntentFactoryModel>(intent_factory_model.data()),
352 /*resources=*/resources_, jni_cache_);
353 ClassificationResult classification = {"test", 1.0};
354 std::vector<RemoteActionTemplate> intents;
355 EXPECT_TRUE(generator->GenerateIntents(
356 JniHelper::NewStringUTF(GetJenv(), "en-US").ValueOrDie().get(),
357 classification,
358 /*reference_time_ms_utc=*/0, "this is a test", {0, 14},
359 GetAndroidContext(),
360 /*annotations_entity_data_schema=*/nullptr,
361 /*enable_add_contact_intent=*/false, /*enable_search_intent=*/false,
362 &intents));
363 EXPECT_THAT(intents, SizeIs(1));
364 EXPECT_EQ(intents[0].data.value(), "encoded=this%20is%20a%20test");
365 EXPECT_THAT(intents[0].category, ElementsAre("test_category"));
366 EXPECT_THAT(intents[0].extra, SizeIs(6));
367 EXPECT_EQ(intents[0].extra["package"].ConstRefValue<std::string>(),
368 "com.google.android.textclassifier.tests"
369 );
370 EXPECT_EQ(intents[0].extra["scheme"].ConstRefValue<std::string>(), "https");
371 EXPECT_EQ(intents[0].extra["host"].ConstRefValue<std::string>(),
372 "google.com");
373 EXPECT_FALSE(intents[0].extra["permission"].Value<bool>());
374 EXPECT_EQ(intents[0].extra["language"].ConstRefValue<std::string>(), "en");
375 EXPECT_TRUE(intents[0].request_code.has_value());
376 EXPECT_EQ(intents[0].extra["description"].ConstRefValue<std::string>(),
377 "world hello");
378 }
379
TEST_F(IntentGeneratorTest,HandlesActionIntentGeneration)380 TEST_F(IntentGeneratorTest, HandlesActionIntentGeneration) {
381 flatbuffers::DetachedBuffer intent_factory_model =
382 BuildTestIntentFactoryModel("view_map", R"lua(
383 return {
384 {
385 title_without_entity = external.android.R.map,
386 description = external.android.R.map_desc,
387 description_with_app_name = external.android.R.map,
388 action = "android.intent.action.VIEW",
389 data = "geo:0,0?q=" ..
390 external.android.urlencode(external.entity.annotation["location"].text),
391 }
392 })lua");
393 std::unique_ptr<IntentGenerator> generator = IntentGenerator::Create(
394 flatbuffers::GetRoot<IntentFactoryModel>(intent_factory_model.data()),
395 /*resources=*/resources_, jni_cache_);
396 Conversation conversation = {{{/*user_id=*/1, "hello there"}}};
397 ActionSuggestionAnnotation annotation;
398 annotation.entity = {"address", 1.0};
399 annotation.span = {/*message_index=*/0,
400 /*span=*/{6, 11},
401 /*text=*/"there"};
402 annotation.name = "location";
403 ActionSuggestion suggestion = {/*response_text=""*/ "",
404 /*type=*/"view_map",
405 /*score=*/1.0,
406 /*priority_score=*/0.0,
407 /*annotations=*/
408 {annotation}};
409 std::vector<RemoteActionTemplate> intents;
410 EXPECT_TRUE(generator->GenerateIntents(
411 JniHelper::NewStringUTF(GetJenv(), "en-US").ValueOrDie().get(),
412 suggestion, conversation, GetAndroidContext(),
413 /*annotations_entity_data_schema=*/nullptr,
414 /*actions_entity_data_schema=*/nullptr, &intents));
415 EXPECT_THAT(intents, SizeIs(1));
416 EXPECT_EQ(intents[0].title_without_entity.value(), "Map");
417 EXPECT_EQ(intents[0].description.value(), "Locate selected address");
418 EXPECT_EQ(intents[0].description_with_app_name.value(), "Map");
419 EXPECT_EQ(intents[0].action.value(), "android.intent.action.VIEW");
420 EXPECT_EQ(intents[0].data.value(), "geo:0,0?q=there");
421 }
422
TEST_F(IntentGeneratorTest,HandlesTimezoneAndReferenceTime)423 TEST_F(IntentGeneratorTest, HandlesTimezoneAndReferenceTime) {
424 flatbuffers::DetachedBuffer intent_factory_model =
425 BuildTestIntentFactoryModel("test", R"lua(
426 local conversation = external.conversation
427 return {
428 {
429 extra = {
430 { name = "timezone", string_value = conversation[#conversation].timezone },
431 { name = "num_messages", int_value = #conversation },
432 { name = "reference_time", long_value = conversation[#conversation].time_ms_utc }
433 },
434 }
435 })lua");
436 std::unique_ptr<IntentGenerator> generator = IntentGenerator::Create(
437 flatbuffers::GetRoot<IntentFactoryModel>(intent_factory_model.data()),
438 /*resources=*/resources_, jni_cache_);
439 Conversation conversation = {
440 {{/*user_id=*/0, "hello there", /*reference_time_ms_utc=*/0,
441 /*reference_timezone=*/"Testing/Test"},
442 {/*user_id=*/1, "general retesti", /*reference_time_ms_utc=*/1000,
443 /*reference_timezone=*/"Europe/Zurich"}}};
444 ActionSuggestion suggestion = {/*response_text=""*/ "",
445 /*type=*/"test",
446 /*score=*/1.0,
447 /*priority_score=*/0.0,
448 /*annotations=*/
449 {}};
450 std::vector<RemoteActionTemplate> intents;
451 EXPECT_TRUE(generator->GenerateIntents(
452 JniHelper::NewStringUTF(GetJenv(), "en-US").ValueOrDie().get(),
453 suggestion, conversation, GetAndroidContext(),
454 /*annotations_entity_data_schema=*/nullptr,
455 /*actions_entity_data_schema=*/nullptr, &intents));
456 EXPECT_THAT(intents, SizeIs(1));
457 EXPECT_EQ(intents[0].extra["timezone"].ConstRefValue<std::string>(),
458 "Europe/Zurich");
459 EXPECT_EQ(intents[0].extra["num_messages"].Value<int>(), 2);
460 EXPECT_EQ(intents[0].extra["reference_time"].Value<int64>(), 1000);
461 }
462
TEST_F(IntentGeneratorTest,HandlesActionIntentGenerationMultipleAnnotations)463 TEST_F(IntentGeneratorTest, HandlesActionIntentGenerationMultipleAnnotations) {
464 flatbuffers::DetachedBuffer intent_factory_model =
465 BuildTestIntentFactoryModel("create_event", R"lua(
466 return {
467 {
468 title_without_entity = external.android.R.add_calendar_event,
469 description = external.android.R.add_calendar_event_desc,
470 extra = {
471 {name = "time", string_value =
472 external.entity.annotation["time"].text},
473 {name = "location",
474 string_value = external.entity.annotation["location"].text},
475 }
476 }
477 })lua");
478 std::unique_ptr<IntentGenerator> generator = IntentGenerator::Create(
479 flatbuffers::GetRoot<IntentFactoryModel>(intent_factory_model.data()),
480 /*resources=*/resources_, jni_cache_);
481 Conversation conversation = {{{/*user_id=*/1, "hello there at 1pm"}}};
482 ActionSuggestionAnnotation location_annotation, time_annotation;
483 location_annotation.entity = {"address", 1.0};
484 location_annotation.span = {/*message_index=*/0,
485 /*span=*/{6, 11},
486 /*text=*/"there"};
487 location_annotation.name = "location";
488 time_annotation.entity = {"datetime", 1.0};
489 time_annotation.span = {/*message_index=*/0,
490 /*span=*/{15, 18},
491 /*text=*/"1pm"};
492 time_annotation.name = "time";
493 ActionSuggestion suggestion = {/*response_text=""*/ "",
494 /*type=*/"create_event",
495 /*score=*/1.0,
496 /*priority_score=*/0.0,
497 /*annotations=*/
498 {location_annotation, time_annotation}};
499 std::vector<RemoteActionTemplate> intents;
500 EXPECT_TRUE(generator->GenerateIntents(
501 JniHelper::NewStringUTF(GetJenv(), "en-US").ValueOrDie().get(),
502 suggestion, conversation, GetAndroidContext(),
503 /*annotations_entity_data_schema=*/nullptr,
504 /*actions_entity_data_schema=*/nullptr, &intents));
505 EXPECT_THAT(intents, SizeIs(1));
506 EXPECT_EQ(intents[0].title_without_entity.value(), "Schedule");
507 EXPECT_THAT(intents[0].extra, SizeIs(2));
508 EXPECT_EQ(intents[0].extra["location"].ConstRefValue<std::string>(), "there");
509 EXPECT_EQ(intents[0].extra["time"].ConstRefValue<std::string>(), "1pm");
510 }
511
TEST_F(IntentGeneratorTest,HandlesActionIntentGenerationMultipleAnnotationsWithIndices)512 TEST_F(IntentGeneratorTest,
513 HandlesActionIntentGenerationMultipleAnnotationsWithIndices) {
514 flatbuffers::DetachedBuffer intent_factory_model =
515 BuildTestIntentFactoryModel("time_range", R"lua(
516 return {
517 {
518 title_without_entity = "test",
519 description = "test",
520 extra = {
521 {name = "from", string_value = external.entity.annotation[1].text},
522 {name = "to", string_value = external.entity.annotation[2].text},
523 }
524 }
525 })lua");
526 std::unique_ptr<IntentGenerator> generator = IntentGenerator::Create(
527 flatbuffers::GetRoot<IntentFactoryModel>(intent_factory_model.data()),
528 /*resources=*/resources_, jni_cache_);
529 Conversation conversation = {{{/*user_id=*/1, "from 1pm to 2pm"}}};
530 ActionSuggestionAnnotation from_annotation, to_annotation;
531 from_annotation.entity = {"datetime", 1.0};
532 from_annotation.span = {/*message_index=*/0,
533 /*span=*/{5, 8},
534 /*text=*/"1pm"};
535 to_annotation.entity = {"datetime", 1.0};
536 to_annotation.span = {/*message_index=*/0,
537 /*span=*/{12, 15},
538 /*text=*/"2pm"};
539 ActionSuggestion suggestion = {/*response_text=""*/ "",
540 /*type=*/"time_range",
541 /*score=*/1.0,
542 /*priority_score=*/0.0,
543 /*annotations=*/
544 {from_annotation, to_annotation}};
545 std::vector<RemoteActionTemplate> intents;
546 EXPECT_TRUE(generator->GenerateIntents(
547 JniHelper::NewStringUTF(GetJenv(), "en-US").ValueOrDie().get(),
548 suggestion, conversation, GetAndroidContext(),
549 /*annotations_entity_data_schema=*/nullptr,
550 /*actions_entity_data_schema=*/nullptr, &intents));
551 EXPECT_THAT(intents, SizeIs(1));
552 EXPECT_THAT(intents[0].extra, SizeIs(2));
553 EXPECT_EQ(intents[0].extra["from"].ConstRefValue<std::string>(), "1pm");
554 EXPECT_EQ(intents[0].extra["to"].ConstRefValue<std::string>(), "2pm");
555 }
556
TEST_F(IntentGeneratorTest,HandlesResources)557 TEST_F(IntentGeneratorTest, HandlesResources) {
558 flatbuffers::DetachedBuffer intent_factory_model =
559 BuildTestIntentFactoryModel("address", R"lua(
560 return {
561 {
562 title_without_entity = external.android.R.map,
563 description = external.android.R.map_desc,
564 action = "android.intent.action.VIEW",
565 data = "geo:0,0?q=" ..
566 external.android.urlencode(external.entity.text),
567 }
568 })lua");
569 std::unique_ptr<IntentGenerator> generator = IntentGenerator::Create(
570 flatbuffers::GetRoot<IntentFactoryModel>(intent_factory_model.data()),
571 resources_, jni_cache_);
572 ClassificationResult classification = {"address", 1.0};
573 std::vector<RemoteActionTemplate> intents;
574 EXPECT_TRUE(generator->GenerateIntents(
575 JniHelper::NewStringUTF(GetJenv(), "de-DE").ValueOrDie().get(),
576 classification,
577 /*reference_time_ms_utc=*/0, "333 E Wonderview Ave", {0, 20},
578 GetAndroidContext(),
579 /*annotations_entity_data_schema=*/nullptr,
580 /*enable_add_contact_intent=*/false, /*enable_search_intent=*/false,
581 &intents));
582 EXPECT_THAT(intents, SizeIs(1));
583 EXPECT_EQ(intents[0].title_without_entity.value(), "Karte");
584 EXPECT_EQ(intents[0].description.value(), "Ausgewählte Adresse finden");
585 EXPECT_EQ(intents[0].action.value(), "android.intent.action.VIEW");
586 EXPECT_EQ(intents[0].data.value(), "geo:0,0?q=333%20E%20Wonderview%20Ave");
587 }
588
TEST_F(IntentGeneratorTest,HandlesIteration)589 TEST_F(IntentGeneratorTest, HandlesIteration) {
590 flatbuffers::DetachedBuffer intent_factory_model =
591 BuildTestIntentFactoryModel("iteration_test", R"lua(
592 local extra = {{ name = "length", int_value = #external.entity.annotation }}
593 for annotation_id, annotation in pairs(external.entity.annotation) do
594 table.insert(extra,
595 { name = annotation.name,
596 string_value = annotation.text })
597 end
598 return {{ extra = extra }})lua");
599 std::unique_ptr<IntentGenerator> generator = IntentGenerator::Create(
600 flatbuffers::GetRoot<IntentFactoryModel>(intent_factory_model.data()),
601 /*resources=*/resources_, jni_cache_);
602 Conversation conversation = {{{/*user_id=*/1, "hello there"}}};
603 ActionSuggestionAnnotation location_annotation;
604 location_annotation.entity = {"address", 1.0};
605 location_annotation.span = {/*message_index=*/0,
606 /*span=*/{6, 11},
607 /*text=*/"there"};
608 location_annotation.name = "location";
609 ActionSuggestionAnnotation greeting_annotation;
610 greeting_annotation.entity = {"greeting", 1.0};
611 greeting_annotation.span = {/*message_index=*/0,
612 /*span=*/{0, 5},
613 /*text=*/"hello"};
614 greeting_annotation.name = "greeting";
615 ActionSuggestion suggestion = {/*response_text=""*/ "",
616 /*type=*/"iteration_test",
617 /*score=*/1.0,
618 /*priority_score=*/0.0,
619 /*annotations=*/
620 {location_annotation, greeting_annotation}};
621 std::vector<RemoteActionTemplate> intents;
622 EXPECT_TRUE(generator->GenerateIntents(
623 JniHelper::NewStringUTF(GetJenv(), "en-US").ValueOrDie().get(),
624 suggestion, conversation, GetAndroidContext(),
625 /*annotations_entity_data_schema=*/nullptr,
626 /*actions_entity_data_schema=*/nullptr, &intents));
627 EXPECT_THAT(intents, SizeIs(1));
628 EXPECT_EQ(intents[0].extra["length"].Value<int>(), 2);
629 EXPECT_EQ(intents[0].extra["location"].ConstRefValue<std::string>(), "there");
630 EXPECT_EQ(intents[0].extra["greeting"].ConstRefValue<std::string>(), "hello");
631 }
632
TEST_F(IntentGeneratorTest,HandlesEntityDataLookups)633 TEST_F(IntentGeneratorTest, HandlesEntityDataLookups) {
634 flatbuffers::DetachedBuffer intent_factory_model =
635 BuildTestIntentFactoryModel("fake", R"lua(
636 local person = external.entity.person
637 return {
638 {
639 title_without_entity = "Add to contacts",
640 extra = {
641 {name = "name", string_value = string.lower(person.name)},
642 {name = "encoded_phone", string_value = external.android.urlencode(person.phone)},
643 {name = "age", int_value = person.age_years},
644 }
645 }
646 })lua");
647
648 // Create fake entity data schema meta data.
649 // Cannot use object oriented API here as that is not available for the
650 // reflection schema.
651 flatbuffers::FlatBufferBuilder schema_builder;
652 std::vector<flatbuffers::Offset<reflection::Field>> person_fields = {
653 reflection::CreateField(
654 schema_builder,
655 /*name=*/schema_builder.CreateString("name"),
656 /*type=*/
657 reflection::CreateType(schema_builder,
658 /*base_type=*/reflection::String),
659 /*id=*/0,
660 /*offset=*/4),
661 reflection::CreateField(
662 schema_builder,
663 /*name=*/schema_builder.CreateString("phone"),
664 /*type=*/
665 reflection::CreateType(schema_builder,
666 /*base_type=*/reflection::String),
667 /*id=*/1,
668 /*offset=*/6),
669 reflection::CreateField(
670 schema_builder,
671 /*name=*/schema_builder.CreateString("age_years"),
672 /*type=*/
673 reflection::CreateType(schema_builder,
674 /*base_type=*/reflection::Int),
675 /*id=*/2,
676 /*offset=*/8),
677 };
678 std::vector<flatbuffers::Offset<reflection::Field>> entity_data_fields = {
679 reflection::CreateField(
680 schema_builder,
681 /*name=*/schema_builder.CreateString("person"),
682 /*type=*/
683 reflection::CreateType(schema_builder,
684 /*base_type=*/reflection::Obj,
685 /*element=*/reflection::None,
686 /*index=*/1),
687 /*id=*/0,
688 /*offset=*/4)};
689 std::vector<flatbuffers::Offset<reflection::Enum>> enums;
690 std::vector<flatbuffers::Offset<reflection::Object>> objects = {
691 reflection::CreateObject(
692 schema_builder,
693 /*name=*/schema_builder.CreateString("EntityData"),
694 /*fields=*/
695 schema_builder.CreateVectorOfSortedTables(&entity_data_fields)),
696 reflection::CreateObject(
697 schema_builder,
698 /*name=*/schema_builder.CreateString("person"),
699 /*fields=*/
700 schema_builder.CreateVectorOfSortedTables(&person_fields))};
701 schema_builder.Finish(reflection::CreateSchema(
702 schema_builder, schema_builder.CreateVectorOfSortedTables(&objects),
703 schema_builder.CreateVectorOfSortedTables(&enums),
704 /*(unused) file_ident=*/0,
705 /*(unused) file_ext=*/0,
706 /*root_table*/ objects[0]));
707 const reflection::Schema* entity_data_schema =
708 flatbuffers::GetRoot<reflection::Schema>(
709 schema_builder.GetBufferPointer());
710
711 std::unique_ptr<IntentGenerator> generator = IntentGenerator::Create(
712 flatbuffers::GetRoot<IntentFactoryModel>(intent_factory_model.data()),
713 /*resources=*/resources_, jni_cache_);
714
715 ClassificationResult classification = {"fake", 1.0};
716
717 // Build test entity data.
718 MutableFlatbufferBuilder entity_data_builder(entity_data_schema);
719 std::unique_ptr<MutableFlatbuffer> entity_data_buffer =
720 entity_data_builder.NewRoot();
721 MutableFlatbuffer* person = entity_data_buffer->Mutable("person");
722 person->Set("name", "Kenobi");
723 person->Set("phone", "1 800 HIGHGROUND");
724 person->Set("age_years", 38);
725 classification.serialized_entity_data = entity_data_buffer->Serialize();
726
727 std::vector<RemoteActionTemplate> intents;
728 EXPECT_TRUE(generator->GenerateIntents(
729 JniHelper::NewStringUTF(GetJenv(), "en-US").ValueOrDie().get(),
730 classification,
731 /*reference_time_ms_utc=*/0, "highground", {0, 10}, GetAndroidContext(),
732 /*annotations_entity_data_schema=*/entity_data_schema,
733 /*enable_add_contact_intent=*/false, /*enable_search_intent=*/false,
734 &intents));
735 EXPECT_THAT(intents, SizeIs(1));
736 EXPECT_THAT(intents[0].extra, SizeIs(3));
737 EXPECT_EQ(intents[0].extra["name"].ConstRefValue<std::string>(), "kenobi");
738 EXPECT_EQ(intents[0].extra["encoded_phone"].ConstRefValue<std::string>(),
739 "1%20800%20HIGHGROUND");
740 EXPECT_EQ(intents[0].extra["age"].Value<int>(), 38);
741 }
742
TEST_F(IntentGeneratorTest,ReadExtras)743 TEST_F(IntentGeneratorTest, ReadExtras) {
744 flatbuffers::DetachedBuffer intent_factory_model =
745 BuildTestIntentFactoryModel("test", R"lua(
746 return {
747 {
748 extra = {
749 { name = "languages", string_array_value = {"en", "zh"}},
750 { name = "scores", float_array_value = {0.6, 0.4}},
751 { name = "ints", int_array_value = {7, 2, 1}},
752 { name = "bundle",
753 named_variant_array_value =
754 {
755 { name = "inner_string", string_value = "a" },
756 { name = "inner_int", int_value = 42 }
757 }
758 }
759 }
760 }}
761 )lua");
762 std::unique_ptr<IntentGenerator> generator = IntentGenerator::Create(
763 flatbuffers::GetRoot<IntentFactoryModel>(intent_factory_model.data()),
764 /*resources=*/resources_, jni_cache_);
765 const ClassificationResult classification = {"test", 1.0};
766 std::vector<RemoteActionTemplate> intents;
767
768 EXPECT_TRUE(generator->GenerateIntents(
769 JniHelper::NewStringUTF(GetJenv(), "en-US").ValueOrDie().get(),
770 classification,
771 /*reference_time_ms_utc=*/0, "test", {0, 4}, GetAndroidContext(),
772 /*annotations_entity_data_schema=*/nullptr,
773 /*enable_add_contact_intent=*/false, /*enable_search_intent=*/false,
774 &intents));
775
776 EXPECT_THAT(intents, SizeIs(1));
777 RemoteActionTemplate intent = intents[0];
778 EXPECT_THAT(intent.extra, SizeIs(4));
779 EXPECT_THAT(
780 intent.extra["languages"].ConstRefValue<std::vector<std::string>>(),
781 ElementsAre("en", "zh"));
782 EXPECT_THAT(intent.extra["scores"].ConstRefValue<std::vector<float>>(),
783 ElementsAre(0.6, 0.4));
784 EXPECT_THAT(intent.extra["ints"].ConstRefValue<std::vector<int>>(),
785 ElementsAre(7, 2, 1));
786 const std::map<std::string, Variant>& map =
787 intent.extra["bundle"].ConstRefValue<std::map<std::string, Variant>>();
788 EXPECT_THAT(map, SizeIs(2));
789 EXPECT_EQ(map.at("inner_string").ConstRefValue<std::string>(), "a");
790 EXPECT_EQ(map.at("inner_int").Value<int>(), 42);
791 }
792
793 } // namespace
794 } // namespace libtextclassifier3
795