-
Notifications
You must be signed in to change notification settings - Fork 433
feat(bedrock_agent): add new Amazon Bedrock Agents Functions Resolver #6564
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
Merged
Merged
Changes from 26 commits
Commits
Show all changes
32 commits
Select commit
Hold shift + click to select a range
41bc401
feat(bedrock_agent): create bedrock agents functions data class
anafalcao bed8f3f
create resolver
anafalcao a3765f0
mypy
anafalcao 44d80f8
add response
anafalcao abbc100
add name param to tool
anafalcao e42ceff
add response optional fields
anafalcao 86c7ab7
bedrockfunctionresponse and response state
anafalcao 34948d7
remove body message
anafalcao 24978cb
add parser
anafalcao 45f85f6
add test for required fields
anafalcao b420a90
Merge branch 'develop' into feat/bedrock_functions
anafalcao 84bb6b0
add more tests for parser and resolver
anafalcao 20bbe9f
Merge branch 'feat/bedrock_functions' of https://github.com/aws-power…
anafalcao d463304
add validation response state
anafalcao b4ab6b9
Merge branch 'develop' into feat/bedrock_functions
leandrodamascena 39e0d36
Merge branch 'develop' into feat/bedrock_functions
leandrodamascena 54a7edf
params injection
anafalcao fdde207
doc event handler, parser and data class
anafalcao c8b1b2f
fix doc typo
anafalcao db7d6b9
fix doc typo
anafalcao 266ebcb
mypy
anafalcao 4211b72
Merge branch 'develop' into feat/bedrock_functions
leandrodamascena 20215ed
Small refactor + documentation
leandrodamascena ef31cb5
Small refactor + documentation
leandrodamascena c200a3a
Small refactor + documentation
leandrodamascena ca700c8
Small refactor + documentation
leandrodamascena c992463
Aligning Python implementation with TS
leandrodamascena 214d061
Adding custom serializer
leandrodamascena 9914ab5
Adding custom serializer
leandrodamascena df3f29b
Merge branch 'develop' into feat/bedrock_functions
leandrodamascena 3694cb2
More documentation
leandrodamascena 13d5569
Merge branch 'develop' into feat/bedrock_functions
leandrodamascena File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
216 changes: 216 additions & 0 deletions
216
aws_lambda_powertools/event_handler/bedrock_agent_function.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,216 @@ | ||
from __future__ import annotations | ||
|
||
import inspect | ||
import warnings | ||
from collections.abc import Callable | ||
from typing import Any, Literal, TypeVar | ||
|
||
from aws_lambda_powertools.utilities.data_classes import BedrockAgentFunctionEvent | ||
from aws_lambda_powertools.warnings import PowertoolsUserWarning | ||
|
||
# Define a generic type for the function | ||
T = TypeVar("T", bound=Callable[..., Any]) | ||
|
||
|
||
class BedrockFunctionResponse: | ||
"""Response class for Bedrock Agent Functions. | ||
|
||
Parameters | ||
---------- | ||
body : Any, optional | ||
Response body to be returned to the caller. | ||
session_attributes : dict[str, str] or None, optional | ||
Session attributes to include in the response for maintaining state. | ||
prompt_session_attributes : dict[str, str] or None, optional | ||
Prompt session attributes to include in the response. | ||
knowledge_bases : list[dict[str, Any]] or None, optional | ||
Knowledge bases to include in the response. | ||
response_state : {"FAILURE", "REPROMPT"} or None, optional | ||
Response state indicating if the function failed or needs reprompting. | ||
|
||
Examples | ||
-------- | ||
>>> @app.tool(description="Function that uses session attributes") | ||
>>> def test_function(): | ||
... return BedrockFunctionResponse( | ||
... body="Hello", | ||
... session_attributes={"userId": "123"}, | ||
... prompt_session_attributes={"lastAction": "login"} | ||
... ) | ||
|
||
Notes | ||
----- | ||
The `response_state` parameter can only be set to "FAILURE" or "REPROMPT". | ||
""" | ||
|
||
def __init__( | ||
self, | ||
body: Any = None, | ||
session_attributes: dict[str, str] | None = None, | ||
prompt_session_attributes: dict[str, str] | None = None, | ||
knowledge_bases: list[dict[str, Any]] | None = None, | ||
response_state: Literal["FAILURE", "REPROMPT"] | None = None, | ||
) -> None: | ||
if response_state and response_state not in ["FAILURE", "REPROMPT"]: | ||
raise ValueError("responseState must be 'FAILURE' or 'REPROMPT'") | ||
|
||
self.body = body | ||
self.session_attributes = session_attributes | ||
self.prompt_session_attributes = prompt_session_attributes | ||
self.knowledge_bases = knowledge_bases | ||
self.response_state = response_state | ||
|
||
|
||
class BedrockFunctionsResponseBuilder: | ||
""" | ||
Bedrock Functions Response Builder. This builds the response dict to be returned by Lambda | ||
when using Bedrock Agent Functions. | ||
""" | ||
|
||
def __init__(self, result: BedrockFunctionResponse | Any) -> None: | ||
self.result = result | ||
|
||
def build(self, event: BedrockAgentFunctionEvent) -> dict[str, Any]: | ||
result_obj = self.result | ||
|
||
# Extract attributes from BedrockFunctionResponse or use defaults | ||
body = getattr(result_obj, "body", result_obj) | ||
session_attributes = getattr(result_obj, "session_attributes", None) | ||
prompt_session_attributes = getattr(result_obj, "prompt_session_attributes", None) | ||
knowledge_bases = getattr(result_obj, "knowledge_bases", None) | ||
response_state = getattr(result_obj, "response_state", None) | ||
|
||
# Build base response structure | ||
# Per AWS Bedrock documentation, currently only "TEXT" is supported as the responseBody content type | ||
# https://docs.aws.amazon.com/bedrock/latest/userguide/agents-lambda.html | ||
response: dict[str, Any] = { | ||
"messageVersion": "1.0", | ||
"response": { | ||
"actionGroup": event.action_group, | ||
"function": event.function, | ||
"functionResponse": { | ||
"responseBody": {"TEXT": {"body": str(body if body is not None else "")}}, | ||
}, | ||
}, | ||
"sessionAttributes": session_attributes or event.session_attributes or {}, | ||
"promptSessionAttributes": prompt_session_attributes or event.prompt_session_attributes or {}, | ||
} | ||
|
||
# Add optional fields when present | ||
if response_state: | ||
response["response"]["functionResponse"]["responseState"] = response_state | ||
|
||
if knowledge_bases: | ||
response["knowledgeBasesConfiguration"] = knowledge_bases | ||
|
||
return response | ||
|
||
|
||
class BedrockAgentFunctionResolver: | ||
"""Bedrock Agent Function resolver that handles function definitions | ||
|
||
Examples | ||
-------- | ||
```python | ||
from aws_lambda_powertools.event_handler import BedrockAgentFunctionResolver | ||
|
||
app = BedrockAgentFunctionResolver() | ||
|
||
@app.tool(description="Gets the current UTC time") | ||
def get_current_time(): | ||
from datetime import datetime | ||
return datetime.utcnow().isoformat() | ||
|
||
def lambda_handler(event, context): | ||
return app.resolve(event, context) | ||
``` | ||
""" | ||
|
||
context: dict | ||
|
||
def __init__(self) -> None: | ||
self._tools: dict[str, dict[str, Any]] = {} | ||
self.current_event: BedrockAgentFunctionEvent | None = None | ||
self.context = {} | ||
self._response_builder_class = BedrockFunctionsResponseBuilder | ||
|
||
def tool( | ||
self, | ||
name: str | None = None, | ||
description: str | None = None, | ||
) -> Callable[[T], T]: | ||
"""Decorator to register a tool function | ||
|
||
Parameters | ||
---------- | ||
name : str | None | ||
Custom name for the tool. If not provided, uses the function name | ||
description : str | None | ||
Description of what the tool does | ||
|
||
Returns | ||
------- | ||
Callable | ||
Decorator function that registers and returns the original function | ||
""" | ||
|
||
def decorator(func: T) -> T: | ||
function_name = name or func.__name__ | ||
if function_name in self._tools: | ||
warnings.warn( | ||
f"Tool '{function_name}' already registered. Overwriting with new definition.", | ||
PowertoolsUserWarning, | ||
stacklevel=2, | ||
) | ||
|
||
self._tools[function_name] = { | ||
"function": func, | ||
"description": description, | ||
} | ||
return func | ||
|
||
return decorator | ||
|
||
def resolve(self, event: dict[str, Any], context: Any) -> dict[str, Any]: | ||
"""Resolves the function call from Bedrock Agent event""" | ||
try: | ||
self.current_event = BedrockAgentFunctionEvent(event) | ||
return self._resolve() | ||
except KeyError as e: | ||
raise ValueError(f"Missing required field: {str(e)}") from e | ||
|
||
def _resolve(self) -> dict[str, Any]: | ||
"""Internal resolution logic""" | ||
if self.current_event is None: | ||
raise ValueError("No event to process") | ||
|
||
function_name = self.current_event.function | ||
|
||
try: | ||
# Extract parameters from the event | ||
parameters = {param.name: param.value for param in getattr(self.current_event, "parameters", [])} | ||
|
||
func = self._tools[function_name]["function"] | ||
# Filter parameters to only include those expected by the function | ||
sig = inspect.signature(func) | ||
valid_params = {name: value for name, value in parameters.items() if name in sig.parameters} | ||
|
||
# Call the function with the filtered parameters | ||
result = func(**valid_params) | ||
|
||
self.clear_context() | ||
|
||
# Build and return the response | ||
return BedrockFunctionsResponseBuilder(result).build(self.current_event) | ||
except Exception as error: | ||
# Return a formatted error response | ||
error_response = BedrockFunctionResponse(body=f"Error: {str(error)}", response_state="FAILURE") | ||
return BedrockFunctionsResponseBuilder(error_response).build(self.current_event) | ||
|
||
def append_context(self, **additional_context): | ||
leandrodamascena marked this conversation as resolved.
Show resolved
Hide resolved
|
||
"""Append key=value data as routing context""" | ||
self.context.update(**additional_context) | ||
|
||
def clear_context(self): | ||
"""Resets routing context""" | ||
self.context.clear() |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
81 changes: 81 additions & 0 deletions
81
aws_lambda_powertools/utilities/data_classes/bedrock_agent_function_event.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,81 @@ | ||
from __future__ import annotations | ||
|
||
from aws_lambda_powertools.utilities.data_classes.common import DictWrapper | ||
|
||
|
||
class BedrockAgentInfo(DictWrapper): | ||
@property | ||
def name(self) -> str: | ||
return self["name"] | ||
|
||
@property | ||
def id(self) -> str: # noqa: A003 | ||
return self["id"] | ||
|
||
@property | ||
def alias(self) -> str: | ||
return self["alias"] | ||
|
||
@property | ||
def version(self) -> str: | ||
return self["version"] | ||
|
||
|
||
class BedrockAgentFunctionParameter(DictWrapper): | ||
@property | ||
def name(self) -> str: | ||
return self["name"] | ||
|
||
@property | ||
def type(self) -> str: # noqa: A003 | ||
return self["type"] | ||
|
||
@property | ||
def value(self) -> str: | ||
return self["value"] | ||
|
||
|
||
class BedrockAgentFunctionEvent(DictWrapper): | ||
""" | ||
Bedrock Agent Function input event | ||
|
||
Documentation: | ||
https://docs.aws.amazon.com/bedrock/latest/userguide/agents-lambda.html | ||
""" | ||
|
||
@property | ||
def message_version(self) -> str: | ||
return self["messageVersion"] | ||
|
||
@property | ||
def input_text(self) -> str: | ||
return self["inputText"] | ||
|
||
@property | ||
def session_id(self) -> str: | ||
return self["sessionId"] | ||
|
||
@property | ||
def action_group(self) -> str: | ||
return self["actionGroup"] | ||
|
||
@property | ||
def function(self) -> str: | ||
return self["function"] | ||
|
||
@property | ||
def parameters(self) -> list[BedrockAgentFunctionParameter]: | ||
parameters = self.get("parameters") or [] | ||
return [BedrockAgentFunctionParameter(x) for x in parameters] | ||
|
||
@property | ||
def agent(self) -> BedrockAgentInfo: | ||
return BedrockAgentInfo(self["agent"]) | ||
|
||
@property | ||
def session_attributes(self) -> dict[str, str]: | ||
return self.get("sessionAttributes", {}) or {} | ||
|
||
@property | ||
def prompt_session_attributes(self) -> dict[str, str]: | ||
return self.get("promptSessionAttributes", {}) or {} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.