Skip to content

Commit 04d22b3

Browse files
VyasGurusanchitmonga22
authored andcommitted
fix calculator toolcall
1 parent b2e94a3 commit 04d22b3

8 files changed

Lines changed: 129 additions & 30 deletions

File tree

examples/ios/RunAnywhereAI/RunAnywhereAI/Features/Chat/ViewModels/LLMViewModel+ToolCalling.swift

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,10 +69,13 @@ extension LLMViewModel {
6969
toolCallInfo = nil
7070
}
7171

72+
// Strip any residual <think> tags before displaying
73+
let displayText = Self.stripThinkTags(from: result.text)
74+
7275
// Update the message with the result
7376
await updateMessageWithToolResult(
7477
at: messageIndex,
75-
text: result.text,
78+
text: displayText,
7679
toolCallInfo: toolCallInfo
7780
)
7881
}

examples/ios/RunAnywhereAI/RunAnywhereAI/Features/Chat/Views/ChatInterfaceView.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ struct ChatInterfaceView: View {
3131
@State private var showingLoRAManagement = false
3232
@State private var pendingLoRAURL: URL?
3333
@State private var loraScale: Float = 1.0
34+
@ObservedObject private var toolSettingsViewModel = ToolSettingsViewModel.shared
3435
@AppStorage("thinkingModeEnabled") private var thinkingModeEnabled = false
3536
@FocusState private var isTextFieldFocused: Bool
3637

@@ -452,7 +453,7 @@ extension ChatInterfaceView {
452453
thinkingModeBadge
453454
}
454455

455-
if viewModel.useToolCalling {
456+
if viewModel.useToolCalling && !toolSettingsViewModel.registeredTools.isEmpty {
456457
toolCallingBadge
457458
}
458459

examples/ios/RunAnywhereAI/RunAnywhereAI/Features/Settings/ToolSettingsView.swift

Lines changed: 32 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -84,11 +84,22 @@ class ToolSettingsViewModel: ObservableObject {
8484
category: "Utility"
8585
),
8686
executor: { args in
87-
let expression = args["expression"]?.stringValue
88-
?? args["input"]?.stringValue
89-
?? args["expr"]?.stringValue
90-
?? args.values.compactMap(\.stringValue).first
91-
?? "0"
87+
// Extract expression from args, handling both string and number ToolValue types
88+
let expression: String = {
89+
let keys = ["expression", "input", "expr"]
90+
for key in keys {
91+
if let val = args[key] {
92+
if let s = val.stringValue { return s }
93+
if let n = val.numberValue { return "\(n)" }
94+
}
95+
}
96+
// Fallback: try any value in the dict
97+
for val in args.values {
98+
if let s = val.stringValue { return s }
99+
if let n = val.numberValue { return "\(n)" }
100+
}
101+
return "0"
102+
}()
92103
print("Calculator received args: \(args), using expression: '\(expression)'")
93104
// Clean the expression - remove any non-math characters
94105
let cleanedExpression = expression
@@ -98,16 +109,22 @@ class ToolSettingsViewModel: ObservableObject {
98109
.replacingOccurrences(of: "÷", with: "/")
99110
.trimmingCharacters(in: .whitespacesAndNewlines)
100111

101-
do {
102-
let exp = NSExpression(format: cleanedExpression)
103-
if let result = exp.expressionValue(with: nil, context: nil) as? NSNumber {
104-
return [
105-
"result": .number(result.doubleValue),
106-
"expression": .string(expression)
107-
]
108-
}
109-
} catch {
110-
// Fall through to error
112+
// Validate expression contains only safe math characters
113+
let allowedChars = CharacterSet(charactersIn: "0123456789.+-*/() ")
114+
guard cleanedExpression.unicodeScalars.allSatisfy({ allowedChars.contains($0) }),
115+
!cleanedExpression.isEmpty else {
116+
return [
117+
"error": .string("Could not evaluate expression: \(expression)"),
118+
"expression": .string(expression)
119+
]
120+
}
121+
122+
let exp = NSExpression(format: cleanedExpression)
123+
if let result = exp.expressionValue(with: nil, context: nil) as? NSNumber {
124+
return [
125+
"result": .number(result.doubleValue),
126+
"expression": .string(expression)
127+
]
111128
}
112129
return [
113130
"error": .string("Could not evaluate expression: \(expression)"),

examples/ios/RunAnywhereAI/RunAnywhereAI/Features/Voice/VoiceAgentViewModel.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -389,7 +389,8 @@ final class VoiceAgentViewModel: ObservableObject {
389389
do {
390390
let settings = SettingsViewModel.shared
391391
let voiceConfig = VoiceSessionConfig(
392-
thinkingModeEnabled: settings.loadedModelSupportsThinking && settings.thinkingModeEnabled
392+
thinkingModeEnabled: settings.loadedModelSupportsThinking && settings.thinkingModeEnabled,
393+
maxTokens: settings.maxTokens
393394
)
394395
session = try await RunAnywhere.startVoiceSession(config: voiceConfig)
395396
sessionState = .listening

sdk/runanywhere-commons/src/features/llm/tool_calling.cpp

Lines changed: 68 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -399,13 +399,41 @@ static bool extract_json_value(const char* json_obj, const char* key, char** out
399399
*out_is_object = true;
400400
return true;
401401
}
402+
} else {
403+
// Scalar value (number, boolean, null)
404+
// Read until comma, closing brace, or whitespace
405+
size_t val_start = pos;
406+
size_t val_end = pos;
407+
while (val_end < len && json_obj[val_end] != ',' &&
408+
json_obj[val_end] != '}' && json_obj[val_end] != ']' &&
409+
json_obj[val_end] != '\n') {
410+
val_end++;
411+
}
412+
// Trim trailing whitespace
413+
while (val_end > val_start &&
414+
(json_obj[val_end - 1] == ' ' || json_obj[val_end - 1] == '\t')) {
415+
val_end--;
416+
}
417+
if (val_end > val_start) {
418+
size_t val_len = val_end - val_start;
419+
*out_value = static_cast<char*>(malloc(val_len + 1));
420+
if (*out_value) {
421+
memcpy(*out_value, json_obj + val_start, val_len);
422+
(*out_value)[val_len] = '\0';
423+
}
424+
*out_is_object = false;
425+
return true;
426+
}
402427
}
403428
}
404429
}
405430
}
406431

407432
// Move to end of key for continued scanning
433+
// Skip the in_string toggle - extract_json_string already
434+
// consumed the closing quote so in_string must stay false.
408435
i = key_end - 1;
436+
continue;
409437
}
410438
}
411439
in_string = !in_string;
@@ -664,10 +692,46 @@ static bool extract_tool_name_and_args(const char* json_obj, char** out_tool_nam
664692
}
665693
}
666694

667-
// No arguments found - use empty object
668-
*out_args_json = static_cast<char*>(malloc(3));
669-
if (*out_args_json) {
670-
std::memcpy(*out_args_json, "{}", 3);
695+
// No standard argument wrapper key found.
696+
// Fallback: collect all remaining keys (excluding the tool name key)
697+
// as flat arguments. This handles LLM output like:
698+
// {"tool": "calculate", "expression": "5 * 100"}
699+
{
700+
std::vector<std::string> all_keys = get_json_keys(json_obj);
701+
std::string flat_args = "{";
702+
bool first = true;
703+
for (const auto& k : all_keys) {
704+
// Skip the key that matched the tool name
705+
bool is_tool_key = false;
706+
for (int t = 0; TOOL_NAME_KEYS[t] != nullptr; t++) {
707+
if (str_equals_ignore_case(k.c_str(), TOOL_NAME_KEYS[t])) {
708+
is_tool_key = true;
709+
break;
710+
}
711+
}
712+
if (is_tool_key) continue;
713+
714+
char* kval = nullptr;
715+
bool kval_is_obj = false;
716+
if (extract_json_value(json_obj, k.c_str(), &kval, &kval_is_obj)) {
717+
if (!first) flat_args += ",";
718+
std::string escaped_key = escape_json_string(k.c_str());
719+
if (kval_is_obj) {
720+
flat_args += "\"" + escaped_key + "\":" + std::string(kval);
721+
} else if (kval) {
722+
std::string escaped_val = escape_json_string(kval);
723+
flat_args += "\"" + escaped_key + "\":\"" + escaped_val + "\"";
724+
}
725+
free(kval);
726+
first = false;
727+
}
728+
}
729+
flat_args += "}";
730+
731+
*out_args_json = static_cast<char*>(malloc(flat_args.size() + 1));
732+
if (*out_args_json) {
733+
std::memcpy(*out_args_json, flat_args.c_str(), flat_args.size() + 1);
734+
}
671735
}
672736
return true;
673737
}

sdk/runanywhere-swift/Sources/RunAnywhere/Public/Extensions/LLM/RunAnywhere+ToolCalling.swift

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -168,8 +168,17 @@ public extension RunAnywhere {
168168
let registeredTools = await ToolRegistry.shared.getAll()
169169
let tools = opts.tools ?? registeredTools
170170

171+
// Extract /no_think prefix before building the full prompt so it stays
172+
// at the beginning where the C++ inference layer expects it.
173+
let noThinkPrefix = "/no_think\n"
174+
let hasNoThink = prompt.hasPrefix(noThinkPrefix)
175+
let cleanPrompt = hasNoThink ? String(prompt.dropFirst(noThinkPrefix.count)) : prompt
176+
171177
let systemPrompt = buildToolSystemPrompt(tools: tools, options: opts)
172-
var fullPrompt = systemPrompt.isEmpty ? prompt : "\(systemPrompt)\n\nUser: \(prompt)"
178+
var fullPrompt = systemPrompt.isEmpty ? cleanPrompt : "\(systemPrompt)\n\nUser: \(cleanPrompt)"
179+
if hasNoThink {
180+
fullPrompt = "\(noThinkPrefix)\(fullPrompt)"
181+
}
173182

174183
var allToolCalls: [ToolCall] = []
175184
var allToolResults: [ToolResult] = []

sdk/runanywhere-swift/Sources/RunAnywhere/Public/Extensions/VoiceAgent/RunAnywhere+VoiceSession.swift

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -228,12 +228,11 @@ public actor VoiceSessionHandle {
228228
effectivePrompt = transcription
229229
}
230230

231-
let rawResponse = try await RunAnywhere.voiceAgentGenerateResponse(effectivePrompt)
232-
233-
// Step 3: Parse out <think> tags from response before TTS
234-
let parsed = ThinkingContentParser.extract(from: rawResponse)
235-
cleanedResponse = parsed.text
236-
thinkingContent = parsed.thinking
231+
let options = LLMGenerationOptions(maxTokens: config.maxTokens ?? 100)
232+
let result = try await RunAnywhere.generate(effectivePrompt, options: options)
233+
// generate() already runs ThinkingContentParser internally
234+
cleanedResponse = result.text
235+
thinkingContent = result.thinkingContent
237236

238237
emit(.responded(text: cleanedResponse, thinkingContent: thinkingContent))
239238

sdk/runanywhere-swift/Sources/RunAnywhere/Public/Extensions/VoiceAgent/VoiceAgentTypes.swift

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -225,18 +225,23 @@ public struct VoiceSessionConfig: Sendable {
225225
/// Whether thinking mode is enabled for the LLM.
226226
public var thinkingModeEnabled: Bool
227227

228+
/// Maximum tokens for LLM generation (nil uses SDK default of 100)
229+
public var maxTokens: Int?
230+
228231
public init(
229232
silenceDuration: TimeInterval = 1.5,
230233
speechThreshold: Float = 0.1,
231234
autoPlayTTS: Bool = true,
232235
continuousMode: Bool = true,
233-
thinkingModeEnabled: Bool = false
236+
thinkingModeEnabled: Bool = false,
237+
maxTokens: Int? = nil
234238
) {
235239
self.silenceDuration = silenceDuration
236240
self.speechThreshold = speechThreshold
237241
self.autoPlayTTS = autoPlayTTS
238242
self.continuousMode = continuousMode
239243
self.thinkingModeEnabled = thinkingModeEnabled
244+
self.maxTokens = maxTokens
240245
}
241246

242247
/// Default configuration

0 commit comments

Comments
 (0)