mirror of
https://github.com/moeru-ai/airi.git
synced 2026-08-29 02:31:49 +08:00
fix(stage-ui): tool definition missing strict & additionalProperties check
This commit is contained in:
@@ -7,17 +7,17 @@ import type { Mock } from 'vitest'
|
||||
import type { UnwrapRef } from 'vue'
|
||||
import type z from 'zod'
|
||||
|
||||
import type { sparkCommandSchema } from './character-orchestrator'
|
||||
import type { StreamEvent } from './llm'
|
||||
import type { AiriCard } from './modules'
|
||||
|
||||
import { createTestingPinia } from '@pinia/testing'
|
||||
import { tool } from '@xsai/tool'
|
||||
import { nanoid } from 'nanoid'
|
||||
import { setActivePinia } from 'pinia'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { useCharacterStore } from './character'
|
||||
import { useCharacterOrchestratorStore } from './character-orchestrator'
|
||||
import { sparkCommandSchema, useCharacterOrchestratorStore } from './character-orchestrator'
|
||||
import { useLLM } from './llm'
|
||||
import { useAiriCardStore, useConsciousnessStore } from './modules'
|
||||
import { useProvidersStore } from './providers'
|
||||
@@ -53,6 +53,54 @@ function mockedStore<TStoreDef extends () => unknown>(
|
||||
return useStore() as any
|
||||
}
|
||||
|
||||
function getObjectSchema(schema?: Record<string, any>) {
|
||||
if (!schema)
|
||||
return undefined
|
||||
|
||||
if (schema.type === 'object')
|
||||
return schema
|
||||
|
||||
const candidates = [...(schema.anyOf ?? []), ...(schema.oneOf ?? [])]
|
||||
return candidates.find((candidate: Record<string, any>) => candidate?.type === 'object')
|
||||
}
|
||||
|
||||
function getArraySchema(schema?: Record<string, any>) {
|
||||
if (!schema)
|
||||
return undefined
|
||||
|
||||
if (schema.type === 'array')
|
||||
return schema
|
||||
|
||||
const candidates = [...(schema.anyOf ?? []), ...(schema.oneOf ?? [])]
|
||||
return candidates.find((candidate: Record<string, any>) => candidate?.type === 'array')
|
||||
}
|
||||
|
||||
describe('sparkCommandSchema', () => {
|
||||
it('emits strict objects in the json schema', async () => {
|
||||
const sparkTool = await tool({
|
||||
name: 'builtIn_sparkCommand',
|
||||
description: 'test',
|
||||
parameters: sparkCommandSchema,
|
||||
execute: async () => undefined,
|
||||
})
|
||||
|
||||
const schema = sparkTool.function.parameters as Record<string, any>
|
||||
const commandsSchema = getArraySchema(schema.properties?.commands)
|
||||
const commandItemSchema = getObjectSchema(commandsSchema?.items)
|
||||
const guidanceSchema = getObjectSchema(commandItemSchema?.properties?.guidance)
|
||||
const personaSchema = getArraySchema(guidanceSchema?.properties?.persona)
|
||||
const personaItemSchema = getObjectSchema(personaSchema?.items)
|
||||
const optionsSchema = getArraySchema(guidanceSchema?.properties?.options)
|
||||
const optionsItemSchema = getObjectSchema(optionsSchema?.items)
|
||||
|
||||
expect(schema.additionalProperties).toBe(false)
|
||||
expect(commandItemSchema?.additionalProperties).toBe(false)
|
||||
expect(guidanceSchema?.additionalProperties).toBe(false)
|
||||
expect(personaItemSchema?.additionalProperties).toBe(false)
|
||||
expect(optionsItemSchema?.additionalProperties).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('store character-orchestrator', () => {
|
||||
beforeEach(() => {
|
||||
const pinia = createTestingPinia({ createSpy: vi.fn, stubActions: false })
|
||||
|
||||
@@ -55,7 +55,7 @@ export const sparkCommandSchema = z.object({
|
||||
persona: z.array(z.object({
|
||||
strength: z.enum(['very-high', 'high', 'medium', 'low', 'very-low']),
|
||||
traits: z.string().describe('Trait name to adjust behavior. For example, "bravery", "cautiousness", "friendliness".'),
|
||||
})).nullable().describe('Personas can be used to adjust the behavior of sub-agents. For example, when using as NPC in games, or player in Minecraft, the persona can help define the character\'s traits and decision-making style.'),
|
||||
}).strict()).nullable().describe('Personas can be used to adjust the behavior of sub-agents. For example, when using as NPC in games, or player in Minecraft, the persona can help define the character\'s traits and decision-making style.'),
|
||||
options: z.array(z.object({
|
||||
label: z.string().describe('Short and brief label for this option, used for identification, should be within a sentence.'),
|
||||
steps: z.array(z.string()).describe('Step-by-step instructions for the sub-agent to follow, useful when providing detailed guidance.'),
|
||||
@@ -65,10 +65,10 @@ export const sparkCommandSchema = z.object({
|
||||
fallback: z.array(z.string()).nullable().describe('Fallback steps if the main steps cannot be completed.'),
|
||||
// TODO: consider to remove or enrich how triggers should work later
|
||||
triggers: z.array(z.string()).nullable().describe('Conditions or events that would trigger this option.'),
|
||||
})),
|
||||
}).nullable().describe('Guidance for the sub-agent on how to interpret and execute the command with given context, persona settings, and reasoning.'),
|
||||
})).describe('List of commands to issue to sub-agents, you may produce multiple commands in response to multiple sub-agents by specifying their IDs in destination field. Empty array can be used for zero commands.'),
|
||||
})
|
||||
}).strict()),
|
||||
}).strict().nullable().describe('Guidance for the sub-agent on how to interpret and execute the command with given context, persona settings, and reasoning.'),
|
||||
}).strict()).describe('List of commands to issue to sub-agents, you may produce multiple commands in response to multiple sub-agents by specifying their IDs in destination field. Empty array can be used for zero commands.'),
|
||||
}).strict()
|
||||
|
||||
export type SparkCommandSchema = z.infer<typeof sparkCommandSchema>
|
||||
|
||||
@@ -97,7 +97,7 @@ export const useCharacterOrchestratorStore = defineStore('character-orchestrator
|
||||
const sparkNoResponseTool = await tool({
|
||||
name: 'builtIn_sparkNoResponse',
|
||||
description: `Indicate that no response or action is needed for the current spark:notify event.`,
|
||||
parameters: z.object({}),
|
||||
parameters: z.object({}).strict(),
|
||||
execute: async (_payload) => {
|
||||
noResponse = true
|
||||
return 'AIRI System: Acknowledged, no response or action will be processed.'
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
import type { JsonSchema } from 'xsschema'
|
||||
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import { mcp } from './mcp'
|
||||
|
||||
describe('tools mcp schema', () => {
|
||||
it('emits strict parameter objects', async () => {
|
||||
const tools = await mcp()
|
||||
const toolNames = [
|
||||
'mcp_list_tools',
|
||||
'mcp_connect_server',
|
||||
'mcp_disconnect_server',
|
||||
'mcp_call_tool',
|
||||
]
|
||||
|
||||
for (const name of toolNames) {
|
||||
const tool = tools.find(entry => entry.function.name === name)
|
||||
expect(tool, `missing tool: ${name}`).toBeDefined()
|
||||
expect(tool?.function.parameters.additionalProperties).toBe(false)
|
||||
}
|
||||
})
|
||||
|
||||
it('keeps mcp_call_tool parameters items strict', async () => {
|
||||
const tools = await mcp()
|
||||
const callTool = tools.find(entry => entry.function.name === 'mcp_call_tool')
|
||||
|
||||
expect(callTool).toBeDefined()
|
||||
const items = ((callTool?.function.parameters as JsonSchema).properties?.parameters as any)?.items
|
||||
expect(items).toBeDefined()
|
||||
expect(items?.additionalProperties).toBe(false)
|
||||
})
|
||||
})
|
||||
@@ -9,7 +9,7 @@ const tools = [
|
||||
execute: async (_, __) => {
|
||||
return await listTools()
|
||||
},
|
||||
parameters: z.object({}),
|
||||
parameters: z.object({}).strict(),
|
||||
}),
|
||||
tool({
|
||||
name: 'mcp_connect_server',
|
||||
@@ -21,7 +21,7 @@ const tools = [
|
||||
parameters: z.object({
|
||||
command: z.string().describe('The command to connect to the MCP server'),
|
||||
args: z.array(z.string()).describe('The arguments to pass to the MCP server'),
|
||||
}),
|
||||
}).strict(),
|
||||
}),
|
||||
tool({
|
||||
name: 'mcp_disconnect_server',
|
||||
@@ -30,7 +30,7 @@ const tools = [
|
||||
await disconnectServer()
|
||||
return 'success'
|
||||
},
|
||||
parameters: z.object({}),
|
||||
parameters: z.object({}).strict(),
|
||||
}),
|
||||
tool({
|
||||
name: 'mcp_call_tool',
|
||||
@@ -50,8 +50,8 @@ const tools = [
|
||||
name: z.string().describe('The name of the tool to call'),
|
||||
parameters: z.array(z.object({
|
||||
name: z.string().describe('The name of the parameter'),
|
||||
value: z.union([z.string(), z.number(), z.boolean(), z.object({})]).describe('The value of the parameter, it can be a string, a number, a boolean, or an object'),
|
||||
})).describe('The parameters to pass to the tool'),
|
||||
value: z.union([z.string(), z.number(), z.boolean(), z.object({}).strict()]).describe('The value of the parameter, it can be a string, a number, a boolean, or an object'),
|
||||
}).strict()).describe('The parameters to pass to the tool'),
|
||||
}),
|
||||
}),
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user