|
| 1 | +""" |
| 2 | +Task: Process a number through a sequence of two steps: |
| 3 | +- Burify: increment the number by 3 |
| 4 | +- Tonify: multiply the number by 4 |
| 5 | +
|
| 6 | +Planner Agent oversees the process, using two worker agents: |
| 7 | +- BurifyAgent: handles the Burify step |
| 8 | +- TonifyAgent: handles the Tonify step |
| 9 | +
|
| 10 | +Planner checks intermediate results and provides feedback to worker agents, |
| 11 | +until their step is complete, before proceeding to the next step. |
| 12 | +
|
| 13 | +Run like this from repo root (omit `-m` to use default model gpt-4.1-mini): |
| 14 | +
|
| 15 | + uv run examples/basic/planner-workflow.py -m gpt-4.1-mini |
| 16 | +""" |
| 17 | + |
| 18 | +from typing import List |
| 19 | +import langroid as lr |
| 20 | +import langroid.language_models as lm |
| 21 | +from langroid.pydantic_v1 import Field |
| 22 | +from langroid.agent.tools.orchestration import AgentDoneTool, ForwardTool |
| 23 | +from fire import Fire |
| 24 | +import logging |
| 25 | + |
| 26 | +logger = logging.getLogger(__name__) |
| 27 | +MODEL = lm.OpenAIChatModel.GPT4_1_MINI |
| 28 | + |
| 29 | + |
| 30 | +class BurifyTool(lr.ToolMessage): |
| 31 | + request: str = "burify_tool" |
| 32 | + purpose: str = "To apply the 'Burify' process to a <number>" |
| 33 | + number: int = Field(..., description="The number (int) to Burify") |
| 34 | + |
| 35 | + def handle(self) -> str: |
| 36 | + # stateless tool: handler used in BurifyAgent |
| 37 | + return f"Burify this number: {self.number}" |
| 38 | + |
| 39 | + |
| 40 | +class TonifyTool(lr.ToolMessage): |
| 41 | + request: str = "tonify_tool" |
| 42 | + purpose: str = "To apply the 'Tonify' process to a <number>" |
| 43 | + number: int = Field(..., description="The number (int) to Tonify") |
| 44 | + |
| 45 | + def handle(self) -> str: |
| 46 | + # stateless tool: handler used in TonifyAgent |
| 47 | + return f"Tonify this number: {self.number}" |
| 48 | + |
| 49 | + |
| 50 | +class BurifyCheckTool(lr.ToolMessage): |
| 51 | + request: str = "burify_check_tool" |
| 52 | + purpose: str = "To check if the Burify process is complete" |
| 53 | + number: int = Field(..., description="The number (int) to check") |
| 54 | + original_number: int = Field( |
| 55 | + ..., |
| 56 | + description="The original number (int) given to the BurifyAgent", |
| 57 | + ) |
| 58 | + |
| 59 | + def handle(self) -> str: |
| 60 | + # stateless tool |
| 61 | + if self.number == self.original_number + 3: |
| 62 | + return AcceptTool(result=self.number) |
| 63 | + else: |
| 64 | + return BurifyRevisionTool( |
| 65 | + feedback="Burify is NOT complete! Please try again.", |
| 66 | + recipient="Burify", |
| 67 | + ) |
| 68 | + |
| 69 | + |
| 70 | +class TonifyCheckTool(lr.ToolMessage): |
| 71 | + request: str = "tonify_check_tool" |
| 72 | + purpose: str = "To check if the Tonify process is complete" |
| 73 | + number: int = Field(..., description="The number (int) to check") |
| 74 | + original_number: int = Field( |
| 75 | + ..., |
| 76 | + description="The original number (int) given to the TonifyAgent", |
| 77 | + ) |
| 78 | + |
| 79 | + def handle(self): |
| 80 | + # stateless tool |
| 81 | + if self.number == self.original_number * 4: |
| 82 | + return AcceptTool(result=self.number) |
| 83 | + else: |
| 84 | + return TonifyRevisionTool( |
| 85 | + feedback="Tonify is NOT complete! Please try again.", |
| 86 | + recipient="Tonify", |
| 87 | + ) |
| 88 | + |
| 89 | + |
| 90 | +class BurifyRevisionTool(lr.ToolMessage): |
| 91 | + request: str = "burify_revision_tool" |
| 92 | + purpose: str = "To give <feedback> to the 'BurifyAgent' on their Burify Attempt" |
| 93 | + feedback: str = Field(..., description="Feedback for the BurifyAgent") |
| 94 | + |
| 95 | + def handle(self): |
| 96 | + return f""" |
| 97 | + Below is feedback on your attempt to Burify: |
| 98 | + <Feedback> |
| 99 | + {self.feedback} |
| 100 | + </Feedback> |
| 101 | + Please try again! |
| 102 | + """ |
| 103 | + |
| 104 | + |
| 105 | +class TonifyRevisionTool(lr.ToolMessage): |
| 106 | + request: str = "tonify_revision_tool" |
| 107 | + purpose: str = "To give <feedback> to the 'TonifyAgent' on their Tonify Attempt" |
| 108 | + feedback: str = Field(..., description="Feedback for the TonifyAgent") |
| 109 | + |
| 110 | + def handle(self): |
| 111 | + return f""" |
| 112 | + Below is feedback on your attempt to Tonify: |
| 113 | + <Feedback> |
| 114 | + {self.feedback} |
| 115 | + </Feedback> |
| 116 | + Please try again! |
| 117 | + """ |
| 118 | + |
| 119 | + |
| 120 | +class BurifySubmitTool(lr.ToolMessage): |
| 121 | + request: str = "burify_submit_tool" |
| 122 | + purpose: str = "To submit the result of an attempt of the Burify process" |
| 123 | + result: int = Field(..., description="The result (int) to submit") |
| 124 | + |
| 125 | + def handle(self): |
| 126 | + return AgentDoneTool(content=str(self.result)) |
| 127 | + |
| 128 | + |
| 129 | +class TonifySubmitTool(lr.ToolMessage): |
| 130 | + request: str = "tonify_submit_tool" |
| 131 | + purpose: str = "To submit the result of an attempt of the Tonify process" |
| 132 | + result: int = Field(..., description="The result (int) to submit") |
| 133 | + |
| 134 | + def handle(self): |
| 135 | + return AgentDoneTool(content=str(self.result)) |
| 136 | + |
| 137 | + |
| 138 | +class AcceptTool(lr.ToolMessage): |
| 139 | + request: str = "accept_tool" |
| 140 | + purpose: str = "To accept the result of the 'Burify' or 'Tonify' process" |
| 141 | + result: int |
| 142 | + |
| 143 | + |
| 144 | +class PlannerConfig(lr.ChatAgentConfig): |
| 145 | + name: str = "Planner" |
| 146 | + steps: List[str] = ["Burify", "Tonify"] |
| 147 | + handle_llm_no_tool: str = "You FORGOT to use one of your TOOLs!" |
| 148 | + system_message: str = f""" |
| 149 | + You are a Planner in charge of PROCESSING a given integer through |
| 150 | + a SEQUENCE of 2 processing STEPS, which you CANNOT do by yourself, but you must |
| 151 | + rely on WORKER AGENTS who will do these for you: |
| 152 | + - Burify - will be done by the BurifyAgent |
| 153 | + - Tonify - will be done by the TonifyAgent |
| 154 | + |
| 155 | + In order to INITIATE each process, you MUST use the appropriate TOOLs: |
| 156 | + - `{BurifyTool.name()}` to Burify the number (the tool will be handled by the BurifyAgent) |
| 157 | + - `{TonifyTool.name()}` to Tonify the number (the tool will be handled by the TonifyAgent) |
| 158 | + |
| 159 | + Each of the WORKER AGENTS works like this: |
| 160 | + - The Agent will ATTEMPT a processing step, using the number you give it. |
| 161 | + - You will VERIFY whether the processing step is COMPLETE or NOT |
| 162 | + using the CORRESPONDING CHECK TOOL: |
| 163 | + - check if the Burify step is complete using the `{BurifyCheckTool.name()}` |
| 164 | + - check if the Tonify step is complete using the `{TonifyCheckTool.name()}` |
| 165 | + - If the step is NOT complete, you will ask the Agent to try again, |
| 166 | + by using the CORRESPONDING Revision TOOL where you can include your FEEDBACK: |
| 167 | + - `{BurifyRevisionTool.name()}` to revise the Burify step |
| 168 | + - `{TonifyRevisionTool.name()}` to revise the Tonify step |
| 169 | + - If you determine (see below) that the step is COMPLETE, you MUST |
| 170 | + use the `{AcceptTool.name()}` to ACCEPT the result of the step. |
| 171 | + """ |
| 172 | + |
| 173 | + |
| 174 | +class PlannerAgent(lr.ChatAgent): |
| 175 | + current_step: int |
| 176 | + current_num: int |
| 177 | + original_num: int |
| 178 | + |
| 179 | + def __init__(self, config: PlannerConfig): |
| 180 | + super().__init__(config) |
| 181 | + self.config: PlannerConfig = config |
| 182 | + self.current_step = 0 |
| 183 | + self.current_num = 0 |
| 184 | + |
| 185 | + def burify_tool(self, msg: BurifyTool) -> str: |
| 186 | + """Handler of BurifyTool: uses/updates Agent state""" |
| 187 | + self.original_num = msg.number |
| 188 | + logger.warning(f"Planner handled BurifyTool: {self.current_num}") |
| 189 | + |
| 190 | + return ForwardTool(agent="Burify") |
| 191 | + |
| 192 | + def tonify_tool(self, msg: TonifyTool) -> str: |
| 193 | + """Handler of TonifyTool: uses/updates Agent state""" |
| 194 | + self.original_num = msg.number |
| 195 | + logger.warning(f"Planner handled TonifyTool: {self.current_num}") |
| 196 | + |
| 197 | + return ForwardTool(agent="Tonify") |
| 198 | + |
| 199 | + def accept_tool(self, msg: AcceptTool) -> str: |
| 200 | + """Handler of AcceptTool: uses/updates Agent state""" |
| 201 | + curr_step_name = self.config.steps[self.current_step] |
| 202 | + n_steps = len(self.config.steps) |
| 203 | + self.current_num = msg.result |
| 204 | + if self.current_step == n_steps - 1: |
| 205 | + # last step -> done |
| 206 | + return AgentDoneTool(content=str(self.current_num)) |
| 207 | + |
| 208 | + self.current_step += 1 |
| 209 | + next_step_name = self.config.steps[self.current_step] |
| 210 | + return f""" |
| 211 | + You have ACCEPTED the result of the {curr_step_name} step. |
| 212 | + Your next step is to apply the {next_step_name} process |
| 213 | + to the result of the {curr_step_name} step, which is {self.current_num}. |
| 214 | + So use a TOOL to initiate the {next_step_name} process! |
| 215 | + """ |
| 216 | + |
| 217 | + |
| 218 | +class BurifyAgentConfig(lr.ChatAgentConfig): |
| 219 | + name: str = "Burify" |
| 220 | + handle_llm_no_tool: str = f"You FORGOT to use the TOOL `{BurifySubmitTool.name()}`!" |
| 221 | + system_message: str = f""" |
| 222 | + You will receive an integer from your supervisor, to apply |
| 223 | + a process Burify to it, which you are not quite sure how to do, |
| 224 | + but you only know that it involves INCREMENTING the number by 1 a few times |
| 225 | + (but you don't know how many times). |
| 226 | + When you first receive a number to Burify, simply return the number + 1. |
| 227 | + If this is NOT sufficient, you will be asked to try again, and |
| 228 | + you must CONTINUE to return your last number, INCREMENTED by 1. |
| 229 | + To send your result, you MUST use the TOOL `{BurifySubmitTool.name()}`. |
| 230 | + """ |
| 231 | + |
| 232 | + |
| 233 | +class TonifyAgentConfig(lr.ChatAgentConfig): |
| 234 | + name: str = "Tonify" |
| 235 | + handle_llm_no_tool: str = f"You FORGOT to use the TOOL `{TonifySubmitTool.name()}`!" |
| 236 | + system_message: str = """ |
| 237 | + You will receive an integer from your supervisor, to apply |
| 238 | + a process Tonify to it, which you are not quite sure how to do, |
| 239 | + but you only know that it involves MULTIPLYING the number by 2 a few times |
| 240 | + (and you don't know how many times). |
| 241 | + When you first receive a number to Tonify, simply return the number * 2. |
| 242 | + If this is NOT sufficient, you will be asked to try again, and |
| 243 | + you must CONTINUE to return your last number, MULTIPLIED by 2. |
| 244 | + To send your result, you MUST use the TOOL `{TonifySubmitTool.name()}`. |
| 245 | + """ |
| 246 | + |
| 247 | + |
| 248 | +def main(model: str = ""): |
| 249 | + planner = PlannerAgent( |
| 250 | + PlannerConfig( |
| 251 | + llm=lm.OpenAIGPTConfig( |
| 252 | + chat_model=model or MODEL, |
| 253 | + ) |
| 254 | + ), |
| 255 | + ) |
| 256 | + |
| 257 | + planner.enable_message( |
| 258 | + [ |
| 259 | + BurifyRevisionTool, |
| 260 | + TonifyRevisionTool, |
| 261 | + ], |
| 262 | + use=True, # LLM allowed to generate |
| 263 | + handle=False, # agent cannot handle |
| 264 | + ) |
| 265 | + |
| 266 | + planner.enable_message( # can use and handle |
| 267 | + [ |
| 268 | + AcceptTool, |
| 269 | + BurifyCheckTool, |
| 270 | + TonifyCheckTool, |
| 271 | + BurifyTool, |
| 272 | + TonifyTool, |
| 273 | + ] |
| 274 | + ) |
| 275 | + |
| 276 | + burifier = lr.ChatAgent( |
| 277 | + BurifyAgentConfig( |
| 278 | + llm=lm.OpenAIGPTConfig( |
| 279 | + chat_model=model or MODEL, |
| 280 | + ) |
| 281 | + ) |
| 282 | + ) |
| 283 | + burifier.enable_message( |
| 284 | + [ |
| 285 | + BurifyTool, |
| 286 | + BurifyRevisionTool, |
| 287 | + ], |
| 288 | + use=False, # LLM cannot generate |
| 289 | + handle=True, # agent can handle |
| 290 | + ) |
| 291 | + burifier.enable_message(BurifySubmitTool) |
| 292 | + |
| 293 | + tonifier = lr.ChatAgent( |
| 294 | + TonifyAgentConfig( |
| 295 | + llm=lm.OpenAIGPTConfig( |
| 296 | + chat_model=model or MODEL, |
| 297 | + ) |
| 298 | + ) |
| 299 | + ) |
| 300 | + |
| 301 | + tonifier.enable_message( |
| 302 | + [ |
| 303 | + TonifyTool, |
| 304 | + TonifyRevisionTool, |
| 305 | + ], |
| 306 | + use=False, # LLM cannot generate |
| 307 | + handle=True, # agent can handle |
| 308 | + ) |
| 309 | + tonifier.enable_message(TonifySubmitTool) |
| 310 | + |
| 311 | + planner_task = lr.Task(planner, interactive=False) |
| 312 | + burifier_task = lr.Task(burifier, interactive=False) |
| 313 | + tonifier_task = lr.Task(tonifier, interactive=False) |
| 314 | + |
| 315 | + planner_task.add_sub_task( |
| 316 | + [ |
| 317 | + burifier_task, |
| 318 | + tonifier_task, |
| 319 | + ] |
| 320 | + ) |
| 321 | + |
| 322 | + # Buify(5) = 5+3 = 8; Tonify(8) = 8*4 = 32 |
| 323 | + result = planner_task.run("Sequentially all processes to this number: 5") |
| 324 | + assert "32" in result.content, f"Expected 32, got {result.content}" |
| 325 | + |
| 326 | + |
| 327 | +if __name__ == "__main__": |
| 328 | + Fire(main) |
0 commit comments