Skip to content

Commit e795036

Browse files
Translate AI Transport example code to Swift
Done using the Claude skill added in 459a3a0. I've reviewed the translations. Decisions: - Have not translated the tool call progress examples that use LiveObjects; we agreed we'll leave those until we have the path-based API in Swift (same decision was already made in the Java translations, I believe). - Vapor seemed like the most appropriate web framework to use for the JWT examples; from what I can tell it's still the dominant one (compared to, say, Hummingbird). Claude validated the Vapor code by running a server with this example code and checking that it generated a JWT that could be used to perform an Ably REST request.
1 parent 459a3a0 commit e795036

12 files changed

Lines changed: 3142 additions & 0 deletions

src/data/languages/languageData.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ export default {
4646
javascript: '2.11',
4747
java: '1.6',
4848
python: '3.0',
49+
swift: '1.2',
4950
},
5051
spaces: {
5152
javascript: '0.4',

src/pages/docs/ai-transport/messaging/accepting-user-input.mdx

Lines changed: 313 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,20 @@ claims = {
5252
Map<String, String> claims = new HashMap<>();
5353
claims.put("x-ably-clientId", "user-123");
5454
```
55+
56+
{/* Swift example test harness
57+
ID: accepting-user-input-1
58+
To verify: copy this comment into a Swift file, paste the example code into the function body, run `swift build`
59+
60+
func example() {
61+
// --- example code starts here ---
62+
*/}
63+
```swift
64+
let claims = [
65+
"x-ably-clientId": "user-123"
66+
]
67+
```
68+
{/* --- end example code --- */}
5569
</Code>
5670

5771
The `clientId` is automatically attached to every message the user publishes, so agents can trust this identity.
@@ -91,6 +105,32 @@ channel.subscribe("user-input", message -> {
91105
processAndRespond(channel, text, promptId, userId);
92106
});
93107
```
108+
109+
{/* Swift example test harness
110+
ID: accepting-user-input-2
111+
To verify: copy this comment into a Swift file, paste the example code into the function body, run `swift build`
112+
113+
func example_accepting_user_input_2(
114+
channel: ARTRealtimeChannel,
115+
processAndRespond: @escaping (_ channel: ARTRealtimeChannel, _ text: String, _ promptID: String, _ userID: String) -> Void
116+
) {
117+
// --- example code starts here ---
118+
*/}
119+
```swift
120+
channel.subscribe("user-input") { message in
121+
guard let userID = message.clientId else { return }
122+
// promptId is a user-generated UUID for correlating responses
123+
guard let data = message.data as? [String: Any],
124+
let promptID = data["promptId"] as? String,
125+
let text = data["text"] as? String else {
126+
return
127+
}
128+
129+
print("Received prompt from user \(userID)")
130+
processAndRespond(channel, text, promptID, userID)
131+
}
132+
```
133+
{/* --- end example code --- */}
94134
</Code>
95135

96136
### Verify by role <a id="verify-role"/>
@@ -114,6 +154,20 @@ claims = {
114154
Map<String, String> claims = new HashMap<>();
115155
claims.put("ably.channel.*", "user");
116156
```
157+
158+
{/* Swift example test harness
159+
ID: accepting-user-input-3
160+
To verify: copy this comment into a Swift file, paste the example code into the function body, run `swift build`
161+
162+
func example() {
163+
// --- example code starts here ---
164+
*/}
165+
```swift
166+
let claims = [
167+
"ably.channel.*": "user"
168+
]
169+
```
170+
{/* --- end example code --- */}
117171
</Code>
118172

119173
The user claim is automatically attached to every message the user publishes, so agents can trust this role information.
@@ -164,6 +218,36 @@ channel.subscribe("user-input", message -> {
164218
processAndRespond(channel, text, promptId);
165219
});
166220
```
221+
222+
{/* Swift example test harness
223+
ID: accepting-user-input-4
224+
To verify: copy this comment into a Swift file, paste the example code into the function body, run `swift build`
225+
226+
func example_accepting_user_input_4(
227+
channel: ARTRealtimeChannel,
228+
processAndRespond: @escaping (_ channel: ARTRealtimeChannel, _ text: String, _ promptID: String) -> Void
229+
) {
230+
// --- example code starts here ---
231+
*/}
232+
```swift
233+
channel.subscribe("user-input") { message in
234+
let role = (try? message.extras?.toJSON())?["userClaim"] as? String
235+
// promptId is a user-generated UUID for correlating responses
236+
guard let data = message.data as? [String: Any],
237+
let promptID = data["promptId"] as? String,
238+
let text = data["text"] as? String else {
239+
return
240+
}
241+
242+
guard role == "user" else {
243+
print("Ignoring message from non-user")
244+
return
245+
}
246+
247+
processAndRespond(channel, text, promptID)
248+
}
249+
```
250+
{/* --- end example code --- */}
167251
</Code>
168252
169253
## Publish user input <a id="publish"/>
@@ -204,6 +288,24 @@ data.addProperty("promptId", promptId);
204288
data.addProperty("text", "What is the weather like today?");
205289
channel.publish("user-input", data);
206290
```
291+
292+
{/* Swift example test harness
293+
ID: accepting-user-input-5
294+
To verify: copy this comment into a Swift file, paste the example code into the function body, run `swift build`
295+
296+
func example_accepting_user_input_5(ably: ARTRealtime) {
297+
// --- example code starts here ---
298+
*/}
299+
```swift
300+
let channel = ably.channels.get("{{RANDOM_CHANNEL_NAME}}")
301+
302+
let promptID = UUID().uuidString
303+
channel.publish("user-input", data: [
304+
"promptId": promptID,
305+
"text": "What is the weather like today?"
306+
])
307+
```
308+
{/* --- end example code --- */}
207309
</Code>
208310
209311
<Aside data-type="note">
@@ -270,6 +372,37 @@ channel.subscribe("user-input", message -> {
270372
processAndRespond(channel, text, promptId);
271373
});
272374
```
375+
376+
{/* Swift example test harness
377+
ID: accepting-user-input-6
378+
To verify: copy this comment into a Swift file, paste the example code into the function body, run `swift build`
379+
380+
func example_accepting_user_input_6(
381+
processAndRespond: @escaping (_ channel: ARTRealtimeChannel, _ text: String, _ promptID: String) -> Void
382+
) {
383+
// --- example code starts here ---
384+
*/}
385+
```swift
386+
import Ably
387+
388+
let ably = ARTRealtime(key: "{{API_KEY}}")
389+
let channel = ably.channels.get("{{RANDOM_CHANNEL_NAME}}")
390+
391+
channel.subscribe("user-input") { message in
392+
guard let data = message.data as? [String: Any],
393+
let promptID = data["promptId"] as? String,
394+
let text = data["text"] as? String else {
395+
return
396+
}
397+
guard let userID = message.clientId else { return }
398+
399+
print("Received prompt from \(userID): \(text)")
400+
401+
// Process the prompt and generate a response
402+
processAndRespond(channel, text, promptID)
403+
}
404+
```
405+
{/* --- end example code --- */}
273406
</Code>
274407
275408
<Aside data-type="note">
@@ -334,6 +467,42 @@ void processAndRespond(Channel channel, String prompt, String promptId) {
334467
channel.publish(message);
335468
}
336469
```
470+
471+
{/* Swift example test harness
472+
ID: accepting-user-input-7
473+
To verify: copy this comment into a Swift file, paste the example code into the function body, run `swift build`
474+
475+
func example_accepting_user_input_7(
476+
channel: ARTRealtimeChannel,
477+
generateAIResponse: @Sendable @escaping (_ prompt: String) async throws -> String
478+
) {
479+
// --- example code starts here ---
480+
*/}
481+
```swift
482+
func processAndRespond(channel: ARTRealtimeChannel, prompt: String, promptID: String) async throws {
483+
// Generate the response (e.g., call your AI model)
484+
let response = try await generateAIResponse(prompt)
485+
486+
// Publish the response with the promptId for correlation
487+
let message = ARTMessage(name: "agent-response", data: response)
488+
message.extras = [
489+
"headers": [
490+
"promptId": promptID
491+
]
492+
] as ARTJsonCompatible
493+
494+
try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<Void, Error>) in
495+
channel.publish([message]) { _, error in
496+
if let error {
497+
continuation.resume(throwing: error)
498+
} else {
499+
continuation.resume()
500+
}
501+
}
502+
}
503+
}
504+
```
505+
{/* --- end example code --- */}
337506
</Code>
338507
339508
The user's client can then match responses to their original prompts:
@@ -415,6 +584,51 @@ channel.subscribe("agent-response", message -> {
415584
}
416585
});
417586
```
587+
588+
{/* Swift example test harness
589+
ID: accepting-user-input-8
590+
To verify: copy this comment into a Swift file, paste the example code into the function body, run `swift build`
591+
592+
@MainActor
593+
func example_accepting_user_input_8(channel: ARTRealtimeChannel) {
594+
// --- example code starts here ---
595+
*/}
596+
```swift
597+
var pendingPrompts: [String: String] = [:]
598+
599+
// Send a prompt and track it
600+
func sendPrompt(text: String) async throws -> String {
601+
let promptID = UUID().uuidString
602+
pendingPrompts[promptID] = text
603+
try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<Void, Error>) in
604+
channel.publish("user-input", data: ["promptId": promptID, "text": text]) { error in
605+
if let error {
606+
continuation.resume(throwing: error)
607+
} else {
608+
continuation.resume()
609+
}
610+
}
611+
}
612+
return promptID
613+
}
614+
615+
// Handle responses
616+
channel.subscribe("agent-response") { message in
617+
MainActor.assumeIsolated {
618+
guard let extras = (try? message.extras?.toJSON()) as? [String: Any],
619+
let headers = extras["headers"] as? [String: Any],
620+
let promptID = headers["promptId"] as? String else {
621+
return
622+
}
623+
624+
if let originalPrompt = pendingPrompts[promptID] {
625+
print("Response for \"\(originalPrompt)\": \(message.data ?? "")")
626+
pendingPrompts.removeValue(forKey: promptID)
627+
}
628+
}
629+
}
630+
```
631+
{/* --- end example code --- */}
418632
</Code>
419633
420634
<Aside data-type="note">
@@ -515,6 +729,67 @@ void streamResponse(Channel channel, String prompt, String promptId) throws Exce
515729
}
516730
}
517731
```
732+
733+
{/* Swift example test harness
734+
ID: accepting-user-input-9
735+
To verify: copy this comment into a Swift file, paste the example code into the function body, run `swift build`
736+
737+
func example_accepting_user_input_9(
738+
channel: ARTRealtimeChannel,
739+
generateTokens: @Sendable @escaping (_ prompt: String) -> any AsyncSequence<String, Never> & Sendable
740+
) {
741+
// --- example code starts here ---
742+
*/}
743+
```swift
744+
func streamResponse(channel: ARTRealtimeChannel, prompt: String, promptID: String) async throws {
745+
// Create initial message for message-per-response pattern
746+
let initialMessage = ARTMessage(name: "agent-response", data: "")
747+
initialMessage.extras = [
748+
"headers": [
749+
"promptId": promptID
750+
]
751+
] as ARTJsonCompatible
752+
753+
let msgSerial: String = try await withCheckedThrowingContinuation { continuation in
754+
channel.publish([initialMessage]) { publishResult, error in
755+
if let error {
756+
continuation.resume(throwing: error)
757+
return
758+
}
759+
760+
guard let serial = publishResult?.serials.first?.value else {
761+
continuation.resume(throwing: NSError(domain: "ExampleDomain", code: 0, userInfo: [NSLocalizedDescriptionKey: "No serial returned"]))
762+
return
763+
}
764+
765+
continuation.resume(returning: serial)
766+
}
767+
}
768+
769+
// Stream tokens by appending to the message
770+
for await token in generateTokens(prompt) {
771+
let messageToAppend = ARTMessage()
772+
messageToAppend.serial = msgSerial
773+
messageToAppend.data = token
774+
messageToAppend.extras = [
775+
"headers": [
776+
"promptId": promptID
777+
]
778+
] as ARTJsonCompatible
779+
780+
try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<Void, Error>) in
781+
channel.append(messageToAppend, operation: nil, params: nil) { _, error in
782+
if let error {
783+
continuation.resume(throwing: error)
784+
} else {
785+
continuation.resume()
786+
}
787+
}
788+
}
789+
}
790+
}
791+
```
792+
{/* --- end example code --- */}
518793
</Code>
519794
520795
<Aside data-type="note">
@@ -592,4 +867,42 @@ channel.subscribe("user-input", message -> {
592867
}
593868
});
594869
```
870+
871+
{/* Swift example test harness
872+
ID: accepting-user-input-10
873+
To verify: copy this comment into a Swift file, paste the example code into the function body, run `swift build`
874+
875+
@MainActor
876+
func example_accepting_user_input_10(
877+
channel: ARTRealtimeChannel,
878+
streamResponse: @escaping @MainActor (_ channel: ARTRealtimeChannel, _ text: String, _ promptID: String) async -> Void
879+
) {
880+
// --- example code starts here ---
881+
*/}
882+
```swift
883+
// Agent handling multiple concurrent prompts
884+
var activeRequests: [String: (userID: String, text: String)] = [:]
885+
886+
channel.subscribe("user-input") { message in
887+
MainActor.assumeIsolated {
888+
guard let data = message.data as? [String: Any],
889+
let promptID = data["promptId"] as? String,
890+
let text = data["text"] as? String else {
891+
return
892+
}
893+
guard let userID = message.clientId else { return }
894+
895+
// Track active request
896+
activeRequests[promptID] = (userID: userID, text: text)
897+
898+
Task { @MainActor in
899+
defer {
900+
activeRequests.removeValue(forKey: promptID)
901+
}
902+
await streamResponse(channel, text, promptID)
903+
}
904+
}
905+
}
906+
```
907+
{/* --- end example code --- */}
595908
</Code>

0 commit comments

Comments
 (0)