Skip to content

Commit dbe0fea

Browse files
authored
feat: Add support for agent invoke with no input, or Message input (#653)
1 parent c18ef93 commit dbe0fea

File tree

4 files changed

+168
-55
lines changed

4 files changed

+168
-55
lines changed

src/strands/agent/agent.py

Lines changed: 89 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -361,14 +361,21 @@ def tool_names(self) -> list[str]:
361361
all_tools = self.tool_registry.get_all_tools_config()
362362
return list(all_tools.keys())
363363

364-
def __call__(self, prompt: Union[str, list[ContentBlock]], **kwargs: Any) -> AgentResult:
364+
def __call__(self, prompt: str | list[ContentBlock] | Messages | None = None, **kwargs: Any) -> AgentResult:
365365
"""Process a natural language prompt through the agent's event loop.
366366
367-
This method implements the conversational interface (e.g., `agent("hello!")`). It adds the user's prompt to
368-
the conversation history, processes it through the model, executes any tool calls, and returns the final result.
367+
This method implements the conversational interface with multiple input patterns:
368+
- String input: `agent("hello!")`
369+
- ContentBlock list: `agent([{"text": "hello"}, {"image": {...}}])`
370+
- Message list: `agent([{"role": "user", "content": [{"text": "hello"}]}])`
371+
- No input: `agent()` - uses existing conversation history
369372
370373
Args:
371-
prompt: User input as text or list of ContentBlock objects for multi-modal content.
374+
prompt: User input in various formats:
375+
- str: Simple text input
376+
- list[ContentBlock]: Multi-modal content blocks
377+
- list[Message]: Complete messages with roles
378+
- None: Use existing conversation history
372379
**kwargs: Additional parameters to pass through the event loop.
373380
374381
Returns:
@@ -387,14 +394,23 @@ def execute() -> AgentResult:
387394
future = executor.submit(execute)
388395
return future.result()
389396

390-
async def invoke_async(self, prompt: Union[str, list[ContentBlock]], **kwargs: Any) -> AgentResult:
397+
async def invoke_async(
398+
self, prompt: str | list[ContentBlock] | Messages | None = None, **kwargs: Any
399+
) -> AgentResult:
391400
"""Process a natural language prompt through the agent's event loop.
392401
393-
This method implements the conversational interface (e.g., `agent("hello!")`). It adds the user's prompt to
394-
the conversation history, processes it through the model, executes any tool calls, and returns the final result.
402+
This method implements the conversational interface with multiple input patterns:
403+
- String input: Simple text input
404+
- ContentBlock list: Multi-modal content blocks
405+
- Message list: Complete messages with roles
406+
- No input: Use existing conversation history
395407
396408
Args:
397-
prompt: User input as text or list of ContentBlock objects for multi-modal content.
409+
prompt: User input in various formats:
410+
- str: Simple text input
411+
- list[ContentBlock]: Multi-modal content blocks
412+
- list[Message]: Complete messages with roles
413+
- None: Use existing conversation history
398414
**kwargs: Additional parameters to pass through the event loop.
399415
400416
Returns:
@@ -411,7 +427,7 @@ async def invoke_async(self, prompt: Union[str, list[ContentBlock]], **kwargs: A
411427

412428
return cast(AgentResult, event["result"])
413429

414-
def structured_output(self, output_model: Type[T], prompt: Optional[Union[str, list[ContentBlock]]] = None) -> T:
430+
def structured_output(self, output_model: Type[T], prompt: str | list[ContentBlock] | Messages | None = None) -> T:
415431
"""This method allows you to get structured output from the agent.
416432
417433
If you pass in a prompt, it will be used temporarily without adding it to the conversation history.
@@ -423,7 +439,11 @@ def structured_output(self, output_model: Type[T], prompt: Optional[Union[str, l
423439
Args:
424440
output_model: The output model (a JSON schema written as a Pydantic BaseModel)
425441
that the agent will use when responding.
426-
prompt: The prompt to use for the agent (will not be added to conversation history).
442+
prompt: The prompt to use for the agent in various formats:
443+
- str: Simple text input
444+
- list[ContentBlock]: Multi-modal content blocks
445+
- list[Message]: Complete messages with roles
446+
- None: Use existing conversation history
427447
428448
Raises:
429449
ValueError: If no conversation history or prompt is provided.
@@ -437,7 +457,7 @@ def execute() -> T:
437457
return future.result()
438458

439459
async def structured_output_async(
440-
self, output_model: Type[T], prompt: Optional[Union[str, list[ContentBlock]]] = None
460+
self, output_model: Type[T], prompt: str | list[ContentBlock] | Messages | None = None
441461
) -> T:
442462
"""This method allows you to get structured output from the agent.
443463
@@ -462,12 +482,8 @@ async def structured_output_async(
462482
try:
463483
if not self.messages and not prompt:
464484
raise ValueError("No conversation history or prompt provided")
465-
# Create temporary messages array if prompt is provided
466-
if prompt:
467-
content: list[ContentBlock] = [{"text": prompt}] if isinstance(prompt, str) else prompt
468-
temp_messages = self.messages + [{"role": "user", "content": content}]
469-
else:
470-
temp_messages = self.messages
485+
486+
temp_messages: Messages = self.messages + self._convert_prompt_to_messages(prompt)
471487

472488
structured_output_span.set_attributes(
473489
{
@@ -499,16 +515,25 @@ async def structured_output_async(
499515
finally:
500516
self.hooks.invoke_callbacks(AfterInvocationEvent(agent=self))
501517

502-
async def stream_async(self, prompt: Union[str, list[ContentBlock]], **kwargs: Any) -> AsyncIterator[Any]:
518+
async def stream_async(
519+
self,
520+
prompt: str | list[ContentBlock] | Messages | None = None,
521+
**kwargs: Any,
522+
) -> AsyncIterator[Any]:
503523
"""Process a natural language prompt and yield events as an async iterator.
504524
505-
This method provides an asynchronous interface for streaming agent events, allowing
506-
consumers to process stream events programmatically through an async iterator pattern
507-
rather than callback functions. This is particularly useful for web servers and other
508-
async environments.
525+
This method provides an asynchronous interface for streaming agent events with multiple input patterns:
526+
- String input: Simple text input
527+
- ContentBlock list: Multi-modal content blocks
528+
- Message list: Complete messages with roles
529+
- No input: Use existing conversation history
509530
510531
Args:
511-
prompt: User input as text or list of ContentBlock objects for multi-modal content.
532+
prompt: User input in various formats:
533+
- str: Simple text input
534+
- list[ContentBlock]: Multi-modal content blocks
535+
- list[Message]: Complete messages with roles
536+
- None: Use existing conversation history
512537
**kwargs: Additional parameters to pass to the event loop.
513538
514539
Yields:
@@ -532,13 +557,15 @@ async def stream_async(self, prompt: Union[str, list[ContentBlock]], **kwargs: A
532557
"""
533558
callback_handler = kwargs.get("callback_handler", self.callback_handler)
534559

535-
content: list[ContentBlock] = [{"text": prompt}] if isinstance(prompt, str) else prompt
536-
message: Message = {"role": "user", "content": content}
560+
# Process input and get message to add (if any)
561+
messages = self._convert_prompt_to_messages(prompt)
562+
563+
self.trace_span = self._start_agent_trace_span(messages)
537564

538-
self.trace_span = self._start_agent_trace_span(message)
539565
with trace_api.use_span(self.trace_span):
540566
try:
541-
events = self._run_loop(message, invocation_state=kwargs)
567+
events = self._run_loop(messages, invocation_state=kwargs)
568+
542569
async for event in events:
543570
if "callback" in event:
544571
callback_handler(**event["callback"])
@@ -555,12 +582,12 @@ async def stream_async(self, prompt: Union[str, list[ContentBlock]], **kwargs: A
555582
raise
556583

557584
async def _run_loop(
558-
self, message: Message, invocation_state: dict[str, Any]
585+
self, messages: Messages, invocation_state: dict[str, Any]
559586
) -> AsyncGenerator[dict[str, Any], None]:
560587
"""Execute the agent's event loop with the given message and parameters.
561588
562589
Args:
563-
message: The user message to add to the conversation.
590+
messages: The input messages to add to the conversation.
564591
invocation_state: Additional parameters to pass to the event loop.
565592
566593
Yields:
@@ -571,7 +598,8 @@ async def _run_loop(
571598
try:
572599
yield {"callback": {"init_event_loop": True, **invocation_state}}
573600

574-
self._append_message(message)
601+
for message in messages:
602+
self._append_message(message)
575603

576604
# Execute the event loop cycle with retry logic for context limits
577605
events = self._execute_event_loop_cycle(invocation_state)
@@ -629,6 +657,34 @@ async def _execute_event_loop_cycle(self, invocation_state: dict[str, Any]) -> A
629657
async for event in events:
630658
yield event
631659

660+
def _convert_prompt_to_messages(self, prompt: str | list[ContentBlock] | Messages | None) -> Messages:
661+
messages: Messages | None = None
662+
if prompt is not None:
663+
if isinstance(prompt, str):
664+
# String input - convert to user message
665+
messages = [{"role": "user", "content": [{"text": prompt}]}]
666+
elif isinstance(prompt, list):
667+
if len(prompt) == 0:
668+
# Empty list
669+
messages = []
670+
# Check if all item in input list are dictionaries
671+
elif all(isinstance(item, dict) for item in prompt):
672+
# Check if all items are messages
673+
if all(all(key in item for key in Message.__annotations__.keys()) for item in prompt):
674+
# Messages input - add all messages to conversation
675+
messages = cast(Messages, prompt)
676+
677+
# Check if all items are content blocks
678+
elif all(any(key in ContentBlock.__annotations__.keys() for key in item) for item in prompt):
679+
# Treat as List[ContentBlock] input - convert to user message
680+
# This allows invalid structures to be passed through to the model
681+
messages = [{"role": "user", "content": cast(list[ContentBlock], prompt)}]
682+
else:
683+
messages = []
684+
if messages is None:
685+
raise ValueError("Input prompt must be of type: `str | list[Contentblock] | Messages | None`.")
686+
return messages
687+
632688
def _record_tool_execution(
633689
self,
634690
tool: ToolUse,
@@ -694,15 +750,15 @@ def _record_tool_execution(
694750
self._append_message(tool_result_msg)
695751
self._append_message(assistant_msg)
696752

697-
def _start_agent_trace_span(self, message: Message) -> trace_api.Span:
753+
def _start_agent_trace_span(self, messages: Messages) -> trace_api.Span:
698754
"""Starts a trace span for the agent.
699755
700756
Args:
701-
message: The user message.
757+
messages: The input messages.
702758
"""
703759
model_id = self.model.config.get("model_id") if hasattr(self.model, "config") else None
704760
return self.tracer.start_agent_span(
705-
message=message,
761+
messages=messages,
706762
agent_name=self.name,
707763
model_id=model_id,
708764
tools=self.tool_names,

src/strands/telemetry/tracer.py

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -408,7 +408,7 @@ def end_event_loop_cycle_span(
408408

409409
def start_agent_span(
410410
self,
411-
message: Message,
411+
messages: Messages,
412412
agent_name: str,
413413
model_id: Optional[str] = None,
414414
tools: Optional[list] = None,
@@ -418,7 +418,7 @@ def start_agent_span(
418418
"""Start a new span for an agent invocation.
419419
420420
Args:
421-
message: The user message being sent to the agent.
421+
messages: List of messages being sent to the agent.
422422
agent_name: Name of the agent.
423423
model_id: Optional model identifier.
424424
tools: Optional list of tools being used.
@@ -451,13 +451,12 @@ def start_agent_span(
451451
span = self._start_span(
452452
f"invoke_agent {agent_name}", attributes=attributes, span_kind=trace_api.SpanKind.CLIENT
453453
)
454-
self._add_event(
455-
span,
456-
"gen_ai.user.message",
457-
event_attributes={
458-
"content": serialize(message["content"]),
459-
},
460-
)
454+
for message in messages:
455+
self._add_event(
456+
span,
457+
f"gen_ai.{message['role']}.message",
458+
{"content": serialize(message["content"])},
459+
)
461460

462461
return span
463462

0 commit comments

Comments
 (0)