kimaki 0.0.3 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,296 @@
1
+ import { describe, it, expect } from 'vitest'
2
+ import { tool } from 'ai'
3
+ import { z } from 'zod'
4
+ import { Type } from '@google/genai'
5
+ import type { FunctionDeclaration, FunctionCall } from '@google/genai'
6
+ import {
7
+ aiToolToGenAIFunction,
8
+ aiToolToCallableTool,
9
+ extractSchemaFromTool,
10
+ } from './ai-tool-to-genai.js'
11
+
12
+ describe('AI Tool to GenAI Conversion', () => {
13
+ it('should convert a simple Zod-based tool', () => {
14
+ const weatherTool = tool({
15
+ description: 'Get the current weather for a location',
16
+ inputSchema: z.object({
17
+ location: z.string().describe('The city name'),
18
+ unit: z.enum(['celsius', 'fahrenheit']).optional(),
19
+ }),
20
+ execute: async ({ location, unit }) => {
21
+ return {
22
+ temperature: 72,
23
+ unit: unit || 'fahrenheit',
24
+ condition: 'sunny',
25
+ }
26
+ },
27
+ })
28
+
29
+ const genAIFunction = aiToolToGenAIFunction(weatherTool)
30
+
31
+ expect(genAIFunction).toMatchInlineSnapshot(`
32
+ {
33
+ "description": "Get the current weather for a location",
34
+ "name": "tool",
35
+ "parameters": {
36
+ "properties": {
37
+ "location": {
38
+ "description": "The city name",
39
+ "type": "STRING",
40
+ },
41
+ "unit": {
42
+ "enum": [
43
+ "celsius",
44
+ "fahrenheit",
45
+ ],
46
+ "type": "STRING",
47
+ },
48
+ },
49
+ "required": [
50
+ "location",
51
+ ],
52
+ "type": "OBJECT",
53
+ },
54
+ }
55
+ `)
56
+ })
57
+
58
+ it('should handle complex nested schemas', () => {
59
+ const complexTool = tool({
60
+ description: 'Process complex data',
61
+ inputSchema: z.object({
62
+ user: z.object({
63
+ name: z.string(),
64
+ age: z.number().int().min(0).max(150),
65
+ email: z.string().email(),
66
+ }),
67
+ preferences: z.array(z.string()),
68
+ metadata: z.record(z.string(), z.unknown()).optional(),
69
+ }),
70
+ execute: async (input) => input,
71
+ })
72
+
73
+ const genAIFunction = aiToolToGenAIFunction(complexTool)
74
+
75
+ expect(genAIFunction.parameters).toMatchInlineSnapshot(`
76
+ {
77
+ "properties": {
78
+ "metadata": {
79
+ "type": "OBJECT",
80
+ },
81
+ "preferences": {
82
+ "items": {
83
+ "type": "STRING",
84
+ },
85
+ "type": "ARRAY",
86
+ },
87
+ "user": {
88
+ "properties": {
89
+ "age": {
90
+ "format": "int32",
91
+ "maximum": 150,
92
+ "minimum": 0,
93
+ "type": "INTEGER",
94
+ },
95
+ "email": {
96
+ "pattern": "^(?!\\.)(?!.*\\.\\.)([A-Za-z0-9_'+\\-\\.]*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$",
97
+ "type": "STRING",
98
+ },
99
+ "name": {
100
+ "type": "STRING",
101
+ },
102
+ },
103
+ "required": [
104
+ "name",
105
+ "age",
106
+ "email",
107
+ ],
108
+ "type": "OBJECT",
109
+ },
110
+ },
111
+ "required": [
112
+ "user",
113
+ "preferences",
114
+ ],
115
+ "type": "OBJECT",
116
+ }
117
+ `)
118
+ })
119
+
120
+ it('should extract schema from tool', () => {
121
+ const testTool = tool({
122
+ inputSchema: z.object({
123
+ test: z.string(),
124
+ }),
125
+ execute: async () => {},
126
+ })
127
+
128
+ const schema = extractSchemaFromTool(testTool)
129
+
130
+ expect(schema).toMatchInlineSnapshot(`
131
+ {
132
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
133
+ "additionalProperties": false,
134
+ "properties": {
135
+ "test": {
136
+ "type": "string",
137
+ },
138
+ },
139
+ "required": [
140
+ "test",
141
+ ],
142
+ "type": "object",
143
+ }
144
+ `)
145
+ })
146
+
147
+ it('should handle tools with no input schema', () => {
148
+ const simpleTool = tool({
149
+ description: 'Simple tool with no inputs',
150
+ inputSchema: z.object({}),
151
+ execute: async () => ({ result: 'done' }),
152
+ })
153
+
154
+ const genAIFunction = aiToolToGenAIFunction(simpleTool)
155
+
156
+ expect(genAIFunction).toMatchInlineSnapshot(`
157
+ {
158
+ "description": "Simple tool with no inputs",
159
+ "name": "tool",
160
+ "parameters": {
161
+ "properties": {},
162
+ "type": "OBJECT",
163
+ },
164
+ }
165
+ `)
166
+ })
167
+
168
+ it('should handle union types', () => {
169
+ const unionTool = tool({
170
+ description: 'Tool with union types',
171
+ inputSchema: z.object({
172
+ value: z.union([z.string(), z.number(), z.boolean()]),
173
+ }),
174
+ execute: async ({ value }) => ({ received: value }),
175
+ })
176
+
177
+ const genAIFunction = aiToolToGenAIFunction(unionTool)
178
+
179
+ expect(genAIFunction.parameters?.properties?.value).toMatchInlineSnapshot(`
180
+ {
181
+ "anyOf": [
182
+ {
183
+ "type": "STRING",
184
+ },
185
+ {
186
+ "format": "float",
187
+ "type": "NUMBER",
188
+ },
189
+ {
190
+ "type": "BOOLEAN",
191
+ },
192
+ ],
193
+ }
194
+ `)
195
+ })
196
+
197
+ it('should create a CallableTool', async () => {
198
+ const weatherTool = tool({
199
+ description: 'Get weather',
200
+ inputSchema: z.object({
201
+ location: z.string(),
202
+ }),
203
+ execute: async ({ location }) => ({
204
+ temperature: 72,
205
+ location,
206
+ }),
207
+ })
208
+
209
+ const callableTool = aiToolToCallableTool(weatherTool, 'weather')
210
+
211
+ // Test tool() method
212
+ const genAITool = await callableTool.tool()
213
+ expect(genAITool.functionDeclarations).toMatchInlineSnapshot(`
214
+ [
215
+ {
216
+ "description": "Get weather",
217
+ "name": "weather",
218
+ "parameters": {
219
+ "properties": {
220
+ "location": {
221
+ "type": "STRING",
222
+ },
223
+ },
224
+ "required": [
225
+ "location",
226
+ ],
227
+ "type": "OBJECT",
228
+ },
229
+ },
230
+ ]
231
+ `)
232
+
233
+ // Test callTool() method
234
+ const functionCall: FunctionCall = {
235
+ id: 'call_123',
236
+ name: 'weather',
237
+ args: { location: 'San Francisco' },
238
+ }
239
+
240
+ const parts = await callableTool.callTool([functionCall])
241
+ expect(parts).toMatchInlineSnapshot(`
242
+ [
243
+ {
244
+ "functionResponse": {
245
+ "id": "call_123",
246
+ "name": "weather",
247
+ "response": {
248
+ "output": {
249
+ "location": "San Francisco",
250
+ "temperature": 72,
251
+ },
252
+ },
253
+ },
254
+ },
255
+ ]
256
+ `)
257
+ })
258
+
259
+ it('should handle tool execution errors', async () => {
260
+ const errorTool = tool({
261
+ description: 'Tool that throws',
262
+ inputSchema: z.object({
263
+ trigger: z.boolean(),
264
+ }),
265
+ execute: async ({ trigger }) => {
266
+ if (trigger) {
267
+ throw new Error('Tool execution failed')
268
+ }
269
+ return { success: true }
270
+ },
271
+ })
272
+
273
+ const callableTool = aiToolToCallableTool(errorTool, 'error_tool')
274
+
275
+ const functionCall: FunctionCall = {
276
+ id: 'call_error',
277
+ name: 'error_tool',
278
+ args: { trigger: true },
279
+ }
280
+
281
+ const parts = await callableTool.callTool([functionCall])
282
+ expect(parts).toMatchInlineSnapshot(`
283
+ [
284
+ {
285
+ "functionResponse": {
286
+ "id": "call_error",
287
+ "name": "error_tool",
288
+ "response": {
289
+ "error": "Tool execution failed",
290
+ },
291
+ },
292
+ },
293
+ ]
294
+ `)
295
+ })
296
+ })
@@ -0,0 +1,251 @@
1
+ import type { Tool, jsonSchema as JsonSchemaType } from 'ai'
2
+ import type {
3
+ FunctionDeclaration,
4
+ Schema,
5
+ Type as GenAIType,
6
+ Tool as GenAITool,
7
+ CallableTool,
8
+ FunctionCall,
9
+ Part,
10
+ } from '@google/genai'
11
+ import { Type } from '@google/genai'
12
+ import { z, toJSONSchema } from 'zod'
13
+
14
+ /**
15
+ * Convert JSON Schema to GenAI Schema format
16
+ * Based on the actual implementation used by the GenAI package:
17
+ * https://github.com/googleapis/js-genai/blob/027f09db662ce6b30f737b10b4d2efcb4282a9b6/src/_transformers.ts#L294
18
+ */
19
+ function jsonSchemaToGenAISchema(jsonSchema: any): Schema {
20
+ const schema: Schema = {}
21
+
22
+ // Map JSON Schema type to GenAI Type
23
+ if (jsonSchema.type) {
24
+ switch (jsonSchema.type) {
25
+ case 'string':
26
+ schema.type = Type.STRING
27
+ break
28
+ case 'number':
29
+ schema.type = Type.NUMBER
30
+ schema.format = jsonSchema.format || 'float'
31
+ break
32
+ case 'integer':
33
+ schema.type = Type.INTEGER
34
+ schema.format = jsonSchema.format || 'int32'
35
+ break
36
+ case 'boolean':
37
+ schema.type = Type.BOOLEAN
38
+ break
39
+ case 'array':
40
+ schema.type = Type.ARRAY
41
+ if (jsonSchema.items) {
42
+ schema.items = jsonSchemaToGenAISchema(jsonSchema.items)
43
+ }
44
+ if (jsonSchema.minItems !== undefined) {
45
+ schema.minItems = jsonSchema.minItems
46
+ }
47
+ if (jsonSchema.maxItems !== undefined) {
48
+ schema.maxItems = jsonSchema.maxItems
49
+ }
50
+ break
51
+ case 'object':
52
+ schema.type = Type.OBJECT
53
+ if (jsonSchema.properties) {
54
+ schema.properties = {}
55
+ for (const [key, value] of Object.entries(jsonSchema.properties)) {
56
+ schema.properties[key] = jsonSchemaToGenAISchema(value)
57
+ }
58
+ }
59
+ if (jsonSchema.required) {
60
+ schema.required = jsonSchema.required
61
+ }
62
+ // Note: GenAI Schema doesn't have additionalProperties field
63
+ // We skip it for now
64
+ break
65
+ default:
66
+ // For unknown types, keep as-is
67
+ schema.type = jsonSchema.type
68
+ }
69
+ }
70
+
71
+ // Copy over common properties
72
+ if (jsonSchema.description) {
73
+ schema.description = jsonSchema.description
74
+ }
75
+ if (jsonSchema.enum) {
76
+ schema.enum = jsonSchema.enum.map(String)
77
+ }
78
+ if (jsonSchema.default !== undefined) {
79
+ schema.default = jsonSchema.default
80
+ }
81
+ if (jsonSchema.example !== undefined) {
82
+ schema.example = jsonSchema.example
83
+ }
84
+ if (jsonSchema.nullable) {
85
+ schema.nullable = true
86
+ }
87
+
88
+ // Handle anyOf/oneOf as anyOf in GenAI
89
+ if (jsonSchema.anyOf) {
90
+ schema.anyOf = jsonSchema.anyOf.map((s: any) => jsonSchemaToGenAISchema(s))
91
+ } else if (jsonSchema.oneOf) {
92
+ schema.anyOf = jsonSchema.oneOf.map((s: any) => jsonSchemaToGenAISchema(s))
93
+ }
94
+
95
+ // Handle number/string specific properties
96
+ if (jsonSchema.minimum !== undefined) {
97
+ schema.minimum = jsonSchema.minimum
98
+ }
99
+ if (jsonSchema.maximum !== undefined) {
100
+ schema.maximum = jsonSchema.maximum
101
+ }
102
+ if (jsonSchema.minLength !== undefined) {
103
+ schema.minLength = jsonSchema.minLength
104
+ }
105
+ if (jsonSchema.maxLength !== undefined) {
106
+ schema.maxLength = jsonSchema.maxLength
107
+ }
108
+ if (jsonSchema.pattern) {
109
+ schema.pattern = jsonSchema.pattern
110
+ }
111
+
112
+ return schema
113
+ }
114
+
115
+ /**
116
+ * Convert AI SDK Tool to GenAI FunctionDeclaration
117
+ */
118
+ export function aiToolToGenAIFunction(
119
+ tool: Tool<any, any>,
120
+ ): FunctionDeclaration {
121
+ // Extract the input schema - assume it's a Zod schema
122
+ const inputSchema = tool.inputSchema as z.ZodType<any>
123
+
124
+ // Get the tool name from the schema or generate one
125
+ let toolName = 'tool'
126
+ let jsonSchema: any = {}
127
+
128
+ if (inputSchema) {
129
+ // Convert Zod schema to JSON Schema
130
+ jsonSchema = toJSONSchema(inputSchema)
131
+
132
+ // Extract name from Zod description if available
133
+ const description = inputSchema.description
134
+ if (description) {
135
+ const nameMatch = description.match(/name:\s*(\w+)/)
136
+ if (nameMatch) {
137
+ toolName = nameMatch[1] || ''
138
+ }
139
+ }
140
+ }
141
+
142
+ // Convert JSON Schema to GenAI Schema
143
+ const genAISchema = jsonSchemaToGenAISchema(jsonSchema)
144
+
145
+ // Create the FunctionDeclaration
146
+ const functionDeclaration: FunctionDeclaration = {
147
+ name: toolName,
148
+ description: tool.description || jsonSchema.description || 'Tool function',
149
+ parameters: genAISchema,
150
+ }
151
+
152
+ return functionDeclaration
153
+ }
154
+
155
+ /**
156
+ * Convert AI SDK Tool to GenAI CallableTool
157
+ */
158
+ export function aiToolToCallableTool(
159
+ tool: Tool<any, any>,
160
+ name: string,
161
+ ): CallableTool & { name: string } {
162
+ const toolName = name || 'tool'
163
+
164
+ return {
165
+ name,
166
+ async tool(): Promise<GenAITool> {
167
+ const functionDeclaration = aiToolToGenAIFunction(tool)
168
+ if (name) {
169
+ functionDeclaration.name = name
170
+ }
171
+
172
+ return {
173
+ functionDeclarations: [functionDeclaration],
174
+ }
175
+ },
176
+
177
+ async callTool(functionCalls: FunctionCall[]): Promise<Part[]> {
178
+ const parts: Part[] = []
179
+
180
+ for (const functionCall of functionCalls) {
181
+ // Check if this function call matches our tool
182
+ if (
183
+ functionCall.name !== toolName &&
184
+ name &&
185
+ functionCall.name !== name
186
+ ) {
187
+ continue
188
+ }
189
+
190
+ // Execute the tool if it has an execute function
191
+ if (tool.execute) {
192
+ try {
193
+ const result = await tool.execute(functionCall.args || {}, {
194
+ toolCallId: functionCall.id || '',
195
+ messages: [],
196
+ })
197
+
198
+ // Convert the result to a Part
199
+ parts.push({
200
+ functionResponse: {
201
+ id: functionCall.id,
202
+ name: functionCall.name || toolName,
203
+ response: {
204
+ output: result,
205
+ },
206
+ },
207
+ } as Part)
208
+ } catch (error) {
209
+ // Handle errors
210
+ parts.push({
211
+ functionResponse: {
212
+ id: functionCall.id,
213
+ name: functionCall.name || toolName,
214
+ response: {
215
+ error: error instanceof Error ? error.message : String(error),
216
+ },
217
+ },
218
+ } as Part)
219
+ }
220
+ }
221
+ }
222
+
223
+ return parts
224
+ },
225
+ }
226
+ }
227
+
228
+ /**
229
+ * Helper to extract schema from AI SDK tool
230
+ */
231
+ export function extractSchemaFromTool(tool: Tool<any, any>): any {
232
+ const inputSchema = tool.inputSchema as z.ZodType<any>
233
+
234
+ if (!inputSchema) {
235
+ return {}
236
+ }
237
+
238
+ // Convert Zod schema to JSON Schema
239
+ return toJSONSchema(inputSchema)
240
+ }
241
+
242
+ /**
243
+ * Given an object of tools, creates an array of CallableTool
244
+ */
245
+ export function callableToolsFromObject(
246
+ tools: Record<string, Tool<any, any>>,
247
+ ): Array<CallableTool & { name: string }> {
248
+ return Object.entries(tools).map(([name, tool]) =>
249
+ aiToolToCallableTool(tool, name),
250
+ )
251
+ }