-
Notifications
You must be signed in to change notification settings - Fork 1
feat: forced result tool #7
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -38,6 +38,16 @@ export interface McpxAnthropicStage { | |
} | ||
|
||
|
||
function anthropicToolCallToMcpxToolCall(submessage: any): any { | ||
return { | ||
method: 'tools/call', | ||
params: { | ||
name: submessage.name, | ||
arguments: submessage.input, | ||
}, | ||
} | ||
} | ||
|
||
/** | ||
* A Driver wrapping an Anthropic client and MCPX session. | ||
* | ||
|
@@ -122,23 +132,16 @@ export class Driver { | |
} | ||
|
||
|
||
private async call(submessage: ToolUseBlock): Promise<ContentBlockParam> { | ||
const { id, input, name } = submessage | ||
private async call(convertedToolCall: any, toolCallId: string): Promise<ContentBlockParam> { | ||
try { | ||
const abortcontroller = new AbortController() | ||
const result = await this.#session.handleCallTool( | ||
{ | ||
method: 'tools/call', | ||
params: { | ||
name, | ||
arguments: input as any, | ||
}, | ||
}, | ||
convertedToolCall, | ||
{ signal: abortcontroller.signal }, | ||
) | ||
|
||
return { | ||
tool_use_id: id, | ||
tool_use_id: toolCallId, | ||
type: 'tool_result', | ||
content: Array.isArray(result.content) | ||
? result.content.map(xs => { | ||
|
@@ -149,15 +152,15 @@ export class Driver { | |
} catch (err: any) { | ||
this.#logger.error( | ||
{ | ||
tool_use_id: id, | ||
tool_use_id: toolCallId, | ||
name, | ||
error: err.message, | ||
stack: err.stack, | ||
}, | ||
'tool use failed', | ||
) | ||
return { | ||
tool_use_id: id, | ||
tool_use_id: toolCallId, | ||
type: 'tool_result', | ||
content: err.toString(), | ||
is_error: true, | ||
|
@@ -169,11 +172,16 @@ export class Driver { | |
const { response, messages, index, status, toolCallIndex } = stage | ||
switch (status) { | ||
case 'pending': { | ||
const tools = config.tools?.map(mcpxToolToAnthropic) || this.#tools | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 4️⃣ this would be the load-bearing change, right? (Assuming users could already pass There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. that's right. I don't love the fact that we are abusing the anthropic-specific |
||
const tool_choice = | ||
config.tool_choice ? { type: 'tool', name: config.tool_choice } : { type: 'auto' } | ||
|
||
let response: Anthropic.Messages.Message | ||
try { | ||
response = await this.#anthropic.messages.create({ | ||
...config, | ||
...(this.#tools.length ? { tools: this.#tools } : {}), | ||
tools, | ||
tool_choice, | ||
messages, | ||
}, requestOptions) | ||
} catch (err: any) { | ||
|
@@ -213,36 +221,60 @@ export class Driver { | |
return { response, messages, index: messageIdx, status: 'ready' } | ||
} | ||
case 'input_wait': { | ||
const toolUseCount = stage.toolCallIndex! | ||
const submessageIdx = stage.submessageIdx! | ||
const inputMessage = messages[index-1] | ||
const newMessage = messages[index] | ||
|
||
this.#logger.info({ m:"message index", index, len: messages.length }) | ||
|
||
// when status == 'input_wait' it is always a tool call, | ||
// newMessage.content is always a ContentBlockParam[] | ||
const submessage = inputMessage.content[submessageIdx] as ToolUseBlock | ||
const content = newMessage.content as ContentBlockParam[] | ||
content.push(await this.call(submessage)) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 1️⃣ If I follow correctly, we moved the normalization of the tool call info from here... |
||
const { tool, submessageIdx, toolCallIndex, toolCallLength, toolCallId } = | ||
this.parseNextToolCall(stage) | ||
|
||
content.push(await this.call(tool, toolCallId!)) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 3️⃣ so we can pass it directly here. But I don't see any other changes to the logic? |
||
|
||
const nextTool = toolUseCount + 1 | ||
const nextSubmessage = submessageIdx + 1 | ||
if (nextSubmessage >= inputMessage.content.length) { | ||
if (submessageIdx >= toolCallLength) { | ||
return { response, messages, index, status: 'pending' } | ||
} else { | ||
return { response, messages, index, status: 'input_wait', toolCallIndex: nextTool, submessageIdx: nextSubmessage } | ||
return { response, messages, index, status: 'input_wait', toolCallIndex, submessageIdx } | ||
} | ||
|
||
} | ||
default: | ||
throw new Error("Illegal status: " + status) | ||
} | ||
} | ||
|
||
parseNextToolCall(stage: McpxAnthropicStage) { | ||
const { status, messages, index } = stage | ||
if (status !== 'input_wait') { | ||
throw new Error("Cannot parse next tool call: invalid status " + status) | ||
} | ||
const toolUseCount = stage.toolCallIndex! | ||
const submessageIdx = stage.submessageIdx! | ||
const inputMessage = messages[index-1] | ||
|
||
// when status == 'input_wait' it is always a tool call, | ||
// newMessage.content is always a ContentBlockParam[] | ||
const submessage = inputMessage.content[submessageIdx] as ToolUseBlock | ||
|
||
const nextTool = toolUseCount + 1 | ||
const nextSubmessage = submessageIdx + 1 | ||
|
||
return { | ||
tool: anthropicToolCallToMcpxToolCall(submessage), | ||
toolCallIndex: nextTool, | ||
toolCallLength: inputMessage.content.length, | ||
submessageIdx: nextSubmessage, | ||
toolCallId: submessage.id | ||
} | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 2️⃣ ...to here |
||
|
||
} | ||
|
||
function mcpxToolToAnthropic(tool: any) { | ||
return { | ||
// So, you're saying you folks write a lot of Python, eh? Well, it certainly doesn't show. | ||
input_schema: tool.inputSchema, | ||
name: tool.name, | ||
description: tool.description, | ||
} | ||
} | ||
|
||
/** Create a driver using an Anthropic client and MCPX Session options. */ | ||
export default async function createDriver(opts: DriverOptions) { | ||
const { anthropic, logger } = opts | ||
|
@@ -264,14 +296,7 @@ export default async function createDriver(opts: DriverOptions) { | |
anthropic, | ||
logger: logger || (session.logger as any) || pino({ level: 'silent' }), | ||
session, | ||
tools: mcpTools.map(tool => { | ||
return { | ||
// So, you're saying you folks write a lot of Python, eh? Well, it certainly doesn't show. | ||
input_schema: tool.inputSchema, | ||
name: tool.name, | ||
description: tool.description, | ||
} | ||
}) | ||
tools: mcpTools.map(mcpxToolToAnthropic) | ||
}) | ||
} | ||
|
||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
oooh, I see – this is a normalization pass to unify the return type with mcpx-openai.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yeah, technically to align it to the MCP
CallToolRequest
definition