Skip to content

Commit 7078c76

Browse files
authored
Add a max_actions_per_run variable on AgentService as a backstop (#535)
An observed failure mode of a reasoning Agent is when it spirals into endlessly calling tools. In a development loop, the programmer can quickly break the loop, but in a hosted situation this can result in surprising costs if each tool involves LLM usage. This PR adds a simple backstop in which the `max_actions_per_run` variable on AgentService (default = 5) acts as a limit on how many actions the agent is permitted to run before being stopped. If the AgentService attempts to run an action after `max_actions_per_run` has been reached, it is treated as an error which will be returned to the chat invoker.
1 parent cbf624f commit 7078c76

3 files changed

Lines changed: 55 additions & 0 deletions

File tree

src/steamship/agents/examples/example_assistant_with_caching.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@
44
from steamship.agents.service.agent_service import AgentService
55
from steamship.agents.tools.image_generation import DalleTool
66
from steamship.agents.tools.search import SearchTool
7+
from steamship.data import TagValueKey
8+
from steamship.invocable import post
9+
from steamship.utils.kv_store import KeyValueStore
710
from steamship.utils.repl import AgentREPL
811

912

@@ -15,6 +18,11 @@ class MyCachingAssistant(AgentService):
1518

1619
def __init__(self, **kwargs):
1720
super().__init__(**kwargs, use_llm_cache=True, use_action_cache=True)
21+
22+
# Load the max_actions_per_run from the saved store for use in testing.
23+
self.kv = KeyValueStore(self.client)
24+
self.max_actions_per_run = self.get_max_actions_per_run()
25+
1826
self.set_default_agent(
1927
FunctionsBasedAgent(
2028
tools=[
@@ -26,6 +34,19 @@ def __init__(self, **kwargs):
2634
)
2735
)
2836

37+
@post("set_max_actions_per_run")
38+
def set_max_actions_per_run(self, value: int):
39+
"""Save the max_actions_per_run value so that it will be reloaded upon next request."""
40+
self.max_actions_per_run = self.kv.set(
41+
"max_actions_per_run", {TagValueKey.NUMBER_VALUE: value}
42+
)
43+
return value
44+
45+
@post("get_max_actions_per_run")
46+
def get_max_actions_per_run(self) -> int:
47+
"""Save the max_actions_per_run value so that it will be reloaded upon next request."""
48+
return (self.kv.get("max_actions_per_run") or {}).get(TagValueKey.NUMBER_VALUE, 5)
49+
2950

3051
if __name__ == "__main__":
3152
# AgentREPL provides a mechanism for local execution of an AgentService method.

src/steamship/agents/service/agent_service.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,15 +24,24 @@ class AgentService(PackageService):
2424
use_action_cache: bool
2525
"""Whether or not to cache agent Actions (for tool runs) by default."""
2626

27+
max_actions_per_run: int
28+
"""The maximum number of actions to permit while the agent is reasoning.
29+
30+
This is intended primarily to act as a backstop to prevent a condition in which the Agent decides to loop endlessly
31+
on tool runs that consume resources with a cost-basis (e.g. prompt completions, embedding operations, vector lookups)
32+
"""
33+
2734
def __init__(
2835
self,
2936
use_llm_cache: Optional[bool] = False,
3037
use_action_cache: Optional[bool] = False,
38+
max_actions_per_run: Optional[int] = 5,
3139
agent: Optional[Agent] = None,
3240
**kwargs,
3341
):
3442
self.use_llm_cache = use_llm_cache
3543
self.use_action_cache = use_action_cache
44+
self.max_actions_per_run = max_actions_per_run
3645
self.agent = agent
3746
super().__init__(**kwargs)
3847

@@ -144,8 +153,22 @@ def run_agent(self, agent: Agent, context: AgentContext):
144153
agent=agent, input_blocks=[context.chat_history.last_user_message], context=context
145154
)
146155

156+
number_of_actions_run = 0
157+
147158
while not isinstance(action, FinishAction):
159+
# Throw an error if we've exceeded our action budget.
160+
if number_of_actions_run >= self.max_actions_per_run:
161+
raise SteamshipError(
162+
message=(
163+
f"Agent reached its Action budget of {self.max_actions_per_run} without arriving at a response. If you are the developer, checking the logs may reveal it was selecting unhelpful tools or receiving unhelpful responses from them."
164+
)
165+
)
166+
167+
# Run the next action
148168
self.run_action(agent=agent, action=action, context=context)
169+
number_of_actions_run += 1
170+
171+
# Select a new next_action and log it
149172
action = self.next_action(agent=agent, input_blocks=action.output, context=context)
150173

151174
# TODO: Arrive at a solid design for the details of this structured log object

tests/steamship_tests/agents/test_agent_service.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,17 @@ def test_example_with_caching_service(client: Steamship):
4242
assert "image" in blocks[0].text
4343
assert blocks[1].is_image()
4444

45+
# attempt with a max_actions_per_run budget of 0 (should fail!)
46+
assert caching_agent.invoke("get_max_actions_per_run") == 5
47+
caching_agent.invoke("set_max_actions_per_run", value=0)
48+
assert caching_agent.invoke("get_max_actions_per_run") == 0
49+
50+
with pytest.raises(SteamshipError, match="budget"):
51+
blocks_json = caching_agent.invoke("prompt", prompt="draw a cat", context_id=context_id)
52+
53+
caching_agent.invoke("set_max_actions_per_run", value=5)
54+
assert caching_agent.invoke("get_max_actions_per_run") == 5
55+
4556
agent_context = AgentContext.get_or_create(
4657
client=client, context_keys=context_keys, use_llm_cache=True, use_action_cache=True
4758
)

0 commit comments

Comments
 (0)