-
Notifications
You must be signed in to change notification settings - Fork 3.6k
fix(voice): answer every tool call #6785
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 9 commits
0ddf33a
717c733
46965b7
7c24814
2c17092
f55d980
f16436f
7286087
e9c3b70
68ce12e
7317da5
f98ac9c
6f6cd3e
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 |
|---|---|---|
|
|
@@ -282,17 +282,7 @@ def _make_update_pair( | |
| extra=dict(self.function_call.extra), | ||
| ) | ||
| tool_output = make_tool_output(fnc_call=fnc_call, output=message, exception=None) | ||
| # fall back to a stub when the message isn't a valid tool output (e.g. raw object) | ||
| if tool_output.fnc_call_out is None: | ||
| fnc_call_out = FunctionCallOutput( | ||
| name=fnc_call.name, | ||
| call_id=fnc_call.call_id, | ||
| output=str(message or ""), | ||
| is_error=False, | ||
| ) | ||
| else: | ||
| fnc_call_out = tool_output.fnc_call_out | ||
| return (fnc_call, fnc_call_out) | ||
| return (fnc_call, tool_output.fnc_call_out) | ||
|
longcw marked this conversation as resolved.
|
||
|
|
||
|
|
||
| EventTypes = Literal[ | ||
|
|
@@ -431,21 +421,19 @@ class FunctionToolsExecutedEvent(BaseModel): | |
| """Emitted after a batch of function tools finishes executing. | ||
|
|
||
| ``function_calls`` and ``function_call_outputs`` are parallel lists: the | ||
| output at a given index belongs to the call at the same index. When an | ||
| output is present, its ``call_id`` matches the paired function call's | ||
| ``call_id``. A ``None`` output means the function call did not produce a | ||
| value that should be sent back to the LLM, such as when a tool raises | ||
| ``StopResponse`` or returns an invalid output. | ||
| output at a given index belongs to the call at the same index and carries | ||
| the same ``call_id``. Every call has one output, even one whose tool raised | ||
| ``StopResponse``; such an output asks for no reply with ``reply_required``. | ||
| """ | ||
|
|
||
| type: Literal["function_tools_executed"] = "function_tools_executed" | ||
| function_calls: list[FunctionCall] | ||
| function_call_outputs: list[FunctionCallOutput | None] | ||
| function_call_outputs: list[FunctionCallOutput] | ||
| created_at: float = Field(default_factory=time.time) | ||
| _reply_required: bool = PrivateAttr(default=False) | ||
|
Member
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. should we replace this attribute with something like def cancel_tool_reply(self) -> None:
if not self.function_call_outputs:
return
for output in self.function_call_outputs:
output.reply_required = False
@property
def has_tool_reply(self) -> bool:
return any(output.reply_required for output in self.function_call_outputs) |
||
| _handoff_required: bool = PrivateAttr(default=False) | ||
|
|
||
| def zipped(self) -> list[tuple[FunctionCall, FunctionCallOutput | None]]: | ||
| def zipped(self) -> list[tuple[FunctionCall, FunctionCallOutput]]: | ||
| """Return calls paired with outputs by list position.""" | ||
| return list(zip(self.function_calls, self.function_call_outputs, strict=False)) | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -884,13 +884,12 @@ async def _traceable_fnc_tool( | |
|
|
||
| output = make_tool_output(fnc_call=fnc_call, output=None, exception=e) | ||
|
|
||
| if fnc_call_out := output.fnc_call_out: | ||
| current_span.set_attribute( | ||
| trace_types.ATTR_FUNCTION_TOOL_OUTPUT, fnc_call_out.output | ||
| ) | ||
| current_span.set_attribute( | ||
| trace_types.ATTR_FUNCTION_TOOL_IS_ERROR, fnc_call_out.is_error | ||
| ) | ||
| current_span.set_attribute( | ||
|
Member
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. Q: is it possible we are storing stale output data if we update them in
Contributor
Author
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. The span attributes are snapshotted the moment the tool returns and the span closes there; |
||
| trace_types.ATTR_FUNCTION_TOOL_OUTPUT, output.fnc_call_out.output | ||
| ) | ||
| current_span.set_attribute( | ||
| trace_types.ATTR_FUNCTION_TOOL_IS_ERROR, output.fnc_call_out.is_error | ||
| ) | ||
|
|
||
| # TODO(theomonnom): Add the agent handoff inside the current_span | ||
| _tool_completed(output) | ||
|
|
@@ -944,11 +943,10 @@ async def _traceable_fnc_tool( | |
| @dataclass | ||
| class ToolExecutionOutput: | ||
| fnc_call: llm.FunctionCall | ||
| fnc_call_out: llm.FunctionCallOutput | None | ||
| fnc_call_out: llm.FunctionCallOutput | ||
| agent_task: Agent | None | ||
| raw_output: Any | ||
| raw_exception: BaseException | None | ||
| reply_required: bool = field(default=True) | ||
|
|
||
|
|
||
| def make_tool_output( | ||
|
|
@@ -989,7 +987,12 @@ def make_tool_output( | |
| ) | ||
| return ToolExecutionOutput( | ||
| fnc_call=fnc_call.model_copy(), | ||
| fnc_call_out=None, | ||
| fnc_call_out=llm.FunctionCallOutput( | ||
| name=fnc_call.name, | ||
| call_id=fnc_call.call_id, | ||
| output="the tool returned more than one agent", | ||
| is_error=True, | ||
| ), | ||
| agent_task=None, | ||
| raw_output=output, | ||
| raw_exception=exception, | ||
|
|
@@ -1013,17 +1016,32 @@ def make_tool_output( | |
| base_result = llm_utils.make_function_call_output( | ||
| fnc_call=fnc_call, output=fnc_out, exception=None | ||
| ) | ||
| # a tool with nothing to say, such as a bare handoff, expects no reply | ||
| base_result.fnc_call_out.reply_required = fnc_out is not None | ||
|
|
||
| return ToolExecutionOutput( | ||
| fnc_call=fnc_call.model_copy(), | ||
| fnc_call_out=base_result.fnc_call_out, | ||
| reply_required=fnc_out is not None, # require a reply if the tool returned an output | ||
| agent_task=task, | ||
| raw_output=output, | ||
| raw_exception=exception, | ||
| ) | ||
|
|
||
|
|
||
| def _interrupted_tool_output(out: ToolExecutionOutput) -> llm.FunctionCallOutput: | ||
| """The output to record for a tool that finished on an interrupted turn. | ||
|
|
||
| A handoff answers as a failure, since the interruption left it unapplied. | ||
| """ | ||
| fnc_call_out = out.fnc_call_out | ||
| if out.agent_task is not None: | ||
| fnc_call_out.output = "the agent handoff was interrupted and did not happen" | ||
| fnc_call_out.is_error = True | ||
|
|
||
| fnc_call_out.reply_required = False | ||
| return fnc_call_out | ||
|
|
||
|
|
||
| INSTRUCTIONS_MESSAGE_ID = "lk.agent_task.instructions" # value must not change | ||
| """ | ||
| The ID of the instructions message in the chat context. (only for stateless LLMs) | ||
|
|
||
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.
this might need the same treatment in #6823