A single "turn" in the task conversation: The "allowed" responders in this turn (which can be either the 3 "entities", or one of the sub-tasks) are tried in sequence, until a _valid_ response is obtained; a _valid_ response is one that contributes to the task, either
(self, turns: int = -1)
| 1301 | return self.pending_message |
| 1302 | |
| 1303 | async def step_async(self, turns: int = -1) -> ChatDocument | None: |
| 1304 | """ |
| 1305 | A single "turn" in the task conversation: The "allowed" responders in this |
| 1306 | turn (which can be either the 3 "entities", or one of the sub-tasks) are |
| 1307 | tried in sequence, until a _valid_ response is obtained; a _valid_ |
| 1308 | response is one that contributes to the task, either by ending it, |
| 1309 | or producing a response to be further acted on. |
| 1310 | Update `self.pending_message` to the latest valid response (or NO_ANSWER |
| 1311 | if no valid response was obtained from any responder). |
| 1312 | |
| 1313 | Args: |
| 1314 | turns (int): number of turns to process. Typically used in testing |
| 1315 | where there is no human to "quit out" of current level, or in cases |
| 1316 | where we want to limit the number of turns of a delegated agent. |
| 1317 | |
| 1318 | Returns (ChatDocument|None): |
| 1319 | Updated `self.pending_message`. Currently the return value is not used |
| 1320 | by the `task.run()` method, but we return this as a convenience for |
| 1321 | other use-cases, e.g. where we want to run a task step by step in a |
| 1322 | different context. |
| 1323 | """ |
| 1324 | self.is_done = False |
| 1325 | parent = self.pending_message |
| 1326 | recipient = ( |
| 1327 | "" |
| 1328 | if self.pending_message is None |
| 1329 | else self.pending_message.metadata.recipient |
| 1330 | ) |
| 1331 | if not self._valid_recipient(recipient): |
| 1332 | logger.warning(f"Invalid recipient: {recipient}") |
| 1333 | error_doc = ChatDocument( |
| 1334 | content=f"Invalid recipient: {recipient}", |
| 1335 | metadata=ChatDocMetaData( |
| 1336 | sender=Entity.AGENT, |
| 1337 | sender_name=Entity.AGENT, |
| 1338 | ), |
| 1339 | ) |
| 1340 | self._process_valid_responder_result(Entity.AGENT, parent, error_doc) |
| 1341 | return error_doc |
| 1342 | |
| 1343 | responders: List[Responder] = self.non_human_responders_async.copy() |
| 1344 | |
| 1345 | if ( |
| 1346 | Entity.USER in self.responders |
| 1347 | and not self.human_tried |
| 1348 | and not self.agent.has_tool_message_attempt(self.pending_message) |
| 1349 | ): |
| 1350 | # Give human first chance if they haven't been tried in last step, |
| 1351 | # and the msg is not a tool-call attempt; |
| 1352 | # This ensures human gets a chance to respond, |
| 1353 | # other than to a LLM tool-call. |
| 1354 | # When there's a tool msg attempt we want the |
| 1355 | # Agent to be the next responder; this only makes a difference in an |
| 1356 | # interactive setting: LLM generates tool, then we don't want user to |
| 1357 | # have to respond, and instead let the agent_response handle the tool. |
| 1358 | responders.insert(0, Entity.USER) |
| 1359 | |
| 1360 | found_response = False |