Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 48 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
name: Build Verification

"on":
push:
branches:
- main
pull_request:
branches:
- main

jobs:
android-build:
runs-on: ubuntu-latest
defaults:
run:
working-directory: samples/CameraAccessAndroid
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Setup Java 17
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: "17"

- name: Create Android secrets file
run: cp app/src/main/java/com/meta/wearable/dat/externalsampleapps/cameraaccess/Secrets.kt.example app/src/main/java/com/meta/wearable/dat/externalsampleapps/cameraaccess/Secrets.kt

- name: Build Android sample
run: ./gradlew :app:assembleDebug -Dorg.gradle.java.home="$JAVA_HOME"
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

ios-build:
runs-on: macos-15
defaults:
run:
working-directory: samples/CameraAccess
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Create iOS secrets file
run: cp CameraAccess/Secrets.swift.example CameraAccess/Secrets.swift

- name: Build iOS sample
run: xcodebuild -project CameraAccess.xcodeproj -scheme CameraAccess -destination 'generic/platform=iOS Simulator' CODE_SIGNING_ALLOWED=NO build
16 changes: 15 additions & 1 deletion samples/CameraAccess/CameraAccess/Gemini/GeminiConfig.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,13 @@ enum GeminiConfig {
static let videoFrameInterval: TimeInterval = 1.0
static let videoJPEGQuality: CGFloat = 0.5

static var systemInstruction: String { SettingsManager.shared.geminiSystemPrompt }
static var systemInstruction: String {
let userPrompt = SettingsManager.shared.geminiSystemPrompt
if userPrompt != defaultSystemInstruction {
return userPrompt
}
return isOpenClawConfigured ? defaultSystemInstruction : defaultSystemInstructionWithoutTools
}

static let defaultSystemInstruction = """
You are an AI assistant for someone wearing Meta Ray-Ban smart glasses. You can see through their camera and have a voice conversation. Keep responses concise and natural.
Expand Down Expand Up @@ -42,6 +48,14 @@ enum GeminiConfig {
For messages, confirm recipient and content before delegating unless clearly urgent.
"""

static let defaultSystemInstructionWithoutTools = """
You are an AI assistant for someone wearing Meta Ray-Ban smart glasses. You can see through their camera and have a voice conversation. Keep responses concise and natural.

In this session, external actions are unavailable. You cannot send messages, search the web, manage lists, set reminders, or control apps/devices.

If the user asks for those actions, clearly say OpenClaw is not configured and ask them to enable it in Settings. Never pretend an action was completed.
"""

// User-configurable values (Settings screen overrides, falling back to Secrets.swift)
static var apiKey: String { SettingsManager.shared.geminiAPIKey }
static var openClawHost: String { SettingsManager.shared.openClawHost }
Expand Down
67 changes: 36 additions & 31 deletions samples/CameraAccess/CameraAccess/Gemini/GeminiLiveService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -164,39 +164,44 @@ class GeminiLiveService: ObservableObject {
}

private func sendSetupMessage() {
let setup: [String: Any] = [
"setup": [
"model": GeminiConfig.model,
"generationConfig": [
"responseModalities": ["AUDIO"],
"thinkingConfig": [
"thinkingBudget": 0
]
],
"systemInstruction": [
"parts": [
["text": GeminiConfig.systemInstruction]
]
],
"tools": [
[
"functionDeclarations": ToolDeclarations.allDeclarations()
]
],
"realtimeInputConfig": [
"automaticActivityDetection": [
"disabled": false,
"startOfSpeechSensitivity": "START_SENSITIVITY_HIGH",
"endOfSpeechSensitivity": "END_SENSITIVITY_LOW",
"silenceDurationMs": 500,
"prefixPaddingMs": 40
],
"activityHandling": "START_OF_ACTIVITY_INTERRUPTS",
"turnCoverage": "TURN_INCLUDES_ALL_INPUT"
var setupPayload: [String: Any] = [
"model": GeminiConfig.model,
"generationConfig": [
"responseModalities": ["AUDIO"],
"thinkingConfig": [
"thinkingBudget": 0
]
],
"systemInstruction": [
"parts": [
["text": GeminiConfig.systemInstruction]
]
],
"realtimeInputConfig": [
"automaticActivityDetection": [
"disabled": false,
"startOfSpeechSensitivity": "START_SENSITIVITY_HIGH",
"endOfSpeechSensitivity": "END_SENSITIVITY_LOW",
"silenceDurationMs": 500,
"prefixPaddingMs": 40
],
"inputAudioTranscription": [:] as [String: Any],
"outputAudioTranscription": [:] as [String: Any]
"activityHandling": "START_OF_ACTIVITY_INTERRUPTS",
"turnCoverage": "TURN_INCLUDES_ALL_INPUT"
],
"inputAudioTranscription": [:] as [String: Any],
"outputAudioTranscription": [:] as [String: Any]
]

if GeminiConfig.isOpenClawConfigured {
setupPayload["tools"] = [
[
"functionDeclarations": ToolDeclarations.allDeclarations()
]
]
}

let setup: [String: Any] = [
"setup": setupPayload
]
sendJSON(setup)
}
Expand Down
11 changes: 11 additions & 0 deletions samples/CameraAccess/CameraAccess/OpenClaw/ToolCallRouter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,17 @@ class ToolCallRouter {
_ call: GeminiFunctionCall,
sendResponse: @escaping ([String: Any]) -> Void
) {
guard GeminiConfig.isOpenClawConfigured else {
let response = buildToolResponse(
callId: call.id,
name: call.name,
result: .failure("OpenClaw is not configured. Configure OpenClaw in Settings to enable tool calls.")
)
NSLog("[ToolCall] Fast-fail %@ (id: %@): OpenClaw is not configured", call.name, call.id)
sendResponse(response)
return
}

let callId = call.id
let callName = call.name

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,16 @@ object GeminiConfig {
const val VIDEO_JPEG_QUALITY = 50

val systemInstruction: String
get() = SettingsManager.geminiSystemPrompt
get() {
val prompt = SettingsManager.geminiSystemPrompt
val isUsingDefaultPrompt = prompt == SettingsManager.DEFAULT_SYSTEM_PROMPT

return if (!isOpenClawConfigured && isUsingDefaultPrompt) {
SettingsManager.DEFAULT_SYSTEM_PROMPT_WITHOUT_TOOLS
} else {
prompt
}
}

val apiKey: String
get() = SettingsManager.geminiAPIKey
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -208,9 +208,11 @@ class GeminiLiveService {
put("text", GeminiConfig.systemInstruction)
}))
})
put("tools", JSONArray().put(JSONObject().apply {
put("functionDeclarations", ToolDeclarations.allDeclarationsJSON())
}))
if (GeminiConfig.isOpenClawConfigured) {
put("tools", JSONArray().put(JSONObject().apply {
put("functionDeclarations", ToolDeclarations.allDeclarationsJSON())
}))
}
put("realtimeInputConfig", JSONObject().apply {
put("automaticActivityDetection", JSONObject().apply {
put("disabled", false)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.meta.wearable.dat.externalsampleapps.cameraaccess.openclaw

import android.util.Log
import com.meta.wearable.dat.externalsampleapps.cameraaccess.gemini.GeminiConfig
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
Expand All @@ -26,6 +27,14 @@ class ToolCallRouter(

Log.d(TAG, "Received: $callName (id: $callId) args: ${call.args}")

if (!GeminiConfig.isOpenClawConfigured) {
val error = "OpenClaw is not configured. Add host and gateway token in Settings to enable tool calls."
Log.w(TAG, "Fast-fail tool call $callName (id: $callId): $error")
bridge.setToolCallStatus(ToolCallStatus.Failed(callName, error))
sendResponse(buildToolResponse(callId, callName, ToolResult.Failure(error)))
return
}

val job = scope.launch {
val taskDesc = call.args["task"]?.toString() ?: call.args.toString()
val result = bridge.delegateTask(task = taskDesc, toolName = callName)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,4 +73,10 @@ IMPORTANT: Before calling execute, ALWAYS speak a brief acknowledgment first. Fo
Never call execute silently -- the user needs verbal confirmation that you heard them and are working on it. The tool may take several seconds to complete, so the acknowledgment lets them know something is happening.

For messages, confirm recipient and content before delegating unless clearly urgent."""

const val DEFAULT_SYSTEM_PROMPT_WITHOUT_TOOLS = """You are an AI assistant for someone wearing Meta Ray-Ban smart glasses. You can see through their camera and have a voice conversation. Keep responses concise and natural.

CRITICAL: You have NO memory, NO storage, and NO ability to take actions on your own. You cannot remember things, keep lists, set reminders, search the web, send messages, or do anything persistent. You are ONLY a voice interface.

If a user asks you to do something that requires external actions, briefly explain that action tools are unavailable right now and offer a practical manual alternative."""
}