Skip to content

feat: tool calling #59

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
Jul 22, 2025
Merged
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
27 changes: 24 additions & 3 deletions apps/example-apple/src/schema-demos.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,37 @@
import { foundationModels } from '@react-native-ai/apple'
import { tool } from 'ai'
import { z } from 'zod'

export async function basicStringDemo() {
const schema = z
.object({
value: z.string().describe('A simple text string'),
response: z.string(),
})
.describe('String response')

return await foundationModels.generateText(
[{ role: 'user', content: 'Generate a city name' }],
{ schema }
[
{
role: 'system',
content: `Help the person with getting weather information.`,
},
{ role: 'user', content: 'Is it hotter in Wroclaw or in Warsaw?' },
],
{
schema,
tools: {
getWeather: tool({
description: 'Get the weather for a given city',
parameters: z.object({
city: z.string().describe('The city to get the weather for'),
}),
execute: async (args) => {
const temperature = Math.floor(Math.random() * 20) + 10
return `Weather forecast for ${args.city}: ${temperature}°C`
},
}),
},
}
)
}

Expand Down
89 changes: 79 additions & 10 deletions packages/apple-llm/ios/AppleLLM.mm
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,25 @@
#import "AppleLLM-Swift.h"
#endif

#import <React/RCTCallInvokerModule.h>
#import <React/RCTCallInvoker.h>
#import <ReactCommon/RCTTurboModule.h>

#import <jsi/jsi.h>

#import <NativeAppleLLM/NativeAppleLLM.h>

@interface AppleLLM : NativeAppleLLMSpecBase <NativeAppleLLMSpec>
@interface AppleLLM : NativeAppleLLMSpecBase <NativeAppleLLMSpec, RCTCallInvokerModule>
@property (strong, nonatomic) AppleLLMImpl *llm;
@end

using namespace facebook;
using namespace JS::NativeAppleLLM;

@implementation AppleLLM

@synthesize callInvoker;

- (instancetype)init {
self = [super init];
if (self) {
Expand All @@ -33,25 +42,87 @@ + (NSString *)moduleName {
return @"AppleLLM";
}

- (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:(const facebook::react::ObjCTurboModule::InitParams &)params {
return std::make_shared<facebook::react::NativeAppleLLMSpecJSI>(params);
- (void)callToolWithName:(NSString *)toolName
arguments:(NSDictionary *)arguments
completion:(void (^)(id result, NSError *error))completion {
[self.callInvoker callInvoker]->invokeAsync([toolName, arguments, completion](jsi::Runtime& rt) {
@try {
auto global = rt.global();
auto tools = global.getPropertyAsObject(rt, "__APPLE_LLM_TOOLS__");
auto tool = tools.getPropertyAsFunction(rt, [toolName UTF8String]);

auto args = react::TurboModuleConvertUtils::convertObjCObjectToJSIValue(rt, arguments).getObject(rt);

auto result = tool.call(rt, args);

auto isPromise = result.isObject() && result.asObject(rt).hasProperty(rt, "then");

if (!isPromise) {
id response = react::TurboModuleConvertUtils::convertJSIValueToObjCObject(rt, result, nullptr, false);
completion(response, nil);
return;
}

auto promiseObj = result.asObject(rt);

auto onResolve = jsi::Function::createFromHostFunction(rt,
jsi::PropNameID::forAscii(rt, "resolve"),
1,
[completion](jsi::Runtime& rt,
const jsi::Value&,
const jsi::Value* args,
size_t count) {
id response = react::TurboModuleConvertUtils::convertJSIValueToObjCObject(rt, args[0], nullptr, false);
completion(response, nil);
return jsi::Value::undefined();
});

auto onReject = jsi::Function::createFromHostFunction(rt,
jsi::PropNameID::forAscii(rt, "reject"),
1,
[completion](jsi::Runtime& rt,
const jsi::Value&,
const jsi::Value* args,
size_t count) {
NSError *error = [NSError errorWithDomain:@"AppleLLM"
code:1
userInfo:@{NSLocalizedDescriptionKey: @"There was an error calling tool"}];
completion(nil, error);
return jsi::Value::undefined();
});

promiseObj.getPropertyAsFunction(rt, "then").callWithThis(rt, promiseObj, onResolve, onReject);
} @catch (NSException *exception) {
NSError *error = [NSError errorWithDomain:@"AppleLLM"
code:1
userInfo:@{NSLocalizedDescriptionKey: exception.reason}];
completion(nil, error);
}
});
}

- (std::shared_ptr<react::TurboModule>)getTurboModule:(const react::ObjCTurboModule::InitParams &)params {
return std::make_shared<react::NativeAppleLLMSpecJSI>(params);
}

- (void)generateText:(nonnull NSArray *)messages
options:(AppleGenerationOptions &)options
resolve:(nonnull RCTPromiseResolveBlock)resolve
reject:(nonnull RCTPromiseRejectBlock)reject {
// TODO: Consider direct C++ struct passing to avoid NSDictionary conversion overhead.
// Current approach converts C++ optional values to NSNull/NSNumber for Swift interop,
// but direct struct marshalling could eliminate this bridge layer entirely.
NSDictionary *opts = @{
@"temperature": options.temperature().has_value() ? @(options.temperature().value()) : [NSNull null],
@"maxTokens": options.maxTokens().has_value() ? @(options.maxTokens().value()) : [NSNull null],
@"topP": options.topP().has_value() ? @(options.topP().value()) : [NSNull null],
@"topK": options.topK().has_value() ? @(options.topK().value()) : [NSNull null],
@"schema": options.schema()
@"schema": options.schema(),
@"tools": options.tools()
};
[_llm generateText:messages options:opts resolve:resolve reject:reject];

auto callToolBlock = ^(NSString *toolName, id parameters, void (^completion)(id, NSError *)) {
[self callToolWithName:toolName arguments:(NSDictionary *)parameters completion:completion];
};

[_llm generateText:messages options:opts resolve:resolve reject:reject toolInvoker:callToolBlock];
}

- (void)cancelStream:(nonnull NSString *)streamId {
Expand Down Expand Up @@ -98,5 +169,3 @@ - (nonnull NSNumber *)isAvailable {
}

@end


9 changes: 9 additions & 0 deletions packages/apple-llm/ios/AppleLLMError.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ enum AppleLLMError: Error, LocalizedError {
case invalidMessage(String)
case conflictingSamplingMethods
case invalidSchema(String)
case toolCallError(Error)
case unknownToolCallError

var errorDescription: String? {
switch self {
Expand All @@ -32,7 +34,12 @@ enum AppleLLMError: Error, LocalizedError {
return "Cannot specify both topP and topK parameters simultaneously. Please use only one sampling method."
case .invalidSchema(let message):
return "Invalid schema: \(message)"
case .toolCallError(let error):
return "Error calling tool: \(error.localizedDescription)"
case .unknownToolCallError:
return "Unknown tool call error"
}

}

var code: Int {
Expand All @@ -44,6 +51,8 @@ enum AppleLLMError: Error, LocalizedError {
case .invalidMessage: return 5
case .conflictingSamplingMethods: return 6
case .invalidSchema: return 7
case .unknownToolCallError: return 8
case .toolCallError: return 9
}
}
}
Loading
Loading