ผู้จัดการผลิตภัณฑ์ของ Google
Android กำลังพัฒนาเป็นระบบอัจฉริยะที่ Agent อย่าง Gemini จะประสานงานต่างๆ ในแอป หากต้องการปลดล็อกประสบการณ์ของผู้ใช้ใหม่และเข้าร่วมเวิร์กโฟลว์ที่เป็น Agent เหล่านี้ แอปของคุณต้องแสดงความสามารถหลักของแอป
เซสชันนี้เป็นคู่มือที่นำไปใช้ได้จริงสำหรับการผสานรวมแอปกับเลเยอร์อัจฉริยะของ Android เราจะสำรวจเส้นทางการผสานรวมที่สำคัญ ซึ่งรวมถึง MCP ของ Android ในอุปกรณ์, MCP ฝั่งเซิร์ฟเวอร์ และการทำงานอัตโนมัติของงาน พร้อมทั้งให้กรอบการตัดสินใจในการเลือกเส้นทางที่เหมาะสมสำหรับกรณีการใช้งานของคุณ นอกจากนี้ เราจะสำรวจประสบการณ์การใช้งานเชิงรุกด้วยบริบทส่วนบุคคลด้วย
คุณจะได้เรียนรู้วิธีต่อไปนี้
AppFunctionDeclaration, ระบุคำอธิบาย KDoc และหลีกเลี่ยงการใช้ค่าเริ่มต้นของ Kotlinข้อความถอดเสียง
โอเค
สวัสดีทุกคน
ผม Dan Galpin จากทีมนักพัฒนาสัมพันธ์
และฉัน Sarah Shamsie ผู้จัดการผลิตภัณฑ์ที่ทำงานเกี่ยวกับเฟรมเวิร์กอัจฉริยะสำหรับนักพัฒนาแอป Android
เรายินดีอย่างยิ่งที่ได้ต้อนรับคุณเข้าสู่เซสชันในวันนี้
เรากำลังเห็นการเปลี่ยนแปลงกระบวนทัศน์ในวิธีที่ผู้ใช้โต้ตอบกับอุปกรณ์ของตน
ในยุคเว็บ ผู้ใช้มักจะเริ่มต้นเส้นทางด้วยการไปยังเว็บเบราว์เซอร์ แล้วใช้ฟังก์ชันการค้นหา
จากนั้นในยุคอุปกรณ์เคลื่อนที่ เราเห็นพฤติกรรมของผู้ใช้เปลี่ยนไปใช้แอปที่มาพร้อมเครื่องซึ่งมีความเฉพาะเจาะจงมากขึ้นเพื่อดำเนินการแบบครั้งเดียว
ดังตัวอย่างต่อไปนี้
มีข้อความจากเพื่อนชวนไปกินข้าวเย็น
ในการตอบกลับคำเชิญนี้ ฉันต้องตรวจสอบปฏิทินเพื่อดูว่าฉันว่างไหม เพิ่มกิจกรรมลงในปฏิทิน จองรถหากต้องการเดินทางไปที่นั่น แล้วส่งข้อความกลับไปหาเพื่อนว่าฉันไปได้
ก่อนหน้านี้ การดำเนินการนี้ต้องใช้สิ่งที่เรียกว่าการสลับแอป ซึ่งโดยพื้นฐานแล้วฉันจะต้องสลับไปมาระหว่างแอปต่างๆ ในอุปกรณ์เพื่อดำเนินการให้เสร็จสมบูรณ์และบรรลุเป้าหมาย
แต่ตอนนี้เราอยู่ในยุคแห่งความอัจฉริยะ
ในยุคแห่งความอัจฉริยะ ผู้ใช้เริ่มเส้นทางด้วยการแสดงเจตนาและเป้าหมายระดับสูง แทนที่จะเป็นการกระทำที่เฉพาะเจาะจงที่ต้องการให้เสร็จสิ้น
มีการเปลี่ยนแปลงขั้นพื้นฐานที่ผู้ใช้มองว่าอุปกรณ์เป็นสิ่งที่คาดการณ์ความต้องการของตนในเชิงรุกและช่วยให้บรรลุเป้าหมายได้จริง แทนที่จะเป็นเพียงสิ่งที่ช่วยให้ดำเนินการต่างๆ ได้
และเราสังเกตเห็นว่าผู้ใช้คาดหวังให้เฟรมเวิร์กอัจฉริยะนี้ทำงานได้ไม่เพียงแค่ในอุปกรณ์เคลื่อนที่เท่านั้น แต่ยังรวมถึงในระบบนิเวศของอุปกรณ์รูปแบบต่างๆ เช่น นาฬิกา รถยนต์ ทีวี แล็ปท็อป ตลอดจน XR และแว่นตาด้วย
ในยุคแห่งความอัจฉริยะ พรอมต์ข้ามแอปเพียงรายการเดียวสามารถช่วยอำนวยความสะดวกในตัวอย่างคำเชิญรับประทานอาหารค่ำจากก่อนหน้านี้ได้
เพียงแค่ใช้พรอมต์เดียว เอเจนต์ในอุปกรณ์ก็ช่วยเปิดใช้การดำเนินการทั้งหมดที่ฉันต้องการได้ ตั้งแต่การตรวจสอบปฏิทิน เพิ่มกิจกรรม จองรถ และส่งข้อความ ทั้งหมดนี้ทำได้ในครั้งเดียว
วันนี้เราจะแชร์ข้อมูลเพิ่มเติมเกี่ยวกับแผนของ Android ในด้านนี้ และวิธีที่นักพัฒนาแอปอย่างคุณสามารถสร้างประสบการณ์การใช้งานระดับเฟิร์สคลาสได้
Dan
ขอบคุณนะ Sarah
Android จึงเพิ่มความอัจฉริยะลงในแกนหลักอย่างต่อเนื่อง ซึ่งจะช่วยให้แอปของคุณมีวิธีใหม่ๆ ในการเตรียมพร้อมสำหรับยุคแห่งความอัจฉริยะนี้
เสาหลักด้านความอัจฉริยะสำหรับนักพัฒนาแอปเสาแรกคือความอัจฉริยะในแอป ซึ่งก็คือการทำให้แอปฉลาดขึ้นอย่างเห็นได้ชัด โดยการนำความอัจฉริยะมาสู่แอป และคุณสามารถใช้โมเดลในอุปกรณ์อย่าง Gemini Nano 4 ผ่าน ML Kit เพื่อสรุปชุดข้อความ เขียนคำตอบใหม่ และทำความเข้าใจรูปภาพ หรือใช้ภาษาธรรมชาติเพื่อเรียกใช้ฟังก์ชันของแอป
นอกจากนี้ คุณยังใช้โมเดล Gemini ในระบบคลาวด์ได้โดยตรงในแอปผ่าน Firebase AI Logic
แต่วันนี้เราจะมุ่งเน้นไปที่เสาหลักที่ 2 ซึ่งก็คือการตรวจสอบว่าความสามารถของแอปพร้อมใช้งานภายในระบบอัจฉริยะ
แล้วระบบอัจฉริยะคืออะไร
ดังนั้น นอกเหนือจากการพูดคุยและโฮสต์เกี่ยวกับประสบการณ์การใช้งานที่อิงตามพรอมต์ที่ Sarah พูดถึงก่อนหน้านี้แล้ว ระบบนี้ยังช่วยให้สามารถผสานรวมอย่างลึกซึ้งเพื่อปรับปรุงประสบการณ์การใช้งานในระบบนิเวศของแอปด้วย
ดังนั้น อุปกรณ์ที่ออกแบบมาสำหรับ Gemini Intelligence จึงใช้ประโยชน์จากจุดผสานรวมเหล่านี้เพื่อให้บริการต่างๆ เช่น คำแนะนำ การแจ้งเตือนแบบสรุป การป้อนข้อมูลด้วยเสียงที่ได้รับการปรับปรุง และการกรอกแบบฟอร์มอัตโนมัติ
ตอนนี้คำแนะนำเป็นส่วนหนึ่งของฟีเจอร์ที่เรียกว่าการให้ความช่วยเหลือเชิงรุก
และจนถึงตอนนี้ เราได้พูดถึงความช่วยเหลือที่ได้รับแจ้งแล้ว
เราทุกคนคุ้นเคยกับเรื่องนี้ดี โดยที่ผู้ใช้จะบอกให้ Agent ทำงานอย่างน้อย 1 อย่างในนามของตน
แต่การให้ความช่วยเหลือเชิงรุกนั้นเป็นเรื่องของการคาดการณ์การดำเนินการถัดไปที่ผู้ใช้อาจทำ
ดังนั้น หากย้อนกลับไปที่ตัวอย่างมื้อค่ำของเรา เอเจนต์อาจแจ้งเตือนผู้ใช้ในเชิงรุกด้วยคำแนะนำเพื่อตอบกลับข้อความที่สามารถเพิ่มคำเชิญลงในปฏิทินหรือจองบริการร่วมเดินทางได้
แต่ระบบจะแสดงข้อความแจ้งผู้ใช้อย่างไร
การให้ความช่วยเหลือเชิงรุกสามารถแสดงการดำเนินการในตำแหน่งต่างๆ เช่น หน้าจอล็อก การแจ้งเตือน แป้นพิมพ์เสมือน และแม้แต่ภายในแอปที่เลือก
แล้วคุณจะทำอย่างไรให้แอปปรากฏในชิปเชิงรุกเหล่านี้เพื่อเพิ่มการเข้าชม
คุณได้ทำสิ่งต่างๆ เช่น การสร้างโดยอิงตาม SDK เวอร์ชันล่าสุดและการสร้างด้วยเครื่องมือที่ทันสมัยที่สุดของเราแล้ว แต่สิ่งที่ดีที่สุดที่คุณทำได้ในตอนนี้คือตรวจสอบว่าแอปของคุณพร้อมสำหรับความช่วยเหลือที่ได้รับแจ้งด้วย ซึ่ง Sarah จะพูดถึงในลำดับถัดไป
ขอบคุณ Dan
โอเค ความช่วยเหลือแบบมีข้อความแจ้งจะเกี่ยวข้องกับการที่ผู้ใช้ไปที่ Agent โดยตรงและขอให้เริ่มต้นและจัดการงานต่างๆ แม้กระทั่งในแอปต่างๆ
ซึ่งจะช่วยเปลี่ยนเวิร์กโฟลว์ที่เคยต้องทำเองและใช้เวลานานให้เป็นระบบอัตโนมัติ
สำหรับระบบอัจฉริยะบน Android เรามุ่งเน้นที่ตัวแทนระบบเป็นหลัก เช่น Gemini ที่สามารถเข้าถึงแอปต่างๆ ในอุปกรณ์ของผู้ใช้ได้
แอปของคุณสามารถปรากฏในประสบการณ์การใช้งานนี้และโต้ตอบกับตัวแทนระบบได้หลายวิธี
เรามีโซลูชันการทำงานอัตโนมัติที่ขับเคลื่อนด้วยแพลตฟอร์ม และการผสานรวม API โดยตรงเข้ากับ System Agent
เราขอแนะนำให้สร้างด้วยการผสานรวม API โดยตรงทุกครั้งที่ทำได้
ซึ่งนักพัฒนาแอปเป็นผู้ควบคุม คุณจึงตัดสินใจและควบคุมลักษณะที่แอปและแบรนด์จะปรากฏได้
นอกจากนี้ยังทำงานได้เร็วขึ้นและเชื่อถือได้มากขึ้น ซึ่งจะมอบประสบการณ์การใช้งานที่พรีเมียมยิ่งขึ้นให้แก่ผู้ใช้
คุณสามารถพิจารณาเส้นทาง API 2 เส้นทางเมื่อต้องการผสานรวมกับ Agent
Model Context Protocol หรือเรียกสั้นๆ ว่า MCP เป็นแนวทางปฏิบัติแนะนำในอุตสาหกรรมสำหรับการเรียกใช้ฟังก์ชันฝั่งเซิร์ฟเวอร์
แต่สำหรับการเรียกใช้เครื่องมือในอุปกรณ์ เราขอประกาศเปิดตัว Android MCP อย่างเป็นทางการ ซึ่งเป็นเฟรมเวิร์กการเรียกใช้ฟังก์ชันแบบรวมใน Android
Android MCP ขับเคลื่อนโดยไลบรารี Jetpack AppFunctions ซึ่งสอดคล้องกับ MCP อย่างใกล้ชิดทั้งในด้านเทคนิคและแนวคิด ขณะเดียวกันก็รองรับการใช้งาน Kotlin ที่คุ้นเคยและเป็นสำนวนที่นักพัฒนาแอป Android หลายคนคุ้นเคย
ซึ่งทำให้การใช้ API ค่อนข้างเข้าใจง่ายและใช้งานง่าย และคุณสามารถใช้ประโยชน์จากสิ่งที่ได้เรียนรู้จากการผสานรวม MCP ฝั่งเซิร์ฟเวอร์ที่สร้างไว้แล้ว
องค์ประกอบพื้นฐานของ MCP หลายรายการจะแมปแบบหนึ่งต่อหนึ่งกับโครงสร้าง Jetpack AppFunctions
ลองดูตัวอย่างต่อไปนี้เพื่อทำความเข้าใจเรื่องนี้
ปลายทางของเครื่องมือ MCP จะแมปกับฟังก์ชัน Kotlin ที่มีคำอธิบายประกอบ AppFunctionDeclaration ได้อย่างลงตัว
โมเดล pydantic ของคุณจะแมปกับคลาสข้อมูล Kotlin ที่มีคำอธิบายประกอบเป็น AppFunctionSerializable
แต่ Android MCP จะทำงานบน Android Binder IPC ที่เพิ่มประสิทธิภาพแล้ว แทนที่จะเป็น HTTP ที่สตรีมได้และ/หรือการรับส่งข้อมูล I/O มาตรฐาน
วันนี้เรายินดีอย่างยิ่งที่จะแจ้งให้ทราบว่าไลบรารี Jetpack อย่าง AppFunctions อยู่ในเวอร์ชันเบต้าแล้ว
คุณอาจสงสัยว่าเมื่อใดควรใช้เส้นทาง API เดียว และเมื่อใดควรใช้ Android MCP โดยเฉพาะ
หลักการโดยรวมคือ Android MCP จะมีประโยชน์อย่างยิ่งเมื่อคุณต้องการใช้ประโยชน์จากบริบทในอุปกรณ์หรือบริบทเฉพาะรายการเพื่อตอบสนองเส้นทางของผู้ใช้
มาเจาะลึกตัวอย่างที่ Android MCP โดดเด่นกัน
โดยเฉพาะอย่างยิ่ง คุณควรพิจารณาใช้ Android MCP เมื่อต้องการเข้าถึงข้อมูลที่เข้ารหัสในเครื่อง
ซึ่งสำคัญอย่างยิ่งสำหรับแอปรับส่งข้อความและแอปการสื่อสารที่ต้องเก็บประวัติข้อความไว้ในอุปกรณ์เนื่องจากการเข้ารหัสจากต้นทางถึงปลายทาง
นอกจากนี้ Android MCP ยังมีประโยชน์สำหรับการดำเนินการในอุปกรณ์และแบบออฟไลน์เท่านั้น ซึ่งตัวแทนรองรับการดำเนินการนี้
Samsung Health ซึ่งเป็นพาร์ทเนอร์ Wear OS ของเราได้เปิดตัวฟังก์ชันเพื่อเริ่มวิ่งและเริ่มออกกำลังกาย ซึ่งจัดการทั้งหมดในอุปกรณ์
ผู้ใช้มักต้องการให้การดำเนินการประเภทนี้เสร็จสิ้นอย่างรวดเร็ว มีความน่าเชื่อถือสูง เวลาในการตอบสนองต่ำ และบางครั้งก็ต้องการให้ดำเนินการได้แม้ในสภาพแวดล้อมที่มีการเชื่อมต่อต่ำ การตรวจสอบว่าการเรียกใช้เครื่องมือ การกำหนดเส้นทาง และการกำหนดเส้นทางการค้นหาทั้งหมดสามารถเกิดขึ้นในอุปกรณ์ได้จะช่วยรับประกันเรื่องนี้
นอกจากนี้ Android MCP ยังเป็นเส้นทาง API ที่แนะนำหากคุณต้องการเข้าถึงเซ็นเซอร์ของอุปกรณ์หรือโต้ตอบกับฮาร์ดแวร์ของอุปกรณ์โดยตรง
ตัวอย่างเช่น แอปยานยนต์จำนวนมากต้องการอนุญาตให้ผู้ใช้ปลดล็อกประตูรถได้ทันทีผ่านการเชื่อมต่อ UWB และมักจะเป็นแบบแฮนด์ฟรี
Android MCP ช่วยคุณได้
และสุดท้าย โซลูชันนี้เหมาะสำหรับคุณหากไม่มี Cloud Endpoints ที่พร้อมใช้งานสำหรับแอปหรือ API
แอปของบุคคลที่หนึ่งของผู้ผลิตอุปกรณ์หลายรายเป็นแบบฝั่งไคลเอ็นต์เท่านั้น จึงใช้ Android MCP ด้วยเหตุผลนี้
ตัวอย่างเช่น คุณสามารถใช้ Gemini เพื่อค้นหาและดึงรูปภาพจากแอปแกลเลอรีที่อยู่ฝั่งไคลเอ็นต์เท่านั้น เช่น Xiaomi Gallery
ตรงนี้ ฉันสามารถเปิด Gemini แล้วถามว่า "ค้นหารูปภาพหุ่นยนต์ Android ที่ฉันถ่ายเมื่อสัปดาห์ที่แล้วใน Xiaomi Gallery"
Xiaomi ได้เปิดเผยฟังก์ชันของแอปเพื่อค้นหารูปภาพ ซึ่ง Gemini จะเรียกใช้ จากนั้นระบบจะแสดงรูปภาพที่เกี่ยวข้องใน UI ของแอป Gemini โดยตรง เพื่อให้ผู้ใช้ไม่ต้องออกจากแอป
และเราใช้สถาปัตยกรรมเดียวกันนี้สำหรับการดำเนินการในอุปกรณ์ในระบบปฏิบัติการ Android
Android MCP ขับเคลื่อนฟีเจอร์ความช่วยเหลือเกี่ยวกับอุปกรณ์ที่เพิ่งเปิดตัว ซึ่งเป็นการผสานรวมระบบปฏิบัติการ Android เข้ากับแอป Gemini โดยตรง
ความสามารถนี้เปิดตัวในอุปกรณ์ Pixel และ Lenovo แล้ว และกำลังเพิ่มโหนดไปยังระบบนิเวศ OEM ของ Android ในวงกว้าง
ผู้ใช้สามารถใช้ความช่วยเหลือเกี่ยวกับอุปกรณ์ได้โดยไปที่ Gemini และถามวิธีเพิ่มประสิทธิภาพการตั้งค่า การตั้งค่าอุปกรณ์ และการตั้งค่าระบบเพื่อปรับปรุงประสบการณ์การใช้งานอุปกรณ์
ตัวอย่างเช่น แทนที่จะต้องค้นหาผ่านเมนูต่างๆ ผู้ใช้ที่ตาบอดสีอาจขอความช่วยเหลือจาก Gemini ว่า "ฉันตาบอดสี
ฉันจะทำให้โทรศัพท์เครื่องนี้ทำงานได้ดีขึ้นสำหรับฉันได้อย่างไร"
Gemini จะเรียกใช้ฟังก์ชันของแอปในเครื่อง และพิจารณาคำขออย่างชาญฉลาด พร้อมทั้งแนะนำวิธีที่ผู้ใช้สามารถเปลี่ยนการตั้งค่าระบบได้จริง และวิธีเฉพาะเจาะจงที่ผู้ใช้สามารถเปลี่ยนการตั้งค่าอุปกรณ์เพื่อปรับปรุงประสบการณ์การใช้งาน
จากนั้น การควบคุมในแอปจะแสดงขึ้นโดยตรงใน UI ของแอป Gemini เพื่อให้ผู้ใช้ควบคุมได้ทันที
เช่น ในการตอบสนองคำขอเพิ่มประสิทธิภาพสำหรับผู้ใช้ตาบอดสีนี้ Gemini ได้แสดงปุ่มเปิด/ปิดการแก้สีนี้ขึ้นมา เพื่อให้ผู้ใช้ทำการเปลี่ยนแปลงได้ทันที
ประสบการณ์การใช้งาน MCP ของ Android พร้อมใช้งานแล้วในอุปกรณ์บางรุ่น และเรากำลังทำงานร่วมกับพาร์ทเนอร์หลายรายเพื่อนำ Use Case เพิ่มเติมมาสู่ระบบนิเวศของ Android
แม้ว่าการประกาศเครื่องมือ MCP ของ Android อย่างชัดเจนจะเป็นมาตรฐานสูงสุดของเรา แต่เราทราบดีว่าจะมีบางกรณีที่ผู้ใช้จะขอให้ Agent ดำเนินการ Use Case ที่ซับซ้อนหรือพบได้ไม่บ่อยนัก และนี่คือจุดที่ฟีเจอร์การทำงานอัตโนมัติของงานที่ขับเคลื่อนโดยแพลตฟอร์มของเราเข้ามามีบทบาท
ภายใต้โมเดลนี้ ตัวแทนที่โหลดไว้ล่วงหน้าของ OEM จะใช้ API การควบคุมคอมพิวเตอร์ของ Android เพื่อจำลองการโต้ตอบแบบสัมผัสบนแพลตฟอร์มของแอปได้
ซึ่งไม่จำเป็นต้องมีการเปลี่ยนแปลงโค้ดจากนักพัฒนาแอป และตัวแทนจะต้องแตะผ่าน UI เหมือนกับที่ผู้ใช้ทำ โดยการวิเคราะห์หน้าจอแอป
เพื่อให้ผู้ใช้ควบคุมและปลอดภัย สิ่งสำคัญคือเอเจนต์ต้องหยุดชั่วคราวเมื่อดำเนินการที่อาจมีความละเอียดอ่อน เช่น ขั้นตอนการชำระเงิน และขอให้ผู้ใช้ยืนยันเพื่อดำเนินการต่อ
มาดูตัวอย่างการใช้งานจริงกัน
ในตัวอย่างนี้ ผู้ใช้ถ่ายรูปทัวร์ชิมกาแฟและช็อกโกแลต และต้องการถาม Expedia หรือต้องการขอความช่วยเหลือจาก Gemini ให้ตรวจสอบ Expedia และดูว่าสามารถค้นหาทัวร์ที่คล้ายกันได้หรือไม่ แต่ปรับจำนวนผู้เข้าร่วมเป็น 6 คน
Gemini จะไปยังส่วนต่างๆ ของ UI ของ Expedia ในเบื้องหลังในลักษณะเดียวกับที่ผู้ใช้ทำ โดยไม่จำเป็นต้องผสานรวม API โดยตรง
จากนั้นจะแสดงตัวเลือกที่เกี่ยวข้องที่พบและส่งต่อให้ผู้ใช้อย่างราบรื่นเมื่อถึงเวลาสำหรับการยืนยันและการจองขั้นสุดท้าย
คุณควรทราบว่าทั้ง 3 แนวทางนี้ไม่ได้แยกออกจากกันโดยสิ้นเชิง และจริงๆ แล้วจะทำงานร่วมกันได้ดีกว่า
ผู้ใช้และนักพัฒนาแอปควรคาดหวังให้ Agent สามารถประสานงานเครื่องมือต่างๆ ในการติดตั้งใช้งานเซิร์ฟเวอร์ รวมถึงแอป Android ในเครื่องเพื่อมอบประสบการณ์ที่ครอบคลุมและเป็นองค์รวมมากที่สุดแก่ผู้ใช้
โอเค
ขอบคุณนะ Sarah
เราจะมาเจาะลึกเกี่ยวกับ MCP ของ Android กัน
และหากเป็นไปได้ ฉันอยากให้คุณทราบว่าฉันกำลังดึงข้อมูลโค้ด MCP จาก Jetpacker ซึ่งเป็นแอปตัวอย่างการเดินทางของเรา ซึ่งสร้างขึ้นโดยใช้ Antigravity
คุณได้เห็นฟีเจอร์นี้ในคีย์โน้ตแล้ว
ซึ่งมาพร้อมฟีเจอร์อัจฉริยะสุดเจ๋งทุกประเภท รวมถึงการผสานรวมในอุปกรณ์กับ ML Kit Gen AI และ ML Kit Speech Recognition API
ตอนนี้ Android MCP และ MCP ในระบบคลาวด์มีโครงสร้างที่คล้ายกัน
ทั้ง 2 ระบบมีไคลเอ็นต์และเซิร์ฟเวอร์
ในโลกของ MCP เซิร์ฟเวอร์จะจัดเตรียมเครื่องมือต่างๆ ส่วนไคลเอ็นต์จะค้นหาและเรียกใช้เครื่องมือเหล่านี้ได้
ตอนนี้เมื่อใช้ Android MCP แอปของคุณจะเป็นเซิร์ฟเวอร์ และ Agent จะเป็นไคลเอ็นต์
แพลตฟอร์ม Android ทำหน้าที่เป็นรีจิสทรีเครื่องมือ
ตอนนี้แอปจะเริ่มด้วยการลงทะเบียนบริการเพื่อจัดหาเซิร์ฟเวอร์ และเราจะเริ่มด้วยการเพิ่มทรัพยากร Dependency ของไลบรารี AppFunctions Jetpack ในตัวอย่างที่นี่
implementation(libs.androidx.appfunctions)
ksp(libs.androidx.appfunctions.compiler)จากนั้นเราจะใช้คำอธิบายประกอบ KSP เพื่อช่วยสร้าง AppFunctionService
บริการนี้หมายความว่าฟังก์ชันของแอปจะทำงานตามวงจรของ Android ได้อย่างถูกต้อง
เราสามารถใช้การแทรกทรัพยากร Dependency กับ Hilt เพื่อแทรกที่เก็บข้อมูลที่ AppFunction ของเราใช้ได้
@RequiresApi(36)
@AndroidEntryPoint
@AppFunctionServiceEntryPoint(
serviceName = "JetPackerAppFunctionService",
appFunctionXmlFileName = "jetpacker_app_function_service"
)
abstract class BaseJetPackerAppFunctionService : AppFunctionService() {
@Inject internal lateinit var tripDao: TripDao
@Inject internal lateinit var expenseDao: ExpenseDao
...ขออภัย
กลับไปที่แผนภาพของเรา
หลังจากตั้งค่าบริการสำหรับเซิร์ฟเวอร์ Android MCP แล้ว แอปจะลงทะเบียนเครื่องมือในรูปแบบ AppFunctions พร้อมกับโมเดลข้อมูลที่เกี่ยวข้องซึ่งเครื่องมือเหล่านั้นใช้
ตอนนี้คุณสามารถลงทะเบียนโมเดลข้อมูลโดยใช้คำอธิบายประกอบ AppFunctionSerializable และ KDoc นี้ก็เจ๋งมาก
/** Represents an event in a trip itinerary. */
@AppFunctionSerializable(isDescribedByKDoc = true)
data class TimelineEventSerializable(
/** The event's unique identifier. */
val id: String,
/** The trip ID this event belongs to. */
val tripId: String,
/** The type of event (e.g., TRANSPORTATION, ACCOMMODATION, FOOD_AND_DRINK). */
val type: String,
/** The timestamp of the event. */
val timestamp: Long,
/** The title of the event. */
val title: String,
/** The location of the event. */
val location: String,
/** A description of the event. */
val description: String?,
/** The title of the trip this event belongs to. */
val tripTitle: String? = null,
/** Optional title of a matched itinerary event when added as a reminder. */
val matchedEventTitle: String? = null,
/** Execution status of the tool call (e.g., SUCCESS). */
val status: String? = "SUCCESS",
/** Human-readable completion message instructing the agent not to repeat the call. */
val message: String? = null,
)แต่จะกลายเป็นข้อมูลเมตาที่ Agent ใช้เพื่อทำความเข้าใจวิธีใช้โมเดล
คุณสามารถใช้คำอธิบายประกอบ AppFunctionDeclaration เพื่อลงทะเบียนฟังก์ชันเป็นเครื่องมือได้
@AppFunctionDeclaration(isDescribedByKDoc = true)
suspend fun getItinerary(tripId: String? = null): List<TimelineEventSerializable> {
return withContext(Dispatchers.IO) {
val trip = resolveTrip(tripId = tripId)
val targetTripId = trip?.id ?: ActiveTripSession.activeTripId ?: tripId?.takeIf { it.isNotBlank() } ?: "2026-1"
val dbEvents = eventDao.getEventsForTrip(targetTripId).first()
val events =
if (dbEvents.isNotEmpty()) { dbEvents } else {
val fallback = DummyData.events.filter { it.tripId == targetTripId }
if (fallback.isNotEmpty()) { eventDao.insertEvents(fallback) }
fallback
}
events.map { it.toSerializable(tripTitle = trip?.title) }
}
}และเมื่อตั้งค่า isDescribedByKDoc เป็น "จริง" KDoc จะให้คำอธิบายแก่ Agent เพื่อให้เข้าใจว่าฟังก์ชันของคุณทำอะไร และยังช่วยให้มนุษย์เข้าใจด้วยเช่นกัน
/**
* Retrieves the itinerary events (with their eventId, title, type, and location) for a specific
* trip. Note: getItinerary returns event summaries only; call getEventDetails(eventId) with a
* specific event's ID to retrieve ticket prices, hotel nightly rates, baggage allowance, or
* visitor tips.
*
* @param tripId Optional ID of the trip (e.g., '2026-1'). Defaults to the active trip if omitted.
* @return A list of events in the itinerary.
*/
@AppFunctionDeclaration(isDescribedByKDoc = true)
suspend fun getItinerary(tripId: String? = null): List<TimelineEventSerializable> {
return withContext(Dispatchers.IO) {
val trip = resolveTrip(tripId = tripId)
val targetTripId = trip?.id ?: ActiveTripSession.activeTripId ?: tripId?.takeIf { it.isNotBlank() } ?: "2026-1"
val dbEvents = eventDao.getEventsForTrip(targetTripId).first()
...และต่อไปนี้คือแนวทางปฏิบัติแนะนำบางส่วนสำหรับการออกแบบพารามิเตอร์
/**
* @param tripId Optional ID of the trip (e.g., '2026-1').
* Defaults to the active trip if omitted.
* @return A list of events in the itinerary.
*/
@AppFunctionDeclaration(isDescribedByKDoc = true)
suspend fun getItinerary(tripId: String? = null): List<TimelineEventSerializable> {
return withContext(Dispatchers.IO) {
// resolveTrip defaults to the active trip if tripId == null
val trip = resolveTrip(tripId = tripId)ก่อนอื่น คุณควรเน้นที่ชุดย่อยของพารามิเตอร์ที่น้อยที่สุดซึ่งมีชื่อที่ชัดเจนและแตกต่างกัน
ทำให้พารามิเตอร์ที่ไม่จำเป็นเป็นค่า Null ได้ และแก้ไขค่าเริ่มต้นในเนื้อหาฟังก์ชัน
นี่ไม่ใช่แค่แนวทางปฏิบัติแนะนำ
ซึ่งจะใช้ไม่ได้ผล เนื่องจากระบบจะไม่สนใจค่าเริ่มต้นของ Kotlin เพราะไลบรารี AppFunctions จะส่งค่า Null สำหรับพารามิเตอร์ที่อนุญาตค่า Null ที่ละไว้
เราจะไม่แสดงตัวอย่างของวิธีแรกนี้ แต่ขอแนะนำว่าอย่าใช้พารามิเตอร์ที่ครอบคลุมทั้งหมด
สิ่งต่างๆ เช่น บริบทแบบเต็มหรือประวัติการสนทนาล้วนไม่ดีทั้งนั้น
ซึ่งอาจทำให้ Agent ส่งข้อมูลจำนวนมากและนำไปสู่การขโมยข้อมูล และเป็นแนวทางปฏิบัติที่ไม่ดี
เนื่องจาก AppFunction จะทำงานในเธรด UI โดยค่าเริ่มต้น คุณจึงต้องตรวจสอบว่าการดำเนินการที่ใช้เวลานานเกิดขึ้นในเบื้องหลังโดยเลือกผู้มอบหมายงาน Coroutine ที่เหมาะสม
@AppFunctionDeclaration(isDescribedByKDoc = true)
suspend fun getItinerary(tripId: String? = null): List<TimelineEventSerializable> {
return withContext(Dispatchers.IO) {
val trip = resolveTrip(tripId = tripId)
val targetTripId = trip?.id ?: ActiveTripSession.activeTripId ?:
tripId?.takeIf { it.isNotBlank() } ?: "2026-1"
val dbEvents = eventDao.getEventsForTrip(targetTripId).first()
val events =
if (dbEvents.isNotEmpty()) { dbEvents } else {
val fallback = DummyData.events.filter { it.tripId == targetTripId }
...คุณควรแสดงข้อยกเว้น AppFunction เมื่อจำเป็นพร้อมข้อมูลโดยละเอียด เพื่อให้ Agent เข้าใจวิธีแก้ไขข้อผิดพลาด
@AppFunctionDeclaration(isDescribedByKDoc = true)
suspend fun send(
message: Message
): Result {
...
throw AppFunctionException(
errorCode = AppFunctionException.ErrorCode.ERROR_INVALID_ARGUMENT,
errorMessage = "To address has invalid syntax!"
)
}เอาล่ะ ตอนนี้เรามี AppFunction แล้ว มาดูแผนภาพสถาปัตยกรรมกันอีกครั้ง
แอปไคลเอ็นต์ของ Agent จะลงทะเบียนกับแพลตฟอร์มเพื่อเรียกใช้ AppFunctions
ตัวอย่างเช่น Gemini
และในการดำเนินการดังกล่าว ตัวแทนจะต้องมีสิทธิ์พิเศษ จากนั้นตัวแทนที่ลงทะเบียนจะสามารถค้นหาข้อมูลเมตาของ AppFunction จากแพลตฟอร์ม Android และตัดสินใจว่าจะเรียกใช้ AppFunction ที่ลงทะเบียนไว้ 1 รายการหรือมากกว่านั้นเพื่อดำเนินการตามคำขอของผู้ใช้หรือไม่
เมื่อพูดถึง AppFunction อย่างน้อย 1 รายการ Agent จะตัดสินใจอย่างไร
สิ่งสุดท้ายที่คุณต้องทำคืออธิบายความสามารถของแอปในไฟล์ข้อมูลเมตาของแอป
<AppFunctionAppMetadata
xmlns:appfn="http://schemas.android.com/apk/androidx.appfunctions"
appfn:description="This app manages trips and travel expenses.
Operational Patterns:
- Use 'getAllTrips' to find the correct 'tripId' before
adding expenses or managing trip details.
Constraints:
- Expenses must be associated with a valid trip ID."
appfn:displayDescription="@string/user_visible_description" />และ Agent จะเลือกเครื่องมือโดยอ่านคำอธิบายนี้ก่อนภายใน AppFunctionAppMetadata แล้วจึงอ่านคำอธิบายพร้อมกับ AppFunction แต่ละรายการอีกครั้ง
ดังนั้น คุณควรเน้นที่ส่วนต้นของคำอธิบายด้วยคำอธิบายสั้นๆ เกี่ยวกับโดเมนหลักของแอปหรือเครื่องมือ
และคุณสามารถทำตามรูปแบบการดำเนินการเพื่อช่วยให้ Agent ทราบถึงขั้นตอนการดำเนินการที่สมเหตุสมผล ซึ่งเหมาะสำหรับการระบุข้อกำหนดเบื้องต้นหรือแนะนำ Agent ไปยังเส้นทางที่เหมาะสมที่สุดสำหรับการทำงานให้เสร็จสมบูรณ์
และคุณสามารถเพิ่มส่วนเกี่ยวกับข้อจำกัดที่กำหนดขีดจำกัดเชิงตรรกะของประเภทข้อมูลอินพุตหรือเสริมรูปแบบการทำงานได้
ที่กล่าวมาคือข้อมูลเบื้องต้นเกี่ยวกับ Android MCP แต่ยังมีสิ่งอื่นๆ ที่คุณทำได้อีก
สิ่งหนึ่งที่คุณทำได้ซึ่งค่อนข้างเจ๋งคือคุณสามารถเปิดและปิดใช้ AppFunctions แบบไดนามิกได้
viewModelScope.launch {
val newState =
if (enabled) {
AppFunctionManager.APP_FUNCTION_STATE_ENABLED
} else {
AppFunctionManager.APP_FUNCTION_STATE_DISABLED
}
appFunctionManager?.setAppFunctionEnabled(id, newState)
}ซึ่งเป็นสิ่งสำคัญ เช่น หากคุณต้องการให้ AppFunction พร้อมใช้งานเฉพาะเมื่อผู้ใช้เข้าสู่ระบบเท่านั้น
อีกสิ่งหนึ่งที่คุณทำได้คือสร้าง AppFunctions ที่กำหนดขอบเขตไว้สำหรับหน้าจอหรือกิจกรรมที่เฉพาะเจาะจงในแอป
@AppFunctionSignature(
scope = AppFunctionMetadata.SCOPE_ACTIVITY,
appFunctionXmlFileName = "cart_functions"
)
fun interface AddCurrentItemToCart {
/** Adds the item currently shown on screen to the cart. Returns the new cart size. */
suspend fun addToCart(quantity: Int): Int
}ดังนั้น แทนที่จะประกาศ AppFunction คุณจะประกาศ AppFunctionSignature แทน
จากนั้นคุณสามารถใช้แฮนเดิลสำหรับ AppFunction ใน UI ได้
val addToCartAppFunctionAdapter =
appFunctionManager.getHandleAppFunctionRequestAdapter<AddCurrentItemToCart>()
coroutineScope.launch {
appFunctionManager.handleAppFunction(
addToCartAppFunctionAdapter.adapt { quantity ->
val newCartSize = cart.addItem(quantity)
newCartSize
}
)
}และส่วนที่น่าสนใจเกี่ยวกับเรื่องนี้คือวงจรของตัวแฮนเดิลจะเชื่อมโยงกับวงจรของขอบเขต Coroutine ที่คุณใช้โดยอัตโนมัติ
ตอนนี้คุณจะต้องแคชอะแดปเตอร์คำขอ AppFunctions นี้ ขออภัย อะแดปเตอร์คำขอ handle AppFunctions เนื่องจากไลบรารีใช้การสะท้อนภายในเพื่อสร้างอินสแตนซ์
นอกจากนี้ยังมีฟังก์ชันการทำงานเพื่อสร้าง AppFunctions ที่กำหนดขอบเขตซึ่งการลงทะเบียนไม่ได้เชื่อมโยงกับวงจรของ Android โดยตรง
สิ่งสุดท้ายคือเรามีสิ่งที่เรียกว่าระดับการเข้าถึง
@AppFunctionDeclaration(isDescribedByKDoc = true)
@AppFunctionAccessLevel(
level = AppFunctionMetadata.ACCESS_LEVEL_SELF,
isCompatEnforcementEnabled = true,
)
suspend fun getItinerary(tripId: String? = null): List<TimelineEventSerializable> {
return withContext(Dispatchers.IO) {
...และคุณสามารถรับและใช้ระดับการเข้าถึงของตนเองหรือระบบ หรือที่เชื่อถือได้ ซึ่งจะระบุว่าฟังก์ชันใดมีไว้สำหรับใช้กับตัวแทนในแอปของคุณเองเท่านั้น เช่น ฟังก์ชันที่สร้างด้วย ML Kit GenAI ฟังก์ชันใดที่ใช้ได้เฉพาะกับระบบ และฟังก์ชันใดที่ใช้ได้กับตัวแทนระบบที่เชื่อถือได้
และเพื่อช่วยคุณสร้าง Agent ในแอปของคุณเอง เราได้ประกาศไปแล้วว่า ML Kit รองรับ AppFunctions โดยค่าเริ่มต้น ซึ่งเป็นส่วนหนึ่งของเฟรมเวิร์กการเรียกใช้เครื่องมือแบบรวม
คุณกำหนดเครื่องมือเหล่านี้เพียงครั้งเดียวและจะได้รับการครอบคลุมทั้ง Agent ระดับระบบและโมเดลในแอปในเครื่อง ซึ่งจะช่วยให้คุณหลีกเลี่ยงการประกาศเครื่องมือที่กระจัดกระจายได้
และหากต้องการดูข้อมูลเพิ่มเติม คุณสามารถดูการพูดคุยนี้ได้ที่หัวข้อ "ปลดล็อกความสามารถด้าน Agentic AI ในแอปด้วย ML Kit GenAI" หรือจะไปดูข้อมูลล่าสุดใน Jetpack ก็ได้
เจ๋งมากเลย
เอาล่ะ กลับมาที่ Sarah
โอเค
Dan เพิ่งแสดงให้คุณเห็นวิธีการทำงานเบื้องหลังของ Android MCP
มาลองนำไปใช้จริงกัน
โปรดทราบว่าแอปของคุณคือเซิร์ฟเวอร์และ Agent คือไคลเอ็นต์ ดังนั้นเป้าหมายจึงควรเป็นการตรวจสอบว่าแอปของคุณเป็นเซิร์ฟเวอร์ที่ดีสำหรับ Agent
เราจะติดตาม Jetpacker ซึ่งเป็นแอปตัวอย่างด้านการเดินทาง ตั้งแต่การประกาศเครื่องมือครั้งแรกไปจนถึงการเรียกใช้ฟังก์ชันจริงผ่าน Gemini
คุณสามารถเขียน AppFunctions ด้วยตนเองหรือใช้เอเจนต์การเขียนโค้ดเพื่อช่วยคุณเริ่มต้นใช้งาน
เราได้เปิดตัวทักษะ AppFunctions ที่จะช่วยคุณเริ่มต้นเขียนฟังก์ชัน ซึ่งคุณสามารถเชื่อมต่อกับ Android Studio ผ่าน Android CLI หรือใช้กับเอเจนต์การเขียนโค้ดที่คุณเลือกได้
android skills add appfunctionsทักษะนี้ทำงานใน 3 ขั้นตอน
ขั้นแรก ระบบจะวิเคราะห์ฐานของโค้ดเพื่อระบุเส้นทางของผู้ใช้หลักที่จะสร้างเครื่องมือที่มีประโยชน์ เช่น การเล่นสื่อ การสร้างโน้ต หรือการส่งข้อความ
จากนั้นจะสร้างการประกาศ AppFunction ที่แสดงเส้นทางเหล่านั้นพร้อมกับการติดตั้งใช้งานและการสนับสนุนแพลตฟอร์มที่จำเป็น
เช่น Gradle และ KSP, XML ข้อมูลเมตาของแอป และ Dependency Injection
สุดท้าย ระบบจะปรับแต่งคำอธิบาย KDoc เนื่องจากนี่คือสิ่งที่ Agent ใช้ในการตัดสินใจว่าจะเรียกใช้ฟังก์ชันของคุณเมื่อใดและอย่างไร
นี่คือประเภทของผลลัพธ์ที่เรากำลังมองหา
นี่เป็นฟังก์ชัน addExpense สำหรับ Jetpacker
/**
* Adds an expense to a trip.
*
* @param tripId ID of the trip. If null, the active trip is used.
* @param category Expense category. If null, it's inferred from the title.
*/
@AppFunctionDeclaration(isDescribedByKDoc = true)
suspend fun addExpense(
title: String,
amount: Double,
currency: String,
tripId: String? = null,
category: String? = null,
): ExpenseSerializable = withContext(Dispatchers.IO) {
if (amount <= 0.0) {
throw AppFunctionInvalidArgumentException("amount must be greater than zero")
}
val trip = resolveTrip(tripId)
?: throw AppFunctionElementNotFoundException("No trip with ID $tripId")
val expense = Expense(trip.id, title, amount, currency,
category = category ?: inferExpenseCategory(title))
expenseDao.insertExpense(expense)
expense.toSerializable(remainingBudgetUsd(trip.id))
}คำอธิบายคือประโยคบรรทัดเดียวที่ขึ้นต้นด้วยคำกริยาและทรัพยากร เช่น เพิ่มค่าใช้จ่ายในการเดินทาง
ฟิลด์การเดินทางและหมวดหมู่เป็นค่า Null ได้เนื่องจากผู้ใช้อาจไม่ได้ระบุค่านี้เสมอไป แต่ฟังก์ชันจะแก้ไขค่าเหล่านี้ในเนื้อหา
และเมื่อคำขอไม่สำเร็จ ระบบจะแสดง AppFunctionException เพื่อให้เอเจนต์ทราบว่าเกิดข้อผิดพลาดอะไรขึ้น
ทีนี้เรามาดูเรื่องการทดสอบกัน
ตอนนี้ฟังก์ชันของคุณเป็นบริการที่ Agent สามารถใช้ได้แล้ว
ดังนั้นคุณจะต้องทดสอบ 2 ส่วน ได้แก่ ลักษณะการทำงานของฟังก์ชันและลักษณะการทำงานของ Agent
สำหรับฟังก์ชันของคุณ เราจะเริ่มด้วย Unit Test ที่คุณอาจเขียนไว้แล้ว แต่มี 2-3 สิ่งที่ควรคำนึงถึงเมื่อ Agent เป็นผู้เรียกใช้ฟังก์ชันจริงๆ
ก่อนอื่น ค่าเริ่มต้นของ Kotlin จะไม่มีผลที่นี่
พารามิเตอร์ที่ละไว้จะแสดงเป็น null, false หรือ 0
ดังนั้น ขีดจำกัดที่ก่อนหน้านี้มีค่าเริ่มต้นเป็น 10 จะมีค่าเป็น 0 ในที่นี้
การเรียกใช้ยังเริ่มต้นในเทรดหลักและการเรียกใช้ฟังก์ชันจากการทดสอบอาจไม่ชัดเจนเสมอไป ดังนั้นคุณจึงควรเรียกใช้ IO มาตรฐานใน Dispatcher ในเบื้องหลัง
เมื่อเกิดข้อผิดพลาด ให้แสดง AppFunctionException
ส่วนข้อผิดพลาดอื่นๆ จะส่งไปยัง Agent เป็นข้อผิดพลาดทั่วไปที่ไม่รู้จักพร้อมข้อความข้อยกเว้นดิบที่แนบมา
และข้อมูลที่ป้อนจะมาจากโมเดล ดังนั้นคุณควรทดสอบสถานการณ์ที่ผิดปกติ เช่น ช่องว่าง คำพ้องความหมายของดอลลาร์ เช่น USD และค่าที่อยู่นอกช่วง
ในฝั่ง Agent คุณจะต้องทราบว่า Agent เลือกเครื่องมือของคุณในเวลาที่เหมาะสมหรือไม่ และเครื่องมือดังกล่าวเรียกใช้ฟังก์ชันได้อย่างถูกต้องหรือไม่
Agent แต่ละตัวจะทำงานแตกต่างกันเล็กน้อย คุณจึงควรทดสอบฟังก์ชันกับ Agent ที่คุณคาดว่าจะใช้ฟังก์ชันเหล่านั้น
คุณสามารถใช้ Agent ตัวอย่างแบบโอเพนซอร์สของเราเพื่อทดสอบ หรือจะใช้ Gemini เพื่อทดสอบการผสานรวมนี้ก็ได้
หากต้องการดูวิธีที่ Gemini โต้ตอบกับ AppFunctions หรือต้องการเปิด Android MCP ในแอป Gemini คุณสามารถทำได้ด้วยแพลตฟอร์มแอปที่เชื่อมต่อ ซึ่งเราเปิดตัวไปเมื่อช่วงเช้าของวันนี้ และปัจจุบันอยู่ในเวอร์ชันตัวอย่าง
ดูข้อมูลเพิ่มเติมได้โดยดูการพูดคุยก่อนหน้านี้เกี่ยวกับการขยายแอปไปยัง Gemini และ Search ผ่านแพลตฟอร์มแอปที่เชื่อมต่อ
เมื่อคุณเข้าร่วมโปรแกรมเวอร์ชันตัวอย่างแล้ว กระบวนการทดสอบกับ Gemini จะมี 2 ขั้นตอน
ก่อนอื่น ใน Google Connected Console เราจะสร้างแอปที่เชื่อมต่อซึ่ง Gemini สามารถใช้งานได้จริง
และประการที่ 2 ใน Play Console เราจะต้องอัปโหลด App Bundle ของคุณเพื่อให้เข้าถึง AppFunctions ได้ และคุณสามารถเริ่มในแทร็กทดสอบภายในเพื่อให้ผู้ทดสอบตรวจสอบฟังก์ชันได้จริง
มาเริ่มกันที่ Google Connected Console
ในส่วนนี้ คุณจะสร้างแอปที่เชื่อมต่อโดยใช้ปุ่ม "สร้างแอป" ที่ด้านขวาบน
เมื่อสร้างแอปที่เชื่อมต่อ ให้เลือก Android MCP ในหน้าจอนี้
หากคุณกำลังสร้างการติดตั้งใช้งาน MCP ฝั่งเซิร์ฟเวอร์ คุณจะต้องอัปโหลดโค้ดเซิร์ฟเวอร์ที่นี่
เมื่อคลิก "ตั้งค่า" แอป Google Play ที่เชื่อมโยงกับบัญชีนักพัฒนาแอปจะแสดงที่นี่เพื่อให้คุณลิงก์กับแอปที่เชื่อมต่อ
หากยังไม่มีแอป Google Play คุณจะต้องสร้างแอปใน Play Console ก่อน
ในที่นี้ เรามี Jetpacker อยู่แล้ว จึงจะลิงก์กับแอปดังกล่าว
ถัดไป ให้เพิ่มรายละเอียดที่ผู้ใช้จะเห็นในส่วนหน้าของ Gemini เมื่อพยายามโต้ตอบกับแอปของคุณ
ซึ่งรวมถึงสิ่งต่างๆ เช่น ไอคอนแอป ชื่อที่แสดง ความสามารถที่แอปที่เชื่อมต่อมี และพรอมต์ตัวอย่างที่ผู้ใช้สามารถใช้เพื่อโต้ตอบกับแอปได้จริง
โดยควรเป็นคำอธิบายเพื่อช่วยแนะนำผู้ใช้ได้ดีที่สุด
เอาล่ะ นั่นคือขั้นตอนการตั้งค่าแอปที่เชื่อมต่อ แต่คุณอาจสงสัยว่าแอปที่เชื่อมต่อจะเข้าถึงฟังก์ชันต่างๆ ได้อย่างไร
เนื่องจากฟังก์ชันต่างๆ อยู่ในโค้ดแอป ตอนนี้เราจึงต้องไปที่ Play Console และอัปโหลด App Bundle และสร้างแทร็กทดสอบ เพื่อให้เราทดสอบกับผู้ใช้จริงได้
ดังนั้นในหน้าจอนี้ เราจะอัปโหลด App Bundle แล้วจะเห็นว่า Google Play สามารถตรวจจับ AppFunctions ได้ และคุณสามารถลิงก์ AppFunctions กับแอปที่เชื่อมต่อได้โดยตรง
ตอนนี้เราจะเห็นแอปที่เชื่อมต่อ Jetpack พร้อมฟังก์ชันที่ผสานรวมแล้ว
ถัดไป เราจะต้องเพิ่มผู้ทดสอบ เพื่อให้มีผู้ที่พร้อมตรวจสอบฟังก์ชันของคุณและตรวจสอบว่าฟังก์ชันทำงานได้อย่างถูกต้อง
ดังนั้น ให้คลิกเวอร์ชันของแอปที่เชื่อมต่อที่นี่
ในหน้าจอนี้ คุณสามารถเลือก "จัดการผู้ทดสอบ" และกำหนดค่ารายชื่อผู้ทดสอบที่จำกัดซึ่งจะเข้าถึงแอปที่เชื่อมต่อใน Gemini ได้เมื่อพร้อมใช้งาน
คุณเพิ่มผู้ทดสอบลงในแทร็กทดสอบนี้ได้ด้วยวิธีเดียวกับที่ใช้กับรุ่นอื่นๆ
ตรวจสอบว่าคุณมีรายชื่อผู้ทดสอบสำหรับแอปที่เชื่อมต่อเหมือนกับรายชื่อผู้ทดสอบสำหรับแอป Google Play
เอาล่ะ ทีนี้มาทดสอบการผสานรวมกัน
นี่คือแอป Jetpacker ในโทรศัพท์ของผู้ใช้ ซึ่งมีการบันทึกการเดินทางสำหรับการประชุมสุดยอดนี้ไว้แล้ว
ฉันจะเปิด Gemini เพื่อเชื่อมต่อแอป
ฉันต้องตรวจสอบว่าได้ติดตั้งแอปเวอร์ชันเดียวกับที่เราอัปโหลดใน Play ก่อนหน้านี้ไว้ในอุปกรณ์ และเราควรใช้บัญชีสำหรับ Gemini ที่เพิ่มลงในรายชื่อผู้ทดสอบ
ในกรณีนี้ ฉันจะพูดถึง Jetpacker และขอหมายเลขยืนยันของห้องพักโรงแรมในเมาน์เทนวิว
Gemini จะเรียกใช้ AppFunctions ของ Jetpacker บนอุปกรณ์และตอบว่า "RED11223 ที่โรงแรม Redwood Grove"
ถัดไป ฉันจะถามว่า "ฉันมีงบประมาณเหลือเท่าใดสำหรับ ADS"
และการเรียกใช้ฟังก์ชันจะแสดงผลเป็น 1,850 บาท
ตอนนี้ฉันต้องการเปลี่ยนแปลงบางอย่าง จึงจะขอให้ Gemini เพิ่มค่า Uber ไปสนามบินจำนวน $45 ลงในทริป ADS ของฉัน
Gemini จะเรียกใช้ฟังก์ชัน addExpense จากก่อนหน้านี้เพื่อเพิ่มข้อมูลนี้ลงในแผนการเดินทางของฉันโดยตรง
และเมื่อเปิด Jetpacker ค่าใช้จ่ายก็จะปรากฏขึ้น และงบประมาณจะลดลงตามนั้น
หากต้องการเริ่มสร้างแอปที่เชื่อมต่อสำหรับ Gemini วันนี้ โปรดสมัครรับสิทธิ์เข้าถึงแพลตฟอร์มแอปที่เชื่อมต่อ ซึ่งปัจจุบันอยู่ในเวอร์ชันตัวอย่าง
โอเค
มาสรุปกันเลย
เรากำลังอยู่ในช่วงเริ่มต้นของการเปลี่ยนแปลงครั้งใหญ่ในด้านการประมวลผลบนอุปกรณ์เคลื่อนที่ และถึงเวลาแล้วที่คุณจะต้องตรวจสอบว่าแอปของคุณพร้อมแล้ว
ดังนั้น ให้เริ่มด้วยการระบุเส้นทางของผู้ใช้ที่มีคุณค่าสูงซึ่งเป็นแกนหลักของแอป
แสดงแบ็กเอนด์ของระบบคลาวด์โดยใช้ MCP ฝั่งเซิร์ฟเวอร์ และใช้ Android MCP เพื่อทำงานกับข้อมูลที่เข้ารหัสจากต้นทางถึงปลายทาง, การดำเนินการในเครื่อง, เซ็นเซอร์ของอุปกรณ์ และสิ่งอื่นๆ ในอุปกรณ์
หากต้องการสร้างต้นแบบตอนนี้ คุณสามารถดาวน์โหลด Android Studio และ Android CLI ได้
คุณสามารถเพิ่มทรัพยากร Dependency ของ AppFunctions เวอร์ชันเบต้าของ AndroidX ลงในไฟล์บิลด์ และทดสอบฟังก์ชันแรกในอุปกรณ์ในเครื่องได้
หากต้องการลงทะเบียนเพื่อเริ่มใช้งาน Gemini ในเวอร์ชันที่ใช้งานจริงก่อนใคร คุณสามารถลงชื่อสมัครใช้ผ่านคิวอาร์โค้ดนี้ได้เลย
และแน่นอนว่าอย่าลืมดูการพูดคุยที่เกี่ยวข้องเหล่านี้เกี่ยวกับการทำงานกับแพลตฟอร์มแอปที่เชื่อมต่อ รวมถึงวิธีใช้ AppFunctions ภายในกับ ML Kit หรือกับ Agent ของคุณเองเพื่อขับเคลื่อน Agent ในแอปของคุณเอง
และสุดท้ายนี้ ขอขอบคุณเป็นอย่างยิ่งที่สละเวลา
เราตื่นเต้นที่จะได้เห็นประสบการณ์ที่ยอดเยี่ยมทั้งหมดที่คุณกำลังสร้างขึ้นสำหรับ Android
implementation(libs.androidx.appfunctions)
ksp(libs.androidx.appfunctions.compiler)@RequiresApi(36)
@AndroidEntryPoint
@AppFunctionServiceEntryPoint(
serviceName = "JetPackerAppFunctionService",
appFunctionXmlFileName = "jetpacker_app_function_service"
)
abstract class BaseJetPackerAppFunctionService : AppFunctionService() {
@Inject internal lateinit var tripDao: TripDao
@Inject internal lateinit var expenseDao: ExpenseDao
.../** Represents an event in a trip itinerary. */
@AppFunctionSerializable(isDescribedByKDoc = true)
data class TimelineEventSerializable(
/** The event's unique identifier. */
val id: String,
/** The trip ID this event belongs to. */
val tripId: String,
/** The type of event (e.g., TRANSPORTATION, ACCOMMODATION, FOOD_AND_DRINK). */
val type: String,
/** The timestamp of the event. */
val timestamp: Long,
/** The title of the event. */
val title: String,
/** The location of the event. */
val location: String,
/** A description of the event. */
val description: String?,
/** The title of the trip this event belongs to. */
val tripTitle: String? = null,
/** Optional title of a matched itinerary event when added as a reminder. */
val matchedEventTitle: String? = null,
/** Execution status of the tool call (e.g., SUCCESS). */
val status: String? = "SUCCESS",
/** Human-readable completion message instructing the agent not to repeat the call. */
val message: String? = null,
)@AppFunctionDeclaration(isDescribedByKDoc = true)
suspend fun getItinerary(tripId: String? = null): List<TimelineEventSerializable> {
return withContext(Dispatchers.IO) {
val trip = resolveTrip(tripId = tripId)
val targetTripId = trip?.id ?: ActiveTripSession.activeTripId ?: tripId?.takeIf { it.isNotBlank() } ?: "2026-1"
val dbEvents = eventDao.getEventsForTrip(targetTripId).first()
val events =
if (dbEvents.isNotEmpty()) { dbEvents } else {
val fallback = DummyData.events.filter { it.tripId == targetTripId }
if (fallback.isNotEmpty()) { eventDao.insertEvents(fallback) }
fallback
}
events.map { it.toSerializable(tripTitle = trip?.title) }
}
}/**
* Retrieves the itinerary events (with their eventId, title, type, and location) for a specific
* trip. Note: getItinerary returns event summaries only; call getEventDetails(eventId) with a
* specific event's ID to retrieve ticket prices, hotel nightly rates, baggage allowance, or
* visitor tips.
*
* @param tripId Optional ID of the trip (e.g., '2026-1'). Defaults to the active trip if omitted.
* @return A list of events in the itinerary.
*/
@AppFunctionDeclaration(isDescribedByKDoc = true)
suspend fun getItinerary(tripId: String? = null): List<TimelineEventSerializable> {
return withContext(Dispatchers.IO) {
val trip = resolveTrip(tripId = tripId)
val targetTripId = trip?.id ?: ActiveTripSession.activeTripId ?: tripId?.takeIf { it.isNotBlank() } ?: "2026-1"
val dbEvents = eventDao.getEventsForTrip(targetTripId).first()
.../**
* @param tripId Optional ID of the trip (e.g., '2026-1').
* Defaults to the active trip if omitted.
* @return A list of events in the itinerary.
*/
@AppFunctionDeclaration(isDescribedByKDoc = true)
suspend fun getItinerary(tripId: String? = null): List<TimelineEventSerializable> {
return withContext(Dispatchers.IO) {
// resolveTrip defaults to the active trip if tripId == null
val trip = resolveTrip(tripId = tripId)@AppFunctionDeclaration(isDescribedByKDoc = true)
suspend fun getItinerary(tripId: String? = null): List<TimelineEventSerializable> {
return withContext(Dispatchers.IO) {
val trip = resolveTrip(tripId = tripId)
val targetTripId = trip?.id ?: ActiveTripSession.activeTripId ?:
tripId?.takeIf { it.isNotBlank() } ?: "2026-1"
val dbEvents = eventDao.getEventsForTrip(targetTripId).first()
val events =
if (dbEvents.isNotEmpty()) { dbEvents } else {
val fallback = DummyData.events.filter { it.tripId == targetTripId }
...@AppFunctionDeclaration(isDescribedByKDoc = true)
suspend fun send(
message: Message
): Result {
...
throw AppFunctionException(
errorCode = AppFunctionException.ErrorCode.ERROR_INVALID_ARGUMENT,
errorMessage = "To address has invalid syntax!"
)
}<AppFunctionAppMetadata
xmlns:appfn="http://schemas.android.com/apk/androidx.appfunctions"
appfn:description="This app manages trips and travel expenses.
Operational Patterns:
- Use 'getAllTrips' to find the correct 'tripId' before
adding expenses or managing trip details.
Constraints:
- Expenses must be associated with a valid trip ID."
appfn:displayDescription="@string/user_visible_description" />viewModelScope.launch {
val newState =
if (enabled) {
AppFunctionManager.APP_FUNCTION_STATE_ENABLED
} else {
AppFunctionManager.APP_FUNCTION_STATE_DISABLED
}
appFunctionManager?.setAppFunctionEnabled(id, newState)
}@AppFunctionSignature(
scope = AppFunctionMetadata.SCOPE_ACTIVITY,
appFunctionXmlFileName = "cart_functions"
)
fun interface AddCurrentItemToCart {
/** Adds the item currently shown on screen to the cart. Returns the new cart size. */
suspend fun addToCart(quantity: Int): Int
}val addToCartAppFunctionAdapter =
appFunctionManager.getHandleAppFunctionRequestAdapter<AddCurrentItemToCart>()
coroutineScope.launch {
appFunctionManager.handleAppFunction(
addToCartAppFunctionAdapter.adapt { quantity ->
val newCartSize = cart.addItem(quantity)
newCartSize
}
)
}@AppFunctionDeclaration(isDescribedByKDoc = true)
@AppFunctionAccessLevel(
level = AppFunctionMetadata.ACCESS_LEVEL_SELF,
isCompatEnforcementEnabled = true,
)
suspend fun getItinerary(tripId: String? = null): List<TimelineEventSerializable> {
return withContext(Dispatchers.IO) {
...android skills add appfunctions/**
* Adds an expense to a trip.
*
* @param tripId ID of the trip. If null, the active trip is used.
* @param category Expense category. If null, it's inferred from the title.
*/
@AppFunctionDeclaration(isDescribedByKDoc = true)
suspend fun addExpense(
title: String,
amount: Double,
currency: String,
tripId: String? = null,
category: String? = null,
): ExpenseSerializable = withContext(Dispatchers.IO) {
if (amount <= 0.0) {
throw AppFunctionInvalidArgumentException("amount must be greater than zero")
}
val trip = resolveTrip(tripId)
?: throw AppFunctionElementNotFoundException("No trip with ID $tripId")
val expense = Expense(trip.id, title, amount, currency,
category = category ?: inferExpenseCategory(title))
expenseDao.insertExpense(expense)
expense.toSerializable(remainingBudgetUsd(trip.id))
}