Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions packages/effect/src/unstable/ai/LanguageModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1165,7 +1165,7 @@ export const make: (params: {

// Construct the response schema with the tools from the toolkit
const ResponseSchema = Schema.mutable(
Schema.Array(Response.Part(toolkit))
Schema.Array(Response.Part(toolkit, { relaxParams: true }))
)

// If tool call resolution is disabled, return the response without
Expand Down Expand Up @@ -1465,7 +1465,7 @@ export const make: (params: {
>
}

const ResponseSchema = Schema.NonEmptyArray(Response.StreamPart(toolkit))
const ResponseSchema = Schema.NonEmptyArray(Response.StreamPart(toolkit, { relaxParams: true }))
const decodeParts = Schema.decodeEffect(ResponseSchema)

// Queue for decoded parts and tool results
Expand Down
10 changes: 6 additions & 4 deletions packages/effect/src/unstable/ai/Response.ts
Original file line number Diff line number Diff line change
Expand Up @@ -262,7 +262,8 @@ export type PartEncoded =
* @since 4.0.0
*/
export const Part = <T extends Toolkit.Any | Toolkit.WithHandler<any>>(
toolkit: T
toolkit: T,
options?: { readonly relaxParams?: boolean }
): Schema.Codec<
Part<T extends Toolkit.Any ? Toolkit.Tools<T> : Toolkit.WithHandlerTools<T>>,
PartEncoded,
Expand All @@ -272,7 +273,7 @@ export const Part = <T extends Toolkit.Any | Toolkit.WithHandler<any>>(
const toolCalls: Array<Schema.Top> = []
const toolResults: Array<Schema.Top> = []
for (const tool of Object.values(toolkit.tools as Record<string, Tool.Any>)) {
const toolCall = ToolCallPart(tool.name, tool.parametersSchema)
const toolCall = ToolCallPart(tool.name, options?.relaxParams === true ? Schema.Unknown : tool.parametersSchema)
const toolResult = ToolResultPart(tool.name, tool.successSchema, tool.failureSchema)
toolCalls.push(toolCall)
toolResults.push(toolResult)
Expand Down Expand Up @@ -354,7 +355,8 @@ export type StreamPartEncoded =
* @since 4.0.0
*/
export const StreamPart = <T extends Toolkit.Any | Toolkit.WithHandler<any>>(
toolkit: T
toolkit: T,
options?: { readonly relaxParams?: boolean }
): Schema.Codec<
StreamPart<T extends Toolkit.Any ? Toolkit.Tools<T> : Toolkit.WithHandlerTools<T>>,
StreamPartEncoded,
Expand All @@ -364,7 +366,7 @@ export const StreamPart = <T extends Toolkit.Any | Toolkit.WithHandler<any>>(
const toolCalls: Array<Schema.Top> = []
const toolResults: Array<Schema.Top> = []
for (const tool of Object.values(toolkit.tools as Record<string, Tool.Any>)) {
const toolCall = ToolCallPart(tool.name, tool.parametersSchema)
const toolCall = ToolCallPart(tool.name, options?.relaxParams === true ? Schema.Unknown : tool.parametersSchema)
const toolResult = ToolResultPart(tool.name, tool.successSchema, tool.failureSchema)
toolCalls.push(toolCall)
toolResults.push(toolResult)
Expand Down
23 changes: 21 additions & 2 deletions packages/effect/src/unstable/ai/Toolkit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -280,7 +280,8 @@ const Proto = {
// Fetch cached schemas / handlers for the tool
const schemas = getSchemas(tool)

// Decode the tool call parameters which will be passed to the handler
// Decode the tool call parameters which will be passed to the handler.
// When decoding fails, respect the tool's failureMode before propagating.
const decodedParams = yield* schemas.decodeParameters(params).pipe(
Effect.mapError((cause) =>
AiError.make({
Expand All @@ -292,8 +293,26 @@ const Proto = {
description: cause.message
})
})
)
),
Effect.matchEffect({
onFailure: (error) =>
tool.failureMode === "error"
? Effect.fail(error)
: schemas.encodeResult(error).pipe(
Effect.map((encodedResult) =>
Stream.succeed({ result: error, encodedResult, isFailure: true, preliminary: false }) as Stream.Stream<
{ readonly result: any; readonly encodedResult: any; readonly isFailure: boolean; readonly preliminary: boolean },
never
>
),
Effect.orDie
),
onSuccess: Effect.succeed
})
)
if (Stream.isStream(decodedParams)) {
return decodedParams
}

// Setup the handler context
const queue = yield* Queue.make<{
Expand Down
92 changes: 64 additions & 28 deletions packages/effect/test/unstable/ai/Tool.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,7 @@ describe("Tool", () => {
deepStrictEqual(response, toolResult)
}))

it.effect("should raise an error when tool call parameters are invalid", () =>
it.effect("should return a tool-result with isFailure when tool call parameters are invalid", () =>
Effect.gen(function*() {
const toolkit = Toolkit.make(FailureModeReturn)

Expand All @@ -182,22 +182,40 @@ describe("Tool", () => {
params: {}
}]
}),
Effect.provide(handlers),
Effect.flip
Effect.provide(handlers)
)

deepStrictEqual(
response,
AiError.make({
module: "Toolkit",
method: "FailureModeReturn.handle",
reason: new AiError.ToolParameterValidationError({
toolName: "FailureModeReturn",
toolParams: {},
description: `Missing key\n at ["testParam"]`
})
const validationError = AiError.make({
module: "Toolkit",
method: "FailureModeReturn.handle",
reason: new AiError.ToolParameterValidationError({
toolName: "FailureModeReturn",
toolParams: {},
description: `Missing key\n at ["testParam"]`
})
)
})

deepStrictEqual(response.toolResults, [
Response.toolResultPart({
id: toolCallId,
name: toolName,
isFailure: true,
providerExecuted: false,
preliminary: false,
result: validationError,
encodedResult: {
_tag: "AiError",
module: "Toolkit",
method: "FailureModeReturn.handle",
reason: {
_tag: "ToolParameterValidationError",
toolName: "FailureModeReturn",
toolParams: {},
description: `Missing key\n at ["testParam"]`
}
}
})
])
}))

it.effect("should return AiError when user returns an AiErrorReason when failure mode is return", () =>
Expand Down Expand Up @@ -870,7 +888,7 @@ describe("Tool", () => {
deepStrictEqual(response, toolResult)
}))

it.effect("should raise an error when tool call parameters are invalid", () =>
it.effect("should return a tool-result with isFailure when tool call parameters are invalid", () =>
Effect.gen(function*() {
const tool = HandlerRequired({
failureMode: "return",
Expand Down Expand Up @@ -902,22 +920,40 @@ describe("Tool", () => {
}
]
}),
Effect.provide(handlers),
Effect.flip
Effect.provide(handlers)
)

deepStrictEqual(
response,
AiError.make({
module: "Toolkit",
method: "HandlerRequired.handle",
reason: new AiError.ToolParameterValidationError({
toolName: "HandlerRequired",
toolParams: {},
description: `Missing key\n at ["testParam"]`
})
const validationError = AiError.make({
module: "Toolkit",
method: "HandlerRequired.handle",
reason: new AiError.ToolParameterValidationError({
toolName: "HandlerRequired",
toolParams: {},
description: `Missing key\n at ["testParam"]`
})
)
})

deepStrictEqual(response.toolResults, [
Response.toolResultPart({
id: toolCallId,
name: tool.name,
isFailure: true,
providerExecuted: false,
preliminary: false,
result: validationError,
encodedResult: {
_tag: "AiError",
module: "Toolkit",
method: "HandlerRequired.handle",
reason: {
_tag: "ToolParameterValidationError",
toolName: "HandlerRequired",
toolParams: {},
description: `Missing key\n at ["testParam"]`
}
}
})
])
}))

describe("addDependency", () => {
Expand Down