From f55dd4109e96e473c1bca04ed99cc4e47b6fb1a8 Mon Sep 17 00:00:00 2001 From: szymonrybczak Date: Wed, 24 Sep 2025 12:44:01 +0200 Subject: [PATCH] feat: add more tools to example app --- apps/example-apple/app.json | 6 + apps/example-apple/package.json | 1 + apps/example-apple/src/screens/LLMScreen.tsx | 9 ++ apps/example-apple/src/tools.ts | 132 ++++++++++++++++++- bun.lock | 3 + 5 files changed, 150 insertions(+), 1 deletion(-) diff --git a/apps/example-apple/app.json b/apps/example-apple/app.json index fb4685c1..2d8b4b75 100644 --- a/apps/example-apple/app.json +++ b/apps/example-apple/app.json @@ -34,6 +34,12 @@ "calendarPermission": "The app needs to access your calendar." } ], + [ + "expo-contacts", + { + "contactsPermission": "Allow $(PRODUCT_NAME) to access your contacts." + } + ], "react-native-bottom-tabs", [ "expo-build-properties", diff --git a/apps/example-apple/package.json b/apps/example-apple/package.json index 75a50b3e..d8c6466f 100644 --- a/apps/example-apple/package.json +++ b/apps/example-apple/package.json @@ -25,6 +25,7 @@ "expo-build-properties": "~0.14.8", "expo-calendar": "~14.1.4", "expo-clipboard": "~7.1.5", + "expo-contacts": "~14.2.5", "expo-document-picker": "~13.1.6", "expo-status-bar": "2.2.3", "nativewind": "^4.1.23", diff --git a/apps/example-apple/src/screens/LLMScreen.tsx b/apps/example-apple/src/screens/LLMScreen.tsx index ba605f15..7ce6ee9a 100644 --- a/apps/example-apple/src/screens/LLMScreen.tsx +++ b/apps/example-apple/src/screens/LLMScreen.tsx @@ -16,7 +16,10 @@ import Animated, { useDerivedValue } from 'react-native-reanimated' import { checkCalendarEvents, createCalendarEvent, + getBatteryLevel, getCurrentTime, + listContacts, + openEmail, } from '../tools' const apple = createAppleProvider({ @@ -24,6 +27,9 @@ const apple = createAppleProvider({ getCurrentTime, createCalendarEvent, checkCalendarEvents, + getBatteryLevel, + listContacts, + openEmail, }, }) @@ -78,6 +84,9 @@ export default function LLMScreen() { getCurrentTime, createCalendarEvent, checkCalendarEvents, + getBatteryLevel, + listContacts, + openEmail, }, }) diff --git a/apps/example-apple/src/tools.ts b/apps/example-apple/src/tools.ts index 038af745..74ccc443 100644 --- a/apps/example-apple/src/tools.ts +++ b/apps/example-apple/src/tools.ts @@ -1,5 +1,8 @@ import { tool } from 'ai' +import * as Battery from 'expo-battery' import * as Calendar from 'expo-calendar' +import * as Contacts from 'expo-contacts' +import { Alert, Linking } from 'react-native' import { z } from 'zod' /** @@ -74,6 +77,133 @@ export const getCurrentTime = tool({ description: 'Get current time and date', inputSchema: z.object({}), execute: async () => { - return `Current time is: ${new Date().toUTCString()}` + const now = new Date() + const currentDay = now.toDateString() + const currentTime = now.toTimeString() + + return `Current day: ${currentDay}\nCurrent time: ${currentTime}` + }, +}) + +/** + * Get device battery level + */ +export const getBatteryLevel = tool({ + description: 'Get current battery level of the device', + inputSchema: z.object({}), + execute: async () => { + const batteryLevel = await Battery.getBatteryLevelAsync() + const batteryState = await Battery.getBatteryStateAsync() + + const percentage = Math.round(batteryLevel * 100) + const state = Battery.BatteryState[batteryState] + + return { + level: percentage, + state, + message: `Battery level: ${percentage}% (${state})`, + } + }, +}) + +/** + * List all contacts from the device + */ +export const listContacts = tool({ + description: 'List all contacts from the device with their basic information', + inputSchema: z.object({ + limit: z + .number() + .optional() + .describe('Maximum number of contacts to return'), + fields: z + .array(z.string()) + .optional() + .describe( + 'Specific fields to retrieve (firstName, lastName, emails, phoneNumbers, etc.)' + ), + }), + execute: async ({ limit, fields }) => { + // Request permissions first + const { status } = await Contacts.requestPermissionsAsync() + + if (status !== 'granted') { + return { + error: 'Contacts permission not granted', + contacts: [], + message: 'Permission to access contacts was denied', + } + } + + // Define which fields to retrieve + const contactFields: Contacts.FieldType[] = fields + ? fields.map((field) => { + // Map common field names to Contacts.Fields constants + const fieldMap: Record = { + firstName: Contacts.Fields.FirstName, + lastName: Contacts.Fields.LastName, + emails: Contacts.Fields.Emails, + phoneNumbers: Contacts.Fields.PhoneNumbers, + company: Contacts.Fields.Company, + jobTitle: Contacts.Fields.JobTitle, + name: Contacts.Fields.Name, + } + return fieldMap[field] || (field as Contacts.FieldType) + }) + : [ + Contacts.Fields.FirstName, + Contacts.Fields.LastName, + Contacts.Fields.Emails, + Contacts.Fields.PhoneNumbers, + Contacts.Fields.Company, + ] + + try { + const { data } = await Contacts.getContactsAsync({ + fields: contactFields, + pageSize: limit || 100, + pageOffset: 0, + }) + + const processedContacts = data.map((contact) => ({ + id: contact.id, + firstName: contact.firstName || '', + lastName: contact.lastName || '', + name: + contact.name || + `${contact.firstName || ''} ${contact.lastName || ''}`.trim(), + emails: contact.emails?.map((email) => email.email) || [], + phoneNumbers: contact.phoneNumbers?.map((phone) => phone.number) || [], + company: contact.company || '', + jobTitle: contact.jobTitle || '', + })) + + return { + contacts: processedContacts, + count: processedContacts.length, + message: `Found ${processedContacts.length} contacts`, + } + } catch (error) { + return { + error: 'Failed to retrieve contacts', + contacts: [], + message: `Error: ${error instanceof Error ? error.message : 'Unknown error'}`, + } + } + }, +}) + +/** + * Open email app with pre-filled recipient + */ +export const openEmail = tool({ + description: 'Open email app', + inputSchema: z.object({ + email: z.string().describe('Email address to send to'), + subject: z.string().optional().describe('Email subject line'), + }), + execute: async ({ email, subject }) => { + Linking.openURL(`mailto:${email}?subject=${subject}`) + return { message: 'Email app opened' } }, }) diff --git a/bun.lock b/bun.lock index 56fe040a..e5fbad5c 100644 --- a/bun.lock +++ b/bun.lock @@ -45,6 +45,7 @@ "expo-build-properties": "~0.14.8", "expo-calendar": "~14.1.4", "expo-clipboard": "~7.1.5", + "expo-contacts": "~14.2.5", "expo-document-picker": "~13.1.6", "expo-status-bar": "2.2.3", "nativewind": "^4.1.23", @@ -1268,6 +1269,8 @@ "expo-constants": ["expo-constants@17.1.7", "", { "dependencies": { "@expo/config": "~11.0.12", "@expo/env": "~1.0.7" }, "peerDependencies": { "expo": "*", "react-native": "*" } }, "sha512-byBjGsJ6T6FrLlhOBxw4EaiMXrZEn/MlUYIj/JAd+FS7ll5X/S4qVRbIimSJtdW47hXMq0zxPfJX6njtA56hHA=="], + "expo-contacts": ["expo-contacts@14.2.5", "", { "peerDependencies": { "expo": "*", "react": "*", "react-native": "*" } }, "sha512-yiVmXrLKBCcBkwsaHFlbs0f7UwE2t7Aa1NBOK4Y06ya0Y5WyE6I/P5ZAtWNjXnKmbV7iNKAiPUzqVaNazhCtWA=="], + "expo-document-picker": ["expo-document-picker@13.1.6", "", { "peerDependencies": { "expo": "*" } }, "sha512-8FTQPDOkyCvFN/i4xyqzH7ELW4AsB6B3XBZQjn1FEdqpozo6rpNJRr7sWFU/93WrLgA9FJEKpKbyr6XxczK6BA=="], "expo-file-system": ["expo-file-system@18.1.11", "", { "peerDependencies": { "expo": "*", "react-native": "*" } }, "sha512-HJw/m0nVOKeqeRjPjGdvm+zBi5/NxcdPf8M8P3G2JFvH5Z8vBWqVDic2O58jnT1OFEy0XXzoH9UqFu7cHg9DTQ=="],