crowdplaysdk 0.4.2 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ios/CrowdPlayRNModule.swift +49 -3
- package/ios/CrowdPlayReactNative.m +3 -0
- package/lib/ConsentScreen.d.ts +4 -3
- package/lib/ConsentScreen.js +11 -9
- package/lib/index.d.ts +37 -0
- package/lib/index.js +40 -1
- package/llms.txt +28 -2
- package/package.json +1 -1
- package/src/ConsentScreen.tsx +12 -7
- package/src/index.ts +64 -0
|
@@ -52,6 +52,17 @@ final class CrowdPlayRNCore {
|
|
|
52
52
|
])
|
|
53
53
|
}.store(in: &cancellables)
|
|
54
54
|
|
|
55
|
+
// Agent tool calls (D-103): the JS layer answers via
|
|
56
|
+
// respondToAgentToolCall, so the native auto-handler stays unset.
|
|
57
|
+
engine.$latestAgentToolCall
|
|
58
|
+
.compactMap { $0 }
|
|
59
|
+
.sink { [weak self] call in
|
|
60
|
+
self?.send("crowdplay:agentToolCall", [
|
|
61
|
+
"callId": call.callId, "name": call.name,
|
|
62
|
+
"argumentsJSON": call.argumentsJSON,
|
|
63
|
+
])
|
|
64
|
+
}.store(in: &cancellables)
|
|
65
|
+
|
|
55
66
|
engine.$uploads
|
|
56
67
|
.combineLatest(engine.$uploadsOnWiFi)
|
|
57
68
|
.sink { [weak self] uploads, onWifi in
|
|
@@ -220,7 +231,7 @@ public final class CrowdPlayRNModule: RCTEventEmitter {
|
|
|
220
231
|
public override static func requiresMainQueueSetup() -> Bool { true }
|
|
221
232
|
|
|
222
233
|
public override func supportedEvents() -> [String]! {
|
|
223
|
-
["crowdplay:phase", "crowdplay:recording", "crowdplay:uploads", "crowdplay:participants", "crowdplay:warning", "crowdplay:audioRoute", "crowdplay:voiceActivity", "crowdplay:caption"]
|
|
234
|
+
["crowdplay:phase", "crowdplay:recording", "crowdplay:uploads", "crowdplay:participants", "crowdplay:warning", "crowdplay:audioRoute", "crowdplay:voiceActivity", "crowdplay:caption", "crowdplay:agentToolCall"]
|
|
224
235
|
}
|
|
225
236
|
|
|
226
237
|
@objc public func startVoiceActivityUpdates() {
|
|
@@ -294,8 +305,30 @@ public final class CrowdPlayRNModule: RCTEventEmitter {
|
|
|
294
305
|
let voice = agent["voice"] as? String
|
|
295
306
|
let prompt = agent["systemPrompt"] as? String
|
|
296
307
|
let context = agent["context"] as? String
|
|
297
|
-
|
|
298
|
-
|
|
308
|
+
// Tools cross the bridge as one JSON string (D-103); the server
|
|
309
|
+
// re-validates every entry.
|
|
310
|
+
var tools: [CrowdPlayAgentTool]?
|
|
311
|
+
if let toolsJSON = agent["toolsJSON"] as? String,
|
|
312
|
+
let data = toolsJSON.data(using: .utf8),
|
|
313
|
+
let entries = try? JSONSerialization.jsonObject(with: data) as? [[String: Any]] {
|
|
314
|
+
let parsed: [CrowdPlayAgentTool] = entries.compactMap { entry in
|
|
315
|
+
guard let name = entry["name"] as? String,
|
|
316
|
+
let description = entry["description"] as? String else { return nil }
|
|
317
|
+
var parametersJSON = "{}"
|
|
318
|
+
if let parameters = entry["parameters"],
|
|
319
|
+
JSONSerialization.isValidJSONObject(parameters),
|
|
320
|
+
let pData = try? JSONSerialization.data(withJSONObject: parameters),
|
|
321
|
+
let pString = String(data: pData, encoding: .utf8) {
|
|
322
|
+
parametersJSON = pString
|
|
323
|
+
}
|
|
324
|
+
return CrowdPlayAgentTool(name: name, description: description,
|
|
325
|
+
parametersJSON: parametersJSON)
|
|
326
|
+
}
|
|
327
|
+
if !parsed.isEmpty { tools = parsed }
|
|
328
|
+
}
|
|
329
|
+
if voice != nil || prompt != nil || context != nil || tools != nil {
|
|
330
|
+
agentOptions = CrowdPlayAgentOptions(voice: voice, systemPrompt: prompt,
|
|
331
|
+
context: context, tools: tools)
|
|
299
332
|
}
|
|
300
333
|
}
|
|
301
334
|
Task { @MainActor in
|
|
@@ -360,6 +393,19 @@ public final class CrowdPlayRNModule: RCTEventEmitter {
|
|
|
360
393
|
}
|
|
361
394
|
}
|
|
362
395
|
|
|
396
|
+
@objc public func respondToAgentToolCall(_ callId: NSString, output: NSString) {
|
|
397
|
+
Task { @MainActor in
|
|
398
|
+
await CrowdPlayRNCore.shared.engine.respondToAgentToolCall(
|
|
399
|
+
callId: callId as String, output: output as String)
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
@objc public func updateAgentContext(_ text: NSString) {
|
|
404
|
+
Task { @MainActor in
|
|
405
|
+
await CrowdPlayRNCore.shared.engine.updateAgentContext(text as String)
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
363
409
|
@objc public func retryRecording() {
|
|
364
410
|
Task { @MainActor in
|
|
365
411
|
let core = CrowdPlayRNCore.shared
|
|
@@ -25,6 +25,9 @@ RCT_EXTERN_METHOD(setCameraEnabled : (BOOL)enabled
|
|
|
25
25
|
resolver : (RCTPromiseResolveBlock)resolve
|
|
26
26
|
rejecter : (RCTPromiseRejectBlock)reject)
|
|
27
27
|
RCT_EXTERN_METHOD(setAudioOutput : (NSString *)output)
|
|
28
|
+
RCT_EXTERN_METHOD(respondToAgentToolCall : (NSString *)callId
|
|
29
|
+
output : (NSString *)output)
|
|
30
|
+
RCT_EXTERN_METHOD(updateAgentContext : (NSString *)text)
|
|
28
31
|
RCT_EXTERN_METHOD(retryRecording)
|
|
29
32
|
RCT_EXTERN_METHOD(retryUploads)
|
|
30
33
|
RCT_EXTERN_METHOD(warmUp)
|
package/lib/ConsentScreen.d.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Drop-in consent screen. Shows CrowdPlay's locked consent wording (fetched from
|
|
3
|
-
* the native SDK so it can never drift from what the manifest records), a
|
|
4
|
-
*
|
|
5
|
-
*
|
|
3
|
+
* the native SDK so it can never drift from what the manifest records), a link
|
|
4
|
+
* to the full CrowdPlay Terms of Use and Privacy Notice, a switch, and a
|
|
5
|
+
* continue button that only enables once the participant agrees. Yields the
|
|
6
|
+
* ConsentGrant that join() requires.
|
|
6
7
|
*
|
|
7
8
|
* <CrowdPlayConsentScreen onConsent={(grant) => joinWith(grant)} />
|
|
8
9
|
*
|
package/lib/ConsentScreen.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/**
|
|
3
3
|
* Drop-in consent screen. Shows CrowdPlay's locked consent wording (fetched from
|
|
4
|
-
* the native SDK so it can never drift from what the manifest records), a
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* the native SDK so it can never drift from what the manifest records), a link
|
|
5
|
+
* to the full CrowdPlay Terms of Use and Privacy Notice, a switch, and a
|
|
6
|
+
* continue button that only enables once the participant agrees. Yields the
|
|
7
|
+
* ConsentGrant that join() requires.
|
|
7
8
|
*
|
|
8
9
|
* <CrowdPlayConsentScreen onConsent={(grant) => joinWith(grant)} />
|
|
9
10
|
*
|
|
@@ -44,14 +45,11 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
44
45
|
return result;
|
|
45
46
|
};
|
|
46
47
|
})();
|
|
47
|
-
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
48
|
-
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
49
|
-
};
|
|
50
48
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
51
49
|
exports.CrowdPlayConsentScreen = CrowdPlayConsentScreen;
|
|
52
50
|
const react_1 = __importStar(require("react"));
|
|
53
51
|
const react_native_1 = require("react-native");
|
|
54
|
-
const index_1 =
|
|
52
|
+
const index_1 = __importStar(require("./index"));
|
|
55
53
|
function CrowdPlayConsentScreen({ onConsent, style }) {
|
|
56
54
|
const [text, setText] = (0, react_1.useState)(null);
|
|
57
55
|
const [agreed, setAgreed] = (0, react_1.useState)(false);
|
|
@@ -74,12 +72,15 @@ function CrowdPlayConsentScreen({ onConsent, style }) {
|
|
|
74
72
|
return (<react_native_1.View style={[styles.container, style]}>
|
|
75
73
|
<react_native_1.Text style={styles.title}>Recording consent</react_native_1.Text>
|
|
76
74
|
{text === null ? (<react_native_1.ActivityIndicator />) : (<react_native_1.Text style={styles.body}>{text}</react_native_1.Text>)}
|
|
75
|
+
<react_native_1.Pressable accessibilityRole="link" onPress={() => void react_native_1.Linking.openURL(index_1.CROWDPLAY_TERMS_URL)}>
|
|
76
|
+
<react_native_1.Text style={styles.link}>Read the Terms of Use and Privacy Notice</react_native_1.Text>
|
|
77
|
+
</react_native_1.Pressable>
|
|
77
78
|
<react_native_1.View style={styles.row}>
|
|
78
79
|
<react_native_1.Switch value={agreed} onValueChange={(value) => {
|
|
79
80
|
setAgreed(value);
|
|
80
81
|
setAgreedAtMs(value ? Date.now() : null);
|
|
81
82
|
}}/>
|
|
82
|
-
<react_native_1.Text style={styles.agree}>I agree
|
|
83
|
+
<react_native_1.Text style={styles.agree}>I have read and agree to the CrowdPlay Terms of Use and Privacy Notice.</react_native_1.Text>
|
|
83
84
|
</react_native_1.View>
|
|
84
85
|
<react_native_1.Text style={styles.note}>
|
|
85
86
|
Recording starts automatically when you join, and ends when you leave.
|
|
@@ -105,7 +106,8 @@ const styles = react_native_1.StyleSheet.create({
|
|
|
105
106
|
title: { fontSize: 20, fontWeight: '700', color: '#111111' },
|
|
106
107
|
body: { fontSize: 15, lineHeight: 21, color: '#222222' },
|
|
107
108
|
row: { flexDirection: 'row', alignItems: 'center', gap: 10 },
|
|
108
|
-
agree: { fontSize: 15, color: '#111111' },
|
|
109
|
+
agree: { flex: 1, fontSize: 15, color: '#111111' },
|
|
110
|
+
link: { fontSize: 15, color: '#1f6feb', textDecorationLine: 'underline' },
|
|
109
111
|
note: { fontSize: 12, color: '#666666' },
|
|
110
112
|
button: { backgroundColor: '#1f6feb', borderRadius: 8, paddingVertical: 12, alignItems: 'center' },
|
|
111
113
|
buttonDisabled: { opacity: 0.4 },
|
package/lib/index.d.ts
CHANGED
|
@@ -59,6 +59,9 @@ export interface CrowdPlayConfig {
|
|
|
59
59
|
otherAudio?: 'interrupt' | 'mix' | 'mixDucked';
|
|
60
60
|
callQuality?: CallQuality;
|
|
61
61
|
}
|
|
62
|
+
/** The CrowdPlay Terms of Use and Privacy Notice every participant accepts,
|
|
63
|
+
* as a PDF. CrowdPlayConsentScreen links here; a custom consent UI must too. */
|
|
64
|
+
export declare const CROWDPLAY_TERMS_URL = "https://dashboard.crowdplay.ai/legal/terms.pdf";
|
|
62
65
|
/** Proof the participant agreed. Produce it with CrowdPlayConsentScreen, or,
|
|
63
66
|
* if you render your own consent UI, call CrowdPlay.consentText() to show the
|
|
64
67
|
* REQUIRED wording and construct the grant at the moment of agreement. */
|
|
@@ -74,6 +77,17 @@ export interface ConsentGrant {
|
|
|
74
77
|
* apply to the whole room. The app can never change the provider, the
|
|
75
78
|
* model, or the API key from here.
|
|
76
79
|
*/
|
|
80
|
+
/** A tool the app offers the voice AI for one session (D-103). Requires
|
|
81
|
+
* the app's dashboard config to use the GPT-Live provider with an AI brain
|
|
82
|
+
* (delegation) enabled; ignored otherwise. */
|
|
83
|
+
export interface AgentTool {
|
|
84
|
+
/** Letters, digits, underscore, dash; max 64 chars. */
|
|
85
|
+
name: string;
|
|
86
|
+
/** What the tool does and when to use it, for the model. */
|
|
87
|
+
description: string;
|
|
88
|
+
/** JSON-schema object describing the arguments. Omit for none. */
|
|
89
|
+
parameters?: Record<string, unknown>;
|
|
90
|
+
}
|
|
77
91
|
export interface AgentOptions {
|
|
78
92
|
/** A voice of the app's configured provider. Gemini: Charon, Aoede,
|
|
79
93
|
* Fenrir, Kore, Puck. OpenAI: marin, cedar, alloy, ash, ballad, coral,
|
|
@@ -87,6 +101,9 @@ export interface AgentOptions {
|
|
|
87
101
|
* keep a few sentences per user in your own storage and pass them
|
|
88
102
|
* here. Max 4000 chars. */
|
|
89
103
|
context?: string;
|
|
104
|
+
/** Tools the AI may call this session (D-103). Handle calls with
|
|
105
|
+
* CrowdPlay.onAgentToolCall(). Max 16. */
|
|
106
|
+
tools?: AgentTool[];
|
|
90
107
|
}
|
|
91
108
|
export type Phase = 'idle' | 'connecting' | 'connected' | 'failed';
|
|
92
109
|
export interface RecordingSnapshot {
|
|
@@ -167,6 +184,13 @@ export interface CrowdPlayEvents {
|
|
|
167
184
|
detectedOutputs: string[];
|
|
168
185
|
headphonesConnected: boolean;
|
|
169
186
|
};
|
|
187
|
+
/** The voice AI asked the app to run a tool (D-103). Prefer
|
|
188
|
+
* CrowdPlay.onAgentToolCall(), which answers for you. */
|
|
189
|
+
agentToolCall: {
|
|
190
|
+
callId: string;
|
|
191
|
+
name: string;
|
|
192
|
+
argumentsJSON: string;
|
|
193
|
+
};
|
|
170
194
|
}
|
|
171
195
|
export type CrowdPlayEventName = keyof CrowdPlayEvents;
|
|
172
196
|
declare const CrowdPlay: {
|
|
@@ -216,6 +240,19 @@ declare const CrowdPlay: {
|
|
|
216
240
|
/** Integration self-check: configuration, permissions, disk, backend
|
|
217
241
|
* reachability, app-key auth. Every failing check names its fix. */
|
|
218
242
|
doctor(): Promise<DoctorCheck[]>;
|
|
243
|
+
/** Handle the voice AI's tool calls (D-103). The handler gets the tool
|
|
244
|
+
* name and its arguments (parsed JSON object) and returns the result as
|
|
245
|
+
* an object or JSON string; return null to decline on this device
|
|
246
|
+
* (another participant's device may answer; first answer wins). Ordinary
|
|
247
|
+
* app code, no AI here. The AI waits about 12 seconds per call. Returns
|
|
248
|
+
* a subscription; call .remove(). */
|
|
249
|
+
onAgentToolCall(handler: (name: string, args: Record<string, unknown>) => Promise<Record<string, unknown> | string | null> | Record<string, unknown> | string | null): {
|
|
250
|
+
remove(): void;
|
|
251
|
+
};
|
|
252
|
+
/** Tell the AI what just happened in the app, mid-session ("Night ended.
|
|
253
|
+
* Sam was eliminated."). Keep it to a sentence or two; long pushes are
|
|
254
|
+
* truncated. GPT-Live provider only; other providers ignore it. */
|
|
255
|
+
updateAgentContext(text: string): void;
|
|
219
256
|
/** Subscribe to state events. Returns a subscription; call .remove(). */
|
|
220
257
|
addListener<E extends CrowdPlayEventName>(event: E, listener: (payload: CrowdPlayEvents[E]) => void): {
|
|
221
258
|
remove(): void;
|
package/lib/index.js
CHANGED
|
@@ -22,8 +22,11 @@
|
|
|
22
22
|
* path that records without it.
|
|
23
23
|
*/
|
|
24
24
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
25
|
-
exports.useLatestCaption = exports.useVoiceActivity = exports.VoiceReactive = exports.CrowdPlayVoiceView = exports.CrowdPlayVideoView = exports.CrowdPlayConsentScreen = void 0;
|
|
25
|
+
exports.useLatestCaption = exports.useVoiceActivity = exports.VoiceReactive = exports.CrowdPlayVoiceView = exports.CrowdPlayVideoView = exports.CrowdPlayConsentScreen = exports.CROWDPLAY_TERMS_URL = void 0;
|
|
26
26
|
const react_native_1 = require("react-native");
|
|
27
|
+
/** The CrowdPlay Terms of Use and Privacy Notice every participant accepts,
|
|
28
|
+
* as a PDF. CrowdPlayConsentScreen links here; a custom consent UI must too. */
|
|
29
|
+
exports.CROWDPLAY_TERMS_URL = 'https://dashboard.crowdplay.ai/legal/terms.pdf';
|
|
27
30
|
function native() {
|
|
28
31
|
const module = react_native_1.NativeModules.CrowdPlayReactNative;
|
|
29
32
|
if (!module) {
|
|
@@ -102,6 +105,11 @@ const CrowdPlay = {
|
|
|
102
105
|
put('voice', agent.voice, 60);
|
|
103
106
|
put('systemPrompt', agent.systemPrompt, 8192);
|
|
104
107
|
put('context', agent.context, 4000);
|
|
108
|
+
// Tools cross the bridge as one JSON string; the native layer and the
|
|
109
|
+
// server both re-validate (D-103).
|
|
110
|
+
if (Array.isArray(agent.tools) && agent.tools.length) {
|
|
111
|
+
put('toolsJSON', JSON.stringify(agent.tools.slice(0, 16)), 8192);
|
|
112
|
+
}
|
|
105
113
|
}
|
|
106
114
|
return native().join(displayName.trim(), roomCode, consent.grantedAtMs, agentFields);
|
|
107
115
|
},
|
|
@@ -144,6 +152,37 @@ const CrowdPlay = {
|
|
|
144
152
|
doctor() {
|
|
145
153
|
return native().doctor();
|
|
146
154
|
},
|
|
155
|
+
/** Handle the voice AI's tool calls (D-103). The handler gets the tool
|
|
156
|
+
* name and its arguments (parsed JSON object) and returns the result as
|
|
157
|
+
* an object or JSON string; return null to decline on this device
|
|
158
|
+
* (another participant's device may answer; first answer wins). Ordinary
|
|
159
|
+
* app code, no AI here. The AI waits about 12 seconds per call. Returns
|
|
160
|
+
* a subscription; call .remove(). */
|
|
161
|
+
onAgentToolCall(handler) {
|
|
162
|
+
const subscription = events().addListener('crowdplay:agentToolCall', (payload) => {
|
|
163
|
+
void (async () => {
|
|
164
|
+
let args = {};
|
|
165
|
+
try {
|
|
166
|
+
args = JSON.parse(payload.argumentsJSON);
|
|
167
|
+
}
|
|
168
|
+
catch { /* {} */ }
|
|
169
|
+
const result = await handler(payload.name, args);
|
|
170
|
+
if (result === null || result === undefined)
|
|
171
|
+
return;
|
|
172
|
+
const output = typeof result === 'string' ? result : JSON.stringify(result);
|
|
173
|
+
native().respondToAgentToolCall(payload.callId, output);
|
|
174
|
+
})();
|
|
175
|
+
});
|
|
176
|
+
return { remove: () => subscription.remove() };
|
|
177
|
+
},
|
|
178
|
+
/** Tell the AI what just happened in the app, mid-session ("Night ended.
|
|
179
|
+
* Sam was eliminated."). Keep it to a sentence or two; long pushes are
|
|
180
|
+
* truncated. GPT-Live provider only; other providers ignore it. */
|
|
181
|
+
updateAgentContext(text) {
|
|
182
|
+
if (typeof text === 'string' && text.trim()) {
|
|
183
|
+
native().updateAgentContext(text.trim());
|
|
184
|
+
}
|
|
185
|
+
},
|
|
147
186
|
/** Subscribe to state events. Returns a subscription; call .remove(). */
|
|
148
187
|
addListener(event, listener) {
|
|
149
188
|
const subscription = events().addListener(`crowdplay:${event}`, listener);
|
package/llms.txt
CHANGED
|
@@ -57,7 +57,8 @@ its video calls invisibly; recording is infrastructure, not identity:
|
|
|
57
57
|
in an app using this SDK. The engine owns the microphone and camera.
|
|
58
58
|
3. NEVER skip or fake consent. CrowdPlay.join() THROWS without a ConsentGrant,
|
|
59
59
|
and the user must actually SEE the consent wording first; use
|
|
60
|
-
<CrowdPlayConsentScreen/>, or show CrowdPlay.consentText() verbatim
|
|
60
|
+
<CrowdPlayConsentScreen/>, or show CrowdPlay.consentText() verbatim (plus a
|
|
61
|
+
link to CROWDPLAY_TERMS_URL, the full Terms of Use and Privacy Notice) in your own
|
|
61
62
|
UI and construct { grantedAtMs: Date.now() } at the moment of
|
|
62
63
|
agreement. Fabricating a grant creates false legal evidence.
|
|
63
64
|
4. iOS only. Guard any non-iOS platform path; the module throws elsewhere
|
|
@@ -169,13 +170,38 @@ the voice and the persona and give a short context text for one session:
|
|
|
169
170
|
agent: { voice: 'Aoede', context: "The user's name is Sam. Last time you practised past tense." } });
|
|
170
171
|
voice must be one of the configured provider's voices (Gemini: Charon,
|
|
171
172
|
Aoede, Fenrir, Kore, Puck; OpenAI: marin, cedar, alloy, ash, ballad,
|
|
172
|
-
coral, echo, sage, shimmer, verse; Grok: Ara, Rex, Sal, Eve, Leo
|
|
173
|
+
coral, echo, sage, shimmer, verse; Grok: Ara, Rex, Sal, Eve, Leo;
|
|
174
|
+
OpenAI GPT-Live: quartz, ripple, vesper, willow, stone, gleam, meridian,
|
|
175
|
+
bossa, tempo, beacon, delta, cinder), unknown
|
|
173
176
|
falls back to the dashboard voice. Caps: systemPrompt 8192 chars, context
|
|
174
177
|
4000. The options of the join that CREATES the AI apply to the whole room;
|
|
175
178
|
fixed for the session (leave and join again to change); provider, model
|
|
176
179
|
and key cannot be changed from the app. MEMORY is built with `context`:
|
|
177
180
|
keep a few sentences per user in the app's own storage and pass them at
|
|
178
181
|
every join.
|
|
182
|
+
Agent tools + live context (0.5.0+, D-103; needs the app's dashboard
|
|
183
|
+
config on the OpenAI GPT-Live provider with an AI brain enabled —
|
|
184
|
+
ignored otherwise, never an error). Tools let the AI SEE app state and
|
|
185
|
+
ACT in the app: pass them at join, handle calls with ordinary JS.
|
|
186
|
+
await CrowdPlay.join({ displayName, roomCode, consent: grant,
|
|
187
|
+
agent: { tools: [
|
|
188
|
+
{ name: 'get_game_state',
|
|
189
|
+
description: 'The current players, roles you know, and votes.' },
|
|
190
|
+
{ name: 'cast_vote', description: 'Vote to eliminate one player.',
|
|
191
|
+
parameters: { type: 'object',
|
|
192
|
+
properties: { player: { type: 'string' } }, required: ['player'] } },
|
|
193
|
+
] } });
|
|
194
|
+
const sub = CrowdPlay.onAgentToolCall(async (name, args) => {
|
|
195
|
+
if (name === 'get_game_state') return game.state();
|
|
196
|
+
if (name === 'cast_vote') return game.castVote(args.player);
|
|
197
|
+
return null; // decline on this device; first device to answer wins
|
|
198
|
+
});
|
|
199
|
+
Return an object (or JSON string), or null to decline. The AI waits
|
|
200
|
+
about 12 seconds per call, then carries on gracefully. Max 16 tools;
|
|
201
|
+
names letters/digits/underscore/dash up to 64 chars. To push events to
|
|
202
|
+
the AI as they happen (it has no other way to know):
|
|
203
|
+
CrowdPlay.updateAgentContext('Night ended. Sam was eliminated.') — a
|
|
204
|
+
sentence or two, long pushes are truncated.
|
|
179
205
|
Conversations are recorded and transcribed by the platform automatically.
|
|
180
206
|
RECOMMENDED: voice-AI apps should be AUDIO-ONLY (audioOnly: true; no
|
|
181
207
|
camera permission, no video anywhere); a camera adds nothing to talking
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "crowdplaysdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.1",
|
|
4
4
|
"description": "CrowdPlay lossless conversation capture for React Native (iOS). Studio-grade per-participant recording during live video calls, delivered to CrowdPlay automatically.",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
6
|
"types": "lib/index.d.ts",
|
package/src/ConsentScreen.tsx
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Drop-in consent screen. Shows CrowdPlay's locked consent wording (fetched from
|
|
3
|
-
* the native SDK so it can never drift from what the manifest records), a
|
|
4
|
-
*
|
|
5
|
-
*
|
|
3
|
+
* the native SDK so it can never drift from what the manifest records), a link
|
|
4
|
+
* to the full CrowdPlay Terms of Use and Privacy Notice, a switch, and a
|
|
5
|
+
* continue button that only enables once the participant agrees. Yields the
|
|
6
|
+
* ConsentGrant that join() requires.
|
|
6
7
|
*
|
|
7
8
|
* <CrowdPlayConsentScreen onConsent={(grant) => joinWith(grant)} />
|
|
8
9
|
*
|
|
@@ -12,8 +13,8 @@
|
|
|
12
13
|
*/
|
|
13
14
|
|
|
14
15
|
import React, { useEffect, useState } from 'react';
|
|
15
|
-
import { ActivityIndicator, Pressable, StyleSheet, Switch, Text, View } from 'react-native';
|
|
16
|
-
import CrowdPlay, { type ConsentGrant } from './index';
|
|
16
|
+
import { ActivityIndicator, Linking, Pressable, StyleSheet, Switch, Text, View } from 'react-native';
|
|
17
|
+
import CrowdPlay, { CROWDPLAY_TERMS_URL, type ConsentGrant } from './index';
|
|
17
18
|
|
|
18
19
|
export interface CrowdPlayConsentScreenProps {
|
|
19
20
|
onConsent: (grant: ConsentGrant) => void;
|
|
@@ -48,6 +49,9 @@ export function CrowdPlayConsentScreen({ onConsent, style }: CrowdPlayConsentScr
|
|
|
48
49
|
) : (
|
|
49
50
|
<Text style={styles.body}>{text}</Text>
|
|
50
51
|
)}
|
|
52
|
+
<Pressable accessibilityRole="link" onPress={() => void Linking.openURL(CROWDPLAY_TERMS_URL)}>
|
|
53
|
+
<Text style={styles.link}>Read the Terms of Use and Privacy Notice</Text>
|
|
54
|
+
</Pressable>
|
|
51
55
|
<View style={styles.row}>
|
|
52
56
|
<Switch
|
|
53
57
|
value={agreed}
|
|
@@ -56,7 +60,7 @@ export function CrowdPlayConsentScreen({ onConsent, style }: CrowdPlayConsentScr
|
|
|
56
60
|
setAgreedAtMs(value ? Date.now() : null);
|
|
57
61
|
}}
|
|
58
62
|
/>
|
|
59
|
-
<Text style={styles.agree}>I agree
|
|
63
|
+
<Text style={styles.agree}>I have read and agree to the CrowdPlay Terms of Use and Privacy Notice.</Text>
|
|
60
64
|
</View>
|
|
61
65
|
<Text style={styles.note}>
|
|
62
66
|
Recording starts automatically when you join, and ends when you leave.
|
|
@@ -88,7 +92,8 @@ const styles = StyleSheet.create({
|
|
|
88
92
|
title: { fontSize: 20, fontWeight: '700', color: '#111111' },
|
|
89
93
|
body: { fontSize: 15, lineHeight: 21, color: '#222222' },
|
|
90
94
|
row: { flexDirection: 'row', alignItems: 'center', gap: 10 },
|
|
91
|
-
agree: { fontSize: 15, color: '#111111' },
|
|
95
|
+
agree: { flex: 1, fontSize: 15, color: '#111111' },
|
|
96
|
+
link: { fontSize: 15, color: '#1f6feb', textDecorationLine: 'underline' },
|
|
92
97
|
note: { fontSize: 12, color: '#666666' },
|
|
93
98
|
button: { backgroundColor: '#1f6feb', borderRadius: 8, paddingVertical: 12, alignItems: 'center' },
|
|
94
99
|
buttonDisabled: { opacity: 0.4 },
|
package/src/index.ts
CHANGED
|
@@ -68,6 +68,10 @@ export interface CrowdPlayConfig {
|
|
|
68
68
|
callQuality?: CallQuality;
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
+
/** The CrowdPlay Terms of Use and Privacy Notice every participant accepts,
|
|
72
|
+
* as a PDF. CrowdPlayConsentScreen links here; a custom consent UI must too. */
|
|
73
|
+
export const CROWDPLAY_TERMS_URL = 'https://dashboard.crowdplay.ai/legal/terms.pdf';
|
|
74
|
+
|
|
71
75
|
/** Proof the participant agreed. Produce it with CrowdPlayConsentScreen, or,
|
|
72
76
|
* if you render your own consent UI, call CrowdPlay.consentText() to show the
|
|
73
77
|
* REQUIRED wording and construct the grant at the moment of agreement. */
|
|
@@ -84,6 +88,18 @@ export interface ConsentGrant {
|
|
|
84
88
|
* apply to the whole room. The app can never change the provider, the
|
|
85
89
|
* model, or the API key from here.
|
|
86
90
|
*/
|
|
91
|
+
/** A tool the app offers the voice AI for one session (D-103). Requires
|
|
92
|
+
* the app's dashboard config to use the GPT-Live provider with an AI brain
|
|
93
|
+
* (delegation) enabled; ignored otherwise. */
|
|
94
|
+
export interface AgentTool {
|
|
95
|
+
/** Letters, digits, underscore, dash; max 64 chars. */
|
|
96
|
+
name: string;
|
|
97
|
+
/** What the tool does and when to use it, for the model. */
|
|
98
|
+
description: string;
|
|
99
|
+
/** JSON-schema object describing the arguments. Omit for none. */
|
|
100
|
+
parameters?: Record<string, unknown>;
|
|
101
|
+
}
|
|
102
|
+
|
|
87
103
|
export interface AgentOptions {
|
|
88
104
|
/** A voice of the app's configured provider. Gemini: Charon, Aoede,
|
|
89
105
|
* Fenrir, Kore, Puck. OpenAI: marin, cedar, alloy, ash, ballad, coral,
|
|
@@ -97,6 +113,9 @@ export interface AgentOptions {
|
|
|
97
113
|
* keep a few sentences per user in your own storage and pass them
|
|
98
114
|
* here. Max 4000 chars. */
|
|
99
115
|
context?: string;
|
|
116
|
+
/** Tools the AI may call this session (D-103). Handle calls with
|
|
117
|
+
* CrowdPlay.onAgentToolCall(). Max 16. */
|
|
118
|
+
tools?: AgentTool[];
|
|
100
119
|
}
|
|
101
120
|
|
|
102
121
|
export type Phase = 'idle' | 'connecting' | 'connected' | 'failed';
|
|
@@ -164,6 +183,9 @@ export interface CrowdPlayEvents {
|
|
|
164
183
|
* "iPhone speaker"), connected candidates, and whether headphones are
|
|
165
184
|
* attached. Drive the call screen's output menu from this (D-079). */
|
|
166
185
|
audioRoute: { currentOutputName: string; detectedOutputs: string[]; headphonesConnected: boolean };
|
|
186
|
+
/** The voice AI asked the app to run a tool (D-103). Prefer
|
|
187
|
+
* CrowdPlay.onAgentToolCall(), which answers for you. */
|
|
188
|
+
agentToolCall: { callId: string; name: string; argumentsJSON: string };
|
|
167
189
|
}
|
|
168
190
|
|
|
169
191
|
export type CrowdPlayEventName = keyof CrowdPlayEvents;
|
|
@@ -177,6 +199,8 @@ interface NativeCrowdPlay {
|
|
|
177
199
|
consentText(): Promise<string>;
|
|
178
200
|
join(displayName: string, roomCode: string, consentGrantedAtMs: number,
|
|
179
201
|
agent: Record<string, string> | null): Promise<void>;
|
|
202
|
+
respondToAgentToolCall(callId: string, output: string): void;
|
|
203
|
+
updateAgentContext(text: string): void;
|
|
180
204
|
leave(): Promise<void>;
|
|
181
205
|
setMicMuted(muted: boolean): Promise<void>;
|
|
182
206
|
setCameraEnabled(enabled: boolean): Promise<void>;
|
|
@@ -279,6 +303,11 @@ const CrowdPlay = {
|
|
|
279
303
|
put('voice', agent.voice, 60);
|
|
280
304
|
put('systemPrompt', agent.systemPrompt, 8192);
|
|
281
305
|
put('context', agent.context, 4000);
|
|
306
|
+
// Tools cross the bridge as one JSON string; the native layer and the
|
|
307
|
+
// server both re-validate (D-103).
|
|
308
|
+
if (Array.isArray(agent.tools) && agent.tools.length) {
|
|
309
|
+
put('toolsJSON', JSON.stringify(agent.tools.slice(0, 16)), 8192);
|
|
310
|
+
}
|
|
282
311
|
}
|
|
283
312
|
return native().join(displayName.trim(), roomCode, consent.grantedAtMs, agentFields);
|
|
284
313
|
},
|
|
@@ -330,6 +359,41 @@ const CrowdPlay = {
|
|
|
330
359
|
return native().doctor();
|
|
331
360
|
},
|
|
332
361
|
|
|
362
|
+
/** Handle the voice AI's tool calls (D-103). The handler gets the tool
|
|
363
|
+
* name and its arguments (parsed JSON object) and returns the result as
|
|
364
|
+
* an object or JSON string; return null to decline on this device
|
|
365
|
+
* (another participant's device may answer; first answer wins). Ordinary
|
|
366
|
+
* app code, no AI here. The AI waits about 12 seconds per call. Returns
|
|
367
|
+
* a subscription; call .remove(). */
|
|
368
|
+
onAgentToolCall(
|
|
369
|
+
handler: (name: string, args: Record<string, unknown>) =>
|
|
370
|
+
Promise<Record<string, unknown> | string | null> | Record<string, unknown> | string | null
|
|
371
|
+
): { remove(): void } {
|
|
372
|
+
const subscription = events().addListener(
|
|
373
|
+
'crowdplay:agentToolCall',
|
|
374
|
+
(payload: { callId: string; name: string; argumentsJSON: string }) => {
|
|
375
|
+
void (async () => {
|
|
376
|
+
let args: Record<string, unknown> = {};
|
|
377
|
+
try { args = JSON.parse(payload.argumentsJSON); } catch { /* {} */ }
|
|
378
|
+
const result = await handler(payload.name, args);
|
|
379
|
+
if (result === null || result === undefined) return;
|
|
380
|
+
const output = typeof result === 'string' ? result : JSON.stringify(result);
|
|
381
|
+
native().respondToAgentToolCall(payload.callId, output);
|
|
382
|
+
})();
|
|
383
|
+
}
|
|
384
|
+
);
|
|
385
|
+
return { remove: () => subscription.remove() };
|
|
386
|
+
},
|
|
387
|
+
|
|
388
|
+
/** Tell the AI what just happened in the app, mid-session ("Night ended.
|
|
389
|
+
* Sam was eliminated."). Keep it to a sentence or two; long pushes are
|
|
390
|
+
* truncated. GPT-Live provider only; other providers ignore it. */
|
|
391
|
+
updateAgentContext(text: string): void {
|
|
392
|
+
if (typeof text === 'string' && text.trim()) {
|
|
393
|
+
native().updateAgentContext(text.trim());
|
|
394
|
+
}
|
|
395
|
+
},
|
|
396
|
+
|
|
333
397
|
/** Subscribe to state events. Returns a subscription; call .remove(). */
|
|
334
398
|
addListener<E extends CrowdPlayEventName>(
|
|
335
399
|
event: E,
|