Skip to content

Commit fa7ab73

Browse files
Merge pull request #47 from SKaiNET-developers/feature/35-tools-calling
Feature/35 tools calling
2 parents abb710f + 1fb532c commit fa7ab73

25 files changed

Lines changed: 1298 additions & 216 deletions

File tree

llm-agent/api/jvm/llm-agent.api

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,17 @@ public final class sk/ainet/apps/kllama/chat/AgentLoop {
7070
public final class sk/ainet/apps/kllama/chat/ChatMLTemplate : sk/ainet/apps/kllama/chat/ChatTemplate {
7171
public fun <init> ()V
7272
public fun apply (Ljava/util/List;Ljava/util/List;Z)Ljava/lang/String;
73+
public fun containsToolCall (Ljava/lang/String;)Z
74+
public fun parseToolCalls (Ljava/lang/String;)Ljava/util/List;
75+
}
76+
77+
public final class sk/ainet/apps/kllama/chat/ChatMLToolCallingSupport : sk/ainet/apps/kllama/chat/ToolCallingSupport {
78+
public fun <init> ()V
79+
public fun createChatTemplate ()Lsk/ainet/apps/kllama/chat/ChatTemplate;
80+
public fun getFamily ()Ljava/lang/String;
81+
public fun parseToolCalls (Ljava/lang/String;)Ljava/util/List;
82+
public fun supports (Lsk/ainet/apps/kllama/chat/ModelMetadata;)Z
83+
public fun toolCallingMode (Lsk/ainet/apps/kllama/chat/ModelMetadata;)Lsk/ainet/apps/kllama/chat/ToolCallingMode;
7384
}
7485

7586
public final class sk/ainet/apps/kllama/chat/ChatMessage {
@@ -104,15 +115,114 @@ public final class sk/ainet/apps/kllama/chat/ChatRole : java/lang/Enum {
104115
public abstract interface class sk/ainet/apps/kllama/chat/ChatTemplate {
105116
public abstract fun apply (Ljava/util/List;Ljava/util/List;Z)Ljava/lang/String;
106117
public static synthetic fun apply$default (Lsk/ainet/apps/kllama/chat/ChatTemplate;Ljava/util/List;Ljava/util/List;ZILjava/lang/Object;)Ljava/lang/String;
118+
public fun containsToolCall (Ljava/lang/String;)Z
119+
public fun parseToolCalls (Ljava/lang/String;)Ljava/util/List;
107120
}
108121

109122
public final class sk/ainet/apps/kllama/chat/ChatTemplate$DefaultImpls {
110123
public static synthetic fun apply$default (Lsk/ainet/apps/kllama/chat/ChatTemplate;Ljava/util/List;Ljava/util/List;ZILjava/lang/Object;)Ljava/lang/String;
124+
public static fun containsToolCall (Lsk/ainet/apps/kllama/chat/ChatTemplate;Ljava/lang/String;)Z
125+
public static fun parseToolCalls (Lsk/ainet/apps/kllama/chat/ChatTemplate;Ljava/lang/String;)Ljava/util/List;
126+
}
127+
128+
public final class sk/ainet/apps/kllama/chat/GemmaChatTemplate : sk/ainet/apps/kllama/chat/ChatTemplate {
129+
public fun <init> ()V
130+
public fun apply (Ljava/util/List;Ljava/util/List;Z)Ljava/lang/String;
131+
public fun containsToolCall (Ljava/lang/String;)Z
132+
public fun parseToolCalls (Ljava/lang/String;)Ljava/util/List;
133+
}
134+
135+
public final class sk/ainet/apps/kllama/chat/GemmaToolCallParserStrategy : sk/ainet/apps/kllama/chat/ToolCallParserStrategy {
136+
public fun <init> ()V
137+
public fun containsToolCall (Ljava/lang/String;)Z
138+
public fun getFormatName ()Ljava/lang/String;
139+
public fun parse (Ljava/lang/String;)Ljava/util/List;
140+
}
141+
142+
public final class sk/ainet/apps/kllama/chat/GemmaToolCallingSupport : sk/ainet/apps/kllama/chat/ToolCallingSupport {
143+
public fun <init> ()V
144+
public fun createChatTemplate ()Lsk/ainet/apps/kllama/chat/ChatTemplate;
145+
public fun getFamily ()Ljava/lang/String;
146+
public fun parseToolCalls (Ljava/lang/String;)Ljava/util/List;
147+
public fun supports (Lsk/ainet/apps/kllama/chat/ModelMetadata;)Z
148+
public fun toolCallingMode (Lsk/ainet/apps/kllama/chat/ModelMetadata;)Lsk/ainet/apps/kllama/chat/ToolCallingMode;
149+
}
150+
151+
public final class sk/ainet/apps/kllama/chat/GenericToolCallingSupport : sk/ainet/apps/kllama/chat/ToolCallingSupport {
152+
public fun <init> ()V
153+
public fun createChatTemplate ()Lsk/ainet/apps/kllama/chat/ChatTemplate;
154+
public fun getFamily ()Ljava/lang/String;
155+
public fun parseToolCalls (Ljava/lang/String;)Ljava/util/List;
156+
public fun supports (Lsk/ainet/apps/kllama/chat/ModelMetadata;)Z
157+
public fun toolCallingMode (Lsk/ainet/apps/kllama/chat/ModelMetadata;)Lsk/ainet/apps/kllama/chat/ToolCallingMode;
111158
}
112159

113160
public final class sk/ainet/apps/kllama/chat/Llama3ChatTemplate : sk/ainet/apps/kllama/chat/ChatTemplate {
114161
public fun <init> ()V
115162
public fun apply (Ljava/util/List;Ljava/util/List;Z)Ljava/lang/String;
163+
public fun containsToolCall (Ljava/lang/String;)Z
164+
public fun parseToolCalls (Ljava/lang/String;)Ljava/util/List;
165+
}
166+
167+
public final class sk/ainet/apps/kllama/chat/Llama3ToolCallingSupport : sk/ainet/apps/kllama/chat/ToolCallingSupport {
168+
public fun <init> ()V
169+
public fun createChatTemplate ()Lsk/ainet/apps/kllama/chat/ChatTemplate;
170+
public fun getFamily ()Ljava/lang/String;
171+
public fun parseToolCalls (Ljava/lang/String;)Ljava/util/List;
172+
public fun supports (Lsk/ainet/apps/kllama/chat/ModelMetadata;)Z
173+
public fun toolCallingMode (Lsk/ainet/apps/kllama/chat/ModelMetadata;)Lsk/ainet/apps/kllama/chat/ToolCallingMode;
174+
}
175+
176+
public final class sk/ainet/apps/kllama/chat/ModelMetadata {
177+
public fun <init> ()V
178+
public fun <init> (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/List;Ljava/lang/String;)V
179+
public synthetic fun <init> (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/List;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
180+
public final fun component1 ()Ljava/lang/String;
181+
public final fun component2 ()Ljava/lang/String;
182+
public final fun component3 ()Ljava/lang/String;
183+
public final fun component4 ()Ljava/util/List;
184+
public final fun component5 ()Ljava/lang/String;
185+
public final fun copy (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/List;Ljava/lang/String;)Lsk/ainet/apps/kllama/chat/ModelMetadata;
186+
public static synthetic fun copy$default (Lsk/ainet/apps/kllama/chat/ModelMetadata;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/List;Ljava/lang/String;ILjava/lang/Object;)Lsk/ainet/apps/kllama/chat/ModelMetadata;
187+
public fun equals (Ljava/lang/Object;)Z
188+
public final fun getArchitecture ()Ljava/lang/String;
189+
public final fun getChatTemplate ()Ljava/lang/String;
190+
public final fun getFamily ()Ljava/lang/String;
191+
public final fun getSourceFormat ()Ljava/lang/String;
192+
public final fun getTokenizerHints ()Ljava/util/List;
193+
public fun hashCode ()I
194+
public fun toString ()Ljava/lang/String;
195+
}
196+
197+
public final class sk/ainet/apps/kllama/chat/QwenChatTemplate : sk/ainet/apps/kllama/chat/ChatTemplate {
198+
public fun <init> ()V
199+
public fun apply (Ljava/util/List;Ljava/util/List;Z)Ljava/lang/String;
200+
public fun containsToolCall (Ljava/lang/String;)Z
201+
public fun parseToolCalls (Ljava/lang/String;)Ljava/util/List;
202+
}
203+
204+
public final class sk/ainet/apps/kllama/chat/QwenToolCallingSupport : sk/ainet/apps/kllama/chat/ToolCallingSupport {
205+
public fun <init> ()V
206+
public fun createChatTemplate ()Lsk/ainet/apps/kllama/chat/ChatTemplate;
207+
public fun getFamily ()Ljava/lang/String;
208+
public fun parseToolCalls (Ljava/lang/String;)Ljava/util/List;
209+
public fun supports (Lsk/ainet/apps/kllama/chat/ModelMetadata;)Z
210+
public fun toolCallingMode (Lsk/ainet/apps/kllama/chat/ModelMetadata;)Lsk/ainet/apps/kllama/chat/ToolCallingMode;
211+
}
212+
213+
public final class sk/ainet/apps/kllama/chat/ResolutionResult {
214+
public fun <init> (Lsk/ainet/apps/kllama/chat/ToolCallingSupport;Lsk/ainet/apps/kllama/chat/ToolCallingMode;Ljava/lang/String;)V
215+
public final fun component1 ()Lsk/ainet/apps/kllama/chat/ToolCallingSupport;
216+
public final fun component2 ()Lsk/ainet/apps/kllama/chat/ToolCallingMode;
217+
public final fun component3 ()Ljava/lang/String;
218+
public final fun copy (Lsk/ainet/apps/kllama/chat/ToolCallingSupport;Lsk/ainet/apps/kllama/chat/ToolCallingMode;Ljava/lang/String;)Lsk/ainet/apps/kllama/chat/ResolutionResult;
219+
public static synthetic fun copy$default (Lsk/ainet/apps/kllama/chat/ResolutionResult;Lsk/ainet/apps/kllama/chat/ToolCallingSupport;Lsk/ainet/apps/kllama/chat/ToolCallingMode;Ljava/lang/String;ILjava/lang/Object;)Lsk/ainet/apps/kllama/chat/ResolutionResult;
220+
public fun equals (Ljava/lang/Object;)Z
221+
public final fun getMode ()Lsk/ainet/apps/kllama/chat/ToolCallingMode;
222+
public final fun getProvider ()Lsk/ainet/apps/kllama/chat/ToolCallingSupport;
223+
public final fun getReason ()Ljava/lang/String;
224+
public fun hashCode ()I
225+
public fun toString ()Ljava/lang/String;
116226
}
117227

118228
public abstract interface class sk/ainet/apps/kllama/chat/Tool {
@@ -138,7 +248,45 @@ public final class sk/ainet/apps/kllama/chat/ToolCall {
138248
public final class sk/ainet/apps/kllama/chat/ToolCallParser {
139249
public static final field INSTANCE Lsk/ainet/apps/kllama/chat/ToolCallParser;
140250
public final fun containsToolCall (Ljava/lang/String;)Z
251+
public final fun generateCallId ()Ljava/lang/String;
141252
public final fun parse (Ljava/lang/String;)Ljava/util/List;
253+
public final fun parseJsonToolCall (Ljava/lang/String;)Lsk/ainet/apps/kllama/chat/ToolCall;
254+
public final fun parseWith (Ljava/lang/String;Ljava/util/List;)Ljava/util/List;
255+
}
256+
257+
public abstract interface class sk/ainet/apps/kllama/chat/ToolCallParserStrategy {
258+
public abstract fun containsToolCall (Ljava/lang/String;)Z
259+
public abstract fun getFormatName ()Ljava/lang/String;
260+
public abstract fun parse (Ljava/lang/String;)Ljava/util/List;
261+
}
262+
263+
public final class sk/ainet/apps/kllama/chat/ToolCallingMode : java/lang/Enum {
264+
public static final field GENERIC Lsk/ainet/apps/kllama/chat/ToolCallingMode;
265+
public static final field NATIVE Lsk/ainet/apps/kllama/chat/ToolCallingMode;
266+
public static final field UNSUPPORTED Lsk/ainet/apps/kllama/chat/ToolCallingMode;
267+
public static fun getEntries ()Lkotlin/enums/EnumEntries;
268+
public static fun valueOf (Ljava/lang/String;)Lsk/ainet/apps/kllama/chat/ToolCallingMode;
269+
public static fun values ()[Lsk/ainet/apps/kllama/chat/ToolCallingMode;
270+
}
271+
272+
public abstract interface class sk/ainet/apps/kllama/chat/ToolCallingSupport {
273+
public abstract fun createChatTemplate ()Lsk/ainet/apps/kllama/chat/ChatTemplate;
274+
public abstract fun getFamily ()Ljava/lang/String;
275+
public abstract fun parseToolCalls (Ljava/lang/String;)Ljava/util/List;
276+
public abstract fun supports (Lsk/ainet/apps/kllama/chat/ModelMetadata;)Z
277+
public abstract fun toolCallingMode (Lsk/ainet/apps/kllama/chat/ModelMetadata;)Lsk/ainet/apps/kllama/chat/ToolCallingMode;
278+
}
279+
280+
public final class sk/ainet/apps/kllama/chat/ToolCallingSupportResolver {
281+
public static final field INSTANCE Lsk/ainet/apps/kllama/chat/ToolCallingSupportResolver;
282+
public final fun register (Lsk/ainet/apps/kllama/chat/ToolCallingSupport;)V
283+
public final fun registeredFamilies ()Ljava/util/List;
284+
public final fun resolve (Lsk/ainet/apps/kllama/chat/ModelMetadata;Ljava/lang/String;)Lsk/ainet/apps/kllama/chat/ToolCallingSupport;
285+
public static synthetic fun resolve$default (Lsk/ainet/apps/kllama/chat/ToolCallingSupportResolver;Lsk/ainet/apps/kllama/chat/ModelMetadata;Ljava/lang/String;ILjava/lang/Object;)Lsk/ainet/apps/kllama/chat/ToolCallingSupport;
286+
public final fun resolveOrFallback (Lsk/ainet/apps/kllama/chat/ModelMetadata;Ljava/lang/String;)Lsk/ainet/apps/kllama/chat/ToolCallingSupport;
287+
public static synthetic fun resolveOrFallback$default (Lsk/ainet/apps/kllama/chat/ToolCallingSupportResolver;Lsk/ainet/apps/kllama/chat/ModelMetadata;Ljava/lang/String;ILjava/lang/Object;)Lsk/ainet/apps/kllama/chat/ToolCallingSupport;
288+
public final fun resolveWithDiagnostics (Lsk/ainet/apps/kllama/chat/ModelMetadata;Ljava/lang/String;)Lsk/ainet/apps/kllama/chat/ResolutionResult;
289+
public static synthetic fun resolveWithDiagnostics$default (Lsk/ainet/apps/kllama/chat/ToolCallingSupportResolver;Lsk/ainet/apps/kllama/chat/ModelMetadata;Ljava/lang/String;ILjava/lang/Object;)Lsk/ainet/apps/kllama/chat/ResolutionResult;
142290
}
143291

144292
public final class sk/ainet/apps/kllama/chat/ToolDefinition {

llm-agent/src/commonMain/kotlin/sk/ainet/apps/kllama/chat/GemmaChatTemplate.kt

Lines changed: 5 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,6 @@ import kotlinx.serialization.json.Json
44
import kotlinx.serialization.json.JsonObject
55
import kotlinx.serialization.json.buildJsonArray
66
import kotlinx.serialization.json.buildJsonObject
7-
import kotlinx.serialization.json.jsonObject
8-
import kotlinx.serialization.json.jsonPrimitive
97
import kotlinx.serialization.json.put
108

119
/**
@@ -30,7 +28,7 @@ import kotlinx.serialization.json.put
3028
*/
3129
public class GemmaChatTemplate : ChatTemplate {
3230

33-
private val json = Json { ignoreUnknownKeys = true }
31+
private val gemmaParser = GemmaToolCallParserStrategy()
3432

3533
override fun apply(
3634
messages: List<ChatMessage>,
@@ -102,61 +100,9 @@ public class GemmaChatTemplate : ChatTemplate {
102100
return sb.toString()
103101
}
104102

105-
override fun parseToolCalls(text: String): List<ToolCall> {
106-
val calls = mutableListOf<ToolCall>()
103+
override fun parseToolCalls(text: String): List<ToolCall> =
104+
gemmaParser.parse(text)
107105

108-
// Find each top-level JSON object that contains "functionCall"
109-
var i = 0
110-
while (i < text.length) {
111-
val start = text.indexOf('{', i)
112-
if (start == -1) break
113-
val jsonStr = extractJsonObject(text, start)
114-
if (jsonStr != null && jsonStr.contains("\"functionCall\"")) {
115-
val call = parseFunctionCall(jsonStr)
116-
if (call != null) calls.add(call)
117-
i = start + jsonStr.length
118-
} else {
119-
i = start + 1
120-
}
121-
}
122-
123-
return calls
124-
}
125-
126-
override fun containsToolCall(text: String): Boolean {
127-
return text.contains("\"functionCall\"")
128-
}
129-
130-
private fun extractJsonObject(text: String, start: Int): String? {
131-
if (start >= text.length || text[start] != '{') return null
132-
var depth = 0
133-
var inString = false
134-
var escape = false
135-
for (i in start until text.length) {
136-
val c = text[i]
137-
if (escape) { escape = false; continue }
138-
if (c == '\\' && inString) { escape = true; continue }
139-
if (c == '"') { inString = !inString; continue }
140-
if (inString) continue
141-
if (c == '{') depth++
142-
else if (c == '}') { depth--; if (depth == 0) return text.substring(start, i + 1) }
143-
}
144-
return null
145-
}
146-
147-
private fun parseFunctionCall(jsonStr: String): ToolCall? {
148-
return try {
149-
val obj = json.parseToJsonElement(jsonStr).jsonObject
150-
val functionCall = obj["functionCall"]?.jsonObject ?: return null
151-
val name = functionCall["name"]?.jsonPrimitive?.content ?: return null
152-
val args = functionCall["args"]?.jsonObject ?: JsonObject(emptyMap())
153-
ToolCall(
154-
id = ToolCallParser.generateCallId(),
155-
name = name,
156-
arguments = args
157-
)
158-
} catch (_: Exception) {
159-
null
160-
}
161-
}
106+
override fun containsToolCall(text: String): Boolean =
107+
gemmaParser.containsToolCall(text)
162108
}
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
package sk.ainet.apps.kllama.chat
2+
3+
import kotlinx.serialization.json.Json
4+
import kotlinx.serialization.json.JsonObject
5+
import kotlinx.serialization.json.jsonObject
6+
import kotlinx.serialization.json.jsonPrimitive
7+
8+
/**
9+
* Parses Gemma-style tool calls that use `functionCall` JSON format:
10+
* ```json
11+
* {"functionCall": {"name": "...", "args": {...}}}
12+
* ```
13+
*/
14+
public class GemmaToolCallParserStrategy : ToolCallParserStrategy {
15+
16+
override val formatName: String = "gemma"
17+
18+
private val json = Json { ignoreUnknownKeys = true }
19+
20+
override fun parse(text: String): List<ToolCall> {
21+
val calls = mutableListOf<ToolCall>()
22+
var i = 0
23+
while (i < text.length) {
24+
val start = text.indexOf('{', i)
25+
if (start == -1) break
26+
val jsonStr = extractJsonObject(text, start)
27+
if (jsonStr != null && jsonStr.contains("\"functionCall\"")) {
28+
val call = parseFunctionCall(jsonStr)
29+
if (call != null) calls.add(call)
30+
i = start + jsonStr.length
31+
} else {
32+
i = start + 1
33+
}
34+
}
35+
return calls
36+
}
37+
38+
override fun containsToolCall(text: String): Boolean =
39+
text.contains("\"functionCall\"")
40+
41+
private fun extractJsonObject(text: String, start: Int): String? {
42+
if (start >= text.length || text[start] != '{') return null
43+
var depth = 0
44+
var inString = false
45+
var escape = false
46+
for (i in start until text.length) {
47+
val c = text[i]
48+
if (escape) { escape = false; continue }
49+
if (c == '\\' && inString) { escape = true; continue }
50+
if (c == '"') { inString = !inString; continue }
51+
if (inString) continue
52+
if (c == '{') depth++
53+
else if (c == '}') { depth--; if (depth == 0) return text.substring(start, i + 1) }
54+
}
55+
return null
56+
}
57+
58+
private fun parseFunctionCall(jsonStr: String): ToolCall? {
59+
return try {
60+
val obj = json.parseToJsonElement(jsonStr).jsonObject
61+
val functionCall = obj["functionCall"]?.jsonObject ?: return null
62+
val name = functionCall["name"]?.jsonPrimitive?.content ?: return null
63+
val args = functionCall["args"]?.jsonObject ?: JsonObject(emptyMap())
64+
ToolCall(
65+
id = ToolCallParser.generateCallId(),
66+
name = name,
67+
arguments = args
68+
)
69+
} catch (_: Exception) {
70+
null
71+
}
72+
}
73+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package sk.ainet.apps.kllama.chat
2+
3+
/**
4+
* Generic / fallback tool-calling provider.
5+
*
6+
* Uses [ChatMLTemplate] (the most widely adopted chat format) and the
7+
* default [ToolCallParser] so that models which are loosely compatible
8+
* can still attempt tool calling without a dedicated provider.
9+
*
10+
* The [toolCallingMode] always returns [ToolCallingMode.GENERIC] so
11+
* callers can clearly distinguish fallback usage from native support.
12+
*/
13+
public class GenericToolCallingSupport : ToolCallingSupport {
14+
15+
override val family: String = "generic"
16+
17+
/**
18+
* The generic fallback never matches automatically — it must be
19+
* selected explicitly via [ToolCallingSupportResolver.resolveOrFallback].
20+
*/
21+
override fun supports(metadata: ModelMetadata): Boolean = false
22+
23+
override fun createChatTemplate(): ChatTemplate = ChatMLTemplate()
24+
25+
override fun parseToolCalls(content: String): List<ToolCall> =
26+
ToolCallParser.parse(content)
27+
28+
override fun toolCallingMode(metadata: ModelMetadata): ToolCallingMode =
29+
ToolCallingMode.GENERIC
30+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package sk.ainet.apps.kllama.chat
2+
3+
/**
4+
* Common metadata about a loaded model, usable across different loaders
5+
* (GGUF, HuggingFace, etc.) for capability detection such as tool-calling
6+
* support and chat template selection.
7+
*
8+
* All fields are optional so that partially-available metadata still works.
9+
*
10+
* @param family Model family identifier, e.g. "qwen", "llama", "gemma".
11+
* @param architecture Low-level architecture name, e.g. "llama", "gemma3n".
12+
* @param chatTemplate Raw chat_template string from GGUF or tokenizer config.
13+
* @param tokenizerHints Special tokens that hint at capabilities, e.g. `<tool_call>`.
14+
* @param sourceFormat Origin of the metadata, e.g. "gguf", "hf".
15+
*/
16+
public data class ModelMetadata(
17+
val family: String? = null,
18+
val architecture: String? = null,
19+
val chatTemplate: String? = null,
20+
val tokenizerHints: List<String> = emptyList(),
21+
val sourceFormat: String? = null
22+
)

0 commit comments

Comments
 (0)