Trong phiên này, chúng ta sẽ khám phá nhiều trường hợp sử dụng AI tạo sinh bằng API Bộ công cụ học máy để hiểu thời điểm AI tạo sinh trên thiết bị là lựa chọn lý tưởng cho một ứng dụng, đồng thời tìm hiểu cách đánh giá các trường hợp sử dụng AI tạo sinh hiện có để xem liệu các mô hình trên thiết bị có thể đáp ứng tiêu chuẩn chất lượng hay không.
Bản chép lời
David Chou
bạn ơi!
Chào mọi người.
Tôi là David Chou.
Tôi là trưởng nhóm sản phẩm Android AICore.
Tôi rất vui được chia sẻ với bạn về các chức năng AI trên thiết bị.
Hôm nay, chúng ta sẽ nói về một vài khía cạnh cốt lõi.
Đầu tiên là lý do bạn nên sử dụng AI trên thiết bị, đặc biệt là khi AI trên đám mây đang phát triển nhanh chóng.
Thứ hai là xem xét một số điểm cải tiến mà chúng tôi đã thực hiện trong năm qua để giúp các mô hình AI trên thiết bị này trở nên cực kỳ mạnh mẽ.
Và điều cuối cùng chúng ta sẽ thảo luận là một ví dụ toàn diện cho thấy cách bạn có thể tận dụng tất cả những lợi ích này bằng cách sử dụng AI trên thiết bị.
Trước tiên, tại sao nên dùng AI trên thiết bị?
Có 4 lý do chính.
Đầu tiên là tốc độ, thứ hai là chi phí, thứ ba là khả năng đáp ứng và thứ tư là quyền riêng tư.
Vì vậy, về tốc độ, bạn có thể thấy rằng suy luận AI trên thiết bị hiện đang đạt đến mức có thể nhanh hơn so với chạy trên đám mây, đơn giản là vì bạn không phải truyền thông tin qua lại giữa thiết bị của người dùng và đám mây.
Vì vậy, trong nhiều trường hợp sử dụng mà chúng tôi đã thử nghiệm, chúng tôi thực sự thấy tốc độ tăng gấp đôi khi so sánh AI trên thiết bị với đám mây.
Thứ hai là cân nhắc về chi phí.
Hãy tưởng tượng bạn phải chạy một triệu suy luận mỗi ngày.
Bạn thực sự có thể tiết kiệm hàng trăm nghìn đô la nếu chạy AI trên thiết bị, cụ thể là từ thiết bị của người dùng so với việc chạy AI trên đám mây.
Và thứ ba là khả năng sử dụng khi không có mạng.
Vì vậy, chúng tôi đã gửi một bản khảo sát vào đầu năm nay và 62% nhà phát triển mà chúng tôi trò chuyện cho biết khả năng sử dụng khi không có mạng là một trong ba lý do hàng đầu để sử dụng AI trên thiết bị.
Vì vậy,cho dù người dùng của bạn đang ở độ cao 10.000 mét hay đang ở tầng hầm của ga Tokyo và không có kết nối Internet, họ vẫn có thể tận dụng tất cả các tính năng này.
Và cuối cùng, quyền riêng tư là một trong những lý do lớn nhất mà chúng tôi nhận thấy khi khảo sát các nhà phát triển.
Vì vậy, 72% người dùng xếp hạng đây là một trong ba lý do hàng đầu.
40% xếp hạng đây là yếu tố quan trọng nhất.
Vì vậy, khi nói đến tin nhắn, ghi chú thoại, ảnh hoặc thậm chí là thông tin tài chính hoặc y tế, người dùng muốn giữ bí mật những thông tin này, nên việc chạy suy luận trên thiết bị sẽ thực sự có giá trị đối với họ.
Vì vậy, với tất cả những lợi ích này, chúng tôi đã xây dựng Bộ công cụ học máy để bạn có thể duy trì và tận dụng tất cả các chức năng này.
Chúng tôi đang cố gắng làm cho việc này trở nên cực kỳ đơn giản.
Vì vậy, nếu bạn có bất kỳ khiếu nại nào, vui lòng liên hệ với tôi.
Tôi sẽ để lại thông tin liên hệ ở cuối buổi trò chuyện này.
Chúng tôi rất mong được tìm hiểu cách hỗ trợ các trường hợp sử dụng của bạn.
Vì vậy, trước tiên, chúng tôi đã bật một loạt tính năng cho các trường hợp sử dụng như lời nói và sáng tạo.
Chẳng hạn như hiệu đính, viết lại và hỗ trợ câu lệnh đa phương thức linh hoạt.
Vì vậy, mục tiêu của chúng tôi là cố gắng làm cho công cụ này dễ sử dụng nhất có thể.
Và đây là lý do nhiều người dùng và nhà phát triển của chúng tôi đã bắt đầu áp dụng tính năng này.
Hiện tại, chúng tôi đã có 9 ứng dụng bên thứ ba với 10 triệu lượt tải xuống đang sử dụng các tính năng AI tạo sinh này trong phiên bản chính thức.
Chúng tôi cũng nhận thấy sự tăng trưởng mạnh mẽ trên cả ứng dụng của bên thứ nhất và bên thứ ba, với mức tăng 110% so với quý trước về số lượt khởi chạy ứng dụng cho các tính năng AI.
Vì vậy, đối với hoạt động nhắn tin, mẫu quảng cáo và tài chính, bạn nên thử dùng các API mà chúng tôi cung cấp.
Chúng tôi nghĩ rằng khả năng là vô tận và bạn có thể có trí tưởng tượng phong phú hơn chúng tôi trong nhiều trường hợp sử dụng, vì vậy, chúng tôi rất muốn hợp tác và tìm ra cách chúng ta có thể làm việc cùng nhau.
Giờ thì bạn đã hiểu những lợi ích chính.
Vậy nên có thể bạn đang thắc mắc rằng phạm vi tiếp cận thực tế sẽ như thế nào?
Hiệu suất trên thiết bị như thế nào?
Khi nói về tốc độ, tốc độ thực tế nhanh đến mức nào?
Hoặc như chất lượng của chính mô hình, đúng không?
Vì vậy, chúng ta sẽ xem xét từng bước một.
Về phạm vi tiếp cận, chúng tôi hiện đã có mặt trên 175 triệu thiết bị AICore.
Chúng tôi dự kiến sẽ mở rộng đáng kể mạng lưới này trong năm tới, kết nối thêm hàng trăm triệu thiết bị khác.
Một phần giúp điều này trở nên khả thi là chúng tôi đã xây dựng một bộ khung rất đơn giản.
Do đó, bạn chỉ cần tích hợp API Bộ công cụ học máy.
AICore có rất nhiều tính năng phức tạp để quản lý tín hiệu an toàn đầu vào và đầu ra, quản lý việc tải mô hình xuống cùng với trọng số, suy nghĩ về cách thiết lập mô hình trong thời gian chạy.
Do đó, mục tiêu của chúng tôi là giúp Bộ công cụ học máy trở nên dễ sử dụng để bạn chỉ cần tập trung vào việc mang lại trải nghiệm tuyệt vời, còn chúng tôi sẽ lo phần còn lại.
David Chou
Chúng tôi cũng rất vui mừng thông báo rằng chúng tôi đã giảm đáng kể dung lượng RAM cần thiết để chạy Gemini Nano, mô hình chính hỗ trợ nhiều API AI tạo sinh này.
Vì vậy, chúng tôi đã giảm mức sử dụng RAM từ 3,6 GB xuống còn 1,1 GB.
Vì vậy, chúng tôi đã thực hiện 4 điểm cải tiến cốt lõi để có thể làm được điều này.
Cách đầu tiên là sử dụng phương pháp lượng tử hoá 2 bit thay vì 4 bit.
Vì vậy, một nửa số bit chỉ có nghĩa là một nửa số byte mà bạn phải truyền qua mô hình.
Cách thứ hai là sử dụng bộ nhớ đệm KV dùng chung.
Vì vậy, chúng tôi đã giảm đáng kể kích thước này bằng cách cho phép 20 lớp đầu tiên của mô hình chia sẻ thông tin đó trong gói 35 MB. Điều này có nghĩa là bạn vẫn có thể truy cập vào kích thước ngữ cảnh 16.000 mã thông báo trong gói có kích thước rất nhỏ này.
Thứ ba là bật bộ mã hoá âm thanh và hình ảnh lười.
Do đó, đối với nhiều trường hợp sử dụng văn bản mà chúng ta đang thấy, bạn thực sự không cần tải các phông chữ này ngay từ đầu.
Và điều đó sẽ giúp bạn tiết kiệm thêm nửa gigabyte RAM.
Cuối cùng, để cải thiện thời lượng pin, chúng tôi cũng đã bật tính năng điều chỉnh xung nhịp NPU.
Tất cả những điều này có nghĩa là NPU có thể chạy nhanh nhất có thể để tiết kiệm điện năng, đồng thời đảm bảo vẫn mang lại chất lượng cao cho các trường hợp sử dụng của bạn.
Chúng tôi cũng đã thực hiện một số điểm cải tiến khác để tăng tốc.
Vì vậy, việc đầu tiên là bật tính năng lưu tiền tố vào bộ nhớ đệm.
Bạn có thể thấy trong ví dụ này, câu lệnh hệ thống được sử dụng lại cho nhiều lượt, đúng không?
Do đó, việc xử lý dữ liệu này trở nên rất tốn kém.
Nhưng với tính năng lưu tiền tố vào bộ nhớ đệm, về cơ bản, chúng ta không cần phải xử lý lại câu lệnh hệ thống đó nhiều lần, đúng không?
Bạn có thể lưu thông tin đó vào bộ nhớ đệm tiền tố, nghĩa là trong ví dụ này, bạn sẽ lưu 60% số mã thông báo mà bạn phải truyền vào, tức là tốc độ sẽ tăng 60% trên mọi thiết bị bạn đang sử dụng.
Điểm cải tiến thứ hai mà chúng tôi đã thực hiện là giải mã suy đoán.
Trong ví dụ này, bạn có thể thấy Gemini Nano đang tạo một mã thông báo cho mỗi lượt truyền.
Nguyên nhân là do các LLM có tính tự hồi quy, tức là chúng phải liên tục xem xét các mã thông báo đã tạo trước đó để tìm ra mã thông báo tiếp theo.
Vì vậy, chúng tôi đã xử lý vấn đề này bằng cách xây dựng một mô hình soạn thảo có thể tạo ra một loạt mã thông báo với tốc độ nhanh hơn nhiều.
Mô hình soạn thảo này nhỏ hơn đáng kể so với Gemini Nano, vì vậy, nó có thể tạo ra một loạt mã thông báo cùng một lúc, chuyển mã thông báo đó đến Gemini để xem mã thông báo nào thực sự phù hợp với phỏng đoán của Gemini Nano.
Trong ví dụ này, bạn có thể thấy ở lượt đầu tiên, mô hình tạo ra 6 mã thông báo, lượt tiếp theo tạo ra 5 mã thông báo và đến lượt thứ ba, mô hình đã hoàn tất.
Và chúng tôi nhận thấy rằng đối với nhiều trường hợp sử dụng nội bộ mà chúng tôi đang xem xét, điều này giúp tăng tốc độ trung bình gấp 2 lần.
Bạn có thể nhận được các điểm cải tiến nhanh hơn, tuỳ thuộc vào phần cứng của bạn.
Được rồi.
Vì vậy, đây là lý do, bạn biết đấy, quay lại vấn đề tốc độ, bạn thực sự có thể thấy tốc độ tăng đáng kể, đúng không, giữa trên thiết bị và đám mây ngay cả trên mạng 5G.
Bạn có thể thấy rằng chỉ riêng việc tải dữ liệu đó lên đám mây cũng sẽ mất khoảng 1 giây cho 1.000 đến 1.300 mã thông báo đầu vào. Vì vậy, ngay cả khi quá trình điền trước và giải mã trong hành trình đám mây có thể nhanh hơn, thì hành trình từ đầu đến cuối vẫn nhanh hơn đáng kể khi bạn chạy trên thiết bị.
Và về chất lượng, chúng tôi rất vui được thông báo rằng mô hình 2B này thực sự có hiệu suất ngang bằng với các mô hình 27B của năm ngoái.
Mặc dù hiệu suất không cao như các mô hình tiên phong hiện nay, nhưng những gì chúng tôi đang trình bày ở đây cho thấy sự tiến bộ đáng kể của AI trên các mô hình trên thiết bị này, đúng không?
Như bạn có thể thấy, đối với MMLU-Pro, một điểm chuẩn kiến thức tiêu chuẩn, mô hình 2B gần như tốt bằng mô hình 27B tham số của năm ngoái.
Và khi nói đến các điểm chuẩn về khoa học, suy luận và sử dụng công cụ tác nhân ở cấp độ sau đại học, mô hình 2B này thực sự tốt hơn, mặc dù kích thước của nó chưa bằng 1/10 mô hình năm ngoái.
Vì vậy, chúng tôi dự đoán những xu hướng này sẽ tiếp tục.
Chúng tôi đang tiếp tục hợp tác chặt chẽ với nhóm GDM để đảm bảo rằng chúng tôi có thể tiếp tục cải tiến mô hình này nhằm giúp bạn khai thác tối đa giá trị của mô hình.
David Chou
Được rồi.
Tôi vừa chia sẻ rất nhiều lợi ích và điểm cải tiến mới, có thể bạn đang thắc mắc: "Chúng ta thực sự có thể làm gì với những mô hình này?"
Và chúng tôi cũng đã cải thiện tất cả các chức năng này trong API.
Vì vậy, với chỉ dẫn hệ thống, bạn có thể đặt một cá tính.
Với tính năng nhập nhiều hình ảnh, bạn có thể tải nhiều hình ảnh lên cùng một lúc.
Chúng tôi cũng đã bật chế độ suy nghĩ vào đầu năm nay để bạn có thể dành nhiều thời gian hơn cho các vấn đề suy luận cũng như gọi công cụ.
Cuối cùng, với kết quả có cấu trúc, chúng tôi cũng đã giúp bạn có thể chú thích một lớp Kotlin và nhận lại một đối tượng khớp với giản đồ cụ thể của bạn.
Ngoài ra, chúng tôi cũng cố gắng hỏi nhà phát triển về những điểm cải tiến mới mà họ muốn thực hiện.
Do đó, 91% nhà phát triển mà chúng tôi đã trao đổi khi khảo sát cho biết họ sẽ ra mắt một trải nghiệm nếu có một trong những tính năng này.
Vì vậy, vào đầu tuần này, chúng tôi đã ra mắt 4 tính năng sau: dịch lời nói, nhúng, chuyển văn bản sang lời nói và gọi hàm.
Với tính năng dịch lời nói, bạn có thể chép lời âm thanh từ hơn 20 ngôn ngữ.
Với tính năng nhúng, bạn có thể tìm kiếm nội dung của ứng dụng theo ý nghĩa ngữ nghĩa, thay vì chỉ sử dụng tính năng tìm kiếm từ khoá.
Nhờ tính năng chuyển văn bản sang lời nói, giờ đây, bạn có thể cung cấp giọng nói giống như người thật cho trải nghiệm của mình, vì vậy, bạn không còn phải sử dụng giọng nói của robot nữa.
Và cuối cùng, với tính năng gọi hàm, bạn có thể cho phép ứng dụng thực hiện hành động thay mặt cho người dùng.
Vì vậy, chúng tôi đã hợp tác với một số nhà phát triển để xem cách họ có thể xây dựng tác nhân trong ứng dụng cho trải nghiệm của họ và chúng tôi hy vọng sẽ ra mắt vào cuối năm sau.
David Chou
Với tất cả những tính năng này, hãy cùng xem một ví dụ toàn diện về cách chúng ta xây dựng bằng tất cả những tính năng này.
Hãy tưởng tượng có một nhiếp ảnh gia người Mỹ đang du lịch ở Nhật Bản.
Và như bạn có thể thấy trong ảnh, họ rõ ràng là không có đủ máy ảnh hoặc ống kính.
Vì vậy, họ đặt mua một ống kính mới và yêu cầu giao đến khách sạn.
Nhưng có một vấn đề: Trong ngày, họ nhận được một thư thoại thông báo rằng họ cần trả phí thông quan 6.800 yên để nhận được gói hàng vào cuối ngày.
Họ cũng nhận được một tin nhắn văn bản có ảnh chụp bưu kiện bị giữ lại.
Vậy nên trong trường hợp này, người dùng muốn có một số biện pháp bảo vệ giúp phát hiện lừa đảo, đúng không?
Họ muốn biết cách xác định thư thoại là hợp pháp hay lừa đảo.
Vì vậy, chúng tôi muốn giữ bí mật mọi thông tin, đảm bảo thông tin đó có cơ sở và có thể giải thích được.
Vì vậy, chúng ta sẽ đảm bảo rằng họ có thể hiểu văn bản tiếng Nhật, đồng thời có thể sử dụng bất kỳ chức năng ngôn ngữ nào có sẵn trong trường hợp họ có nhu cầu hỗ trợ tiếp cận.
Vậy là chúng ta sẽ có 4 bước ở đây, đúng không?
Bước đầu tiên là xử lý thư thoại đó thành văn bản, tức là dịch thư thoại đó.
Chúng ta sẽ sử dụng Speech Translation API cho việc đó.
Sau đó, họ có thể đặt câu hỏi nếu muốn, vì vậy, chúng ta sẽ sử dụng API Nhận dạng lời nói.
Sau đó, chúng ta muốn xác định các hàm có thể truy xuất thông tin cần thiết để người dùng có được thông tin họ cần.
Vì vậy, chúng ta sẽ sử dụng Prompt API và tính năng gọi hàm.
Bây giờ, thao tác này sẽ chạy ở chế độ nền mà người dùng không nhìn thấy gì.
Thứ ba, chúng ta sẽ tìm hiểu cách lưu trữ và truy xuất thông tin đó bằng cách sử dụng API nhúng và AppSearch.
Thao tác này cũng sẽ chạy ở chế độ nền.
Cuối cùng, chúng tôi sẽ đưa ra phán quyết dựa trên thông tin thu thập được, sử dụng Prompt API, kết quả có cấu trúc và tính năng tổng hợp lời nói.
Được rồi.
Nào, chúng ta bắt đầu.
Chúng ta sẽ sử dụng tính năng dịch lời nói và nhận dạng lời nói.
Vậy nên, bước đầu tiên thực ra chỉ là lấy thư thoại và tin nhắn văn bản đó, rồi biến chúng thành thông tin hữu ích, đúng không?
Và thế là người dùng nhận được thông báo: "Này, bạn cần thanh toán khoản phí 6.800 yên này.
Có thể bạn chưa trả trước thuế nên cần phải trả thuế."
Vì vậy, họ muốn có bản dịch đó, đúng không?
Vì vậy, một trong những điều chúng tôi muốn chỉ ra ở đây là cả bản dịch cũng như câu hỏi của người dùng về thông tin đều được chuyển đến Gemini Nano 4 bằng cùng một bộ mã hoá âm thanh, vì bộ mã hoá âm thanh này đã được huấn luyện trên hơn 100 ngôn ngữ.
Và bạn có thể nhận được bản dịch đó, cũng như bản dịch ASR cho văn bản của câu hỏi của người dùng.
Vì vậy, để dịch, tất cả những gì chúng ta phải làm là thiết lập ngôn ngữ nguồn và ngôn ngữ đích.
val translator = SpeechTranslation.getClient(
SpeechTranslatorOptions.Builder(
Locale.JAPANESE, Locale.US
).build()
)Trong trường hợp này, chúng ta sẽ sử dụng tiếng Nhật cũng như tiếng Anh Mỹ.
Chúng ta sẽ thiết lập một luồng Kotlin để truyền trực tiếp các byte vào trình dịch lời nói.
val translator = SpeechTranslation.getClient(
SpeechTranslatorOptions.Builder(
Locale.JAPANESE, Locale.US
).build()
)
val audioFlow = flow {
emit(TranslationAudio(Locale.JAPANESE, courierVoicemailBytes))
}
val request = SpeechTranslatorRequest.Builder(audioFlow).build()
translator.startTranslation(request).collect { response ->
if (response is SpeechTranslatorResponse.Text) {
translatedCourierClaim = response.translatedText
}
}Sau đó, chúng ta có thể thu thập câu trả lời bằng văn bản đã dịch.
Được rồi.
Tuyệt vời.
Vậy là giờ chúng ta đã hiểu nội dung thực sự của thư thoại rồi, đúng không?
Có một bưu kiện chứa ống kính máy ảnh.
Hàng đang được giữ tại Hải quan.
Họ phải trả khoản phí 6.800 yên đó.
Và giờ đây, người dùng muốn đặt câu hỏi về nội dung đó.
Được rồi.
Vậy, liệu thông báo giữ hàng tại hải quan này có thật không?
Trong trường hợp này, chúng ta sẽ sử dụng API ASR.
val recognizer = SpeechRecognition.getClient(
speechRecognizerOptions {
locale = Locale.US
preferredMode = SpeechRecognizerOptions.Mode.MODE_ADVANCED
}
)Chúng tôi vẫn đang đặt ngôn ngữ là tiếng Anh Mỹ.
Sau đó, chúng ta có thể đặt thành chế độ nâng cao, chế độ này tận dụng bộ mã hoá âm thanh.
Sau đó, chúng ta cũng có thể truyền thông tin đó, thu thập câu trả lời cuối cùng rồi chuyển thông tin đó thành thông tin mà chúng ta sẽ truyền đến Gemini Nano.
val recognizer = SpeechRecognition.getClient(
speechRecognizerOptions {
locale = Locale.US
preferredMode = SpeechRecognizerOptions.Mode.MODE_ADVANCED
}
)
val request = speechRecognizerRequest {
audioSource = AudioSource.fromMic()
recognitionOptions { enableWordTiming = true } // optional
}
recognizer.startRecognition(request).collect { response ->
when (response) {
is SpeechRecognizerResponse.PartialTextResponse ->
liveCaption = response.text
is SpeechRecognizerResponse.FinalTextResponse ->
planAndCallVaultTool(translatedCourierClaim, response.text)
}
}Được rồi.
Vậy là chúng ta đã hoàn tất phần đầu tiên của quy trình tiếp nhận.
Như bạn thấy, chúng ta không cần viết quá nhiều dòng mã.
David Chou
Vì vậy, phần thứ hai là phân loại, tức là suy nghĩ về cách tận dụng Prompt API và lệnh gọi hàm để tìm ra loại thông tin cần thiết.
Được rồi.
Vì vậy, để xây dựng cấu trúc của ứng dụng này, trước tiên, chúng ta sẽ thiết lập một số chỉ dẫn hệ thống.
Sau đó, chúng ta cũng sẽ chuyển thư thoại, tin nhắn văn bản và ảnh, cũng như câu hỏi của người dùng, làm dữ liệu đầu vào cho Gemini Nano.
Sau đó, chúng ta sẽ xác định các hàm thực sự truy xuất thông tin mà chúng ta cần.
retrieveOrderAndDeliveryPhotos(
searchQuery = "camera lens customs",
claimedFeeUsd = 45,
comparePackagePhotos = true
)Vì vậy, để bắt đầu, chúng ta sẽ thiết lập chỉ dẫn hệ thống.
// Keep SystemInstruction static across Turn 1 & Turn 2:
val securitySystemInstruction = SystemInstruction(
"""
You are an on-device parcel & phishing security auditor.
1. Cross-check packaging, carrier labels and tracking across all images.
2. Verify customs fee claims against local DDP (Delivered Duty Paid) order records.
3. Flag any payment demand when the order record shows duties prepaid.
""".trimIndent()
)Ngay tại đây, bạn có thể thấy một ví dụ rất đơn giản về việc thiết lập một kiểm tra viên bảo mật, trong đó cho biết chúng ta sẽ kiểm tra chéo bao bì và nhãn mác, xem xét các khoản phí tuỳ chỉnh, cũng như gắn cờ mọi yêu cầu thanh toán cần thiết.
Bạn có thể thấy rằng việc tối ưu hoá câu lệnh sẽ thực sự hữu ích trong việc giúp bạn có được chất lượng tốt hơn đáng kể.
Sau khi có tập lệnh hệ thống đó, chúng ta có thể lấy tất cả nội dung từ bước trước, bản dịch của cả thư thoại và tin nhắn văn bản, cũng như câu hỏi của người dùng và chuyển tất cả vào trường TextPart này, cùng với chỉ dẫn hệ thống để lấy thông tin đó.
// Pull in the Speech Translation (AST) & Speech Recognition (ASR) outputs:
val turn1Prompt = """
Translated voicemail: $translatedCourierClaim
Incoming text message: $callerText
User's spoken question: $userQuestion
""".trimIndent()
val turn1Request = generateContentRequest(
securitySystemInstruction,
TextPart(turn1Prompt)
) { /* enableThinking & setTools */ }Sau đó, chúng ta muốn xác định một hàm mà chúng ta có thể sử dụng.
Vì vậy, đối với ví dụ cụ thể này, chúng tôi rất vui mừng được thông báo rằng chúng tôi đã tích hợp với chức năng của ứng dụng.
Vì vậy, nếu đã quen thuộc với khái niệm này, có lẽ bạn đã hiểu sơ qua về cách hoạt động của nó.
Vì vậy, tại đây, chúng ta thiết lập một điểm truy cập AppFunctionService, sau đó xác định một hàm ứng dụng.
// Registered in AndroidManifest.xml
@AppFunctionServiceEntryPoint("ScamInvestigationService", "scam_tools")
abstract class ScamInvestigationTools : AppFunctionService() {
}Bạn có thể thấy rằng quy trình này hoàn toàn giống với quy trình thiết lập chức năng của ứng dụng cho một tác nhân hệ thống.
Bạn đặt trong các tham số của searchQuery, phí yêu cầu bồi thường và cả việc chúng ta có nên so sánh ảnh của gói hàng hay không, chỉ là một số tham số ví dụ, phải không?
// Registered in AndroidManifest.xml
@AppFunctionServiceEntryPoint("ScamInvestigationService", "scam_tools")
abstract class ScamInvestigationTools : AppFunctionService() {
/**
* Searches local orders and order images in AppSearch.
* @param searchQuery Semantic query to match the order
* @param claimedFeeUsd Customs fee claimed in USD
* @param comparePackagePhotos Whether to fetch order images to compare
*/
@AppFunctionDeclaration(isDescribedByKDoc = true)
suspend fun retrieveOrderAndDeliveryPhotos(
searchQuery: String,
claimedFeeUsd: Int,
comparePackagePhotos: Boolean
): VaultMatch = withContext(Dispatchers.IO) { executeVaultSearch(searchQuery) }
}Bạn có thể xác định các giá trị khác.
Và cuối cùng, đối với hàm đó, chúng ta có một hàm executeVaultSearch mà chúng ta sẽ xác định sau.
Một điều tôi muốn nhấn mạnh ở đây là chúng tôi có chế độ tư duy.
val vaultTools = AppFunctionTools.getClient().getAppFunctions()
val turn1Request = generateContentRequest(securitySystemInstruction, TextPart(turn1Prompt)) {
enableThinking = true
setTools(vaultTools)
}
val call = generativeModel.generateContent(turn1Request)
.candidates.first().functionCalls.first()Bạn nên sử dụng chế độ này khi chạy lệnh gọi công cụ.
Lý do là vì chúng tôi đã tiến hành nhiều thử nghiệm cho thấy độ chính xác tăng 13 điểm (trên thang điểm 100) khi chạy lệnh gọi công cụ trên Gemini Nano 4.
Và vì vậy, ngay cả trong một số trường hợp, mức tăng này thực sự cao hơn đáng kể, nên có thể mang lại cho bạn kết quả tốt hơn nhiều.
Nếu bạn muốn tìm hiểu thêm về tính năng gọi hàm, vui lòng tham dự phiên của các đồng nghiệp của tôi là Jolanda và Victoria, sẽ diễn ra sau.
Đây là nội dung về khả năng của tác nhân AI trong ứng dụng của bạn.
Tôi nghĩ Victoria có thể đang ngồi đâu đó trong đám đông, nên các bạn cứ thoải mái chào cô ấy nhé.
David Chou
Được rồi, tuyệt.
Vậy là chúng ta đã hoàn thành hai bước đầu tiên.
Hãy cùng tìm hiểu cách hoạt động của tính năng truy xuất.
Vì vậy, chúng ta sẽ sử dụng các mục nhúng và AppSearch.
Vì vậy, có thể lùi lại vài bước, đúng không, trong hành trình của người dùng, tại thời điểm họ đặt hàng ống kính, điều chúng ta muốn làm là thực sự lập chỉ mục thông tin đó để người dùng cũng như nhà phát triển có thể truy cập, đúng không?
Vì vậy, trong phần này, chúng ta có thông tin chi tiết về số gói đơn đặt hàng.
Nó cũng có số vận đơn.
Thông báo này có một số thông tin chi tiết về nơi gửi.
Và bạn có thể thấy ở đây đã đề cập đến việc thuế quan được trả trước.
Vì vậy, chúng ta sẽ tạo một mục nhúng đa phương thức để lập chỉ mục thông tin đó cùng với mọi thông tin khác có thể quan trọng đối với chúng ta.
Sau đây là hình ảnh minh hoạ.
val embedModel = EmbeddingGeneration.getEmbeddingModels().first()
val docVector = embedModel.generateEmbedding(
EmbeddingRequest(
Content(listOf(
ImagePart(orderEmailLensBmp), // the lens photo in the email
TextPart(orderEmailBody) // the order details
))
)
).embedding.vectorChúng tôi nhận email đặt hàng.
Một lần nữa, rất đơn giản, chúng ta chỉ truyền một hình ảnh và một văn bản, nhưng bạn có thể có nhiều nội dung hơn mà bạn muốn lập chỉ mục tại đây.
Sau đó, chúng ta sẽ tạo vectơ nhúng.
Vì vậy, sau khi thực hiện việc đó, chúng ta sẽ sử dụng AppSearch để tạo một tài liệu, sau đó đặt bất kỳ thuộc tính nào mà chúng ta cho là cần thiết để tạo tài liệu cụ thể đó.
val embedModel = EmbeddingGeneration.getEmbeddingModels().first()
val docVector = embedModel.generateEmbedding(
EmbeddingRequest(
Content(listOf(
ImagePart(orderEmailLensBmp), // the lens photo in the email
TextPart(orderEmailBody) // the order details
))
)
).embedding.vector
val doc = GenericDocument.Builder<GenericDocument.Builder<*>>(
"orders", "order_jp_904", "OrderRecord"
)
.setPropertyString("summary", "Shipped DDP, duties prepaid ($0 owed). In transit.")
.setPropertyString("photoUris", "order_email_lens.png")
.setPropertyEmbedding(
"embedding",
EmbeddingVector(docVector, embedModel.getModelSignature())
).build()Và sau đó, một điều cần lưu ý ở đây là lý do chúng ta muốn thực hiện nhúng đa phương thức, đúng không, là vì có thể không có bất kỳ từ nào được chia sẻ ở đây, nhưng vì hình ảnh và văn bản có nội dung tương tự, nên điều này giúp chúng ta thực sự có được sự tương đồng về ngữ nghĩa.
Vì vậy, chúng ta có thể sử dụng độ tương đồng cosin để tìm những mục không có từ nào chung.
Được rồi, đây là hàm executeVaultSearch mà chúng ta đã nói đến trước đó.
suspend fun executeVaultSearch(searchQuery: String): VaultMatch {
val qVec = embedModel.generateEmbedding(
EmbeddingRequest(
content = Content(listOf(TextPart(searchQuery)))
)
).embedding.vector
}Đây là hàm được gọi bởi hàm ứng dụng mà chúng ta đã xác định.
Vì vậy, trong ví dụ này, chúng ta sẽ truyền cụm từ tìm kiếm do Gemini Nano tạo.
Sau đó, chúng ta sẽ sử dụng một chiến lược xếp hạng, độ tương đồng cosine.
suspend fun executeVaultSearch(searchQuery: String): VaultMatch {
val qVec = embedModel.generateEmbedding(
EmbeddingRequest(
content = Content(listOf(TextPart(searchQuery)))
)
).embedding.vector
val spec = SearchSpec.Builder()
.addEmbeddingParameters(EmbeddingVector(qVec, embedModel.getModelSignature()))
.setDefaultEmbeddingSearchMetricType(SearchSpec.EMBEDDING_SEARCH_METRIC_TYPE_COSINE)
.setRankingStrategy("sum(this.matchedSemanticScores(getEmbeddingParameter(0)))")
.setListFilterQueryLanguageEnabled(true).build()
}Sau đó, this.matchedSemanticScores chỉ giúp chúng ta lấy thông tin đó.
Cuối cùng, chúng ta sẽ chỉ định ngưỡng 0, 68 để xác định độ tương đồng cosin tối thiểu để chúng ta tìm nạp thông tin.
suspend fun executeVaultSearch(searchQuery: String): VaultMatch {
val qVec = embedModel.generateEmbedding(
EmbeddingRequest(
content = Content(listOf(TextPart(searchQuery)))
)
).embedding.vector
val spec = SearchSpec.Builder()
.addEmbeddingParameters(EmbeddingVector(qVec, embedModel.getModelSignature()))
.setDefaultEmbeddingSearchMetricType(SearchSpec.EMBEDDING_SEARCH_METRIC_TYPE_COSINE)
.setRankingStrategy("sum(this.matchedSemanticScores(getEmbeddingParameter(0)))")
.setListFilterQueryLanguageEnabled(true).build()
val results = appSearchSession.search(
"semanticSearch(getEmbeddingParameter(0), 0.68)", spec
)
// Cleanly close the cursor.
return results.use { it.nextPageAsync.await().first().genericDocument.toVaultMatch() }
}Vì vậy, bạn có thể thử nghiệm điều này khi chạy các trường hợp sử dụng của riêng mình để xem điều gì thực sự phù hợp với bạn.
Và sau đó, một điều mà chúng tôi cũng muốn nêu ra ở đây là khi bạn nghĩ về tính bảo mật của mô hình này, Gemini Nano thực sự không phải là mô hình chạy bất kỳ mã nào, phải không?
Vì vậy, Gemini Nano chỉ đặt câu hỏi, nhưng ứng dụng của bạn xác định mã, sau đó ứng dụng thực sự chạy mã, truy xuất thông tin đó rồi xác định nội dung cần truyền cho Gemini Nano.
Nhờ đó, bạn có thể kiểm soát chặt chẽ hơn những gì bạn muốn hiển thị cho Gemini Nano, đồng thời giúp đảm bảo quyền riêng tư và bảo mật cho người dùng.
David Chou
Được rồi, giờ là bước cuối cùng.
Vậy là chúng ta đã có tất cả thông tin cần thiết.
Chúng ta sẽ sử dụng Prompt API, kết quả có cấu trúc và tính năng tổng hợp lời nói để so sánh, sau đó trả về thông tin đó cho người dùng theo cách dễ hiểu.
Được rồi, về cơ bản, điều chúng ta muốn biết là liệu bưu kiện này có thực sự giống với thông tin của người dùng hay không, tức là đơn đặt hàng thực tế.
Và để làm được điều đó, chúng ta cũng sẽ tạo nhúng, đúng vậy, trên thông tin đó từ thư thoại cũng như văn bản, biến thông tin đó thành cùng một vectơ nhúng mà chúng ta đã làm trước đó.
val callerVector = embedModel.generateEmbedding(
EmbeddingRequest(
Content(listOf(
ImagePart(callerParcelPhotoBmp), // brown box w/ shipping label
TextPart(callerText) // "Clearance fee ¥6,800 due today."
))
)
).embedding.vector
val matchScore = cosineSimilarity(callerVector, vaultMatch.embedding)Sau đó, chúng ta sẽ sử dụng matchScore, bằng cách sử dụng độ tương đồng cosine, chúng ta có thể so sánh giữa kết quả trùng khớp mà chúng ta đã truy xuất từ kho lưu trữ cũng như thông tin từ người gọi đó.
Vì vậy, như bạn có thể thấy ở đây, ngay cả khi hình ảnh có thể không khớp, thì văn bản vẫn thực sự hữu ích.
Vì vậy, đây là lúc các mục nhúng đa phương thức thực sự phát huy giá trị.
Bạn có thể nhập các phần thông tin khác nhau rồi nhận được một ý nghĩa ngữ nghĩa chung, đúng không?
Vì vậy, việc nhúng đa phương thức giúp bạn có thể liên kết tất cả vào cùng một không gian vectơ.
Một lần nữa, chúng ta sẽ sử dụng chế độ tư duy ở đây để cải thiện tư duy của mình.
val forensicRequest = generateContentRequest(
securitySystemInstruction,
content {
image(callerParcelPhotoBmp)
image(orderEmailLensBmp)
text(forensicAuditPrompt)
}
) {
// Enable Gemini Nano 4 reasoning before it answers
enableThinking = true
}Sau đó, chúng ta cũng sẽ xác định một lớp đầu ra, đúng không?
@Generable(description = "Details of a visual contradiction found during an audit")
data class VisualContradiction(
@Guide(
description = "The type of contradiction",
enumValues = ["PACKAGING_MISMATCH", "STATUS_CONFLICT", "DUTY_FRAUD"],
)
val contradictionType: String,
@Guide(description = "Detailed explanation of the visual contradiction")
val explanation: String,
)Vì vậy, tất cả những gì chúng ta đang làm ở đây là thiết lập một VisualContradiction và thiết lập một vài enum khác nhau của các loại mâu thuẫn có thể xảy ra, phải không?
Cho dù đó là DUTY_FRAUD, PACKAGING_MISMATCH hay STATUS_CONFLICT.
Sau đó, chúng ta sẽ có một lời giải thích ở đây.
Sau đó, chúng ta sẽ chuyển tất cả thông tin đó vào một kết quả kiểm tra, đúng không?
@Generable(description = "Verdict and details of a phishing audit")
data class PhishingAuditVerdict(
@Guide(
description = "The audit verdict",
enumValues = ["CONFIRMED_PHISHING_SCAM", "LEGITIMATE_HOLD"],
)
val verdict: String,
@Guide(description = "The fraud risk score from 0 to 100", minimum = 0.0, maximum = 100.0)
val fraudRiskScore: Int,
@Guide(description = "Estimated amount of money saved in USD", minimum = 0.0)
val moneySavedUsd: Int,
@Guide(description = "List of visual contradictions identified during audit", minItems = 0)
val contradictions: List<VisualContradiction>,
// @Guide(description = "Summary of the warning to be spoken to the user")
// `@Guide` annotations are optional.
val spokenWarningSummary: String,
)Như bạn có thể thấy ở đây, việc này rất đơn giản.
Chúng tôi chỉ muốn biết đây có phải là một thủ đoạn lừa đảo hay không?
Chúng ta có thể có một fraudRiskScore, trong đó bạn có thể đặt cả giá trị tối thiểu và tối đa từ 0 đến 100.
Bạn thậm chí có thể cho người dùng biết số tiền bạn đã tiết kiệm được cho họ nếu muốn theo dõi số tiền đó theo thời gian.
Sau đó, bạn cũng có thể liệt kê mọi mâu thuẫn về hình ảnh mà chúng ta vừa xác định.
Một điều khác cần lưu ý ở đây là chú thích @Guide thực sự không bắt buộc.
Bạn không nhất thiết phải đặt và cung cấp loại nội dung mô tả đó.
Lý do là vì bạn có thể không giải thích được điều gì đang xảy ra chỉ bằng định nghĩa biến.
Vì vậy, nội dung mô tả đó có thể giúp Gemini Nano tìm ra những hàm cụ thể cần gọi và cách thiết lập để nhập đúng thông tin vào đúng trường.
Và sau khi thiết lập, mọi thứ sẽ rất đơn giản.
Bạn chỉ cần chạy ba dòng mã tại đây.
val typedRequest = generateTypedContentRequest(forensicRequest, PhishingAuditVerdict::class)
val response = generativeModel.generateContent(typedRequest)
val verdict: PhishingAuditVerdict? = response.candidates.first().responsegenerateTypedContentRequest, bạn tạo nội dung đó, sau đó bạn có thể truy xuất câu trả lời đó.
Và đây là hình ảnh minh hoạ cho kết quả, đúng không?
{
"verdict": "CONFIRMED_PHISHING_SCAM",
"fraudRiskScore": 98,
"moneySavedUsd": 45,
"contradictions": [
{ "contradictionType": "DUTY_FRAUD", "explanation": "Fee demanded, but duties were prepaid." },
{ "contradictionType": "PACKAGING_MISMATCH", "explanation": "Carton has no carrier label or tracking." },
],
"spokenWarningSummary": "Scam blocked. Do not pay the 45 dollar fee. Your lens shipped duties prepaid."
}Vì vậy, chúng tôi xác nhận rằng đây là một thủ đoạn lừa đảo, chúng tôi nhận được điểm rủi ro gian lận rất cao, điều này giải thích những mâu thuẫn rõ ràng trong dữ liệu mà bạn đã truy xuất so với thông tin của người gọi.
Chúng tôi cũng đã giúp người dùng này tiết kiệm được một khoản tiền nhất định, vì vậy, chúng tôi có thể theo dõi khoản tiền đó.
Vâng.
Và đó là tất cả thông tin mà chúng tôi cần.
Và bước cuối cùng là hiển thị thông tin này cho người dùng theo cách dễ tiếp cận.
Ví dụ: nếu họ đang di chuyển hoặc có thể bị khiếm thị, bạn nên đọc to thông tin đó thay vì để họ phải tự đọc văn bản.
Vì vậy, chúng ta sẽ triển khai tính năng chuyển văn bản sang lời nói để xem tính năng này hoạt động như thế nào.
val synthesizer = SpeechSynthesis.getClient(SpeechSynthesizerOptions.builder(Locale.US).build())
try {
val voice = synthesizer.getSupportedVoices(Locale.US).firstOrNull()
if (voice != null) {
val ttsRequest = SpeechSynthesizerRequest(
input = SpeechSynthesizerRequest.Input.Text(verdict.spokenWarningSummary, voice),
audioAttributes = AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_ASSISTANT)
.setContentType(AudioAttributes.CONTENT_TYPE_SPEECH)
.build(),
)
/* more code here */
}
} finally {
synthesizer.close() // safe here: the flow completes after DONE, STOP, or Error
}Một lần nữa, rất đơn giản, chúng ta chỉ định ngôn ngữ và ngôn ngữ, sau đó truyền vào kết quả mà chúng ta vừa tạo bằng @Generable và @Guide.
Sau khi có được thông tin đó, chúng tôi có thể xử lý gần như ngay lập tức.
val synthesizer = SpeechSynthesis.getClient(SpeechSynthesizerOptions.builder(Locale.US).build())
try {
val voice = synthesizer.getSupportedVoices(Locale.US).firstOrNull()
if (voice != null) {
val ttsRequest = SpeechSynthesizerRequest(
input = SpeechSynthesizerRequest.Input.Text(verdict.spokenWarningSummary, voice),
audioAttributes = AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_ASSISTANT)
.setContentType(AudioAttributes.CONTENT_TYPE_SPEECH)
.build(),
)
synthesizer.synthesize(ttsRequest).collect { response ->
when (response) {
is SpeechSynthesizerResponse.StatusUpdate ->
/* playback finished */
if (response.status == SpeechSynthesizerResponse.Status.DONE) { }
/* response.errorCode is a TextToSpeech.ERROR_* code */
is SpeechSynthesizerResponse.Error -> { }
else -> {}
}
}
}
} finally {
synthesizer.close() // safe here: the flow completes after DONE, STOP, or Error
}Quá trình này sẽ mất chưa đến 120 mili giây trên hầu hết các thiết bị để tạo ra phản hồi đó.
Chúng tôi sẽ xoá mọi thông tin cần thiết trong bộ nhớ.
Sau đó, nếu cần, chúng ta cũng có thể đặt một phương án dự phòng bằng cách sử dụng API nền tảng.
val synthesizer = SpeechSynthesis.getClient(SpeechSynthesizerOptions.builder(Locale.US).build())
try {
val voice = synthesizer.getSupportedVoices(Locale.US).firstOrNull()
if (voice != null) {
val ttsRequest = SpeechSynthesizerRequest(
input = SpeechSynthesizerRequest.Input.Text(verdict.spokenWarningSummary, voice),
audioAttributes = AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_ASSISTANT)
.setContentType(AudioAttributes.CONTENT_TYPE_SPEECH)
.build(),
)
synthesizer.synthesize(ttsRequest).collect { response ->
when (response) {
is SpeechSynthesizerResponse.StatusUpdate ->
/* playback finished */
if (response.status == SpeechSynthesizerResponse.Status.DONE) { }
/* response.errorCode is a TextToSpeech.ERROR_* code */
is SpeechSynthesizerResponse.Error -> {
/* handle fallback to platform API */
}
else -> {}
}
}
}
// more code here ...Tính năng này vẫn có giọng nói của robot, nhưng ít nhất cũng cung cấp cho người dùng nội dung để tương tác trong trường hợp họ không thể đọc.
David Chou
Được rồi.
Vậy là chúng ta đã tìm hiểu rất nhiều nội dung. Để tóm tắt những việc đã làm, chúng ta đã tận dụng tất cả các API này trong các quy trình tiếp nhận, phân loại, truy xuất và kiểm tra.
Bạn biết đấy, sử dụng API nhận dạng và dịch lời nói, Prompt API và lệnh gọi hàm, nhúng và AppSearch, cũng như đầu ra có cấu trúc và tổng hợp lời nói.
Đây chỉ là một ví dụ về cách bạn có thể tận dụng tất cả các chức năng này một cách nhất quán và chạy hoàn toàn trên thiết bị.
Và quay lại những gì chúng ta đã nói ở phần đầu, đúng không, có những lợi ích đáng kể khi chạy tính năng này, đúng không?
Nếu thực sự chạy mô hình này trên thiết bị của mình ngay hôm nay, bạn sẽ thấy mô hình này chạy nhanh hơn đáng kể trên hầu hết các phần cứng.
Bạn không phải trả phí, đúng không? Và bạn có thể tiết kiệm hàng trăm nghìn đô la nếu luôn chạy tính năng này.
Và sau đó, tính năng này sẽ hoạt động, bất kể người dùng đang ở trên núi hay trong tầng hầm và không có kết nối Internet.
Và tính năng này hoàn toàn riêng tư vì bạn không cần gửi bất kỳ thông tin nào lên đám mây.
Với tất cả những tính năng này, vui lòng xem tài liệu, xem mã QR hoặc truy cập vào đường liên kết.
Đây là tất cả những tính năng mà chúng tôi đã ra mắt trong khoảng 6 tháng qua.
Cảm ơn các bạn rất nhiều vì đã dành thời gian tham dự buổi trò chuyện này.
Rất cảm ơn bạn đã dành thời gian cho chúng tôi.
Cảm ơn các bạn rất nhiều.
val translator = SpeechTranslation.getClient(
SpeechTranslatorOptions.Builder(
Locale.JAPANESE, Locale.US
).build()
)val translator = SpeechTranslation.getClient(
SpeechTranslatorOptions.Builder(
Locale.JAPANESE, Locale.US
).build()
)
val audioFlow = flow {
emit(TranslationAudio(Locale.JAPANESE, courierVoicemailBytes))
}
val request = SpeechTranslatorRequest.Builder(audioFlow).build()
translator.startTranslation(request).collect { response ->
if (response is SpeechTranslatorResponse.Text) {
translatedCourierClaim = response.translatedText
}
}val recognizer = SpeechRecognition.getClient(
speechRecognizerOptions {
locale = Locale.US
preferredMode = SpeechRecognizerOptions.Mode.MODE_ADVANCED
}
)val recognizer = SpeechRecognition.getClient(
speechRecognizerOptions {
locale = Locale.US
preferredMode = SpeechRecognizerOptions.Mode.MODE_ADVANCED
}
)
val request = speechRecognizerRequest {
audioSource = AudioSource.fromMic()
recognitionOptions { enableWordTiming = true } // optional
}
recognizer.startRecognition(request).collect { response ->
when (response) {
is SpeechRecognizerResponse.PartialTextResponse ->
liveCaption = response.text
is SpeechRecognizerResponse.FinalTextResponse ->
planAndCallVaultTool(translatedCourierClaim, response.text)
}
}retrieveOrderAndDeliveryPhotos(
searchQuery = "camera lens customs",
claimedFeeUsd = 45,
comparePackagePhotos = true
)// Keep SystemInstruction static across Turn 1 & Turn 2:
val securitySystemInstruction = SystemInstruction(
"""
You are an on-device parcel & phishing security auditor.
1. Cross-check packaging, carrier labels and tracking across all images.
2. Verify customs fee claims against local DDP (Delivered Duty Paid) order records.
3. Flag any payment demand when the order record shows duties prepaid.
""".trimIndent()
)// Pull in the Speech Translation (AST) & Speech Recognition (ASR) outputs:
val turn1Prompt = """
Translated voicemail: $translatedCourierClaim
Incoming text message: $callerText
User's spoken question: $userQuestion
""".trimIndent()
val turn1Request = generateContentRequest(
securitySystemInstruction,
TextPart(turn1Prompt)
) { /* enableThinking & setTools */ }// Registered in AndroidManifest.xml
@AppFunctionServiceEntryPoint("ScamInvestigationService", "scam_tools")
abstract class ScamInvestigationTools : AppFunctionService() {
}// Registered in AndroidManifest.xml
@AppFunctionServiceEntryPoint("ScamInvestigationService", "scam_tools")
abstract class ScamInvestigationTools : AppFunctionService() {
/**
* Searches local orders and order images in AppSearch.
* @param searchQuery Semantic query to match the order
* @param claimedFeeUsd Customs fee claimed in USD
* @param comparePackagePhotos Whether to fetch order images to compare
*/
@AppFunctionDeclaration(isDescribedByKDoc = true)
suspend fun retrieveOrderAndDeliveryPhotos(
searchQuery: String,
claimedFeeUsd: Int,
comparePackagePhotos: Boolean
): VaultMatch = withContext(Dispatchers.IO) { executeVaultSearch(searchQuery) }
}val vaultTools = AppFunctionTools.getClient().getAppFunctions()
val turn1Request = generateContentRequest(securitySystemInstruction, TextPart(turn1Prompt)) {
enableThinking = true
setTools(vaultTools)
}
val call = generativeModel.generateContent(turn1Request)
.candidates.first().functionCalls.first()val embedModel = EmbeddingGeneration.getEmbeddingModels().first()
val docVector = embedModel.generateEmbedding(
EmbeddingRequest(
Content(listOf(
ImagePart(orderEmailLensBmp), // the lens photo in the email
TextPart(orderEmailBody) // the order details
))
)
).embedding.vectorval embedModel = EmbeddingGeneration.getEmbeddingModels().first()
val docVector = embedModel.generateEmbedding(
EmbeddingRequest(
Content(listOf(
ImagePart(orderEmailLensBmp), // the lens photo in the email
TextPart(orderEmailBody) // the order details
))
)
).embedding.vector
val doc = GenericDocument.Builder<GenericDocument.Builder<*>>(
"orders", "order_jp_904", "OrderRecord"
)
.setPropertyString("summary", "Shipped DDP, duties prepaid ($0 owed). In transit.")
.setPropertyString("photoUris", "order_email_lens.png")
.setPropertyEmbedding(
"embedding",
EmbeddingVector(docVector, embedModel.getModelSignature())
).build()suspend fun executeVaultSearch(searchQuery: String): VaultMatch {
val qVec = embedModel.generateEmbedding(
EmbeddingRequest(
content = Content(listOf(TextPart(searchQuery)))
)
).embedding.vector
}suspend fun executeVaultSearch(searchQuery: String): VaultMatch {
val qVec = embedModel.generateEmbedding(
EmbeddingRequest(
content = Content(listOf(TextPart(searchQuery)))
)
).embedding.vector
val spec = SearchSpec.Builder()
.addEmbeddingParameters(EmbeddingVector(qVec, embedModel.getModelSignature()))
.setDefaultEmbeddingSearchMetricType(SearchSpec.EMBEDDING_SEARCH_METRIC_TYPE_COSINE)
.setRankingStrategy("sum(this.matchedSemanticScores(getEmbeddingParameter(0)))")
.setListFilterQueryLanguageEnabled(true).build()
}suspend fun executeVaultSearch(searchQuery: String): VaultMatch {
val qVec = embedModel.generateEmbedding(
EmbeddingRequest(
content = Content(listOf(TextPart(searchQuery)))
)
).embedding.vector
val spec = SearchSpec.Builder()
.addEmbeddingParameters(EmbeddingVector(qVec, embedModel.getModelSignature()))
.setDefaultEmbeddingSearchMetricType(SearchSpec.EMBEDDING_SEARCH_METRIC_TYPE_COSINE)
.setRankingStrategy("sum(this.matchedSemanticScores(getEmbeddingParameter(0)))")
.setListFilterQueryLanguageEnabled(true).build()
val results = appSearchSession.search(
"semanticSearch(getEmbeddingParameter(0), 0.68)", spec
)
// Cleanly close the cursor.
return results.use { it.nextPageAsync.await().first().genericDocument.toVaultMatch() }
}val callerVector = embedModel.generateEmbedding(
EmbeddingRequest(
Content(listOf(
ImagePart(callerParcelPhotoBmp), // brown box w/ shipping label
TextPart(callerText) // "Clearance fee ¥6,800 due today."
))
)
).embedding.vector
val matchScore = cosineSimilarity(callerVector, vaultMatch.embedding)val forensicRequest = generateContentRequest(
securitySystemInstruction,
content {
image(callerParcelPhotoBmp)
image(orderEmailLensBmp)
text(forensicAuditPrompt)
}
) {
// Enable Gemini Nano 4 reasoning before it answers
enableThinking = true
}@Generable(description = "Details of a visual contradiction found during an audit")
data class VisualContradiction(
@Guide(
description = "The type of contradiction",
enumValues = ["PACKAGING_MISMATCH", "STATUS_CONFLICT", "DUTY_FRAUD"],
)
val contradictionType: String,
@Guide(description = "Detailed explanation of the visual contradiction")
val explanation: String,
)@Generable(description = "Verdict and details of a phishing audit")
data class PhishingAuditVerdict(
@Guide(
description = "The audit verdict",
enumValues = ["CONFIRMED_PHISHING_SCAM", "LEGITIMATE_HOLD"],
)
val verdict: String,
@Guide(description = "The fraud risk score from 0 to 100", minimum = 0.0, maximum = 100.0)
val fraudRiskScore: Int,
@Guide(description = "Estimated amount of money saved in USD", minimum = 0.0)
val moneySavedUsd: Int,
@Guide(description = "List of visual contradictions identified during audit", minItems = 0)
val contradictions: List<VisualContradiction>,
// @Guide(description = "Summary of the warning to be spoken to the user")
// `@Guide` annotations are optional.
val spokenWarningSummary: String,
)val typedRequest = generateTypedContentRequest(forensicRequest, PhishingAuditVerdict::class)
val response = generativeModel.generateContent(typedRequest)
val verdict: PhishingAuditVerdict? = response.candidates.first().response{
"verdict": "CONFIRMED_PHISHING_SCAM",
"fraudRiskScore": 98,
"moneySavedUsd": 45,
"contradictions": [
{ "contradictionType": "DUTY_FRAUD", "explanation": "Fee demanded, but duties were prepaid." },
{ "contradictionType": "PACKAGING_MISMATCH", "explanation": "Carton has no carrier label or tracking." },
],
"spokenWarningSummary": "Scam blocked. Do not pay the 45 dollar fee. Your lens shipped duties prepaid."
}val synthesizer = SpeechSynthesis.getClient(SpeechSynthesizerOptions.builder(Locale.US).build())
try {
val voice = synthesizer.getSupportedVoices(Locale.US).firstOrNull()
if (voice != null) {
val ttsRequest = SpeechSynthesizerRequest(
input = SpeechSynthesizerRequest.Input.Text(verdict.spokenWarningSummary, voice),
audioAttributes = AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_ASSISTANT)
.setContentType(AudioAttributes.CONTENT_TYPE_SPEECH)
.build(),
)
/* more code here */
}
} finally {
synthesizer.close() // safe here: the flow completes after DONE, STOP, or Error
}val synthesizer = SpeechSynthesis.getClient(SpeechSynthesizerOptions.builder(Locale.US).build())
try {
val voice = synthesizer.getSupportedVoices(Locale.US).firstOrNull()
if (voice != null) {
val ttsRequest = SpeechSynthesizerRequest(
input = SpeechSynthesizerRequest.Input.Text(verdict.spokenWarningSummary, voice),
audioAttributes = AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_ASSISTANT)
.setContentType(AudioAttributes.CONTENT_TYPE_SPEECH)
.build(),
)
synthesizer.synthesize(ttsRequest).collect { response ->
when (response) {
is SpeechSynthesizerResponse.StatusUpdate ->
/* playback finished */
if (response.status == SpeechSynthesizerResponse.Status.DONE) { }
/* response.errorCode is a TextToSpeech.ERROR_* code */
is SpeechSynthesizerResponse.Error -> { }
else -> {}
}
}
}
} finally {
synthesizer.close() // safe here: the flow completes after DONE, STOP, or Error
}val synthesizer = SpeechSynthesis.getClient(SpeechSynthesizerOptions.builder(Locale.US).build())
try {
val voice = synthesizer.getSupportedVoices(Locale.US).firstOrNull()
if (voice != null) {
val ttsRequest = SpeechSynthesizerRequest(
input = SpeechSynthesizerRequest.Input.Text(verdict.spokenWarningSummary, voice),
audioAttributes = AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_ASSISTANT)
.setContentType(AudioAttributes.CONTENT_TYPE_SPEECH)
.build(),
)
synthesizer.synthesize(ttsRequest).collect { response ->
when (response) {
is SpeechSynthesizerResponse.StatusUpdate ->
/* playback finished */
if (response.status == SpeechSynthesizerResponse.Status.DONE) { }
/* response.errorCode is a TextToSpeech.ERROR_* code */
is SpeechSynthesizerResponse.Error -> {
/* handle fallback to platform API */
}
else -> {}
}
}
}
// more code here ...