-
Notifications
You must be signed in to change notification settings - Fork 2.5k
fix(core): Infer tool invoke
return type from provided function
#8056
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
base: main
Are you sure you want to change the base?
Conversation
The latest updates on your projects. Learn more about Vercel for Git ↗︎
1 Skipped Deployment
|
Hi @hntrl - looks like you might've forgotten to do a top-level |
Ah quick correction. I think the exports tests are actually just smoke testing bundled versions of the project, and they tend to catch breakages. I suspect this may be a case where the code in question is making invalid assumptions about the return type of Apologies for the confusion on my part - still learning the finer details about our build/test pipeline process. |
content: await tools[0].invoke(result.tool_calls![0]), | ||
}) | ||
); | ||
messages.push(await tools[0].invoke(result.tool_calls![0])); |
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.
StructuredTool.invoke will return a tool message when a tool call is passed, so we don't have to create a new tool message here
No worries and thanks for the callout! I think you're right in that the failing test was a false positive. The most recent commits I pushed fix the CI failures. a0da856 fixes the failing test from the latest CI run. I added comments explaining the change. 3cbaceb fixes some other build failures I found that come from changing the interface type specifically. From what I can gather, changing that type has a (maybe unnecessary?) rippling effect through a bunch of provider packages, so I'll avoid trying to tackle that for now. At least this way the tool method works as expected when imported from the core package directly. |
3cbaceb
to
6947702
Compare
langchain-core/src/tools/types.ts
Outdated
): Promise<TArg extends ToolCall ? ToolMessage : ToolOutputT>; | ||
): Promise<ToolOutputT>; |
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.
Still assessing, but I'm not sure that this change is valid.
It's a bit confusing because we don't have StructuredTool
actually implementing StructuredToolInterface
(I forget why that is), but the latter is meant to reflect the public interface of the former. Same goes for ToolInterface
and Tool
.
Additionally, extensions of StructuredTool
should always be assignable to StructuredToolInterface
, and extensions of Tool
and DynamicTool
should be assignable to ToolInterface
. We should probably have a test that guards this property if we can't make it so the aforementioned classes actually implement their corresponding interfaces 🤔.
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.
I think additionally all of those should be assignable to StructuredToolInterface
(lack of type params is intentional on this and all of the above - default params should be wide enough to allow this).
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.
Makes sense. Two paths I can see going from here (both of which I'm happy to do, more just a matter of preference):
(1) Regress the tool classes to return just ToolOutputT
instead of switching based on input (which doesn't really have any impact since it was just returning an any
alias before, at least this way it's an optional type param on the classes + interfaces that we can use to control the output type)
(2) Bring the output type switching back to the interface and fix the side effects in the provider packages
Option 1 gets this PR merged quicker, option 2 is more "feature complete".
Unrelated but I'd be interested to know why that direct inheritance isn't there.
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.
I think the diff makes it confusing but this change specifically is reverting a previous change I made. On main the return type is set to Promise<ToolReturnType>
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.
Unrelated but I'd be interested to know why that inheritance isn't there.
We've been iterating in this area recently (see #7973), so the types are a bit more well-aligned between the classes and interfaces now than when they initially came together. Looking into these issues myself, I see one bug that I introduced in that PR, where I should've aligned the constraint for SchemaT
on ToolInterface
with the constraint for SchemaT
on Tool
. I think that's all that's necessary to make Tool
implement ToolInterface
and StructuredTool
implement StructuredToolInterface
.
Two paths I can see going from here (both of which I'm happy to do, more just a matter of preference):
(1) Regress the tool classes to return just ToolOutputT instead of switching based on input (which doesn't really have any impact since it was just returning an any alias before, at least this way it's an optional type param on the classes + interfaces that we can use to control the output type)
My concern here would be that we'd drop the ToolMessage
type from the output, or am I mistaken? That would trigger cases where we return something that's not expressed by the return type. Unfortunately I think it'd be kinder to consumers to regress all the way back to any
as it'd be better to have no "documentation" of types here than to be misleading.
(2) Bring the output type switching back to the interface and fix the side effects in the provider packages
I think this would be the preferred approach, with a bias toward better alignment of the type hierarchy and against using casts to fix the errors outside of @langchain/core
(if possible). I can probably pick this up Monday if you don't have the time for it, though.
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.
My concern here would be that we'd drop the ToolMessage type from the output, or am I mistaken? That would trigger cases where we return something that's not expressed by the return type.
Good catch. I guess I was thinking more-so on the lines of consuming from the interface end instead of the consumer end (i.e. not creating tools using the tool
method) where the output type is always implicitly any since the default for the type param is ToolReturnType
. Agree this would be a bad change.
I'll have some more time to work on this tomorrow. If you don't hear from me by Monday then feel free to work off this PR
@benjamincburns Some updates on my findings: It turns out the error I was trying to corner has to do more with how typescript computes type inheritance rather than integration problems. When I add the conditional return types back to the interfaces (284f3e5), the error that occurs downstream is with the prebuilt agents and how it passes tools around. I haven't been able to address this yet so I imagine CI is failing atm. The crux of the error is If you try to add the interface inheritance directly on the class, the error comes from the It's almost like when evaluating if the dynamic tool conforms to the interface, TypeScript is computing the This is just specifically for |
So I confess that I got a bit nerd sniped by this last week and worked up a fix separately. I branched off of an earlier version of this PR, so unfortunately our work has diverged. My fixes are in c8d08fe and 84cf083, with the latter commit having the more substantial adjustment. The main problem that needed fixing was that I defined the default schema type on My branch includes your initial submission, so I'd be happy to merge it as-is, but if you want to review and reconcile this work against those changes I'd be happy to wait for you to do that. I suspect that you may have made some improvements to the types on internal functions that I'm missing. |
No shame in that! I know I'm guilty of that sometimes too. I missed the detail about the config object also determining the output type, so I appreciate you taking a crack at it. I merged your branch here, added some changes for readabilities sake in how the return type is defined/used, and fixed the override for the invoke method on both the interface and the class. |
Fixes #8010. Instead of just using an
any
alias as the return type to invoke, this adds a genericToolOutputT
type param on the tool classes that decides the return type of invoke.A later enhancement could be to restrict the return type of the provided function if "content_and_artifact" is provided as the response format (i.e. instead of
any[]
, the return type is forced to beextends [any, any]
in the tool function)cc @benjamincburns