-
-
Notifications
You must be signed in to change notification settings - Fork 37
Open
Description
Summary
AgentBot currently constructs OpenAI/LiteLLM function-calling payloads using raw dicts rather than messages.py classes. This is because our message classes do not serialize the provider-required fields (assistant.tool_calls and tool.tool_call_id). This gap led to OpenAI 400 errors (“messages with role 'tool' must be a response to a preceding message with 'tool_calls'”) and forces AgentBot to bypass typed messages when interacting with providers.
Impact
- Forces AgentBot to build raw message dicts to satisfy function-calling protocol.
- Causes provider errors if the tool_call protocol isn’t precisely followed.
- Spreads message serialization logic outside
messages.py, making maintenance harder.
Error example
OpenAI 400:
Invalid parameter: messages with role 'tool' must be a response to a preceeding message with 'tool_calls'.
Root cause
AIMessagedoesn’t serializetool_callsinto the OpenAI/LiteLLM shape:
[{ id, type: 'function', function: { name, arguments } }]ToolMessagelackstool_call_id, so we cannot emit:
{ role: 'tool', content: '...', tool_call_id: '<assistant.tool_calls[i].id>' }SimpleBot.make_responsefilters messages down to onlyroleandcontent, stripping function-calling metadata.
Proposed changes
- Extend
ToolMessageto includetool_call_idand preserve it in serialization.
class ToolMessage(BaseMessage):
content: str
role: str = "tool"
tool_call_id: str | None = None
def model_dump(self):
d = super().model_dump()
if self.tool_call_id is not None:
d["tool_call_id"] = self.tool_call_id
return d- Teach
AIMessageto includetool_callsinmodel_dump()using the provider’s expected shape.
class AIMessage(BaseMessage):
content: str
role: str = "assistant"
def model_dump(self):
d = {"role": "assistant", "content": self.content}
if self.tool_calls:
def _tc(tc):
return {
"id": getattr(tc, "id", None),
"type": "function",
"function": {
"name": tc.function.name,
"arguments": tc.function.arguments,
},
}
d["tool_calls"] = [_tc(tc) for tc in self.tool_calls]
return d- Stop stripping fields in
SimpleBot.make_response.
- Replace:
messages_dumped = [
{k: v for k, v in m.model_dump().items() if k in ("role", "content")}
for m in messages
]- With:
messages_dumped = [m.model_dump() for m in messages]- Use
messages.pyclasses end-to-end in AgentBot.
- When the model returns assistant
tool_calls, append anAIMessagewith thosetool_calls. - For tool results, append a
ToolMessage(content=str(result), tool_call_id=<assistant_call.id>). - Build outbound payloads exclusively via
message.model_dump().
Acceptance criteria
- AgentBot uses only
messages.pyclasses to drive function-calling (no raw dicts). - OpenAI/OpenRouter/Groq/Ollama tool-calling works without 400 errors.
- Tests cover presence of
assistant.tool_callsand matchingtool.tool_call_id.
Nice-to-haves
- Helper to convert LiteLLM
ChatCompletionMessageToolCallinto serialized dicts. - Docs note on function-calling protocol and message serialization.
Metadata
Metadata
Assignees
Labels
No labels