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
133 changes: 118 additions & 15 deletions iOS/LeapChatExample/LeapChatExample/ChatStore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,16 +23,16 @@ class ChatStore {
messages.append(
MessageBubble(
content: "🔍 Checking LeapSDK integration...",
isUser: false))
messageType: .assistant))

guard
let modelURL = Bundle.main.url(
forResource: "qwen3-1_7b_8da4w", withExtension: "bundle")
forResource: "LFM2-1.2B-8da4w_output_8da8w-seq_4096", withExtension: "bundle")
else {
messages.append(
MessageBubble(
content: "❗️ Could not find qwen3-1_7b_8da4w.bundle in the bundle.",
isUser: false))
content: "❗️ Could not find LFM2-1.2B-8da4w_output_8da8w-seq_4096.bundle in the bundle.",
messageType: .assistant))
isModelLoading = false
return
}
Expand All @@ -41,25 +41,44 @@ class ChatStore {
messages.append(
MessageBubble(
content: "📁 Found model bundle at: \(modelURL.lastPathComponent)",
isUser: false))
messageType: .assistant))

let modelRunner = try await Leap.load(url: modelURL)
self.modelRunner = modelRunner
conversation = Conversation(modelRunner: modelRunner, history: [])
let conversation = Conversation(modelRunner: modelRunner, history: [])

// Register the compute_sum function
conversation.registerFunction(
LeapFunction(
name: "compute_sum",
description: "Compute sum of a series of numbers",
parameters: [
LeapFunctionParameter(
name: "values",
type: LeapFunctionParameterType.array(ArrayType(
itemType: LeapFunctionParameterType.string(StringType())
)),
description: "Numbers to compute sum. Values should be represented as strings."
)
]
)
)

self.conversation = conversation
messages.append(
MessageBubble(
content: "✅ Model loaded successfully! You can start chatting.",
isUser: false))
messageType: .assistant))
} catch {
print("Error loading model: \(error)")
let errorMessage = "🚨 Failed to load model: \(error.localizedDescription)"
messages.append(MessageBubble(content: errorMessage, isUser: false))
messages.append(MessageBubble(content: errorMessage, messageType: .assistant))

// Check if it's a LeapError
if let leapError = error as? LeapError {
print("LeapError details: \(leapError)")
messages.append(
MessageBubble(content: "📋 Error type: \(String(describing: leapError))", isUser: false))
MessageBubble(content: "📋 Error type: \(String(describing: leapError))", messageType: .assistant))
}
}

Expand All @@ -74,12 +93,14 @@ class ChatStore {
guard !trimmed.isEmpty else { return }

let userMessage = ChatMessage(role: .user, content: [.text(trimmed)])
messages.append(MessageBubble(content: trimmed, isUser: true))
messages.append(MessageBubble(content: trimmed, messageType: .user))
input = ""
isLoading = true
currentAssistantMessage = ""

let stream = conversation!.generateResponse(message: userMessage)
var functionCallsToProcess: [LeapFunctionCall] = []

do {
for try await resp in stream {
print(resp)
Expand All @@ -88,19 +109,101 @@ class ChatStore {
currentAssistantMessage.append(str)
case .complete(_, _):
if !currentAssistantMessage.isEmpty {
messages.append(MessageBubble(content: currentAssistantMessage, isUser: false))
messages.append(MessageBubble(content: currentAssistantMessage, messageType: .assistant))
}
currentAssistantMessage = ""
isLoading = false
case .functionCall(_):
break // Function calls not used in this example
case default:
case .functionCall(let calls):
functionCallsToProcess.append(contentsOf: calls)
default:
break // Handle any other case
}
}

// Process function calls after the generation is complete
if !functionCallsToProcess.isEmpty {
await processFunctionCalls(functionCallsToProcess)
}
} catch {
currentAssistantMessage = "Error: \(error.localizedDescription)"
messages.append(MessageBubble(content: currentAssistantMessage, messageType: .assistant))
currentAssistantMessage = ""
isLoading = false
}
}

