From bde3e06e0357969c9b5d3a67d9bdb7d0751920bf Mon Sep 17 00:00:00 2001 From: root Date: Fri, 27 Feb 2026 00:49:48 +0800 Subject: [PATCH 1/2] fix: conditionally include tools when OpenClaw is configured (fixes #12) --- .github/workflows/build.yml | 48 +++++++++++++ .../CameraAccess/Gemini/GeminiConfig.swift | 16 ++++- .../Gemini/GeminiLiveService.swift | 67 ++++++++++--------- .../OpenClaw/ToolCallRouter.swift | 11 +++ .../cameraaccess/gemini/GeminiConfig.kt | 11 ++- .../cameraaccess/gemini/GeminiLiveService.kt | 8 ++- .../cameraaccess/openclaw/ToolCallRouter.kt | 9 +++ .../cameraaccess/settings/SettingsManager.kt | 6 ++ 8 files changed, 140 insertions(+), 36 deletions(-) create mode 100644 .github/workflows/build.yml diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 00000000..615b90dd --- /dev/null +++ b/.github/workflows/build.yml @@ -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 + 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 diff --git a/samples/CameraAccess/CameraAccess/Gemini/GeminiConfig.swift b/samples/CameraAccess/CameraAccess/Gemini/GeminiConfig.swift index 5c124f66..43d31e14 100644 --- a/samples/CameraAccess/CameraAccess/Gemini/GeminiConfig.swift +++ b/samples/CameraAccess/CameraAccess/Gemini/GeminiConfig.swift @@ -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. @@ -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 } diff --git a/samples/CameraAccess/CameraAccess/Gemini/GeminiLiveService.swift b/samples/CameraAccess/CameraAccess/Gemini/GeminiLiveService.swift index 38b45009..87e55637 100644 --- a/samples/CameraAccess/CameraAccess/Gemini/GeminiLiveService.swift +++ b/samples/CameraAccess/CameraAccess/Gemini/GeminiLiveService.swift @@ -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) } diff --git a/samples/CameraAccess/CameraAccess/OpenClaw/ToolCallRouter.swift b/samples/CameraAccess/CameraAccess/OpenClaw/ToolCallRouter.swift index d81d20fb..239fcb3c 100644 --- a/samples/CameraAccess/CameraAccess/OpenClaw/ToolCallRouter.swift +++ b/samples/CameraAccess/CameraAccess/OpenClaw/ToolCallRouter.swift @@ -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 diff --git a/samples/CameraAccessAndroid/app/src/main/java/com/meta/wearable/dat/externalsampleapps/cameraaccess/gemini/GeminiConfig.kt b/samples/CameraAccessAndroid/app/src/main/java/com/meta/wearable/dat/externalsampleapps/cameraaccess/gemini/GeminiConfig.kt index 10ba908e..fac1bf0b 100644 --- a/samples/CameraAccessAndroid/app/src/main/java/com/meta/wearable/dat/externalsampleapps/cameraaccess/gemini/GeminiConfig.kt +++ b/samples/CameraAccessAndroid/app/src/main/java/com/meta/wearable/dat/externalsampleapps/cameraaccess/gemini/GeminiConfig.kt @@ -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 diff --git a/samples/CameraAccessAndroid/app/src/main/java/com/meta/wearable/dat/externalsampleapps/cameraaccess/gemini/GeminiLiveService.kt b/samples/CameraAccessAndroid/app/src/main/java/com/meta/wearable/dat/externalsampleapps/cameraaccess/gemini/GeminiLiveService.kt index a320f9ee..6ae0ea6c 100644 --- a/samples/CameraAccessAndroid/app/src/main/java/com/meta/wearable/dat/externalsampleapps/cameraaccess/gemini/GeminiLiveService.kt +++ b/samples/CameraAccessAndroid/app/src/main/java/com/meta/wearable/dat/externalsampleapps/cameraaccess/gemini/GeminiLiveService.kt @@ -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) diff --git a/samples/CameraAccessAndroid/app/src/main/java/com/meta/wearable/dat/externalsampleapps/cameraaccess/openclaw/ToolCallRouter.kt b/samples/CameraAccessAndroid/app/src/main/java/com/meta/wearable/dat/externalsampleapps/cameraaccess/openclaw/ToolCallRouter.kt index 7763656f..6a0c38c4 100644 --- a/samples/CameraAccessAndroid/app/src/main/java/com/meta/wearable/dat/externalsampleapps/cameraaccess/openclaw/ToolCallRouter.kt +++ b/samples/CameraAccessAndroid/app/src/main/java/com/meta/wearable/dat/externalsampleapps/cameraaccess/openclaw/ToolCallRouter.kt @@ -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 @@ -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) diff --git a/samples/CameraAccessAndroid/app/src/main/java/com/meta/wearable/dat/externalsampleapps/cameraaccess/settings/SettingsManager.kt b/samples/CameraAccessAndroid/app/src/main/java/com/meta/wearable/dat/externalsampleapps/cameraaccess/settings/SettingsManager.kt index 44bed719..c05946c1 100644 --- a/samples/CameraAccessAndroid/app/src/main/java/com/meta/wearable/dat/externalsampleapps/cameraaccess/settings/SettingsManager.kt +++ b/samples/CameraAccessAndroid/app/src/main/java/com/meta/wearable/dat/externalsampleapps/cameraaccess/settings/SettingsManager.kt @@ -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.""" } From ba15aca01e210b08aaf2eab148e0cec0890590d6 Mon Sep 17 00:00:00 2001 From: root Date: Fri, 27 Feb 2026 00:57:13 +0800 Subject: [PATCH 2/2] ci: override Gradle java home in Android workflow --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 615b90dd..8555f638 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -28,7 +28,7 @@ jobs: 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 + run: ./gradlew :app:assembleDebug -Dorg.gradle.java.home="$JAVA_HOME" env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}