8000 v0.1.2 (#40) · strands-agents/sdk-python@aec6928 · GitHub
[go: up one dir, main page]

Skip to content

Commit aec6928

Browse files
v0.1.2 (#40)
* Update README.md mention of tools repo (#29) Typo in the examples tools header referencing the wrong repo * Update README to mention Meta Llama API as a supported model provider (#21) Co-authored-by: Ryan Coleman <rycolez@amazon.com> * fix: tracing of non-serializable values, e.g. bytes (#34) * fix(bedrock): use the AWS_REGION environment variable for the Bedrock model provider region if set and boto_session is not passed (#39) * v0.1.2 --------- Co-authored-by: Ryan Coleman <ryan@coleman.cafe> Co-authored-by: Ryan Coleman <rycolez@amazon.com>
1 parent 7445fdb commit aec6928

File tree

6 files changed

+202
-21
lines changed

6 files changed

+202
-21
lines changed

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ Strands Agents is a simple yet powerful SDK that takes a model-driven approach t
2626
## Feature Overview
2727

2828
- **Lightweight & Flexible**: Simple agent loop that just works and is fully customizable
29-
- **Model Agnostic**: Support for Amazon Bedrock, Anthropic, Ollama, and custom providers
29+
- **Model Agnostic**: Support for Amazon Bedrock, Anthropic, Llama, Ollama, and custom providers
3030
- **Advanced Capabilities**: Multi-agent systems, autonomous agents, and streaming support
3131
- **Built-in MCP**: Native support for Model Context Protocol (MCP) servers, enabling access to thousands of pre-built tools
3232

@@ -152,7 +152,7 @@ agent = Agent(tools=[calculator])
152152
agent("What is the square root of 1764")
153153
```
154154

155-
It's also available on GitHub via [strands-agents-tools](https://github.com/strands-agents/strands-agents-tools).
155+
It's also available on GitHub via [strands-agents/tools](https://github.com/strands-agents/tools).
156156

157157
## Documentation
158158

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
44

55
[project]
66
name = "strands-agents"
7-
version = "0.1.1"
7+
version = "0.1.2"
88
description = "A model-driven approach to building AI agents in just a few lines of code"
99
readme = "README.md"
1010
requires-python = ">=3.10"

src/strands/models/bedrock.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
"""
55

66
import logging
7+
import os
78
from typing import Any, Iterable, Literal, Optional, cast
89

910
import boto3
@@ -96,7 +97,8 @@ def __init__(
9697
Args:
9798
boto_session: Boto Session to use when calling the Bedrock Model.
9899
boto_client_config: Configuration to use when creating the Bedrock-Runtime Boto Client.
99-
region_name: AWS region to use for the Bedrock service. Defaults to "us-west-2".
100+
region_name: AWS region to use for the Bedrock service.
101+
Defaults to the AWS_REGION environment variable if set, or "us-west-2" if not set.
100102
**model_config: Configuration options for the Bedrock model.
101103
"""
102104
if region_name and boto_session:
@@ -108,7 +110,7 @@ def __init__(
108110
logger.debug("config=<%s> | initializing", self.config)
109111

110112
session = boto_session or boto3.Session(
111-
region_name=region_name or "us-west-2",
113+
region_name=region_name or os.getenv("AWS_REGION") or "us-west-2",
112114
)
113115
client_config = boto_client_config or BotocoreConfig(user_agent_extra="strands-agents")
114116
self.client = session.client(

src/strands/telemetry/tracer.py

Lines changed: 44 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
import json
88
import logging
99
import os
10-
from datetime import datetime, timezone
10+
from datetime import date, datetime, timezone
1111
from importlib.metadata import version
1212
from typing import Any, Dict, Mapping, Optional
1313

@@ -30,21 +30,49 @@
3030
class JSONEncoder(json.JSONEncoder):
3131
"""Custom JSON encoder that handles non-serializable types."""
3232

33-
def default(self, obj: Any) -> Any:
34-
"""Handle non-serializable types.
33+
def encode(self, obj: Any) -> str:
34+
"""Recursively encode objects, preserving structure and only replacing unserializable values.
3535
3636
Args:
37-
obj: The object to serialize
37+
obj: The object to encode
3838
3939
Returns:
40-
A JSON serializable version of the object
40+
JSON string representation of the object
4141
"""
42-
value = ""
43-
try:
44-
value = super().default(obj)
45-
except TypeError:
46-
value = "<replaced>"
47-
return value
42+
# Process the object to handle non-serializable values
43+
processed_obj = self._process_value(obj)
44+
# Use the parent class to encode the processed object
45+
return super().encode(processed_obj)
46+
47+
def _process_value(self, value: Any) -> Any:
48+
"""Process any value, handling containers recursively.
49+
50+
Args:
51+
value: The value to process
52+
53+
Returns:
54+
Processed value with unserializable parts replaced
55+
"""
56+
# Handle datetime objects directly
57+
if isinstance(value, (datetime, date)):
58+
return value.isoformat()
59+
60+
# Handle dictionaries
61+
elif isinstance(value, dict):
62+
return {k: self._process_value(v) for k, v in value.items()}
63+
64+
# Handle lists
65+
elif isinstance(value, list):
66+
return [self._process_value(item) for item in value]
< FAAC /code>
67+
68+
# Handle all other values
69+
else:
70+
try:
71+
# Test if the value is JSON serializable
72+
json.dumps(value)
73+
return value
74+
except (TypeError, OverflowError, ValueError):
75+
return "<replaced>"
4876

4977

5078
class Tracer:
@@ -332,6 +360,7 @@ def start_tool_call_span(
332360
The created span, or None if tracing is not enabled.
333361
"""
334362
attributes: Dict[str, AttributeValue] = {
363+
"gen_ai.prompt": json.dumps(tool, cls=JSONEncoder),
335364
"tool.name": tool["name"],
336365
"tool.id": tool["toolUseId"],
337366
"tool.parameters": json.dumps(tool["input"], cls=JSONEncoder),
@@ -358,10 +387,11 @@ def end_tool_call_span(
358387
status = tool_result.get("status")
359388
status_str = str(status) if status is not None else ""
360389

390+
tool_result_content_json = json.dumps(tool_result.get("content"), cls=JSONEncoder)
361391
attributes.update(
362392
{
363-
"tool.result": json.dumps(tool_result.get("content"), cls=JSONEncoder),
364-
"gen_ai.completion": json.dumps(tool_result.get("content"), cls=JSONEncoder),
393+
"tool.result": tool_result_content_json,
394+
"gen_ai.completion": tool_result_content_json,
365395
"tool.status": status_str,
366396
}
367397
)
@@ -492,7 +522,7 @@ def end_agent_span(
492522
if response:
493523
attributes.update(
494524
{
495-
"gen_ai.completion": json.dumps(response, cls=JSONEncoder),
525+
"gen_ai.completion": str(response),
496526
}
497527
)
498528

tests/strands/models/test_bedrock.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import os
12
import unittest.mock
23

34
import boto3
@@ -99,6 +100,16 @@ def test__init__with_custom_region(bedrock_client):
99100
mock_session_cls.assert_called_once_with(region_name=custom_region)
100101

101102

103+
def test__init__with_environment_variable_region(bedrock_client):
104+
"""Test that BedrockModel uses the provided region."""
105+
_ = bedrock_client
106+
os.environ["AWS_REGION"] = "eu-west-1"
107+
108+
with unittest.mock.patch("strands.models.bedrock.boto3.Session") as mock_session_cls:
109+
_ = BedrockModel()
110+
mock_session_cls.assert_called_once_with(region_name="eu-west-1")
111+
112+
102113
def test__init__with_region_and_session_raises_value_error():
103114
"""Test that BedrockModel raises ValueError when both region and session are provided."""
104115
with pytest.raises(ValueError):

tests/strands/telemetry/test_tracer.py

Lines changed: 140 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
import json
22
import os
3+
from datetime import date, datetime, timezone
34
from unittest import mock
45

56
import pytest
67
from opentelemetry.trace import StatusCode # type: ignore
78

8-
from strands.telemetry.tracer import Tracer, get_tracer
9+
from strands.telemetry.tracer import JSONEncoder, Tracer, get_tracer
910
from strands.types.streaming import Usage
1011

1112

@@ -268,6 +269,9 @@ def test_start_tool_call_span(mock_tracer):
268269

269270
mock_tracer.start_span.assert_called_once()
270271
assert mock_tracer.start_span.call_args[1]["name"] == "Tool: test-tool"
272+
mock_span.set_attribute.assert_any_call(
273+
"gen_ai.prompt", json.dumps({"name": "test-tool", "toolUseId": "123", "input": {"param": "value"}})
274+
)
271275
mock_span.set_attribute.assert_any_call("tool.name", "test-tool")
272276
mock_span.set_attribute.assert_any_call("tool.id", "123")
273277
mock_span.set_attribute.assert_any_call("tool.parameters", json.dumps({"param": "value"}))
@@ -369,7 +373,7 @@ def test_end_agent_span(mock_span):
369373

370374
tracer.end_agent_span(mock_span, mock_response)
371375

372-
mock_span.set_attribute.assert_any_call("gen_ai.completion", '"<replaced>"')
376+
mock_span.set_attribute.assert_any_call("gen_ai.completion", "Agent response")
373377
mock_span.set_attribute.assert_any_call("gen_ai.usage.prompt_tokens", 50)
374378
mock_span.set_attribute.assert_any_call("gen_ai.usage.completion_tokens", 100)
375379
mock_span.set_attribute.assert_any_call("gen_ai.usage.total_tokens", 150)
@@ -497,3 +501,137 @@ def test_start_model_invoke_span_with_parent(mock_tracer):
497501

498502
# Verify span was returned
499503
assert span is mock_span
504+
505+
506+
@pytest.mark.parametrize(
507+
"input_data, expected_result",
508+
[
509+
("test string", '"test string"'),
510+
(1234, "1234"),
511+
(13.37, "13.37"),
512+
(False, "false"),
513+
(None, "null"),
514+
],
515+
)
516+
def test_json_encoder_serializable(input_data, expected_result):
517+
"""Test encoding of serializable values."""
518+
encoder = JSONEncoder()
519+
520+
result = encoder.encode(input_data)
521+
assert result == expected_result
522+
523+
524+
def test_json_encoder_datetime():
525+
"""Test encoding datetime and date objects."""
526+
encoder = JSONEncoder()
527+
528+
dt = datetime(2025, 1, 1, 12, 0, 0, tzinfo=timezone.utc)
529+
result = encoder.encode(dt)
530+
assert result == f'"{dt.isoformat()}"'
531+
532+
d = date(2025, 1, 1)
533+
result = encoder.encode(d)
534+
assert result == f'"{d.isoformat()}"'
535+
536+
537+
def test_json_encoder_list():
538+
"""Test encoding a list with mixed content."""
539+
encoder = JSONEncoder()
540+
541+
non_serializable = lambda x: x # noqa: E731
542+
543+
data = ["value", 42, 13.37, non_serializable, None, {"key": True}, ["value here"]]
544+
545+
result = json.loads(encoder.encode(data))
546+
assert result == ["value", 42, 13.37, "<replaced>", None, {"key": True}, ["value here"]]
547+
548+
549+
def test_json_encoder_dict():
550+
"""Test encoding a dict with mixed content."""
551+
encoder = JSONEncoder()
552+
553+
class UnserializableClass:
554+
def __str__(self):
555+
return "Unserializable Object"
556+
557+
non_serializable = lambda x: x # noqa: E731
558+
559+
now = datetime.now(timezone.utc)
560+
561+
data = {
562+
"metadata": {
563+
"timestamp": now,
564+
"version": "1.0",
565+
"debug_info": {"object": non_serializable, "callable": lambda x: x + 1}, # noqa: E731
566+
},
567+
"content": [
568+
{"type": "text", "value": "Hello world"},
569+
{"type": "binary", "value": non_serializable},
570+
{"type": "mixed", "values": [1, "text", non_serializable, {"nested": non_serializable}]},
571+
],
572+
"statistics": {
573+
"processed": 100,
574+
"failed": 5,
575+
"details": [{"id": 1, "status": "ok"}, {"id": 2, "status": "error", "error_obj": non_serializable}],
576+
},
577+
"list": [
578+
non_serializable,
579+
1234,
580+
13.37,
581+
True,
582+
None,
583+
"string here",
584+
],
585+
}
586+
587+
expected = {
588+
"metadata": {
589+
"timestamp": now.isoformat(),
590+
"version": "1.0",
591+
"debug_info": {"object": "<replaced>", "callable": "<replaced>"},
592+
},
593+
"content": [
594+
{"type": "text", "value": "Hello world"},
595+
{"type": "binary", "value": "<replaced>"},
596+
{"type": "mixed", "values": [1, "text", "<replaced>", {"nested": "<replaced>"}]},
597+
],
598+
"statistics": {
599+
"processed": 100,
600+
"failed": 5,
601+
"details": [{"id": 1, "status": "ok"}, {"id": 2, "status": "error", "error_obj": "<replaced>"}],
602+
},
603+
"list": [
604+
"<replaced>",
605+
1234,
606+
13.37,
607+
True,
608+
None,
609+
"string here",
610+
],
611+
}
612+
613+
result = json.loads(encoder.encode(data))
614+
615+
assert result == expected
616+
617+
618+
def test_json_encoder_value_error():
619+
"""Test encoding values that cause ValueError."""
620+
encoder = JSONEncoder()
621+
622+
# A very large integer that exceeds JSON limits and throws ValueError
623+
huge_number = 2**100000
624+
625+
# Test in a dictionary
626+
dict_data = {"normal": 42, "huge": huge_number}
627+
result = json.loads(encoder.encode(dict_data))
628+
assert result == {"normal": 42, "huge": "<replaced>"}
629+
630+
# Test in a list
631+
list_data = [42, huge_number]
632+
result = json.loads(encoder.encode(list_data))
633+
assert result == [42, "<replaced>"]
634+
635+
# Test just the value
636+
result = json.loads(encoder.encode(huge_number))
637+
assert result == "<replaced>"

0 commit comments

Comments
 (0)
0