Objective-C
Objective-C
Streaming AI with Manual AI Tool Function Calling
See more AI Examples
Demonstrates how to get AI responses in streaming mode, including manual tool function calls.Chilkat Objective-C Downloads
#import <CkoJsonObject.h>
#import <CkoAi.h>
#import <NSString.h>
#import <CkoStringBuilder.h>
BOOL success = NO;
// Create the following JSON to define tool functions available for the AI to use.
// Note: You'll use the following JSON format regardless of the AI provider, whether
// it be ChatGPT, Gemini, Claude, Grok, etc. Chilkat automatically converts to the required
// format needed for a given AI provider.
// In this example, the application is providing a single function the AI may choose to call.
// {
// "tools": [
// {
// "name": "get_horoscope",
// "description": "Get today's horoscope for an astrological sign.",
// "parameters": {
// "properties": {
// "sign": {
// "type": "string",
// "description": "An astrological sign like Taurus or Aquarius"
// }
// }
// }
// }
// ]
// }
CkoJsonObject *jsonTools = [[CkoJsonObject alloc] init];
int toolIdx = 0;
jsonTools.I = [NSNumber numberWithInt: toolIdx];
[jsonTools UpdateString: @"tools[i].name" value: @"get_horoscope"];
[jsonTools UpdateString: @"tools[i].description" value: @"Get today's horoscope for an astrological sign."];
[jsonTools UpdateString: @"tools[i].parameters.properties.sign.type" value: @"string"];
[jsonTools UpdateString: @"tools[i].parameters.properties.sign.description" value: @"An astrological sign like Taurus or Aquarius"];
// More tools can be added as desired..
jsonTools.EmitCompact = NO;
NSLog(@"%@",[jsonTools Emit]);
CkoAi *ai = [[CkoAi alloc] init];
// Register the tools that will be made available to the AI.
[ai RegisterManualTools: jsonTools];
// The provider can be "openai", "google", "claude", "grok", "mistral", "custom", etc.
ai.Provider = @"openai";
// Use your provider's API key.
ai.ApiKey = @"MY_API_KEY";
// Choose a model.
ai.Model = @"gpt-5-mini";
// Tool function calling must always occur within a conversation.
NSString *conversation_name = @"convo_astrology";
NSString *sysMessage = @"You are a helpful astrologer";
NSString *devMessage = @"Respond only with markdown.";
[ai NewConvo: conversation_name sysMsg: sysMessage devMsg: devMessage];
// Provide inputs
[ai InputAddText: @"What is my horoscope? I am an Aquarius."];
// Get the response in streaming mode.
ai.Streaming = YES;
// In streaming mode, if we receive an AI event that is a request for tool use,
// we'll need to make the call to the JavaScript and then continue with a followup Ask,
// until the final response is received.
CkoStringBuilder *sbEventName = [[CkoStringBuilder alloc] init];
CkoStringBuilder *sbDelta = [[CkoStringBuilder alloc] init];
CkoStringBuilder *sbFullResponse = [[CkoStringBuilder alloc] init];
// When PollAi returns with an event, it's highly unlikely the
// call to NextAiEvent does not immediately return. Setting a max
// timeout is just a precaution..
int maxWaitMs = 5000;
CkoJsonObject *jsonFn = [[CkoJsonObject alloc] init];
BOOL finished = NO;
int numAsks = 0;
// Set a max # of followup Asks to prevent any unexpected infinite looping.
while (!finished && (numAsks < 10)) {
// Send the request to the AI model.
success = [ai Ask: @"text"];
if (success == NO) {
NSLog(@"%@",ai.LastErrorText);
return;
}
BOOL madeFunctionCalls = NO;
BOOL streamingDone = NO;
while (!streamingDone) {
int result = [[ai PollAi: NO] intValue];
if (result < 0) {
NSLog(@"%@",ai.LastErrorText);
NSLog(@"%@",@"Failed.");
return;
}
if (result > 0) {
// We have an event..
success = [ai NextAiEvent: [NSNumber numberWithInt: maxWaitMs] sbName: sbEventName sbDelta: sbDelta];
if (success == NO) {
NSLog(@"%@",ai.LastErrorText);
return;
}
// Is this an event where the AI is requesting a function call?
if ([sbEventName ContentsEqual: @"function_call" caseSensitive: YES]) {
[jsonFn LoadSb: sbDelta];
// Note: Chilkat will convert responses from all AI providers to this format:
// {
// "function_call": [
// {
// "name": "get_horoscope",
// "call_id": "call_RYmeysYQFocFc7Z2ofkv61dW",
// "arguments": "{\"sign\":\"Aquarius\"}",
// "args": {
// "sign": "Aquarius"
// }
// }
// ]
// }
int numFnCalls = [[jsonFn SizeOfArray: @"function_call"] intValue];
int fn_idx = 0;
while ((fn_idx < numFnCalls)) {
jsonFn.I = [NSNumber numberWithInt: fn_idx];
CkoStringBuilder *sbFnName = [[CkoStringBuilder alloc] init];
[jsonFn StringOfSb: @"function_call[i].name" sb: sbFnName];
NSString *callId = [jsonFn StringOf: @"function_call[i].call_id"];
if ([sbFnName ContentsEqual: @"get_horoscope" caseSensitive: YES] == YES) {
// The get_horoscope function (as defined above) has one argument named "sign".
NSString *zodiac_sign = [jsonFn StringOf: @"function_call[i].args.sign"];
NSLog(@"%@%@",@"zodiac_sign = ",zodiac_sign);
// Insert application code here to call your app's get_horoscope function, passing the zodiac_sign to it..
// For this example, we'll pretend the app's get_horoscope function returned the following:
NSString *applicationFnCallResult = @"Aquarius: Next Tuesday you will befriend a baby otter.";
// Provide the tool call result as an input for the followup Ask.
[ai InputAddFnResult: callId fnResult: applicationFnCallResult];
madeFunctionCalls = YES;
}
// Your application would add code to check for and handle each possible function call.
fn_idx = fn_idx + 1;
}
}
else {
if (![sbEventName ContentsEqual: @"empty" caseSensitive: YES]) {
[sbFullResponse AppendSb: sbDelta];
if ([sbEventName ContentsEqual: @"null_terminator" caseSensitive: YES]) {
streamingDone = YES;
}
}
}
}
else {
// No event arrived, so wait a short time rather than spin in a loop..
[ai SleepMs: [NSNumber numberWithInt: 100]];
}
}
if (!madeFunctionCalls) {
finished = YES;
}
numAsks = numAsks + 1;
}
NSLog(@"%@",@"Full Response:");
NSLog(@"%@",[sbFullResponse GetAsString]);