@MainActor
private func processFunctionCalls(_ functionCalls: [LeapFunctionCall]) async {
for call in functionCalls {
switch call.name {
case "compute_sum":
// Extract values from arguments
if let valuesArray = call.arguments["values"] as? [String] {
var sum = 0.0
for value in valuesArray {
sum += Double(value) ?? 0.0
}
let result = "Sum = \(sum)"

// Add tool message to display
messages.append(MessageBubble(content: result, messageType: .tool))

// Send tool response back to conversation
let toolMessage = ChatMessage(role: .tool, content: [.text(result)])
await sendToolResponse(toolMessage)
} else {
let errorResult = "Error: Could not process values for compute_sum"
messages.append(MessageBubble(content: errorResult, messageType: .tool))

let toolMessage = ChatMessage(role: .tool, content: [.text(errorResult)])
await sendToolResponse(toolMessage)
}
default:
let unknownResult = "Tool: \(call.name) is not available"
messages.append(MessageBubble(content: unknownResult, messageType: .tool))

let toolMessage = ChatMessage(role: .tool, content: [.text(unknownResult)])
await sendToolResponse(toolMessage)
}
}
}

@MainActor
private func sendToolResponse(_ toolMessage: ChatMessage) async {
guard let conversation = conversation else { return }

isLoading = true
currentAssistantMessage = ""

let stream = conversation.generateResponse(message: toolMessage)
var functionCallsToProcess: [LeapFunctionCall] = []

do {
for try await resp in stream {
print(resp)
switch resp {
case .chunk(let str):
currentAssistantMessage.append(str)
case .complete(_, _):
if !currentAssistantMessage.isEmpty {
messages.append(MessageBubble(content: currentAssistantMessage, messageType: .assistant))
}
currentAssistantMessage = ""
isLoading = false
case .functionCall(let calls):
functionCallsToProcess.append(contentsOf: calls)
default:
break
}
}

// Process any additional function calls
if !functionCallsToProcess.isEmpty {
await processFunctionCalls(functionCallsToProcess)
}
} catch {
currentAssistantMessage = "Error: \(error.localizedDescription)"
messages.append(MessageBubble(content: currentAssistantMessage, isUser: false))
messages.append(MessageBubble(content: currentAssistantMessage, messageType: .assistant))
currentAssistantMessage = ""
isLoading = false
}
Expand Down
16 changes: 15 additions & 1 deletion iOS/LeapChatExample/LeapChatExample/MessageBubble.swift
Original file line number Diff line number Diff line change
@@ -1,8 +1,22 @@
import Foundation

enum MessageType {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It will be better to use the widely accepted term of Role instead of types here.

case user
case assistant
case tool
}

struct MessageBubble {
let id = UUID()
let content: String
let isUser: Bool
let messageType: MessageType
let timestamp: Date = Date()

var isUser: Bool {
return messageType == .user
}

var isTool: Bool {
return messageType == .tool
}
}
64 changes: 47 additions & 17 deletions iOS/LeapChatExample/LeapChatExample/MessageRow.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,27 +4,57 @@ struct MessageRow: View {
let message: MessageBubble

var body: some View {
HStack {
if message.isUser {
if message.isTool {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It could be a switch case based on the message role.

// Tool message display
HStack(alignment: .top, spacing: 8) {
Image(systemName: "wrench.and.screwdriver")
.foregroundColor(.secondary)
.frame(width: 24, height: 24)
.background(
RoundedRectangle(cornerRadius: 6)
.stroke(Color.secondary, lineWidth: 1)
)

VStack(alignment: .leading, spacing: 4) {
Text("Tool")
.font(.caption)
.foregroundColor(.secondary)
.fontWeight(.medium)

Text(message.content)
.padding(.horizontal, 12)
.padding(.vertical, 8)
.background(Color(.systemGray6))
.foregroundColor(.primary)
.clipShape(RoundedRectangle(cornerRadius: 12))
}

Spacer(minLength: 60)
}
.padding(.horizontal, 4)
} else {
HStack {
if message.isUser {
Spacer(minLength: 60)

Text(message.content)
.padding(.horizontal, 16)
.padding(.vertical, 10)
.background(Color.blue)
.foregroundColor(.white)
.clipShape(RoundedRectangle(cornerRadius: 18))
} else {
Text(message.content)
.padding(.horizontal, 16)
.padding(.vertical, 10)
.background(Color(.systemGray5))
.foregroundColor(.primary)
.clipShape(RoundedRectangle(cornerRadius: 18))
Text(message.content)
.padding(.horizontal, 16)
.padding(.vertical, 10)
.background(Color.blue)
.foregroundColor(.white)
.clipShape(RoundedRectangle(cornerRadius: 18))
} else {
Text(message.content)
.padding(.horizontal, 16)
.padding(.vertical, 10)
.background(Color(.systemGray5))
.foregroundColor(.primary)
.clipShape(RoundedRectangle(cornerRadius: 18))

Spacer(minLength: 60)
Spacer(minLength: 60)
}
}
.padding(.horizontal, 4)
}
.padding(.horizontal, 4)
}
}
2 changes: 1 addition & 1 deletion iOS/LeapChatExample/LeapChatExample/MessagesListView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ struct MessagesListView: View {

if store.isLoading && !store.currentAssistantMessage.isEmpty {
MessageRow(
message: MessageBubble(content: store.currentAssistantMessage, isUser: false)
message: MessageBubble(content: store.currentAssistantMessage, messageType: .assistant)
)
.id("streaming")
} else if store.isLoading {
Expand Down
38 changes: 35 additions & 3 deletions iOS/LeapChatExample/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ A comprehensive chat application demonstrating advanced LeapSDK features includi
- **Message History**: Persistent conversation management
- **Rich UI Components**: Custom message rows, input views, and animations
- **Error Handling**: Robust error management and user feedback
- **Function Calling**: Tool support with compute_sum example function

## Requirements

Expand Down Expand Up @@ -55,12 +56,13 @@ LeapChatExample/
├── ChatStore.swift # Core business logic
├── MessagesListView.swift # Message list component
├── MessageRow.swift # Individual message display
├── MessageBubble.swift # Message bubble UI
├── MessageBubble.swift # Message bubble data model
├── ToolMessageRow.swift # Tool message display component
├── ChatInputView.swift # Input field component
├── TypingIndicator.swift # Typing animation
├── Assets.xcassets # App assets
└── Resources/ # Model bundles
└── qwen3-1_7b_8da4w.bundle
└── LFM2-1.2B-8da4w_output_8da8w-seq_4096.bundle
```

## Code Overview
Expand Down Expand Up @@ -118,6 +120,36 @@ for try await chunk in modelRunner.generateResponse(for: conversation) {
}
```

### Function Calling
The app includes tool support with a `compute_sum` function that can add numbers:
```swift
// Function registration
conversation.registerFunction(
LeapFunction(
name: "compute_sum",
description: "Compute sum of a series of numbers",
parameters: [
LeapFunctionParameter(
name: "values",
type: .array(.string),
description: "Numbers to compute sum. Values should be represented as strings."
)
]
)
)

// Function call handling
case .functionCall(let calls):
for call in calls {
if call.name == "compute_sum" {
let result = computeSum(call.arguments["values"])
// Display tool result and continue conversation
}
}
```

Try asking the model: "Can you compute the sum of 5, 10, and 15?"

### Conversation Management
Maintains chat context across messages:
```swift
Expand Down Expand Up @@ -146,7 +178,7 @@ Smooth animations for message appearance and typing indicators.
Update the model path in `ChatStore.swift`:
```swift
let modelRunner = try await Leap.load(
modelPath: Bundle.main.bundlePath + "/qwen3-1_7b_8da4w.bundle"
modelPath: Bundle.main.bundlePath + "/LFM2-1.2B-8da4w_output_8da8w-seq_4096.bundle"
)
```

Expand Down