Skip to content

Commit b66d68e

Browse files
timurrakhimzhanKhan Life
authored andcommitted
feat: added allowed tools (#5377)
Co-authored-by: Khan Life <[email protected]>
1 parent 892fe69 commit b66d68e

File tree

6 files changed

+45
-3
lines changed

6 files changed

+45
-3
lines changed

.changeset/shiny-points-call.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@effect/ai": minor
3+
---
4+
5+
Added "oneOf" property for generateText and streamText "toolChoice" to be able to pick a subset of tools, which are going to be passed to LLM

packages/ai/ai/src/AiLanguageModel.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,12 +61,15 @@ export interface IdentifiedSchema<A, I, R> extends Schema.Schema<A, I, R> {
6161
* - `required`: The model **must** call a tool but can decide which tool will be called.
6262
* - `none`: The model **must not** call a tool.
6363
* - `{ tool: <tool_name> }`: The model must call the specified tool.
64-
*
64+
* - `{ mode?: "auto" (default) | "required", oneOf: [<tool_names>] }`: The model is restricted to a subset of tools. When `mode` is `"auto"` or omitted, the model can decide whether to call a tool from the allowed subset. When `mode` is `"required"`, the model must call one tool from the allowed subset.
6565
* @since 1.0.0
6666
* @category Models
6767
*/
6868
export type ToolChoice<Tool extends AiTool.Any> = "auto" | "none" | "required" | {
6969
readonly tool: Tool["name"]
70+
} | {
71+
readonly mode?: "auto" | "required"
72+
readonly oneOf: Array<Tool["name"]>
7073
}
7174

7275
/**
@@ -390,8 +393,12 @@ export const make: (
390393
return response
391394
}
392395
modelOptions.toolChoice = toolChoice
396+
const hasUnallowedTools = typeof toolChoice === "object" && "oneOf" in toolChoice
393397
const actualToolkit = Effect.isEffect(toolkit) ? yield* toolkit : toolkit
394398
for (const tool of actualToolkit.tools) {
399+
if (hasUnallowedTools && !toolChoice.oneOf.includes(tool.name)) {
400+
continue
401+
}
395402
modelOptions.tools.push(convertTool(tool))
396403
}
397404
const response = yield* opts.generateText(modelOptions)
@@ -426,7 +433,11 @@ export const make: (
426433
const actualToolkit = Effect.isEffect(toolkit)
427434
? yield* (toolkit as Effect.Effect<AiToolkit.ToHandler<any>>)
428435
: toolkit
436+
const hasUnallowedTools = typeof toolChoice === "object" && "oneOf" in toolChoice
429437
for (const tool of actualToolkit.tools) {
438+
if (hasUnallowedTools && !toolChoice.oneOf.includes(tool.name)) {
439+
continue
440+
}
430441
modelOptions.tools.push(convertTool(tool))
431442
}
432443
const stream = opts.streamText(modelOptions)

packages/ai/amazon-bedrock/src/AmazonBedrockLanguageModel.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,15 @@ export const make = Effect.fnUntraced(function*(options: {
151151
toolConfig.toolChoice = { tool: { name: options.tools[0].name } }
152152
return toolConfig
153153
}
154+
const hasUnallowedTools = typeof options.toolChoice === "object" && "oneOf" in options.toolChoice
155+
if (hasUnallowedTools) {
156+
if (options.toolChoice.mode === "required") {
157+
toolConfig.toolChoice = { any: {} }
158+
} else {
159+
toolConfig.toolChoice = { auto: {} }
160+
}
161+
return toolConfig
162+
}
154163
if (options.toolChoice === "auto") {
155164
toolConfig.toolChoice = { auto: {} }
156165
} else if (options.toolChoice === "none") {

packages/ai/anthropic/src/AnthropicLanguageModel.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,9 +142,16 @@ export const make = Effect.fnUntraced(function*(options: {
142142
function*(method: string, { prompt, system, toolChoice, tools }: AiLanguageModel.AiLanguageModelOptions) {
143143
const context = yield* Effect.context<never>()
144144
const useStructured = tools.length === 1 && tools[0].structured
145+
const hasUnallowedTools = typeof toolChoice === "object" && "oneOf" in toolChoice
145146
let tool_choice: typeof Generated.ToolChoice.Encoded | undefined = undefined
146147
if (useStructured) {
147148
tool_choice = { type: "tool", name: tools[0].name }
149+
} else if (hasUnallowedTools) {
150+
if (toolChoice.mode === "required") {
151+
tool_choice = { type: "any" }
152+
} else {
153+
tool_choice = { type: "auto" }
154+
}
148155
} else if (tools.length > 0) {
149156
if (toolChoice === "required") {
150157
tool_choice = { type: "any" }

packages/ai/google/src/GoogleAiLanguageModel.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,8 @@ export const make = Effect.fnUntraced(function*(options: {
158158
? "application/json"
159159
: (options.config?.generationConfig?.responseMimeType ?? perRequestConfig?.generationConfig?.responseMimeType)
160160
let toolConfig: typeof Generated.ToolConfig.Encoded | undefined = options.config?.toolConfig
161-
if (Predicate.isNotUndefined(toolChoice) && !useStructured && tools.length > 0) {
161+
const hasUnallowedTools = typeof toolChoice === "object" && "oneOf" in toolChoice
162+
if (Predicate.isNotUndefined(toolChoice) && !useStructured && !hasUnallowedTools && tools.length > 0) {
162163
if (toolChoice === "none") {
163164
toolConfig = { functionCallingConfig: { ...toolConfig?.functionCallingConfig, mode: "NONE" } }
164165
} else if (toolChoice === "auto") {
@@ -168,6 +169,12 @@ export const make = Effect.fnUntraced(function*(options: {
168169
} else {
169170
toolConfig = { functionCallingConfig: { allowedFunctionNames: [toolChoice.tool], mode: "ANY" } }
170171
}
172+
} else if (hasUnallowedTools && !useStructured) {
173+
if (toolChoice.mode === "required") {
174+
toolConfig = { functionCallingConfig: { ...toolConfig?.functionCallingConfig, mode: "ANY" } }
175+
} else {
176+
toolConfig = { functionCallingConfig: { ...toolConfig?.functionCallingConfig, mode: "AUTO" } }
177+
}
171178
}
172179
const contents = yield* makeContents(method, prompt)
173180
return {

packages/ai/openai/src/OpenAiLanguageModel.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -169,12 +169,15 @@ export const make = Effect.fnUntraced(function*(options: {
169169
const context = yield* Effect.context<never>()
170170
const useStructured = tools.length === 1 && tools[0].structured
171171
let tool_choice: typeof Generated.ChatCompletionToolChoiceOption.Encoded | undefined = undefined
172-
if (Predicate.isNotUndefined(toolChoice) && !useStructured && tools.length > 0) {
172+
const hasUnallowedTools = typeof toolChoice === "object" && "oneOf" in toolChoice
173+
if (Predicate.isNotUndefined(toolChoice) && !useStructured && !hasUnallowedTools && tools.length > 0) {
173174
if (toolChoice === "auto" || toolChoice === "required") {
174175
tool_choice = toolChoice
175176
} else if (typeof toolChoice === "object") {
176177
tool_choice = { type: "function", function: { name: toolChoice.tool } }
177178
}
179+
} else if (hasUnallowedTools && !useStructured) {
180+
tool_choice = toolChoice.mode ?? "auto"
178181
}
179182
const messages = yield* makeMessages(method, system, prompt)
180183
return {

0 commit comments

Comments
 (0)