-
Notifications
You must be signed in to change notification settings - Fork 444
Description
So just had a conversation with Mackenzie in the below I'll outline what the current behavior is with our code, the bedrock api and what solutions we have.
The following recursive reference will fail in our current code with a ValueError
due to the circular reference
class User(BaseModel):
id: int
name: str
friend: Optional['User'] = None
agent = Agent(
system_prompt="You excel at structured output"
)
circ = agent.structured_output(User , "John Smith is a 30-year-old software engineer with id of 3 and has a friend named jack with id of 9")
circ
The same code will fail if we remove our circular reference check and call bedrock with the actual Pydantic schema. The following is the pydantic schema which we'll send to bedrock directly:
{
"$defs": {
"User": {
"properties": {
"id": {"title": "Id", "type": "integer"},
"name": {"title": "Name", "type": "string"},
"friend": {
"anyOf": [{"$ref": "#/$defs/User"}, {"type": "null"}],
"default": None,
},
},
"required": ["id", "name"],
"title": "User",
"type": "object",
}
},
"$ref": "#/$defs/User",
}
Bedrock Error:
---------------------------------------------------------------------------
ValidationException Traceback (most recent call last)
Cell In[4], [line 9](vscode-notebook-cell:?execution_count=4&line=9)
4 friend: Optional['User'] = None
6 agent = Agent(
7 system_prompt="You excel at structured output"
8 )
----> [9](vscode-notebook-cell:?execution_count=4&line=9) circ = agent.structured_output(User , "John Smith is a 30-year-old software engineer with id of 3 and has a friend named jack with id of 9")
10 circ
File /Volumes/workplace/dev/structured_output/sdk-python/src/strands/agent/agent.py:468, in Agent.structured_output(self, output_model, prompt)
466 with ThreadPoolExecutor() as executor:
467 future = executor.submit(execute)
--> [468](https://file+.vscode-resource.vscode-cdn.net/Volumes/workplace/dev/structured_output/sdk-python/src/strands/agent/agent.py:468) return future.result()
File ~/.local/share/uv/python/cpython-3.10.18-macos-aarch64-none/lib/python3.10/concurrent/futures/_base.py:458, in Future.result(self, timeout)
456 raise CancelledError()
457 elif self._state == FINISHED:
--> [458](https://file+.vscode-resource.vscode-cdn.net/Volumes/workplace/dev/structured_output/sdk-python/afarnsandbox/695/~/.local/share/uv/python/cpython-3.10.18-macos-aarch64-none/lib/python3.10/concurrent/futures/_base.py:458) return self.__get_result()
459 else:
460 raise TimeoutError()
File ~/.local/share/uv/python/cpython-3.10.18-macos-aarch64-none/lib/python3.10/concurrent/futures/_base.py:403, in Future.__get_result(self)
401 if self._exception:
402 try:
...
-> [1078](https://file+.vscode-resource.vscode-cdn.net/Volumes/workplace/dev/structured_output/sdk-python/.venv/lib/python3.10/site-packages/botocore/client.py:1078) raise error_class(parsed_response, operation_name)
1079 else:
1080 return parsed_response
ValidationException: An error occurred (ValidationException) when calling the ConverseStream operation: The value at toolConfig.tools.0.toolSpec.inputSchema.json.type must be one of the following: object.
However, interestingly enough, if we were to move that recursive model one level down into another model then Bedrock will not throw an error and instead will correctly return a result that conforms to the pydantic schema:
class User(BaseModel):
id: int
name: str
friend: Optional['User'] = None
class NestedCircular(BaseModel):
leave_blank: str = Field(description="A field that should be left blank", default='just a field')
child: User = Field(description="A child user")
agent = Agent(
system_prompt="You excel at structured output"
)
circ = agent.structured_output(NestedCircular, "John Smith is a 30-year-old software engineer with id of 3 and has a friend named jack with id of 9")
circ
and then model.model_json_schema()
will yield:
{
"$defs": {
"User": {
"properties": {
"id": {"title": "Id", "type": "integer"},
"name": {"title": "Name", "type": "string"},
"friend": {
"anyOf": [{"$ref": "#/$defs/User"}, {"type": "null"}],
"default": None,
},
},
"required": ["id", "name"],
"title": "User",
"type": "object",
}
},
"properties": {
"leave_blank": {
"default": "just a field",
"description": "A field that should be left blank",
"title": "Leave Blank",
"type": "string",
},
"child": {"type": "object", "description": "A child user", "properties": {}},
},
"required": ["child"],
"title": "NestedCircular",
"type": "object",
}
which the model handles correctly.
Note: that as of the current main
branch code, the above object will not be detected, no error will be thrown, however, we try to do some incomplete ref
parsing which sends Bedrock an incomplete json schema which results in incomplete Model results which results in a pydantic error when we try to put the bedrock results into the Pydantic object.
Mackenzie suggested a possible approach we can investigate in a new issue where we can:
-
use the native pydantic json and send that directly to the model. as we can see it works when there is a recursive pydantic object as long as it's not the top level object.
-
for the case where it's a top level object we can attempt to add the actual json schema to the payload when sending it to bedrock so that the top level payload doesn't just have a
ref...
instead it will have theref
data so that the model can then treat it just like the case where the recursive pydantic object is nested inside another pydantic object
Originally posted by @afarntrog in #817 (comment)