crowdplaysdk 0.4.2 → 0.5.0
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/index.d.ts +34 -0
- package/lib/index.js +36 -0
- package/llms.txt +26 -1
- package/package.json +1 -1
- package/src/index.ts +60 -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/index.d.ts
CHANGED
|
@@ -74,6 +74,17 @@ export interface ConsentGrant {
|
|
|
74
74
|
* apply to the whole room. The app can never change the provider, the
|
|
75
75
|
* model, or the API key from here.
|
|
76
76
|
*/
|
|
77
|
+
/** A tool the app offers the voice AI for one session (D-103). Requires
|
|
78
|
+
* the app's dashboard config to use the GPT-Live provider with an AI brain
|
|
79
|
+
* (delegation) enabled; ignored otherwise. */
|
|
80
|
+
export interface AgentTool {
|
|
81
|
+
/** Letters, digits, underscore, dash; max 64 chars. */
|
|
82
|
+
name: string;
|
|
83
|
+
/** What the tool does and when to use it, for the model. */
|
|
84
|
+
description: string;
|
|
85
|
+
/** JSON-schema object describing the arguments. Omit for none. */
|
|
86
|
+
parameters?: Record<string, unknown>;
|
|
87
|
+
}
|
|
77
88
|
export interface AgentOptions {
|
|
78
89
|
/** A voice of the app's configured provider. Gemini: Charon, Aoede,
|
|
79
90
|
* Fenrir, Kore, Puck. OpenAI: marin, cedar, alloy, ash, ballad, coral,
|
|
@@ -87,6 +98,9 @@ export interface AgentOptions {
|
|
|
87
98
|
* keep a few sentences per user in your own storage and pass them
|
|
88
99
|
* here. Max 4000 chars. */
|
|
89
100
|
context?: string;
|
|
101
|
+
/** Tools the AI may call this session (D-103). Handle calls with
|
|
102
|
+
* CrowdPlay.onAgentToolCall(). Max 16. */
|
|
103
|
+
tools?: AgentTool[];
|
|
90
104
|
}
|
|
91
105
|
export type Phase = 'idle' | 'connecting' | 'connected' | 'failed';
|
|
92
106
|
export interface RecordingSnapshot {
|
|
@@ -167,6 +181,13 @@ export interface CrowdPlayEvents {
|
|
|
167
181
|
detectedOutputs: string[];
|
|
168
182
|
headphonesConnected: boolean;
|
|
169
183
|
};
|
|
184
|
+
/** The voice AI asked the app to run a tool (D-103). Prefer
|
|
185
|
+
* CrowdPlay.onAgentToolCall(), which answers for you. */
|
|
186
|
+
agentToolCall: {
|
|
187
|
+
callId: string;
|
|
188
|
+
name: string;
|
|
189
|
+
argumentsJSON: string;
|
|
190
|
+
};
|
|
170
191
|
}
|
|
171
192
|
export type CrowdPlayEventName = keyof CrowdPlayEvents;
|
|
172
193
|
declare const CrowdPlay: {
|
|
@@ -216,6 +237,19 @@ declare const CrowdPlay: {
|
|
|
216
237
|
/** Integration self-check: configuration, permissions, disk, backend
|
|
217
238
|
* reachability, app-key auth. Every failing check names its fix. */
|
|
218
239
|
doctor(): Promise<DoctorCheck[]>;
|
|
240
|
+
/** Handle the voice AI's tool calls (D-103). The handler gets the tool
|
|
241
|
+
* name and its arguments (parsed JSON object) and returns the result as
|
|
242
|
+
* an object or JSON string; return null to decline on this device
|
|
243
|
+
* (another participant's device may answer; first answer wins). Ordinary
|
|
244
|
+
* app code, no AI here. The AI waits about 12 seconds per call. Returns
|
|
245
|
+
* a subscription; call .remove(). */
|
|
246
|
+
onAgentToolCall(handler: (name: string, args: Record<string, unknown>) => Promise<Record<string, unknown> | string | null> | Record<string, unknown> | string | null): {
|
|
247
|
+
remove(): void;
|
|
248
|
+
};
|
|
249
|
+
/** Tell the AI what just happened in the app, mid-session ("Night ended.
|
|
250
|
+
* Sam was eliminated."). Keep it to a sentence or two; long pushes are
|
|
251
|
+
* truncated. GPT-Live provider only; other providers ignore it. */
|
|
252
|
+
updateAgentContext(text: string): void;
|
|
219
253
|
/** Subscribe to state events. Returns a subscription; call .remove(). */
|
|
220
254
|
addListener<E extends CrowdPlayEventName>(event: E, listener: (payload: CrowdPlayEvents[E]) => void): {
|
|
221
255
|
remove(): void;
|
package/lib/index.js
CHANGED
|
@@ -102,6 +102,11 @@ const CrowdPlay = {
|
|
|
102
102
|
put('voice', agent.voice, 60);
|
|
103
103
|
put('systemPrompt', agent.systemPrompt, 8192);
|
|
104
104
|
put('context', agent.context, 4000);
|
|
105
|
+
// Tools cross the bridge as one JSON string; the native layer and the
|
|
106
|
+
// server both re-validate (D-103).
|
|
107
|
+
if (Array.isArray(agent.tools) && agent.tools.length) {
|
|
108
|
+
put('toolsJSON', JSON.stringify(agent.tools.slice(0, 16)), 8192);
|
|
109
|
+
}
|
|
105
110
|
}
|
|
106
111
|
return native().join(displayName.trim(), roomCode, consent.grantedAtMs, agentFields);
|
|
107
112
|
},
|
|
@@ -144,6 +149,37 @@ const CrowdPlay = {
|
|
|
144
149
|
doctor() {
|
|
145
150
|
return native().doctor();
|
|
146
151
|
},
|
|
152
|
+
/** Handle the voice AI's tool calls (D-103). The handler gets the tool
|
|
153
|
+
* name and its arguments (parsed JSON object) and returns the result as
|
|
154
|
+
* an object or JSON string; return null to decline on this device
|
|
155
|
+
* (another participant's device may answer; first answer wins). Ordinary
|
|
156
|
+
* app code, no AI here. The AI waits about 12 seconds per call. Returns
|
|
157
|
+
* a subscription; call .remove(). */
|
|
158
|
+
onAgentToolCall(handler) {
|
|
159
|
+
const subscription = events().addListener('crowdplay:agentToolCall', (payload) => {
|
|
160
|
+
void (async () => {
|
|
161
|
+
let args = {};
|
|
162
|
+
try {
|
|
163
|
+
args = JSON.parse(payload.argumentsJSON);
|
|
164
|
+
}
|
|
165
|
+
catch { /* {} */ }
|
|
166
|
+
const result = await handler(payload.name, args);
|
|
167
|
+
if (result === null || result === undefined)
|
|
168
|
+
return;
|
|
169
|
+
const output = typeof result === 'string' ? result : JSON.stringify(result);
|
|
170
|
+
native().respondToAgentToolCall(payload.callId, output);
|
|
171
|
+
})();
|
|
172
|
+
});
|
|
173
|
+
return { remove: () => subscription.remove() };
|
|
174
|
+
},
|
|
175
|
+
/** Tell the AI what just happened in the app, mid-session ("Night ended.
|
|
176
|
+
* Sam was eliminated."). Keep it to a sentence or two; long pushes are
|
|
177
|
+
* truncated. GPT-Live provider only; other providers ignore it. */
|
|
178
|
+
updateAgentContext(text) {
|
|
179
|
+
if (typeof text === 'string' && text.trim()) {
|
|
180
|
+
native().updateAgentContext(text.trim());
|
|
181
|
+
}
|
|
182
|
+
},
|
|
147
183
|
/** Subscribe to state events. Returns a subscription; call .remove(). */
|
|
148
184
|
addListener(event, listener) {
|
|
149
185
|
const subscription = events().addListener(`crowdplay:${event}`, listener);
|
package/llms.txt
CHANGED
|
@@ -169,13 +169,38 @@ the voice and the persona and give a short context text for one session:
|
|
|
169
169
|
agent: { voice: 'Aoede', context: "The user's name is Sam. Last time you practised past tense." } });
|
|
170
170
|
voice must be one of the configured provider's voices (Gemini: Charon,
|
|
171
171
|
Aoede, Fenrir, Kore, Puck; OpenAI: marin, cedar, alloy, ash, ballad,
|
|
172
|
-
coral, echo, sage, shimmer, verse; Grok: Ara, Rex, Sal, Eve, Leo
|
|
172
|
+
coral, echo, sage, shimmer, verse; Grok: Ara, Rex, Sal, Eve, Leo;
|
|
173
|
+
OpenAI GPT-Live: quartz, ripple, vesper, willow, stone, gleam, meridian,
|
|
174
|
+
bossa, tempo, beacon, delta, cinder), unknown
|
|
173
175
|
falls back to the dashboard voice. Caps: systemPrompt 8192 chars, context
|
|
174
176
|
4000. The options of the join that CREATES the AI apply to the whole room;
|
|
175
177
|
fixed for the session (leave and join again to change); provider, model
|
|
176
178
|
and key cannot be changed from the app. MEMORY is built with `context`:
|
|
177
179
|
keep a few sentences per user in the app's own storage and pass them at
|
|
178
180
|
every join.
|
|
181
|
+
Agent tools + live context (0.5.0+, D-103; needs the app's dashboard
|
|
182
|
+
config on the OpenAI GPT-Live provider with an AI brain enabled —
|
|
183
|
+
ignored otherwise, never an error). Tools let the AI SEE app state and
|
|
184
|
+
ACT in the app: pass them at join, handle calls with ordinary JS.
|
|
185
|
+
await CrowdPlay.join({ displayName, roomCode, consent: grant,
|
|
186
|
+
agent: { tools: [
|
|
187
|
+
{ name: 'get_game_state',
|
|
188
|
+
description: 'The current players, roles you know, and votes.' },
|
|
189
|
+
{ name: 'cast_vote', description: 'Vote to eliminate one player.',
|
|
190
|
+
parameters: { type: 'object',
|
|
191
|
+
properties: { player: { type: 'string' } }, required: ['player'] } },
|
|
192
|
+
] } });
|
|
193
|
+
const sub = CrowdPlay.onAgentToolCall(async (name, args) => {
|
|
194
|
+
if (name === 'get_game_state') return game.state();
|
|
195
|
+
if (name === 'cast_vote') return game.castVote(args.player);
|
|
196
|
+
return null; // decline on this device; first device to answer wins
|
|
197
|
+
});
|
|
198
|
+
Return an object (or JSON string), or null to decline. The AI waits
|
|
199
|
+
about 12 seconds per call, then carries on gracefully. Max 16 tools;
|
|
200
|
+
names letters/digits/underscore/dash up to 64 chars. To push events to
|
|
201
|
+
the AI as they happen (it has no other way to know):
|
|
202
|
+
CrowdPlay.updateAgentContext('Night ended. Sam was eliminated.') — a
|
|
203
|
+
sentence or two, long pushes are truncated.
|
|
179
204
|
Conversations are recorded and transcribed by the platform automatically.
|
|
180
205
|
RECOMMENDED: voice-AI apps should be AUDIO-ONLY (audioOnly: true; no
|
|
181
206
|
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.0",
|
|
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/index.ts
CHANGED
|
@@ -84,6 +84,18 @@ export interface ConsentGrant {
|
|
|
84
84
|
* apply to the whole room. The app can never change the provider, the
|
|
85
85
|
* model, or the API key from here.
|
|
86
86
|
*/
|
|
87
|
+
/** A tool the app offers the voice AI for one session (D-103). Requires
|
|
88
|
+
* the app's dashboard config to use the GPT-Live provider with an AI brain
|
|
89
|
+
* (delegation) enabled; ignored otherwise. */
|
|
90
|
+
export interface AgentTool {
|
|
91
|
+
/** Letters, digits, underscore, dash; max 64 chars. */
|
|
92
|
+
name: string;
|
|
93
|
+
/** What the tool does and when to use it, for the model. */
|
|
94
|
+
description: string;
|
|
95
|
+
/** JSON-schema object describing the arguments. Omit for none. */
|
|
96
|
+
parameters?: Record<string, unknown>;
|
|
97
|
+
}
|
|
98
|
+
|
|
87
99
|
export interface AgentOptions {
|
|
88
100
|
/** A voice of the app's configured provider. Gemini: Charon, Aoede,
|
|
89
101
|
* Fenrir, Kore, Puck. OpenAI: marin, cedar, alloy, ash, ballad, coral,
|
|
@@ -97,6 +109,9 @@ export interface AgentOptions {
|
|
|
97
109
|
* keep a few sentences per user in your own storage and pass them
|
|
98
110
|
* here. Max 4000 chars. */
|
|
99
111
|
context?: string;
|
|
112
|
+
/** Tools the AI may call this session (D-103). Handle calls with
|
|
113
|
+
* CrowdPlay.onAgentToolCall(). Max 16. */
|
|
114
|
+
tools?: AgentTool[];
|
|
100
115
|
}
|
|
101
116
|
|
|
102
117
|
export type Phase = 'idle' | 'connecting' | 'connected' | 'failed';
|
|
@@ -164,6 +179,9 @@ export interface CrowdPlayEvents {
|
|
|
164
179
|
* "iPhone speaker"), connected candidates, and whether headphones are
|
|
165
180
|
* attached. Drive the call screen's output menu from this (D-079). */
|
|
166
181
|
audioRoute: { currentOutputName: string; detectedOutputs: string[]; headphonesConnected: boolean };
|
|
182
|
+
/** The voice AI asked the app to run a tool (D-103). Prefer
|
|
183
|
+
* CrowdPlay.onAgentToolCall(), which answers for you. */
|
|
184
|
+
agentToolCall: { callId: string; name: string; argumentsJSON: string };
|
|
167
185
|
}
|
|
168
186
|
|
|
169
187
|
export type CrowdPlayEventName = keyof CrowdPlayEvents;
|
|
@@ -177,6 +195,8 @@ interface NativeCrowdPlay {
|
|
|
177
195
|
consentText(): Promise<string>;
|
|
178
196
|
join(displayName: string, roomCode: string, consentGrantedAtMs: number,
|
|
179
197
|
agent: Record<string, string> | null): Promise<void>;
|
|
198
|
+
respondToAgentToolCall(callId: string, output: string): void;
|
|
199
|
+
updateAgentContext(text: string): void;
|
|
180
200
|
leave(): Promise<void>;
|
|
181
201
|
setMicMuted(muted: boolean): Promise<void>;
|
|
182
202
|
setCameraEnabled(enabled: boolean): Promise<void>;
|
|
@@ -279,6 +299,11 @@ const CrowdPlay = {
|
|
|
279
299
|
put('voice', agent.voice, 60);
|
|
280
300
|
put('systemPrompt', agent.systemPrompt, 8192);
|
|
281
301
|
put('context', agent.context, 4000);
|
|
302
|
+
// Tools cross the bridge as one JSON string; the native layer and the
|
|
303
|
+
// server both re-validate (D-103).
|
|
304
|
+
if (Array.isArray(agent.tools) && agent.tools.length) {
|
|
305
|
+
put('toolsJSON', JSON.stringify(agent.tools.slice(0, 16)), 8192);
|
|
306
|
+
}
|
|
282
307
|
}
|
|
283
308
|
return native().join(displayName.trim(), roomCode, consent.grantedAtMs, agentFields);
|
|
284
309
|
},
|
|
@@ -330,6 +355,41 @@ const CrowdPlay = {
|
|
|
330
355
|
return native().doctor();
|
|
331
356
|
},
|
|
332
357
|
|
|
358
|
+
/** Handle the voice AI's tool calls (D-103). The handler gets the tool
|
|
359
|
+
* name and its arguments (parsed JSON object) and returns the result as
|
|
360
|
+
* an object or JSON string; return null to decline on this device
|
|
361
|
+
* (another participant's device may answer; first answer wins). Ordinary
|
|
362
|
+
* app code, no AI here. The AI waits about 12 seconds per call. Returns
|
|
363
|
+
* a subscription; call .remove(). */
|
|
364
|
+
onAgentToolCall(
|
|
365
|
+
handler: (name: string, args: Record<string, unknown>) =>
|
|
366
|
+
Promise<Record<string, unknown> | string | null> | Record<string, unknown> | string | null
|
|
367
|
+
): { remove(): void } {
|
|
368
|
+
const subscription = events().addListener(
|
|
369
|
+
'crowdplay:agentToolCall',
|
|
370
|
+
(payload: { callId: string; name: string; argumentsJSON: string }) => {
|
|
371
|
+
void (async () => {
|
|
372
|
+
let args: Record<string, unknown> = {};
|
|
373
|
+
try { args = JSON.parse(payload.argumentsJSON); } catch { /* {} */ }
|
|
374
|
+
const result = await handler(payload.name, args);
|
|
375
|
+
if (result === null || result === undefined) return;
|
|
376
|
+
const output = typeof result === 'string' ? result : JSON.stringify(result);
|
|
377
|
+
native().respondToAgentToolCall(payload.callId, output);
|
|
378
|
+
})();
|
|
379
|
+
}
|
|
380
|
+
);
|
|
381
|
+
return { remove: () => subscription.remove() };
|
|
382
|
+
},
|
|
383
|
+
|
|
384
|
+
/** Tell the AI what just happened in the app, mid-session ("Night ended.
|
|
385
|
+
* Sam was eliminated."). Keep it to a sentence or two; long pushes are
|
|
386
|
+
* truncated. GPT-Live provider only; other providers ignore it. */
|
|
387
|
+
updateAgentContext(text: string): void {
|
|
388
|
+
if (typeof text === 'string' && text.trim()) {
|
|
389
|
+
native().updateAgentContext(text.trim());
|
|
390
|
+
}
|
|
391
|
+
},
|
|
392
|
+
|
|
333
393
|
/** Subscribe to state events. Returns a subscription; call .remove(). */
|
|
334
394
|
addListener<E extends CrowdPlayEventName>(
|
|
335
395
|
event: E,
|