diff --git a/rasa/cdu/command_generator/command_prompt_template.jinja2 b/rasa/cdu/command_generator/command_prompt_template.jinja2 index 4491e5b8ca9a..d4aefd05afe9 100644 --- a/rasa/cdu/command_generator/command_prompt_template.jinja2 +++ b/rasa/cdu/command_generator/command_prompt_template.jinja2 @@ -1,8 +1,8 @@ -Your task is to analyze the current conversation context and start new business processes that we call flows and to extract slots to advance active flows. +Your task is to analyze the current conversation context and generate a list of actions to start new business processes that we call flows, to extract slots, or respond to small talk and knowledge requests. These are the flows that can be started, with their description and slots: {% for flow in available_flows %} -flow: {{ flow.name }}: {{ flow.description }} +{{ flow.name }}: {{ flow.description }} {% for slot in flow.slots -%} slot: {{ slot.name }}{% if slot.description %} ({{ slot.description }}){% endif %} {% endfor %} @@ -35,23 +35,18 @@ The user just said """{{ user_message }}""". Based on this information generate a list of actions you want to take. Your job is to start flows and to fill slots where appropriate. Any logic of what happens afterwards is handled by the flow engine. These are your available actions: * Slot setting, described by "SetSlot(slot_name, slot_value)". An example would be "SetSlot(recipient, Freddy)" * Starting another flow, described by "StartFlow(flow_name)". An example would be "StartFlow(transfer_money)" -* Starting another flow, and already filling a slot for that flow. An example would be "StartFlow(transfer_money), SetSlot(recipient, Freddy)" -* Setting a slot for the current flow, before starting a new flow and setting a slot for it. An example would be "SetSlot(confirmation, True), StartFlow(transfer_money), SetSlot(recipient, Joe)" * Cancelling the current flow, described by "CancelFlow()" -* Asking the user to choose from two or more possible flows. An example would be ClarifyNextStep(transfer_money, list_transactions, check_balance) -* Taking no action because the user's message has nothing to do with the available flows. An example would be AllowInterruption(chitchat) if the user is making small talk, or AllowInterruption(information) if the user has a question unrelated to the available flows. -* Handing off to a human, in case the user seems frustrated or explicitly asks to speak to one. An example would be HumanHandoff() -* Taking no action because the user's message does not invite a further response. For example, if the user asks for more time, or simply makes a comment. +* Disambiguating which flow should be started when there are multiple likely candidates. An example would be Disambiguate(transfer_money, list_transactions, check_balance) +* Responding to a non-task-oriented user message, described by "ChitChat()". +* Responding to a user message that requires additional knowledge from a knowledge database, described by "KnowledgeAnswer()" +* Handing off to a human, in case the user seems frustrated or explicitly asks to speak to one, described by "HumanHandoff()". -Write out the actions you want to take for the last user message, one per line, in the order they should take place. +=== +Write out the actions you want to take, one per line, in the order they should take place. Do not fill slots with abstract values or placeholders. Only use information provided by the user. -Only start a flow if it's completely clear what the user wants. Imagine you were a person reading this message. If it's not 100% clear, clarify the next step. -Don't be overly confident. Take a conservative approach and clarify before proceeding. -If the user asks for two things which seem contradictory, clarify before starting a flow. -If a user does multiple things in one message, you can output multiple actions. An example would be if the user fills a slot but also makes some smalltalk: "SetSlot(cuisine, chinese), AllowInterruption(chitchat)" -Acknowledge everything a user does in a message. Strictly adhere to the provided action types listed above. Focus on the last message and take it one step at a time. Use the previous conversation steps only to aid understanding. -The action list: + +Your action list: diff --git a/rasa/cdu/command_generator/llm_command_generator.py b/rasa/cdu/command_generator/llm_command_generator.py index dc3b780ae67a..cdfd5d6af63a 100644 --- a/rasa/cdu/command_generator/llm_command_generator.py +++ b/rasa/cdu/command_generator/llm_command_generator.py @@ -12,6 +12,9 @@ SetSlotCommand, CancelFlowCommand, StartFlowCommand, + HumanHandoffCommand, + ListenCommand, + CorrectSlotCommand, ) from rasa.core.policies.flow_policy import FlowStack @@ -146,17 +149,19 @@ def predict_commands( ) flow_prompt = self.render_template(message, tracker, flows_without_patterns) structlogger.info( - "llm_command_generator.process.prompt_rendered", prompt=flow_prompt + "llm_command_generator.predict_commands.prompt_rendered", prompt=flow_prompt ) action_list = self._generate_action_list_using_llm(flow_prompt) structlogger.info( - "llm_command_generator.process.actions_generated", action_list=action_list + "llm_command_generator.predict_commands.actions_generated", + action_list=action_list, ) - commands = self.parse_commands(action_list, tracker, flows_without_patterns) + commands = self.parse_commands(action_list) structlogger.info( - "llm_command_generator.process.finished", + "llm_command_generator.predict_commands.finished", commands=commands, ) + return commands @staticmethod @@ -169,9 +174,7 @@ def is_hallucinated_value(value: str) -> bool: } @classmethod - def parse_commands( - cls, actions: Optional[str], tracker: DialogueStateTracker, flows: FlowsList - ) -> List[Command]: + def parse_commands(cls, actions: Optional[str]) -> List[Command]: """Parse the actions returned by the llm into intent and entities.""" if not actions: return [ErrorCommand()] @@ -182,12 +185,17 @@ def parse_commands( r"""SetSlot\(([a-zA-Z_][a-zA-Z0-9_-]*?), ?\"?([^)]*?)\"?\)""" ) start_flow_re = re.compile(r"StartFlow\(([a-zA-Z_][a-zA-Z0-9_-]*?)\)") - cancel_flow_re = re.compile(r"CancelFlow") - interruption_flow_re = re.compile(r"AllowInterruption") + cancel_flow_re = re.compile(r"CancelFlow\(\)") + chitchat_re = re.compile(r"ChitChat\(\)") + knowledge_re = re.compile(r"KnowledgeAnswer\(\)") + humand_handoff_re = re.compile(r"HumandHandoff\(\)") + listen_re = re.compile(r"Listen\(\)") + for action in actions.strip().splitlines(): if m := slot_set_re.search(action): slot_name = m.group(1).strip() slot_value = m.group(2).strip() + # error case where the llm tries to start a flow using a slot set if slot_name == "flow_name": commands.append(StartFlowCommand(flow=slot_value)) elif cls.is_hallucinated_value(slot_value): @@ -198,8 +206,12 @@ def parse_commands( commands.append(StartFlowCommand(flow=m.group(1).strip())) elif cancel_flow_re.search(action): commands.append(CancelFlowCommand()) - elif interruption_flow_re.search(action): + elif chitchat_re.search(action) or knowledge_re.search(action): commands.append(HandleInterruptionCommand()) + elif humand_handoff_re.search(action): + commands.append(HumanHandoffCommand()) + # elif listen_re.search(action): + # commands.append(ListenCommand()) return commands