في هذه الجلسة، سنستكشف حالات استخدام مختلفة للذكاء الاصطناعي التوليدي باستخدام واجهات برمجة تطبيقات "حزمة تعلّم الآلة" لفهم متى يكون الذكاء الاصطناعي التوليدي على الجهاز فقط مثاليًا لتطبيق ما، وفهم كيفية تقييم حالات الاستخدام الحالية للذكاء الاصطناعي التوليدي لمعرفة ما إذا كانت النماذج على الجهاز فقط يمكنها تلبية معايير الجودة.
النص
ديفيد تشو
ماذا
مرحبًا بالجميع.
اسمي "ديفيد تشو".
أنا مسؤول منتجات في Android AICore.
أنا متحمّس للغاية للحديث معكم حول إمكانات الذكاء الاصطناعي على الجهاز فقط.
سنتناول اليوم بعض المجالات الأساسية.
أولاً، لنتعرّف على أهمية استخدام الذكاء الاصطناعي على الجهاز فقط، خاصةً في ظل التقدّم السريع في مجال الذكاء الاصطناعي في السحابة الإلكترونية.
ثانيًا، نفكّر في بعض التحسينات التي أجريناها خلال العام الماضي لجعل نماذج الذكاء الاصطناعي هذه على الجهاز فقط فعّالة للغاية.
وأخيرًا، سنعرض مثالاً شاملاً يوضّح كيفية الاستفادة من كل هذه المزايا باستخدام الذكاء الاصطناعي على الجهاز فقط.
أولاً، ما هي أهمية استخدام الذكاء الاصطناعي على الجهاز فقط؟
هناك 4 أسباب رئيسية.
الأول هو السرعة، والثاني هو التكلفة، والثالث هو مدى التوفّر، والرابع هو الخصوصية.
لذلك، عندما يتعلق الأمر بالسرعة، يمكنك أن ترى أن استدلال الذكاء الاصطناعي على الجهاز فقط يصل الآن إلى النقطة التي يمكن أن يكون فيها أسرع من التشغيل في السحابة الإلكترونية لمجرد أنك لست مضطرًا إلى تمرير المعلومات ذهابًا وإيابًا بين جهاز المستخدم وفي السحابة الإلكترونية.
لذلك، في الكثير من حالات الاستخدام التي اختبرناها، نلاحظ زيادة في السرعة بمقدار الضعف عند مقارنة الذكاء الاصطناعي على الجهاز فقط بالسحابة الإلكترونية.
والثاني هو التفكير في التكلفة.
لنفترض أنّك تريد إجراء مليون استنتاج في اليوم.
يمكنك توفير مئات الآلاف من الدولارات إذا كنت تستخدم الذكاء الاصطناعي على الجهاز فقط، وتحديدًا من جهاز المستخدم، بدلاً من تشغيله في السحابة الإلكترونية.
والثالث هو التوفّر بلا إنترنت.
لذلك، أرسلنا استطلاعًا في وقت سابق من هذا العام، وأشار %62 من المطوّرين الذين تواصلنا معهم إلى أنّ توفّر الذكاء الاصطناعي بلا إنترنت كان أحد أهم ثلاثة أسباب لاستخدامه على الجهاز فقط.
وبالتالي، سواء كان المستخدم على ارتفاع 35,000 قدم في الجو أو في الطابق السفلي من محطة طوكيو ولا يمكنه الوصول إلى الإنترنت، سيظل بإمكانه الاستفادة من كل هذه الإمكانات.
وأخيرًا، كانت الخصوصية أحد أهم الأسباب التي لاحظناها عند استطلاع آراء المطوّرين.
لذا، صنّفها %72 من المشاركين ضمن أهم ثلاثة أسباب.
واعتبره %40 من المشاركين في الاستطلاع أهم ميزة.
لذلك، عندما يتعلق الأمر برسائلك أو ملاحظاتك الصوتية أو صورك أو حتى معلوماتك المالية أو الطبية، يريد المستخدمون الحفاظ على خصوصيتها، وبالتالي فإنّ تشغيل الاستدلال على الجهاز فقط سيكون مفيدًا جدًا لهم.
لذلك، وبالنظر إلى كل هذه المزايا، قمنا بإنشاء حزمة تعلّم الآلة حتى تتمكن من الحفاظ على كل هذه الإمكانات والاستفادة منها.
نحاول جعل هذه العملية بسيطة للغاية.
لذا، إذا كانت لديك أي شكاوى، يُرجى التواصل معي.
سأترك معلومات الاتصال الخاصة بي في نهاية هذه المحادثة.
نريد معرفة كيف يمكننا تفعيل حالات الاستخدام لديك.
لذلك، بدأنا بتفعيل مجموعة من الإمكانات لحالات الاستخدام الإبداعية والمتعلّقة بالكلام.
مثل التدقيق اللغوي وإعادة الكتابة، بالإضافة إلى إتاحة الطلبات المتعددة الوسائط المرنة.
لذلك، هدفنا هو محاولة جعل هذا قابلاً للاستخدام قدر الإمكان.
لهذا السبب، بدأ الكثير من المستخدمين والمطوّرين في استخدام هذه الميزة.
لدينا حاليًا 9 تطبيقات خارجية تم تنزيلها 10 ملايين مرة وتستخدم ميزات الذكاء الاصطناعي التوليدي هذه في مرحلة الإنتاج.
ونلاحظ زيادة كبيرة في استخدام هذه الميزات في تطبيقاتنا وتطبيقات الجهات الخارجية، مع نمو بنسبة %110 على أساس ربع سنوي في عمليات تشغيل التطبيقات التي تتضمّن ميزات إنتاجية مستندة إلى الذكاء الاصطناعي.
لذلك، ننصحك بتجربة واجهات برمجة التطبيقات المتاحة لدينا في ما يتعلّق بالرسائل والمواد الإبداعية والتمويل.
نعتقد أنّ الاحتمالات لا حصر لها، ومن المرجّح أنّ خيالك أوسع من خيالنا في العديد من حالات الاستخدام هذه، لذا نودّ التعاون معك ومعرفة كيف يمكننا العمل معًا.
والآن، بعد أن فهمت المزايا العالية المستوى،
قد تتساءلون عن مدى الوصول إلى الجمهور.
كيف هو الأداء على الجهاز؟
عندما نتحدث عن السرعة، ما مدى جودتها في الواقع؟
أو مثل جودة النموذج نفسه، أليس كذلك؟
لذا، سنستعرضها واحدة تلو الأخرى.
في ما يتعلّق بمدى الوصول، يتوفّر Gemini حاليًا على 175 مليون جهاز مزوّد بخدمات AICore.
ونتوقّع أن يتوسّع هذا النطاق بشكل كبير خلال العام المقبل، ليشمل مئات الملايين من الأجهزة الأخرى.
ويعود الفضل في ذلك جزئيًا إلى أنّنا صمّمنا حزامًا بسيطًا جدًا.
وبالتالي، ما عليك سوى دمج واجهة برمجة التطبيقات لحزمة تعلّم الآلة.
هناك الكثير من التعقيدات في AICore لإدارة إشارات الأمان الخاصة بالمدخلات والمخرجات، وإدارة تنزيل النموذج مع الأوزان، والتفكير في كيفية إعداده في وقت التشغيل.
لذلك، هدفنا هو جعل "حزمة تعلّم الآلة" سهلة الاستخدام لكي تتمكّنوا من التركيز على تقديم تجربة رائعة، ونحن سنتولى الباقي.
ديفيد تشو
يسرّنا أيضًا الإعلان عن أنّنا خفّضنا بشكل كبير حجم ذاكرة RAM المطلوبة لتشغيل Gemini Nano، وهو النموذج الأساسي الذي يشغّل العديد من واجهات برمجة التطبيقات هذه المستندة إلى الذكاء الاصطناعي التوليدي.
لذلك، خفّضنا استخدام ذاكرة الوصول العشوائي من 3.6 غيغابايت إلى 1.1 غيغابايت.
لذلك، هناك 4 تحسينات أساسية أجريناها لتحقيق ذلك.
الأولى هي استخدام التكميم الثنائي بدلاً من التكميم الرباعي.
وبالتالي، فإنّ نصف وحدات البت تعني نصف وحدات البايت التي يجب تمريرها عبر النموذج.
والطريقة الثانية هي استخدام ذاكرة تخزين مؤقت مشتركة لـ KV.
لذلك، خفّضنا هذا الحجم بشكل كبير من خلال السماح لأول 20 طبقة من النموذج بمشاركة ذلك في حزمة تبلغ سعتها 35 ميغابايت، ما يعني أنّه لا يزال بإمكانك الوصول إلى حجم سياق يبلغ 16 ألف رمز مميّز ضمن حجم الحزمة الصغير جدًا هذا.
والثالث هو تفعيل برامج الترميز البطيئة للصوت والصورة.
وبالتالي، بالنسبة إلى العديد من حالات استخدام النصوص التي نراها، لا تحتاج في الواقع إلى تحميل هذه الخطوط في البداية.
وبالتالي، سيوفّر ذلك نصف غيغابايت آخر من ذاكرة الوصول العشوائي.
وأخيرًا، من أجل إطالة عمر البطارية، فعّلنا أيضًا ميزة "تعديل سرعة وحدة المعالجة العصبية".
يعني ذلك أنّ وحدة NPU يمكنها العمل بالسرعة اللازمة لتوفير الطاقة مع ضمان تقديم جودة عالية في حالات الاستخدام.
لقد أجرينا بعض التحسينات الأخرى في ما يتعلق بتسريع التشغيل.
الأول هو تفعيل ميزة "التخزين المؤقت للبادئة".
يمكنك أن ترى في هذا المثال هنا، أنّه تتم إعادة استخدام طلب النظام لعدة جولات، أليس كذلك؟
وبالتالي، تصبح معالجة هذه البيانات مكلفة للغاية.
ولكن مع ميزة "التخزين المؤقت للبادئة"، لا داعي لإعادة معالجة طلب النظام مرارًا وتكرارًا.
يمكنك حفظ ذلك في ذاكرة التخزين المؤقت للبادئة، ما يعني أنّه في هذا المثال، يمكنك حفظ %60 من الرموز المميّزة التي يجب تمريرها، ما يعني أيضًا زيادة السرعة بنسبة %60 على أي جهاز تستخدمه.
التحسين الثاني الذي أجريناه هو في ما يتعلّق بفك الترميز التخميني.
في هذا المثال، يمكنكم ملاحظة أنّ Gemini Nano ينشئ رمزًا مميزًا واحدًا في كل مرة.
ويعود ذلك إلى أنّ النماذج اللغوية الكبيرة هي نماذج انحدار ذاتي، ما يعني أنّها يجب أن تواصل النظر إلى الرموز المميّزة السابقة التي تم إنشاؤها لمعرفة الرموز المميّزة التالية.
لذا، تعاملنا مع هذه المشكلة من خلال إنشاء نموذج صياغة ينشئ مجموعة من الرموز المميّزة بسرعة أكبر بكثير.
هذا النموذج أصغر بكثير من Gemini Nano، لذا يمكنه إنشاء مجموعة من الرموز المميّزة في آنٍ واحد، ثم تمريرها إلى Gemini لمعرفة أي منها يتوافق فعليًا مع تخمينات Gemini Nano.
في هذا المثال، يمكنكم ملاحظة أنّه في الجولة الأولى، يتم إنشاء ستة رموز مميزة، وفي الجولة التالية يتم إنشاء خمسة رموز، ثم في الجولة الثالثة، يتم الانتهاء من العملية.
وقد لاحظنا أنّ هذا يؤدي إلى تسريع العمل بمعدّل الضعف في الكثير من حالات الاستخدام الداخلية التي نراها.
قد تحصل على تحسينات أسرع حسب جهازك.
حسنًا
لهذا السبب، وبالعودة إلى السرعة، يمكنك ملاحظة هذا التسريع الكبير بين المعالجة على الجهاز وفي السحابة الإلكترونية، حتى على شبكة الجيل الخامس.
يمكنكم ملاحظة أنّ تحميل هذه البيانات إلى السحابة الإلكترونية سيستغرق ثانية واحدة فقط، وذلك لـ 1,000 إلى 1,300 رمز إدخال، ومع أنّ عمليتَي الملء المسبق وفك الترميز في السحابة الإلكترونية قد تكونان أسرع، إلا أنّ العملية الشاملة تظل أسرع بكثير عند تشغيلها على الجهاز.
أما بالنسبة إلى الجودة، فيسرّنا الإعلان عن أنّ هذا النموذج الذي يضمّ 2 مليار مَعلمة يتفوّق في أدائه على نماذج العام الماضي التي تضمّ 27 مليار مَعلمة.
على الرغم من أنّ أداء هذه النماذج الرائدة التي نملكها اليوم ليس بالمستوى المطلوب، فإنّ ما نوضّحه هنا هو أنّ هناك تقدّمًا كبيرًا في ما يتعلّق بالذكاء الاصطناعي في هذه النماذج التي تعمل على الجهاز فقط، أليس كذلك؟
كما ترون، في اختبار MMLU-Pro، وهو معيار قياسي للمعرفة، يحقّق نموذج 2B أداءً يضاهي تقريبًا أداء نموذج 27B من العام الماضي.
وعندما يتعلق الأمر بمقاييس الأداء في العلوم على مستوى الدراسات العليا، والاستدلال، واستخدام الأدوات المستندة إلى الوكلاء، فإنّ هذا النموذج الذي يضمّ 2 مليار مَعلمة هو في الواقع أفضل، على الرغم من أنّه أقل من عُشر حجم نموذج العام الماضي.
لذلك، نتوقّع أن تستمر هذه المؤشرات.
نواصل العمل عن كثب مع فريق GDM للتأكّد من مواصلة إجراء هذه التحسينات على النموذج لكي تتمكّن من الاستفادة منها إلى أقصى حدّ.
ديفيد تشو
حسنًا
لقد ذكرتُ الكثير من المزايا والتحسينات الجديدة، وقد تتساءلون: "ما الذي يمكننا فعله بهذه النماذج؟"
لذلك، هناك كل هذه التحسينات في الإمكانات التي أجريناها في واجهة برمجة التطبيقات أيضًا.
وبالتالي، يمكنك تحديد شخصية من خلال تعليمات النظام.
من خلال ميزة إدخال صور متعددة، يمكنك تحميل مجموعة من الصور دفعة واحدة.
وقد فعّلنا أيضًا "وضع التفكير" في وقت سابق من هذا العام، لكي تتمكّن من قضاء المزيد من الوقت في حلّ المشاكل التي تتطلّب الاستدلال واستدعاء الأدوات.
وأخيرًا، من خلال ميزة "النتائج المنظَّمة"، أصبح بإمكانك إضافة تعليقات توضيحية إلى فئة Kotlin والحصول على عنصر يتطابق مع المخطط المحدّد.
بالإضافة إلى ذلك، نحاول أيضًا أن نطلب من المطوّرين إجراء تحسينات جديدة يرغبون في إجرائها.
لذلك، قال %91 من المطوّرين الذين تحدّثنا إليهم في الاستطلاع إنّهم سيطلقون تجربة إذا كانت إحدى هذه الميزات متاحة.
لذلك، أطلقنا في وقت سابق من هذا الأسبوع هذه الميزات الأربع: ترجمة الكلام، والتضمينات، وتحويل النص إلى كلام، واستدعاء الدوال.
باستخدام ميزة "ترجمة الكلام"، يمكنك تحويل الصوت إلى نص من أكثر من 20 لغة.
باستخدام التضمينات، يمكنك البحث في محتوى تطبيقك حسب المعنى الدلالي، بدلاً من استخدام البحث بالكلمات الرئيسية فقط.
باستخدام ميزة "تحويل النص إلى كلام"، يمكنك الآن توفير أصوات شبيهة بأصوات البشر لتجاربك، وبالتالي لن تضطر إلى استخدام أصوات آلية.
وأخيرًا، من خلال ميزة "استدعاء الدوال"، يمكنك السماح لتطبيقك باتّخاذ إجراءات نيابةً عن المستخدمين.
لذلك، بدأنا بالتعاون مع بعض المطوّرين لمعرفة كيف يمكنهم إنشاء وكلاء داخل التطبيق في تجاربهم، ونأمل أن نطلق هذه الميزة في وقت لاحق من العام المقبل.
ديفيد تشو
لذا، بالنظر إلى كل هذه الإمكانات، لنتناول مثالاً شاملاً نستخدم فيه كل هذه الإمكانات.
لنفترض أنّ هناك مصوّرًا أمريكيًا يسافر في اليابان.
وكما ترون في الصورة، من الواضح أنّهم لا يملكون ما يكفي من الكاميرات أو العدسات.
لذلك، يطلب عدسة جديدة ويرسلها إلى فندقه.
ولكن هناك مشكلة: أثناء ممارسة أنشطته اليومية، يتلقّى بريدًا صوتيًا يطلب منه دفع رسوم تخليص بقيمة 6,800 ين ياباني لاستلام الطرد بحلول نهاية اليوم.
وتلقّى أيضًا رسالة نصية تتضمّن صورة للطرد المحجوز.
في هذه الحالة، يهتم المستخدم بالحصول على نوع من الحماية من رصد عمليّة خداع، أليس كذلك؟
ويريد معرفة كيف يمكنه تحديد ما إذا كانت رسائل البريد الصوتي شرعية أو احتيالية.
لذلك، نريد الحفاظ على خصوصية جميع المعلومات، والتأكّد من أنّها مستندة إلى مصادر موثوقة، وأنّها قابلة للتفسير أيضًا.
لذلك سنتأكد من أنه يمكنهم فهم النص الياباني، وكذلك، أن يكونوا قادرين على استخدام أي إمكانيات لغوية متاحة في حالة وجود احتياجات لتسهيل الاستخدام.
إذًا، ستكون هناك أربع خطوات هنا، أليس كذلك؟
الخطوة الأولى هي معالجة البريد الصوتي في النص، لذلك سيتم ترجمة هذا البريد الصوتي.
سنستخدم واجهة برمجة التطبيقات Speech Translation API لهذا الغرض.
وبعد ذلك، سيتمكّن المستخدمون من طرح سؤال إذا أرادوا ذلك، لذا سنستخدم واجهة برمجة التطبيقات Speech Recognition API.
بعد ذلك، نريد تحديد الدوال التي يمكنها استرداد المعلومات اللازمة للمستخدم للحصول على ما يحتاج إليه.
لذا، سنستخدم Prompt API وميزة استدعاء الدوال.
الآن، سيتم تشغيل هذا في الخلفية دون ظهور أي شيء للمستخدم.
ثالثًا، سنكتشف كيفية تخزين هذه المعلومات واستردادها باستخدام واجهة برمجة التطبيقات للتضمين وAppSearch.
سيتم أيضًا تشغيل هذه الميزة في الخلفية.
وأخيرًا، سنصدر حكمًا استنادًا إلى المعلومات التي نجمعها، باستخدام Prompt API والنتائج المنظَّمة وميزة "تحويل النص إلى كلام".
حسنًا
لذا، لنبدأ.
سنستخدم ميزتَي "ترجمة الكلام" و"التعرّف على الكلام".
الخطوة الأولى هي أخذ البريد الصوتي والنص وتحويلهما إلى معلومات مفيدة.
وبالتالي، تلقّى المستخدم هذه الرسالة: "مرحبًا، عليك دفع هذه الرسوم البالغة 6,800 ين ياباني.
قد لا تكون الرسوم مدفوعة مسبقًا، لذا عليك دفعها".
وبالتالي، يريدون أن يتمكّنوا من الحصول على هذه الترجمة، أليس كذلك؟
لذلك، من الأمور التي نودّ الإشارة إليها هنا هي أنّ الترجمة وسؤال المستخدم حول المعلومات يتم إرسالهما إلى Gemini Nano 4 باستخدام برنامج ترميز الصوت نفسه، لأنّ برنامج ترميز الصوت تم تدريبه على أكثر من 100 لغة.
وبالتالي، يمكنك الحصول على هذه الترجمة، بالإضافة إلى النص الذي تم التعرّف عليه تلقائيًا لسؤال المستخدم.
وبالتالي، للترجمة، كل ما علينا فعله هو إعداد لغة المصدر واللغة الوجهة.
val translator = SpeechTranslation.getClient(
SpeechTranslatorOptions.Builder(
Locale.JAPANESE, Locale.US
).build()
)في هذه الحالة، سنستخدم اللغتين اليابانية والإنجليزية الأمريكية.
سنقوم بإعداد تدفق Kotlin الذي يمرر البايتات مباشرة إلى مترجم الكلام.
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
}
}وبعد ذلك، يمكننا جمع الردّ باستخدام النص المترجَم.
حسنًا
رائع.
الآن، فهمنا ما يقوله البريد الصوتي، أليس كذلك؟
هناك طرد يحتوي على عدسة كاميرا.
تم احتجازها في الجمارك.
وعليهم دفع رسوم بقيمة 6,800 ين ياباني.
والآن يريد المستخدم أن يكون قادرًا على طرح سؤال حوله.
حسنًا
إذًا، هل هذا الاحتجاز الجمركي حقيقي؟
في هذه الحالة، سنستخدم واجهة برمجة التطبيقات ASR API.
val recognizer = SpeechRecognition.getClient(
speechRecognizerOptions {
locale = Locale.US
preferredMode = SpeechRecognizerOptions.Mode.MODE_ADVANCED
}
)ما نفعله هو ضبط الإعدادات المحلية على اللغة الإنجليزية الأمريكية.
بعد ذلك، يمكننا ضبطه على الوضع المتقدّم الذي يستفيد من برنامج ترميز الصوت.
بعد ذلك، يمكننا تمرير هذه المعلومات أيضًا، وجمع الردّ النهائي، ثم تحويله إلى معلومات سنمرّرها إلى 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)
}
}حسنًا
لقد انتهينا الآن من الجزء الأول من عملية الاستقبال.
كما ترون، لم يتطلّب ذلك الكثير من أسطر الرموز البرمجية.
ديفيد تشو
لذا، الجزء الثاني هو الفرز، وهو التفكير في كيفية الاستفادة من Prompt API واستدعاء الدوال لمعرفة نوع المعلومات المطلوبة.
حسنًا
لإنشاء بنية هذا التطبيق، سنبدأ بإعداد بعض تعليمات النظام.
بعد ذلك، سنرسل أيضًا البريد الصوتي والنص والصورة، بالإضافة إلى سؤال المستخدم، كإدخال إلى Gemini Nano.
بعد ذلك، سنحدّد الدوال التي تستردّ المعلومات التي نحتاجها.
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()
)يمكنك هنا الاطّلاع على مثال بسيط جدًا لما سيبدو عليه إعداد مدقّق أمان، وهو يشير إلى أنّنا سنتحقّق من التغليف والملصقات، ونطّلع على الرسوم المخصّصة، بالإضافة إلى الإبلاغ عن أي طلبات دفع ضرورية.
قد تجد أنّ تحسين الطلبات سيكون مفيدًا جدًا في هذه الحالة للحصول على جودة أفضل بكثير.
بعد ضبط مجموعة تعليمات النظام، يمكننا أخذ كل المحتوى الذي حصلنا عليه من الخطوة السابقة، أي ترجمة كل من البريد الصوتي والنص، بالإضافة إلى سؤال المستخدم، وإدخال كل ذلك في حقل TextPart، إلى جانب تعليمات النظام للحصول على تلك المعلومات.
// 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 */ }بعد ذلك، نريد تحديد دالة يمكننا استخدامها.
لذا، في هذا المثال تحديدًا، يسرّنا الإعلان عن أنّنا دمجنا هذه الميزة مع وظيفة التطبيق.
لذا، إذا كان أي منكم على دراية بذلك، فمن المحتمل أن تفهموا تقريبًا كيفية عمل ذلك بالفعل.
لذا، نُعدّ هنا نقطة دخول AppFunctionService، ثم نحدّد وظيفة التطبيق.
// Registered in AndroidManifest.xml
@AppFunctionServiceEntryPoint("ScamInvestigationService", "scam_tools")
abstract class ScamInvestigationTools : AppFunctionService() {
}يمكنك ملاحظة أنّها مماثلة تمامًا لما يحدث عند إعداد وظائف التطبيق لوكيل النظام.
يمكنك ضبط مَعلمات searchQuery ورسوم المطالبة، وكذلك ما إذا كان علينا مقارنة صور الطرود أم لا، كبعض أمثلة المَعلمات، أليس كذلك؟
// 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) }
}ويمكنك تحديد أهداف مختلفة.
وأخيرًا، لدينا دالة executeVaultSearch سنحدّدها لاحقًا.
أريد أن أشير هنا إلى أنّ "وضع التفكير" متاح.
val vaultTools = AppFunctionTools.getClient().getAppFunctions()
val turn1Request = generateContentRequest(securitySystemInstruction, TextPart(turn1Prompt)) {
enableThinking = true
setTools(vaultTools)
}
val call = generativeModel.generateContent(turn1Request)
.candidates.first().functionCalls.first()أنصحك بشدة باستخدامها عند تشغيل استدعاء الأدوات.
والسبب في ذلك هو أنّنا أجرينا مجموعة من الاختبارات التي أظهرت تحسّنًا في الدقة بمقدار 13 نقطة من أصل 100 عند تشغيل ميزة "استدعاء الأدوات" على Gemini Nano 4.
وبالتالي، قد تكون أعلى بكثير في بعض الحالات، ما قد يمنحك نتائج أفضل بكثير.
لمعرفة المزيد عن ميزة "استدعاء الدوال"، يُرجى حضور جلسة زميلتَي "يولاندا" و"فيكتوريا" التي ستُعقد لاحقًا.
هذا هو موضوعنا حول الإمكانات بالذكاء الاصطناعي الوكيل في تطبيقك.
أعتقد أنّ "فيكتوريا" تجلس في مكان ما بين الحضور، لذا لا تتردّدوا في إلقاء التحية عليها أيضًا.
ديفيد تشو
حسنًا، رائع.
لقد انتهينا الآن من الخطوتَين الأوليَين.
لنتحدّث عن طريقة عمل عملية الاسترجاع.
لذا، سنستخدم التضمينات وAppSearch.
لذا، ربما نعود بضع خطوات إلى الوراء، في رحلة المستخدم، في النقطة التي طلب فيها العدسة، ما نريد القيام به هو في الواقع فهرسة تلك المعلومات بحيث يمكن للمستخدم وكذلك المطور الوصول إليها، أليس كذلك؟
لذا، لدينا في هذا تفاصيل معلومات حول رقم حزمة الطلب.
ويتضمّن أيضًا رقم تتبُّع.
وتتضمّن بعض التفاصيل حول مكان إرسالها.
ويمكنك أن ترى هنا أنه يذكر بالفعل أن الرسوم تم دفعها مسبقًا.
لذا، سننشئ تضمينًا متعدد الوسائط من أجل فهرسة هذه المعلومات مع أي معلومات أخرى قد تكون مهمة بالنسبة إلينا.
إليك كيف يبدو ذلك.
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نأخذ البريد الإلكتروني الخاص بالطلب.
مرة أخرى، الأمر بسيط للغاية، فنحن نمرر صورة واحدة ونصًا واحدًا فقط، ولكن قد يكون لديك المزيد من المحتوى الذي تريد فهرسته هنا.
بعد ذلك، سننشئ متجه التضمين.
بعد ذلك، سنستخدم AppSearch لإنشاء مستند، ثم نضبط أي سمات نعتقد أنّها ضرورية لإنشاء هذا المستند المحدّد.
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()والآن، من المهم الإشارة إلى أنّنا نستخدم التضمينات المتعددة الوسائط لأنّه قد لا تكون هناك أي كلمات مشتركة هنا، ولكن بما أنّ الصور والنص يتناولان محتوًى مشابهًا، يساعدنا ذلك في الحصول على التشابه الدلالي.
لذلك، يمكننا استخدام تشابه جيب التمام للعثور على العناصر التي لا تحتوي على أي كلمات مشتركة.
حسنًا، هذه هي دالة executeVaultSearch التي تحدثنا عنها سابقًا.
suspend fun executeVaultSearch(searchQuery: String): VaultMatch {
val qVec = embedModel.generateEmbedding(
EmbeddingRequest(
content = Content(listOf(TextPart(searchQuery)))
)
).embedding.vector
}هذا ما يتم استدعاؤه بواسطة دالة التطبيق التي حددناها.
في هذا المثال، نمرّر طلب البحث الذي أنشأه Gemini Nano.
بعد ذلك، سنستخدم استراتيجية ترتيب، وهي تشابه جيب التمام.
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()
}بعد ذلك، يساعدنا this.matchedSemanticScores في الحصول على هذه المعلومات.
وأخيرًا، سنحدّد حدًّا أدنى يبلغ 0.68 لتحديد الحدّ الأدنى لتشابه جيب التمام من أجل جلب المعلومات.
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() }
}لذا، قد ترغب في تجربة هذا الأمر أثناء تشغيل حالات الاستخدام الخاصة بك فقط لترى ما يناسبك بالفعل.
هناك نقطة أخرى نريد الإشارة إليها هنا، وهي أنّه عند التفكير في أمان هذا النموذج، فإنّ Gemini Nano ليس هو الذي يشغّل أيًا من الرموز البرمجية، أليس كذلك؟
لذا، يطرح Gemini Nano سؤالاً فقط، ولكن تطبيقك يحدّد الرمز البرمجي، ثم يشغّله ويستردّ تلك المعلومات، ثم يحدّد ما سيتم تمريره إلى Gemini Nano.
يمنحك ذلك مزيدًا من التحكّم في ما تريد عرضه على Gemini Nano، ويساعد أيضًا في ضمان المزيد من الخصوصية والأمان للمستخدمين.
ديفيد تشو
حسنًا، والآن هذه هي الخطوة الأخيرة.
لذلك، لدينا كل المعلومات التي نحتاجها.
سنستخدم Prompt API والردود المنظَّمة وميزة "تحويل النص إلى كلام" لإجراء المقارنات، ثم عرض هذه المعلومات للمستخدم بطريقة مفهومة.
حسنًا، ما نريد معرفته بشكل أساسي هو ما إذا كان هذا الطرد هو نفسه المعلومات الواردة من المستخدم، وهو الطلب الحقيقي.
ولتنفيذ ذلك، سننشئ أيضًا تضمينًا لتلك المعلومات من البريد الصوتي والنص، ونحوّله إلى متجه التضمين نفسه الذي أنشأناه سابقًا.
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)بعد ذلك، سنستخدم matchScore، من خلال استخدام تشابه جيب التمام، يمكننا إجراء مقارنة بين تلك المطابقة التي استرجعناها من المخزن بالإضافة إلى المعلومات من ذلك المتصل.
وهكذا، ما يمكنكم رؤيته هنا هو أنّه حتى لو لم تتطابق الصور، يبقى النص مفيدًا جدًا.
هنا تكمن أهمية التضمينات المتعددة الوسائط.
يمكنك إدخال أجزاء مختلفة من المعلومات ثم الوصول إلى معنى دلالي مشترك، أليس كذلك؟
لذلك، تتيح لك التضمينات المتعدّدة الوسائط ربط كل هذه البيانات بمساحة المتّجه نفسها.
سنستخدم هنا أيضًا وضع التفكير لتحسين تفكيرنا.
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,
)ما نفعله هنا هو إعداد VisualContradiction وبعض التعدادات المختلفة لأنواع التناقضات المحتملة التي قد نواجهها، أليس كذلك؟
سواء كان ذلك DUTY_FRAUD أو PACKAGING_MISMATCH أو STATUS_CONFLICT.
وبعد ذلك، سنقدّم شرحًا هنا.
بعد ذلك، سنرسل كل هذه المعلومات إلى قرار التدقيق، أليس كذلك؟
@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,
)كما ترون، الأمر بسيط جدًا.
كل ما نقوله هو هل هذه عملية خداع أم لا؟
قد يكون لدينا fraudRiskScore حيث يمكنك ضبط الحدّ الأدنى والحدّ الأقصى للقيمة من 0 إلى 100.
يمكنك أيضًا إعلام المستخدم بمقدار الأموال التي وفّرتها له إذا أردت تتبُّع ذلك بمرور الوقت.
ويمكنك أيضًا إدراج أي تناقضات مرئية حددناها للتو.
من الأمور الأخرى التي يجب ملاحظتها هنا أنّ التعليق التوضيحي @Guide اختياري في الواقع.
ليس من الضروري ضبطها وتقديم هذا النوع من الوصف.
السبب في كونه مفيدًا هو أنك قد لا تتمكن من شرح ما يحدث فقط من خلال تعريف المتغير.
وبالتالي، يمكن أن يساعد هذا الوصف Gemini Nano في تحديد الوظائف المحدّدة التي يجب استدعاؤها وكيفية إعدادها لإدخال المعلومات الصحيحة في الحقول المناسبة.
بعد إعداد هذه الميزة، يصبح استخدامها بسيطًا جدًا.
ما عليك سوى تشغيل ثلاثة أسطر من الرموز هنا.
val typedRequest = generateTypedContentRequest(forensicRequest, PhishingAuditVerdict::class)
val response = generativeModel.generateContent(typedRequest)
val verdict: PhishingAuditVerdict? = response.candidates.first().responsegenerateTypedContentRequest، يمكنك إنشاء هذا المحتوى، ثم استرداد هذه الاستجابة.
وهنا مثال يوضّح كيف سيبدو ذلك.
{
"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
}مرة أخرى، الأمر بسيط للغاية، نحدّد لغة ومنطقة، ثم نمرّر القرار الذي أنشأناه للتو باستخدام @Generable و@Guide.
وبعد ذلك، يصبح الأمر فوريًا تقريبًا.
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
}سيستغرق إنشاء هذا الردّ أقل من 120 ملي ثانية على معظم الأجهزة.
سنمحو أي معلومات محفوظة ضرورية.
وإذا لزم الأمر، يمكننا أيضًا ضبط إجراء احتياطي باستخدام واجهات برمجة التطبيقات الخاصة بالمنصة.
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 ...سيظل هذا يحتوي على الصوت الآلي، ولكنه على الأقل يمنح المستخدم شيئًا يتفاعل معه في حالة عدم قدرته على القراءة.
ديفيد تشو
حسنًا
كانت هذه معلومات كثيرة، ولكن لنلخّص ما فعلناه، لقد استفدنا من جميع واجهات برمجة التطبيقات المختلفة هذه في عمليات الاستقبال والفرز والاسترداد والتدقيق.
مثلاً، يمكن استخدام واجهات برمجة التطبيقات للتعرّف على الكلام والترجمة، وواجهة برمجة التطبيقات Prompt API، وميزة استدعاء الدوال، والتضمينات، وAppSearch، بالإضافة إلى المخرجات المنظَّمة وتوليف الكلام.
هذا مجرد مثال على كيفية الاستفادة من كل هذه الإمكانات بطريقة متسقة وتشغيلها بالكامل على الجهاز.
وبالعودة إلى ما تحدثنا عنه في البداية، هناك فوائد كبيرة من تشغيل هذه الحملات، أليس كذلك؟
إذا كنت ستشغّل هذا النموذج على جهازك اليوم، فستلاحظ أنّه أسرع بكثير على الجهاز في معظم الأجهزة.
لن يكلّفك ذلك أي شيء، ويمكنك توفير مئات الآلاف من الدولارات إذا كنت تستخدم هذه الميزة طوال الوقت.
وبعد ذلك، يصبح هذا متاحًا، بغض النظر عمّا إذا كان المستخدم في الجبال أو في قبو ولا يمكنه الوصول إلى الإنترنت.
وهي خاصة بالكامل لأنّك لست بحاجة إلى إرسال أي معلومات إلى السحابة الإلكترونية.
وبالنظر إلى كل هذه الإمكانات، يُرجى الاطّلاع على المستندات أو رمز الاستجابة السريعة أو الانتقال إلى الرابط.
هذه هي جميع الإمكانات التي أطلقناها خلال الأشهر الستة الماضية تقريبًا.
شكرًا جزيلاً لكم جميعًا على تخصيص وقت لحضور هذه المحادثة.
نقدّر حقًا الوقت الذي تقضيه معنا.
لذا، شكرًا جزيلاً لكم.
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 ...