Skip to content

Commit 7b7585e

Browse files
committed
feat(agent): add Routing node to agent graph
This commit introduces the Routing node to the Agent graph: - Adds AgentGraphNode::Route and adapts AgentGraph to start from Route, calling agent.route() before CreateTodos. - Adds the route method to Agent trait and implements it in ButBot to select a workflow path by running pick_route(), which inspects the conversation and chooses Simple or Planning routes. - ButBotRoute and RouteResponse types introduced to enable flexible agent workflow control. - butbot.rs now contains logic to determine route based on conversation, plus initiates the correct node. - lib.rs exposes structured_output_blocking in but-action for routing logic. This refactor enables dynamic agent flow control between direct/simple actions and planning-based workflows.
1 parent 203e790 commit 7b7585e

File tree

3 files changed

+86
-3
lines changed

3 files changed

+86
-3
lines changed

crates/but-action/src/lib.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,8 @@ pub use action::Source;
3434
pub use action::list_actions;
3535
use but_graph::VirtualBranchesTomlMetadata;
3636
pub use openai::{
37-
ChatMessage, ToolCallContent, ToolResponseContent, tool_calling_loop, tool_calling_loop_stream,
37+
ChatMessage, ToolCallContent, ToolResponseContent, structured_output_blocking,
38+
tool_calling_loop, tool_calling_loop_stream,
3839
};
3940
use strum::EnumString;
4041
use uuid::Uuid;

crates/but-bot/src/agent.rs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
pub enum AgentGraphNode {
2+
Route,
23
CreateTodos,
34
ExecuteTodo,
45
Done(String),
@@ -17,13 +18,16 @@ impl Default for AgentGraph {
1718
impl AgentGraph {
1819
pub fn new() -> Self {
1920
Self {
20-
current_node: AgentGraphNode::CreateTodos,
21+
current_node: AgentGraphNode::Route,
2122
}
2223
}
2324

2425
pub fn start(&mut self, agent: &mut dyn Agent) -> anyhow::Result<String> {
2526
loop {
2627
match self.current_node {
28+
AgentGraphNode::Route => {
29+
self.current_node = agent.route()?;
30+
}
2731
AgentGraphNode::CreateTodos => {
2832
self.current_node = agent.create_todos()?;
2933
}
@@ -39,6 +43,7 @@ impl AgentGraph {
3943
}
4044

4145
pub trait Agent {
46+
fn route(&mut self) -> anyhow::Result<AgentGraphNode>;
4247
fn create_todos(&mut self) -> anyhow::Result<AgentGraphNode>;
4348
fn execute_todo(&mut self) -> anyhow::Result<AgentGraphNode>;
4449
}

crates/but-bot/src/butbot.rs

Lines changed: 78 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,40 @@ use crate::{
88
state::{AgentState, Todo},
99
};
1010

11+
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, schemars::JsonSchema)]
12+
#[serde(rename_all = "camelCase")]
13+
pub enum ButBotRoute {
14+
#[schemars(description = "
15+
<description>
16+
Simple routing, no complex planning needed.
17+
</description>
18+
19+
<when_to_use>
20+
Use this route when the user request is straightforward and can be handled with simple actions.
21+
This is the fastest route, and is suitable whenever the objectives are clear and there are not multiple steps involved.
22+
</when_to_use>
23+
")]
24+
Simple,
25+
#[schemars(description = "
26+
<description>
27+
Complex routing, requires planning and multiple steps.
28+
</description>
29+
30+
<when_to_use>
31+
Use this route when the user request is complex, or there is some investigation that needs to be done and requires multiple steps to complete.
32+
This route is slower, but allows for more complex actions to be performed.
33+
</when_to_use>
34+
")]
35+
Planning,
36+
}
37+
38+
#[derive(Debug, serde::Serialize, serde::Deserialize, schemars::JsonSchema)]
39+
#[schemars(deny_unknown_fields)]
40+
pub struct ButButRouteResponse {
41+
#[schemars(description = "The route that was taken by the agent.")]
42+
pub route: ButBotRoute,
43+
}
44+
1145
const MODEL: &str = "gpt-4.1";
1246

1347
const SYS_PROMPT: &str = "
@@ -87,6 +121,41 @@ impl<'a> ButBot<'a> {
87121
}
88122
}
89123

124+
pub fn pick_route(&mut self) -> anyhow::Result<ButBotRoute> {
125+
let routing_sys_prompt = "
126+
Your are an expert in determining the best workflow path to take based on the user request.
127+
Please, take a look at the provided conversation and return which is the route that should be taken.
128+
";
129+
130+
let conversation = self
131+
.chat_messages
132+
.iter()
133+
.map(|msg| msg.to_string())
134+
.collect::<Vec<_>>()
135+
.join("\n\n");
136+
137+
let messages = vec![but_action::ChatMessage::User(format!(
138+
"
139+
Take a look at the conversation, specifically, the last user request below, and choose the best route to take.
140+
<CONVERSATION>
141+
{}
142+
</CONVERSATION>
143+
",
144+
conversation
145+
))];
146+
147+
let response = but_action::structured_output_blocking::<ButButRouteResponse>(
148+
self.openai,
149+
routing_sys_prompt,
150+
messages,
151+
)?;
152+
153+
match response {
154+
Some(route) => Ok(route.route),
155+
None => Err(anyhow::anyhow!("Failed to determine the route to take.")),
156+
}
157+
}
158+
90159
/// Update the agent's state and the provided todos.
91160
///
92161
/// Based on the provided chat messages and the project status, this function will
@@ -372,6 +441,15 @@ If you need to perform actions, do so, and be concise in the description of the
372441
}
373442

374443
impl Agent for ButBot<'_> {
444+
fn route(&mut self) -> anyhow::Result<AgentGraphNode> {
445+
match self.pick_route()? {
446+
ButBotRoute::Planning => Ok(AgentGraphNode::CreateTodos),
447+
ButBotRoute::Simple => {
448+
let response = self.workspace_loop()?;
449+
Ok(AgentGraphNode::Done(response))
450+
}
451+
}
452+
}
375453
fn create_todos(&mut self) -> anyhow::Result<AgentGraphNode> {
376454
self.update_state()?;
377455

@@ -385,7 +463,6 @@ impl Agent for ButBot<'_> {
385463

386464
fn execute_todo(&mut self) -> anyhow::Result<AgentGraphNode> {
387465
if let Some(todo) = self.state.next_todo() {
388-
println!("Executing todo: {}", todo.clone().as_prompt());
389466
let (text_response, messages) = self.execute_todo(&todo)?;
390467
self.chat_messages = messages;
391468
self.text_response_buffer.push(text_response);

0 commit comments

Comments
 (0)