公式ドキュメントからダウンロードできる CoffeeShop ゲームのサンプルコードを読み解いて、自分でも実際に作ってどの程度正確に動作するかも確認してみたいと思います。最終的にはそれを使って類似するゲームをリリースするところまで実践していければと思います。
独学・初心者のため、コードの見解など間違っている箇所があれば勉強になりますのでコメントをいただけると幸いです。
前回までの内容↓
ContactsTool.swift ファイル
これは、以前勉強した Foundation Model のすべてがオンデバイスで処理される Tool プロトコルを使用している。特徴としてはプライバシーを保護しながらデバイス内のデータにアクセスすることが可能。
フレームワークとしてユーザー端末の連絡先のアクセスするためのフレームワークとしてContacts を追加。↓
import FoundationModels
import Contacts
Beta版 の際より調整された箇所が。
まずは Tool プロトコル。Beta版 までは Error になり、安全に並行処理間でデータを共有できることを示すプロトコル Sendable を要求されていましたが RC版 からは使用可能になりました。
これにより次のように書くことができる↓
struct ContactsTool: Tool {
// ツールの一意識別子(短く・読みやすく)
let name = "連絡先を見つける"
// AIがこのツールの用途を理解するための1文程度の簡潔な説明。実際の詳細は含めない(プロンプトに直接組み込まれるため)
let description = """
今月生まれの連絡先を取得します。\
今日の日付は\(Date().formatted(date: .complete, time: .omitted))
"""
// defaultName: エラー時のフォールバック名
let defaultName = "ナオミ"
@Generable
struct Arguments {
let month: Int
}
さらに、
@Generable
struct Arguments {
let month: Int
}
↑ この箇所で @Generable を使いスキーマを生成し、指定した月の関する構造を生成しています。
これを Call メソッドを使ってこの関数を呼び出します。
func call(arguments: Arguments) async -> String {
do {
// その人の連絡先にアクセスする許可をリクエストします。
let store = CNContactStore()
try await store.requestAccess(for: .contacts)
// 必要最小限のデータのみ取得(プライバシー配慮)
let keysToFatch = [CNContactGivenNameKey, CNContactBirthdayKey] as [CNKeyDescriptor]
let request = CNContactFetchRequest(keysToFetch: keysToFatch)
// モデルが引数で指定した範囲内の誕生日を持つ連絡先のリストを取得します。
var contacts: [CNContact] = []
try store.enumerateContacts(with: request) { contact, stop in
if let month = contact.birthday?.month {
if arguments.month == month { // AI指定の月と一致
contacts.append(contact)
}
}
}
guard let pickedContact = contacts.shuffled().first else {
Logging.general.log("Contact Tool: 連絡先が見つかりませんでした。")
return defaultName
}
Logging.general.log("Contact Tool: 連絡先が見つかりました。 \(pickedContact.givenName)")
return pickedContact.givenName
} catch {
Logging.general.log("プレイヤーの連絡先にアクセスする際にツール呼び出しエラーが発生しました。: \(error)")
return defaultName
}
}
プライバシー保護設計として名前と誕生日のみ取得し電話番号、住所等の機密情報は取得しない。またそれらはユーザー同意を得てからアクセスするようにする(ここはこれだけ念押しされているのでもちろん実装しないと確実にリジェクトされるだろうな)。
その上で、処理の流れとして全連絡先を順次確認し誕生日データが存在するかチェック。AIが指定した月と一致する人のみを抽出するようにしている。
また、Error ハンドリングでは該当者なしの場合はデフォルト名で継続し、アクセス拒否の際も同じくデフォルト名で継続する(拒否時にエラーでクラッシュする場合の設計だとリジェクトするよと Apple がいっているようなものかも)。
なので簡易とはいえ完全なエラー時もにゲーム体験を中断させないようにしてゲーム続行を優先できるように設計している。
以前の勉強時に Call メソッド時戻り値 ToolOutput について今回のアップデートで対応したのではなく、公式ドキュメントのファイルをみる限り、PromptRepresentable 「プロンプトとして表現できるデータ」 String / Int / Double / Bool やその他基本型などで対応するのが正解みたい。なので今回は String 型で対応している。
CalendarTool.swift ファイル
これは、カレンダー内のイベント情報を取得してAIの生成に利用するためのファイル。
import FoundationModels
import EventKit
フレームワークとして EventKit を追加。これでユーザー端末のカレンダーにアクセスできる。
struct CalendarTool: Tool {
let name = "カレンダーイベントを取得する。"
let description: String
let contactName: String
init(contactName: String) {
self.contactName = contactName
description = """
プレイヤーのカレンダーからイベントを取得するには \(contactName). \
今日は \(Date().formatted(date: .complete, time: .omitted))
"""
}
@Generable
struct Arguments {
let day: Int
let month: Int
let year: Int
}
ポイントとしては contactName に基づくツール特化。
現在日付を含む動的説明文でAIの判断を支援し NPC ごとに個別のツールインスタンスを生成。
これを同じく Call メソッドを使ってこの関数を呼び出します↓
func call(arguments: Arguments) async -> String {
do {
Logging.general.log("カレンダーツールの呼び出し")
// カレンダー イベントへのアクセス許可をリクエストします。
let eventStore = EKEventStore()
try await eventStore.requestFullAccessToEvents()
let calenders = eventStore.calendars(for: .event)
// モデルが渡す引数から開始日と終了日を構築します。
let dateComponents = DateComponents(
year: arguments.year,
month: arguments.month,
day: arguments.day
)
let startDate = Calendar.current.date(from: dateComponents)!
let endDate = Calendar.current.date(byAdding: .day, value: 1, to: startDate)!
let predicate = eventStore.predicateForEvents(
withStart: startDate,
end: endDate,
calendars: calenders
)
// カレンダーで、その人物と生成されたNPCとのイベントを確認します。
let events = eventStore.events(matching: predicate)
let relevantEvents = events.filter { event in
event.attendees?.contains(where: {$0.name == contactName }) == true
}
if relevantEvents.isEmpty {
return "プレイヤーは今日 \(events.count) 件のイベントに参加します, しかし、あなたと \(contactName) とのイベントはありません。"
} else {
return """
\(contactName) とのイベント:
\((relevantEvents.map { $0.startDate.formatted() + ": " + $0.title }).joined(separator: "\n"))
"""
}
} catch {
Logging.general.log("Error: \(error)")
return "申し訳ありません。カレンダーが表示されませんでした。"
}
}
この関数の主なポイントとしては、検索範囲の精密性として指定日の00:00から23:59:59までに限定。全カレンダーを横断検索しさらに効率的な述語ベース検索を行う。
マッチング条件としてイベントの参加者リストを確認し、contactNameと完全一致する参加者を検索。(例:contactName = "田中太郎"なら田中太郎が参加者の予定のみ抽出)
エラーハンドリングとしては、ContactsTool 同様に完全なエラー時もにゲーム体験を中断させないようにしてゲーム続行を優先できるように設計してある。AIでの生成には Error はつきものなのでこの点は特に念押ししている感じかな。
とりあえず今回はここまで。
書き方・解釈に間違いがあれば勉強になりますのでぜひコメントお願いします!
引き続きAIApp開発に向けて勉強した内容をまとめつつ記録していきたいと思います。