Skip to content

Commit d43145f

Browse files
awsarronpgrayyjosephgultekinShubhamraut01fede-dash
authored
v0.1.5 (#122)
* models - openai - argument none (#97) * docs(readme): add open PRs badge + link to samples repo + change 'Docs' to 'Documentation' (#100) * docs(readme): add logo (#101) * docs(readme): add logo, title, badges, links to other repos, standardize headings (#102) * style(readme): use dark logo for clearer visibility when system is using light color scheme (#104) * fix(readme): use logo that changes color automatically depending on user's color preference scheme (#105) * feat(handlers): add reasoning text to callback handler and related tests (#109) * feat(handlers): add reasoning text to callback handler and related tests * feat(handlers): removed redundant comment in .gitignore file * feat(handlers): Updated reasoningText type as (Optional[str] * feat: Add dynamic system prompt override functionality (#108) * Modularizing Event Loop (#106) * fix(telemetry): fix agent span start and end when using Agent.stream_async() (#119) * feat: Update SlidingWindowConversationManager (#120) * v0.1.5 --------- Co-authored-by: Patrick Gray <[email protected]> Co-authored-by: Gokhan (Joe) Gultekin <[email protected]> Co-authored-by: Shubham Raut <[email protected]> Co-authored-by: fede-dash <[email protected]> Co-authored-by: Nick Clegg <[email protected]>
1 parent 0ec2df5 commit d43145f

File tree

13 files changed

+440
-229
lines changed

13 files changed

+440
-229
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,4 @@ __pycache__*
77
.pytest_cache
88
.ruff_cache
99
*.bak
10+
.vscode

README.md

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,34 @@
1-
# Strands Agents
2-
31
<div align="center">
2+
<div>
3+
<a href="https://strandsagents.com">
4+
<img src="https://strandsagents.com/latest/assets/logo-auto.svg" alt="Strands Agents" width="55px" height="105px">
5+
</a>
6+
</div>
7+
8+
<h1>
9+
Strands Agents
10+
</h1>
11+
412
<h2>
513
A model-driven approach to building AI agents in just a few lines of code.
614
</h2>
715

816
<div align="center">
917
<a href="https://github.com/strands-agents/sdk-python/graphs/commit-activity"><img alt="GitHub commit activity" src="https://img.shields.io/github/commit-activity/m/strands-agents/sdk-python"/></a>
1018
<a href="https://github.com/strands-agents/sdk-python/issues"><img alt="GitHub open issues" src="https://img.shields.io/github/issues/strands-agents/sdk-python"/></a>
19+
<a href="https://github.com/strands-agents/sdk-python/pulls"><img alt="GitHub open pull requests" src="https://img.shields.io/github/issues-pr/strands-agents/sdk-python"/></a>
1120
<a href="https://github.com/strands-agents/sdk-python/blob/main/LICENSE"><img alt="License" src="https://img.shields.io/github/license/strands-agents/sdk-python"/></a>
1221
<a href="https://pypi.org/project/strands-agents/"><img alt="PyPI version" src="https://img.shields.io/pypi/v/strands-agents"/></a>
1322
<a href="https://python.org"><img alt="Python versions" src="https://img.shields.io/pypi/pyversions/strands-agents"/></a>
1423
</div>
1524

1625
<p>
17-
<a href="https://strandsagents.com/">Docs</a>
18-
◆ <a href="https://github.com/strands-agents/docs/tree/main/docs/examples">Samples</a>
26+
<a href="https://strandsagents.com/">Documentation</a>
27+
◆ <a href="https://github.com/strands-agents/samples">Samples</a>
28+
◆ <a href="https://github.com/strands-agents/sdk-python">Python SDK</a>
1929
◆ <a href="https://github.com/strands-agents/tools">Tools</a>
2030
◆ <a href="https://github.com/strands-agents/agent-builder">Agent Builder</a>
31+
◆ <a href="https://github.com/strands-agents/mcp-server">MCP Server</a>
2132
</p>
2233
</div>
2334

@@ -26,7 +37,7 @@ Strands Agents is a simple yet powerful SDK that takes a model-driven approach t
2637
## Feature Overview
2738

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

@@ -138,6 +149,7 @@ Built-in providers:
138149
- [LiteLLM](https://strandsagents.com/latest/user-guide/concepts/model-providers/litellm/)
139150
- [LlamaAPI](https://strandsagents.com/latest/user-guide/concepts/model-providers/llamaapi/)
140151
- [Ollama](https://strandsagents.com/latest/user-guide/concepts/model-providers/ollama/)
152+
- [OpenAI](https://strandsagents.com/latest/user-guide/concepts/model-providers/openai/)
141153

142154
Custom providers can be implemented using [Custom Providers](https://strandsagents.com/latest/user-guide/concepts/model-providers/custom_model_provider/)
143155

@@ -165,9 +177,9 @@ For detailed guidance & examples, explore our documentation:
165177
- [API Reference](https://strandsagents.com/latest/api-reference/agent/)
166178
- [Production & Deployment Guide](https://strandsagents.com/latest/user-guide/deploy/operating-agents-in-production/)
167179

168-
## Contributing
180+
## Contributing ❤️
169181

170-
We welcome contributions! See our [Contributing Guide](https://github.com/strands-agents/sdk-python/blob/main/CONTRIBUTING.md) for details on:
182+
We welcome contributions! See our [Contributing Guide](CONTRIBUTING.md) for details on:
171183
- Reporting bugs & features
172184
- Development setup
173185
- Contributing via Pull Requests
@@ -178,6 +190,10 @@ We welcome contributions! See our [Contributing Guide](https://github.com/strand
178190

179191
This project is licensed under the Apache License 2.0 - see the [LICENSE](LICENSE) file for details.
180192

193+
## Security
194+
195+
See [CONTRIBUTING](CONTRIBUTING.md#security-issue-notifications) for more information.
196+
181197
## ⚠️ Preview Status
182198

183199
Strands Agents is currently in public preview. During this period:

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.4"
7+
version = "0.1.5"
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/agent/agent.py

Lines changed: 70 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -328,27 +328,17 @@ def __call__(self, prompt: str, **kwargs: Any) -> AgentResult:
328328
- metrics: Performance metrics from the event loop
329329
- state: The final state of the event loop
330330
"""
331-
model_id = self.model.config.get("model_id") if hasattr(self.model, "config") else None
332-
333-
self.trace_span = self.tracer.start_agent_span(
334-
prompt=prompt,
335-
model_id=model_id,
336-
tools=self.tool_names,
337-
system_prompt=self.system_prompt,
338-
custom_trace_attributes=self.trace_attributes,
339-
)
331+
self._start_agent_trace_span(prompt)
340332

341333
try:
342334
# Run the event loop and get the result
343335
result = self._run_loop(prompt, kwargs)
344336

345-
if self.trace_span:
346-
self.tracer.end_agent_span(span=self.trace_span, response=result)
337+
self._end_agent_trace_span(response=result)
347338

348339
return result
349340
except Exception as e:
350-
if self.trace_span:
351-
self.tracer.end_agent_span(span=self.trace_span, error=e)
341+
self._end_agent_trace_span(error=e)
352342

353343
# Re-raise the exception to preserve original behavior
354344
raise
@@ -383,6 +373,8 @@ async def stream_async(self, prompt: str, **kwargs: Any) -> AsyncIterator[Any]:
383373
yield event["data"]
384374
```
385375
"""
376+
self._start_agent_trace_span(prompt)
377+
386378
_stop_event = uuid4()
387379

388380
queue = asyncio.Queue[Any]()
@@ -400,8 +392,10 @@ def target_callback() -> None:
400392
nonlocal kwargs
401393

402394
try:
403-
self._run_loop(prompt, kwargs, supplementary_callback_handler=queuing_callback_handler)
404-
except BaseException as e:
395+
result = self._run_loop(prompt, kwargs, supplementary_callback_handler=queuing_callback_handler)
396+
self._end_agent_trace_span(response=result)
397+
except Exception as e:
398+
self._end_agent_trace_span(error=e)
405399
enqueue(e)
406400
finally:
407401
enqueue(_stop_event)
@@ -414,7 +408,7 @@ def target_callback() -> None:
414408
item = await queue.get()
415409
if item == _stop_event:
416410
break
417-
if isinstance(item, BaseException):
411+
if isinstance(item, Exception):
418412
raise item
419413
yield item
420414
finally:
@@ -457,27 +451,28 @@ def _execute_event_loop_cycle(self, callback_handler: Callable, kwargs: dict[str
457451
Returns:
458452
The result of the event loop cycle.
459453
"""
460-
kwargs.pop("agent", None)
461-
kwargs.pop("model", None)
462-
kwargs.pop("system_prompt", None)
463-
kwargs.pop("tool_execution_handler", None)
464-
kwargs.pop("event_loop_metrics", None)
465-
kwargs.pop("callback_handler", None)
466-
kwargs.pop("tool_handler", None)
467-
kwargs.pop("messages", None)
468-
kwargs.pop("tool_config", None)
454+
# Extract parameters with fallbacks to instance values
455+
system_prompt = kwargs.pop("system_prompt", self.system_prompt)
456+
model = kwargs.pop("model", self.model)
457+
tool_execution_handler = kwargs.pop("tool_execution_handler", self.thread_pool_wrapper)
458+
event_loop_metrics = kwargs.pop("event_loop_metrics", self.event_loop_metrics)
459+
callback_handler_override = kwargs.pop("callback_handler", callback_handler)
460+
tool_handler = kwargs.pop("tool_handler", self.tool_handler)
461+
messages = kwargs.pop("messages", self.messages)
462+
tool_config = kwargs.pop("tool_config", self.tool_config)
463+
kwargs.pop("agent", None) # Remove agent to avoid conflicts
469464

470465
try:
471466
# Execute the main event loop cycle
472467
stop_reason, message, metrics, state = event_loop_cycle(
473-
model=self.model,
474-
system_prompt=self.system_prompt,
475-
messages=self.messages, # will be modified by event_loop_cycle
476-
tool_config=self.tool_config,
477-
callback_handler=callback_handler,
478-
tool_handler=self.tool_handler,
479-
tool_execution_handler=self.thread_pool_wrapper,
480-
event_loop_metrics=self.event_loop_metrics,
468+
model=model,
469+
system_prompt=system_prompt,
470+
messages=messages, # will be modified by event_loop_cycle
471+
tool_config=tool_config,
472+
callback_handler=callback_handler_override,
473+
tool_handler=tool_handler,
474+
tool_execution_handler=tool_execution_handler,
475+
event_loop_metrics=event_loop_metrics,
481476
agent=self,
482477
event_loop_parent_span=self.trace_span,
483478
**kwargs,
@@ -488,8 +483,8 @@ def _execute_event_loop_cycle(self, callback_handler: Callable, kwargs: dict[str
488483
except ContextWindowOverflowException as e:
489484
# Try reducing the context size and retrying
490485

491-
self.conversation_manager.reduce_context(self.messages, e=e)
492-
return self._execute_event_loop_cycle(callback_handler, kwargs)
486+
self.conversation_manager.reduce_context(messages, e=e)
487+
return self._execute_event_loop_cycle(callback_handler_override, kwargs)
493488

494489
def _record_tool_execution(
495490
self,
@@ -545,3 +540,43 @@ def _record_tool_execution(
545540
messages.append(tool_use_msg)
546541
messages.append(tool_result_msg)
547542
messages.append(assistant_msg)
543+
544+
def _start_agent_trace_span(self, prompt: str) -> None:
545+
"""Starts a trace span for the agent.
546+
547+
Args:
548+
prompt: The natural language prompt from the user.
549+
"""
550+
model_id = self.model.config.get("model_id") if hasattr(self.model, "config") else None
551+
552+
self.trace_span = self.tracer.start_agent_span(
553+
prompt=prompt,
554+
model_id=model_id,
555+
tools=self.tool_names,
556+
system_prompt=self.system_prompt,
557+
custom_trace_attributes=self.trace_attributes,
558+
)
559+
560+
def _end_agent_trace_span(
561+
self,
562+
response: Optional[AgentResult] = None,
563+
error: Optional[Exception] = None,
564+
) -> None:
565+
"""Ends a trace span for the agent.
566+
567+
Args:
568+
span: The span to end.
569+
response: Response to record as a trace attribute.
570+
error: Error to record as a trace attribute.
571+
"""
572+
if self.trace_span:
573+
trace_attributes: Dict[str, Any] = {
574+
"span": self.trace_span,
575+
}
576+
577+
if response:
578+
trace_attributes["response"] = response
579+
if error:
580+
trace_attributes["error"] = error
581+
582+
self.tracer.end_agent_span(**trace_attributes)

src/strands/agent/conversation_manager/sliding_window_conversation_manager.py

Lines changed: 21 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,10 @@
11
"""Sliding window conversation history management."""
22

3-
import json
43
import logging
5-
from typing import List, Optional, cast
4+
from typing import Optional
65

7-
from ...types.content import ContentBlock, Message, Messages
6+
from ...types.content import Message, Messages
87
from ...types.exceptions import ContextWindowOverflowException
9-
from ...types.tools import ToolResult
108
from .conversation_manager import ConversationManager
119

1210
logger = logging.getLogger(__name__)
@@ -110,8 +108,9 @@ def _remove_dangling_messages(self, messages: Messages) -> None:
110108
def reduce_context(self, messages: Messages, e: Optional[Exception] = None) -> None:
111109
"""Trim the oldest messages to reduce the conversation context size.
112110
113-
The method handles special cases where tool results need to be converted to regular content blocks to maintain
114-
conversation coherence after trimming.
111+
The method handles special cases where trimming the messages leads to:
112+
- toolResult with no corresponding toolUse
113+
- toolUse with no corresponding toolResult
115114
116115
Args:
117116
messages: The messages to reduce.
@@ -126,52 +125,24 @@ def reduce_context(self, messages: Messages, e: Optional[Exception] = None) -> N
126125
# If the number of messages is less than the window_size, then we default to 2, otherwise, trim to window size
127126
trim_index = 2 if len(messages) <= self.window_size else len(messages) - self.window_size
128127

129-
# Throw if we cannot trim any messages from the conversation
130-
if trim_index >= len(messages):
131-
raise ContextWindowOverflowException("Unable to trim conversation context!") from e
132-
133-
# If the message at the cut index has ToolResultContent, then we map that to ContentBlock. This gets around the
134-
# limitation of needing ToolUse and ToolResults to be paired.
135-
if any("toolResult" in content for content in messages[trim_index]["content"]):
136-
if len(messages[trim_index]["content"]) == 1:
137-
messages[trim_index]["content"] = self._map_tool_result_content(
138-
cast(ToolResult, messages[trim_index]["content"][0]["toolResult"])
128+
# Find the next valid trim_index
129+
while trim_index < len(messages):
130+
if (
131+
# Oldest message cannot be a toolResult because it needs a toolUse preceding it
132+
any("toolResult" in content for content in messages[trim_index]["content"])
133+
or (
134+
# Oldest message can be a toolUse only if a toolResult immediately follows it.
135+
any("toolUse" in content for content in messages[trim_index]["content"])
136+
and trim_index + 1 < len(messages)
137+
and not any("toolResult" in content for content in messages[trim_index + 1]["content"])
139138
)
140-
141-
# If there is more content than just one ToolResultContent, then we cannot cut at this index.
139+
):
140+
trim_index += 1
142141
else:
143-
raise ContextWindowOverflowException("Unable to trim conversation context!") from e
142+
break
143+
else:
144+
# If we didn't find a valid trim_index, then we throw
145+
raise ContextWindowOverflowException("Unable to trim conversation context!") from e
144146

145147
# Overwrite message history
146148
messages[:] = messages[trim_index:]
147-
148-
def _map_tool_result_content(self, tool_result: ToolResult) -> List[ContentBlock]:
149-
"""Convert a ToolResult to a list of standard ContentBlocks.
150-
151-
This method transforms tool result content into standard content blocks that can be preserved when trimming the
152-
conversation history.
153-
154-
Args:
155-
tool_result: The ToolResult to convert.
156-
157-
Returns:
158-
A list of content blocks representing the tool result.
159-
"""
160-
contents = []
161-
text_content = "Tool Result Status: " + tool_result["status"] if tool_result["status"] else ""
162-
163-
for tool_result_content in tool_result["content"]:
164-
if "text" in tool_result_content:
165-
text_content = "\nTool Result Text Content: " + tool_result_content["text"] + f"\n{text_content}"
166-
elif "json" in tool_result_content:
167-
text_content = (
168-
"\nTool Result JSON Content: " + json.dumps(tool_result_content["json"]) + f"\n{text_content}"
169-
)
170-
elif "image" in tool_result_content:
171-
contents.append(ContentBlock(image=tool_result_content["image"]))
172-
elif "document" in tool_result_content:
173-
contents.append(ContentBlock(document=tool_result_content["document"]))
174-
else:
175-
logger.warning("unsupported content type")
176-
contents.append(ContentBlock(text=text_content))
177-
return contents

0 commit comments

Comments
 (0)