From 6d47adafb3324d6489db8b3ba84260783c2d0b9e Mon Sep 17 00:00:00 2001 From: akhisud3195 Date: Fri, 2 May 2025 17:10:37 +0530 Subject: [PATCH 01/32] Remove old swarm code and unused imports --- apps/rowboat_agents/src/app/main.py | 4 +- apps/rowboat_agents/src/graph/core.py | 194 +------------------------- 2 files changed, 4 insertions(+), 194 deletions(-) diff --git a/apps/rowboat_agents/src/app/main.py b/apps/rowboat_agents/src/app/main.py index 04bb7301..143a5084 100644 --- a/apps/rowboat_agents/src/app/main.py +++ b/apps/rowboat_agents/src/app/main.py @@ -8,12 +8,10 @@ from hypercorn.config import Config from hypercorn.asyncio import serve import asyncio -from src.graph.core import run_turn, run_turn_streamed +from src.graph.core import run_turn_streamed from src.graph.tools import RAG_TOOL, CLOSE_CHAT_TOOL from src.utils.common import common_logger, read_json_from_file -from pprint import pprint - logger = common_logger app = Quart(__name__) config = read_json_from_file("./configs/default_config.json") diff --git a/apps/rowboat_agents/src/graph/core.py b/apps/rowboat_agents/src/graph/core.py index f16196af..c9a08538 100644 --- a/apps/rowboat_agents/src/graph/core.py +++ b/apps/rowboat_agents/src/graph/core.py @@ -9,11 +9,9 @@ from .helpers.access import ( get_external_tools, get_prompt_by_type ) -from .helpers.state import ( - construct_state_from_response -) -from .helpers.control import get_latest_assistant_msg, get_latest_non_assistant_messages, get_last_agent_name -from .swarm_wrapper import run as swarm_run, run_streamed as swarm_run_streamed, create_response, get_agents + +from .helpers.control import get_last_agent_name +from .swarm_wrapper import run_streamed as swarm_run_streamed, get_agents from src.utils.common import common_logger as logger from .types import PromptType @@ -58,192 +56,6 @@ def set_sys_message(messages): return messages -def clean_up_history(agent_data): - """ - Ensures each agent's history is sorted using order_messages. - """ - for data in agent_data: - data["history"] = order_messages(data["history"]) - return agent_data - -def create_final_response(response, turn_messages, tokens_used, all_agents): - """ - Constructs the final response data (messages, tokens_used, updated state) that a caller would need. - """ - # Ensure response has a messages attribute - if not hasattr(response, 'messages'): - response.messages = [] - - # Assign the appropriate messages to the response - response.messages = turn_messages - - # Ensure tokens_used is a valid dictionary - if not isinstance(tokens_used, dict): - tokens_used = {"total": 0, "prompt": 0, "completion": 0} # Default values if not a dictionary - - # Ensure response has a tokens_used attribute that's a dictionary - if not hasattr(response, 'tokens_used') or not isinstance(response.tokens_used, dict): - response.tokens_used = {} - - response.tokens_used = tokens_used - - # Ensure response has an agent attribute for state construction - if not hasattr(response, 'agent'): - if all_agents and len(all_agents) > 0: - response.agent = all_agents[0] # Set default agent if missing - - new_state = construct_state_from_response(response, all_agents) - return response.messages, response.tokens_used, new_state - - -async def run_turn( - messages, start_agent_name, agent_configs, tool_configs, start_turn_with_start_agent, state={}, additional_tool_configs=[], complete_request={} -): - """ - Coordinates a single 'turn' of conversation or processing among agents. - Includes validation, agent setup, optional greeting logic, error handling, and post-processing steps. - """ - logger.info("Running stateless turn") - print("Running stateless turn") - - # Sort messages by the specified ordering - #messages = order_messages(messages) - - # Merge any additional tool configs - tool_configs = tool_configs + additional_tool_configs - - # Determine if this is a greeting turn - greeting_turn = not any(msg.get("role") != "system" for msg in messages) - turn_messages = [] - # Initialize tokens_used as a dictionary - tokens_used = {"total": 0, "prompt": 0, "completion": 0} - - agent_data = state.get("agent_data", []) - - # If not a greeting turn, localize the last user or system messages - if not greeting_turn: - latest_assistant_msg = get_latest_assistant_msg(messages) - latest_non_assistant_msgs = get_latest_non_assistant_messages(messages) - msg_type = latest_non_assistant_msgs[-1]["role"] - - # Determine the last agent from state/config - last_agent_name = get_last_agent_name( - state=state, - agent_configs=agent_configs, - start_agent_name=start_agent_name, - msg_type=msg_type, - latest_assistant_msg=latest_assistant_msg, - start_turn_with_start_agent=start_turn_with_start_agent - ) - else: - # For a greeting turn, we assume the last agent is the start_agent_name - last_agent_name = start_agent_name - - state["agent_data"] = agent_data - - # Initialize all agents - logger.info("Initializing agents") - print("Initializing agents") - new_agents = get_agents( - agent_configs=agent_configs, - tool_configs=tool_configs, - complete_request=complete_request - ) - # Prepare escalation agent - last_new_agent = get_agent_by_name(last_agent_name, new_agents) - - # Gather external tools for Swarm - external_tools = get_external_tools(tool_configs) - logger.info(f"Found {len(external_tools)} external tools") - print(f"Found {len(external_tools)} external tools") - - # If no validation error yet, proceed with the main run - - logger.info("Running swarm run") - print("Running swarm run") - - response = await swarm_run( - agent=last_new_agent, - messages=messages, - external_tools=external_tools, - tokens_used=tokens_used - ) - - logger.info("Swarm run completed") - print("Swarm run completed") - - # Initialize response.messages if it doesn't exist - if not hasattr(response, 'messages'): - response.messages = [] - - # Convert the ResponseOutputMessage to a standard message format - if hasattr(response, 'new_items') and response.new_items and hasattr(response.new_items[-1], 'raw_item'): - raw_item = response.new_items[-1].raw_item - # Extract text content from ResponseOutputText objects - content = "" - if hasattr(raw_item, 'content') and raw_item.content: - for content_item in raw_item.content: - if hasattr(content_item, 'text'): - content += content_item.text - - # Create a standard message dictionary - standard_message = { - "role": raw_item.role if hasattr(raw_item, 'role') else "assistant", - "content": content, - "sender": last_new_agent.name, - "created_at": None, - "response_type": "internal" - } - - # Add the converted message to response messages - response.messages.append(standard_message) - - logger.info("Converted message added to response messages") - print("Converted message added to response messages") - - # Use a dictionary for tokens_used - tokens_used = {"total": 0, "prompt": 0, "completion": 0} # Default values as placeholders - - # Ensure turn_messages can be extended with response.messages - if hasattr(response, 'messages') and isinstance(response.messages, list): - turn_messages.extend(response.messages) - - logger.info(f"Completed run of agent: {last_new_agent.name}") - print(f"Completed run of agent: {last_new_agent.name}") - - # Otherwise, duplicate the last response as external - logger.info("No post-processing agent found. Duplicating last response and setting to external.") - print("No post-processing agent found. Duplicating last response and setting to external.") - if turn_messages: - duplicate_msg = deepcopy(turn_messages[-1]) - duplicate_msg["response_type"] = "external" - duplicate_msg["sender"] += " >> External" - - # Ensure tokens_used remains a proper dictionary - if not isinstance(tokens_used, dict): - tokens_used = {"total": 0, "prompt": 0, "completion": 0} # Default values if not a dictionary - - response = create_response( - messages=[duplicate_msg], - tokens_used=tokens_used, - agent=last_new_agent, - error_msg='' - ) - - # Ensure response has messages attribute - if hasattr(response, 'messages') and isinstance(response.messages, list): - turn_messages.extend(response.messages) - - # Finalize the response - logger.info("Finalizing response") - print("Finalizing response") - return create_final_response( - response=response, - turn_messages=turn_messages, - tokens_used=tokens_used, - all_agents=new_agents - ) - async def run_turn_streamed( messages, start_agent_name, From 817f7893a2cd4641468ffa737323d40bf68927c6 Mon Sep 17 00:00:00 2001 From: akhisud3195 Date: Fri, 2 May 2025 17:26:05 +0530 Subject: [PATCH 02/32] Remove legacy swarm_wrapper code --- .../rowboat_agents/src/graph/swarm_wrapper.py | 74 ------------------- 1 file changed, 74 deletions(-) diff --git a/apps/rowboat_agents/src/graph/swarm_wrapper.py b/apps/rowboat_agents/src/graph/swarm_wrapper.py index e3c9859e..d632a395 100644 --- a/apps/rowboat_agents/src/graph/swarm_wrapper.py +++ b/apps/rowboat_agents/src/graph/swarm_wrapper.py @@ -281,80 +281,6 @@ def get_agents(agent_configs, tool_configs, complete_request): print("="*100) return new_agents - -def create_response(messages=None, tokens_used=None, agent=None, error_msg=''): - """ - Create a Response object with the given parameters. - - Args: - messages: List of messages - tokens_used: Dictionary tracking token usage - agent: The agent that generated the response - error_msg: Error message if any - - Returns: - Response object - """ - if messages is None: - messages = [] - if tokens_used is None: - tokens_used = {} - - return NewResponse( - messages=messages, - agent=agent, - tokens_used=tokens_used, - error_msg=error_msg - ) - - -async def run( - agent, - messages, - external_tools=None, - tokens_used=None -): - """ - Wrapper function for initializing and running the Swarm client. - """ - logger.info(f"Initializing Swarm client for agent: {agent.name}") - print(f"Initializing Swarm client for agent: {agent.name}") - - # Initialize default parameters - if external_tools is None: - external_tools = [] - if tokens_used is None: - tokens_used = {} - - # Format messages to ensure they're compatible with the OpenAI API - formatted_messages = [] - for msg in messages: - if isinstance(msg, dict) and "content" in msg: - formatted_msg = { - "role": msg.get("role", "user"), - "content": msg["content"] - } - formatted_messages.append(formatted_msg) - else: - formatted_messages.append({ - "role": "user", - "content": str(msg) - }) - - logger.info("Beginning Swarm run") - print("Beginning Swarm run") - - try: - response = await Runner.run(agent, formatted_messages) - except Exception as e: - logger.error(f"Error during run: {str(e)}") - print(f"Error during run: {str(e)}") - raise - - logger.info(f"Completed Swarm run for agent: {agent.name}") - print(f"Completed Swarm run for agent: {agent.name}") - return response - async def run_streamed( agent, messages, From 1246ea47b9ab38e77b9a6632d941d10c5678511f Mon Sep 17 00:00:00 2001 From: akhisud3195 Date: Fri, 2 May 2025 18:45:20 +0530 Subject: [PATCH 03/32] Refactor web_search tool handling --- apps/rowboat_agents/src/graph/core.py | 534 ++++++++++-------- .../src/graph/helpers/library_tools.py | 171 ++++++ apps/rowboat_agents/src/graph/types.py | 5 +- 3 files changed, 488 insertions(+), 222 deletions(-) create mode 100644 apps/rowboat_agents/src/graph/helpers/library_tools.py diff --git a/apps/rowboat_agents/src/graph/core.py b/apps/rowboat_agents/src/graph/core.py index c9a08538..b9f897bc 100644 --- a/apps/rowboat_agents/src/graph/core.py +++ b/apps/rowboat_agents/src/graph/core.py @@ -9,12 +9,12 @@ from .helpers.access import ( get_external_tools, get_prompt_by_type ) - +from .helpers.library_tools import handle_web_search_event from .helpers.control import get_last_agent_name from .swarm_wrapper import run_streamed as swarm_run_streamed, get_agents from src.utils.common import common_logger as logger -from .types import PromptType +from .types import PromptType, VisibilityType, ControlType # Create a dedicated logger for swarm wrapper logger.setLevel(logging.INFO) @@ -56,6 +56,266 @@ def set_sys_message(messages): return messages +def handle_web_search_event(event, current_agent): + """ + Helper function to handle all web search related events. + Returns a list of messages to yield. + """ + messages = [] + + # Handle raw response web search + if event.type == "raw_response_event": + if hasattr(event, 'data') and hasattr(event.data, 'raw_item'): + raw_item = event.data.raw_item + if (hasattr(raw_item, 'type') and raw_item.type == 'web_search_call') or ( + isinstance(raw_item, dict) and raw_item.get('type') == 'web_search_call' + ): + call_id = None + if hasattr(raw_item, 'id'): + call_id = raw_item.id + elif isinstance(raw_item, dict) and 'id' in raw_item: + call_id = raw_item['id'] + else: + call_id = str(uuid.uuid4()) + + status = 'unknown' + if hasattr(raw_item, 'status'): + status = raw_item.status + elif isinstance(raw_item, dict) and 'status' in raw_item: + status = raw_item['status'] + + messages.append({ + 'content': None, + 'role': 'assistant', + 'sender': current_agent.name if current_agent else None, + 'tool_calls': [{ + 'function': { + 'name': 'web_search', + 'arguments': json.dumps({ + 'search_id': call_id, + 'status': status + }) + }, + 'id': call_id, + 'type': 'function' + }], + 'tool_call_id': None, + 'tool_name': None, + 'response_type': 'internal' + }) + + # Handle run item web search events + elif event.type == "run_item_stream_event": + if event.item.type == "tool_call_item": + if hasattr(event.item.raw_item, 'type') and event.item.raw_item.type == 'web_search_call': + call_id = event.item.raw_item.id if hasattr(event.item.raw_item, 'id') else str(uuid.uuid4()) + messages.append({ + 'content': None, + 'role': 'assistant', + 'sender': current_agent.name if current_agent else None, + 'tool_calls': [{ + 'function': { + 'name': 'web_search', + 'arguments': json.dumps({ + 'search_id': call_id + }) + }, + 'id': call_id, + 'type': 'function' + }], + 'tool_call_id': None, + 'tool_name': None, + 'response_type': 'internal' + }) + messages.append({ + 'content': "Web search done", + 'role': 'tool', + 'sender': None, + 'tool_calls': None, + 'tool_call_id': call_id, + 'tool_name': 'web_search', + 'response_type': 'internal' + }) + + elif event.item.type == "tool_call_output_item": + if isinstance(event.item.raw_item, dict) and event.item.raw_item.get('type') == 'web_search_results': + call_id = event.item.raw_item.get('search_id', event.item.raw_item.get('id', str(uuid.uuid4()))) + messages.append({ + 'content': str(event.item.output), + 'role': 'tool', + 'sender': None, + 'tool_calls': None, + 'tool_call_id': call_id, + 'tool_name': 'web_search', + 'response_type': 'internal' + }) + + elif event.item.type == "web_search_call_item" or ( + hasattr(event.item, 'raw_item') and + hasattr(event.item.raw_item, 'type') and + event.item.raw_item.type == 'web_search_call' + ): + call_id = None + if hasattr(event.item.raw_item, 'id'): + call_id = event.item.raw_item.id + + messages.append({ + 'content': None, + 'role': 'assistant', + 'sender': current_agent.name if current_agent else None, + 'tool_calls': [{ + 'function': { + 'name': 'web_search', + 'arguments': json.dumps({ + 'search_id': call_id + }) + }, + 'id': call_id or str(uuid.uuid4()), + 'type': 'function' + }], + 'tool_call_id': None, + 'tool_name': None, + 'response_type': 'internal' + }) + + elif event.item.type == "web_search_results_item" or ( + hasattr(event.item, 'raw_item') and ( + (hasattr(event.item.raw_item, 'type') and event.item.raw_item.type == 'web_search_results') or + (isinstance(event.item.raw_item, dict) and event.item.raw_item.get('type') == 'web_search_results') + ) + ): + raw_item = event.item.raw_item + call_id = None + + if hasattr(raw_item, 'search_id'): + call_id = raw_item.search_id + elif isinstance(raw_item, dict) and 'search_id' in raw_item: + call_id = raw_item['search_id'] + elif hasattr(raw_item, 'id'): + call_id = raw_item.id + elif isinstance(raw_item, dict) and 'id' in raw_item: + call_id = raw_item['id'] + else: + call_id = str(uuid.uuid4()) + + results = {} + if hasattr(event.item, 'output'): + results = event.item.output + elif hasattr(raw_item, 'results'): + results = raw_item.results + elif isinstance(raw_item, dict) and 'results' in raw_item: + results = raw_item['results'] + + results_str = "" + try: + results_str = json.dumps(results) if results else "" + except Exception as e: + print(f"Error serializing results: {str(e)}") + results_str = str(results) + + messages.append({ + 'content': results_str, + 'role': 'tool', + 'sender': None, + 'tool_calls': None, + 'tool_call_id': call_id, + 'tool_name': 'web_search', + 'response_type': 'internal' + }) + + return messages + +""" Example workflow config +{ + "agents": [ + { + "name": "Credit Card Hub", + "type": "conversation", + "description": "Hub agent to route credit card related queries to the appropriate specialized agent.", + "instructions": "## šŸ§‘ā€šŸ’¼ Role:\nYou are the hub for all credit card related queries. Your job is to understand the user's intent and route their query to the correct specialized agent.\n\n---\n## āš™ļø Steps to Follow:\n1. Greet the user and ask how you can help with their credit card needs.\n2. If the user asks about card recommendations, call [@agent:Card Recommendation](#mention).\n3. If the user asks about card benefits or rewards, call [@agent:Card Benefits and Rewards](#mention).\n4. If the user asks about the application process, call [@agent:Card Application Process](#mention).\n5. If the user asks for general credit card advice, call [@agent:General Credit Card Advice](#mention).\n6. If the query is out of scope, politely inform the user.\n\n---\n## šŸŽÆ Scope:\nāœ… In Scope:\n- Routing credit card related queries to the correct agent.\n\nāŒ Out of Scope:\n- Answering credit card questions directly.\n- Handling non-credit card queries.\n\n---\n## šŸ“‹ Guidelines:\nāœ”ļø Dos:\n- Be professional and friendly.\n- Route queries efficiently.\n\n🚫 Don'ts:\n- Do not answer questions directly.\n- Do not provide user-facing text such as 'I will connect you now...' when calling another agent.", + "model": "claude-3-7-sonnet-latest", + "toggleAble": true, + "ragReturnType": "chunks", + "ragK": 3, + "controlType": "retain", + "examples": "- **User** : Can you recommend a credit card for travel?\n - **Agent actions**: Call [@agent:Card Recommendation](#mention)\n\n- **User** : What are the benefits of the Platinum card?\n - **Agent actions**: Call [@agent:Card Benefits and Rewards](#mention)\n\n- **User** : How do I apply for a credit card?\n - **Agent actions**: Call [@agent:Card Application Process](#mention)\n\n- **User** : Should I get a credit card or a debit card?\n - **Agent actions**: Call [@agent:General Credit Card Advice](#mention)\n\n- **User** : Hi!\n - **Agent response**: Hello! How can I help you with your credit card needs today?" + }, + { + "name": "Card Recommendation", + "type": "conversation", + "description": "Provides personalized credit card recommendations based on user needs.", + "disabled": false, + "instructions": "## šŸ§‘ā€šŸ’¼ Role:\nYou help users find the best credit card for their needs.\n\n---\n## āš™ļø Steps to Follow:\n1. Ask the user about their preferences (e.g., travel, cashback, low interest, rewards).\n2. Use [@tool:web_search](#mention) to find suitable credit card options.\n3. Present 2-3 card recommendations with a brief explanation for each.\n4. If the user asks about benefits or application process, call the relevant agent.\n5. If the query is out of scope, call [@agent:Credit Card Hub](#mention).\n\n---\n## šŸŽÆ Scope:\nāœ… In Scope:\n- Recommending credit cards based on user needs.\n\nāŒ Out of Scope:\n- Detailed card benefits (refer to Card Benefits and Rewards agent).\n- Application process (refer to Card Application Process agent).\n\n---\n## šŸ“‹ Guidelines:\nāœ”ļø Dos:\n- Be professional and friendly.\n- Tailor recommendations to user preferences.\n\n🚫 Don'ts:\n- Recommend cards without understanding user needs.", + "model": "claude-3-7-sonnet-latest", + "locked": false, + "toggleAble": true, + "ragReturnType": "chunks", + "ragK": 3, + "controlType": "retain", + "examples": "- **User** : I want a card with good travel rewards.\n - **Agent response**: Great! Are you looking for international travel benefits or domestic? Any airline preferences?\n\n- **User** : I prefer cashback cards.\n - **Agent response**: Understood. Do you spend more on groceries, fuel, or online shopping?\n\n- **User** : I want a card with no annual fee.\n - **Agent response**: Thanks for sharing. I'll look up the best no-annual-fee cards for you.\n - **Agent actions**: Call [@tool:web_search](#mention)\n\n- **User** : What are the benefits of the Platinum card?\n - **Agent actions**: Call [@agent:Card Benefits and Rewards](#mention)\n\n- **User** : How do I apply for a card?\n - **Agent actions**: Call [@agent:Card Application Process](#mention)" + }, + { + "name": "Card Benefits and Rewards", + "type": "conversation", + "description": "Provides detailed information about credit card benefits and rewards.", + "disabled": false, + "instructions": "## šŸ§‘ā€šŸ’¼ Role:\nYou answer questions about credit card benefits and rewards.\n\n---\n## āš™ļø Steps to Follow:\n1. Ask the user which card or type of benefit they are interested in.\n2. Use [@tool:web_search](#mention) to find up-to-date information.\n3. Present the benefits and rewards in a clear, concise manner.\n4. If the user asks about recommendations or application process, call the relevant agent.\n5. If the query is out of scope, call [@agent:Credit Card Hub](#mention).\n\n---\n## šŸŽÆ Scope:\nāœ… In Scope:\n- Explaining card benefits and rewards.\n\nāŒ Out of Scope:\n- Recommending cards (refer to Card Recommendation agent).\n- Application process (refer to Card Application Process agent).\n\n---\n## šŸ“‹ Guidelines:\nāœ”ļø Dos:\n- Be accurate and clear.\n- Use up-to-date information.\n\n🚫 Don'ts:\n- Speculate about benefits without verification.", + "model": "claude-3-7-sonnet-latest", + "locked": false, + "toggleAble": true, + "ragReturnType": "chunks", + "ragK": 3, + "controlType": "retain", + "examples": "- **User** : What are the benefits of the Gold card?\n - **Agent response**: Let me check the latest benefits for the Gold card.\n - **Agent actions**: Call [@tool:web_search](#mention)\n\n- **User** : Does this card offer airport lounge access?\n - **Agent response**: I'll find out if this card includes airport lounge access.\n - **Agent actions**: Call [@tool:web_search](#mention)\n\n- **User** : Which card has the best rewards for shopping?\n - **Agent actions**: Call [@agent:Card Recommendation](#mention)\n\n- **User** : How do I apply for the Platinum card?\n - **Agent actions**: Call [@agent:Card Application Process](#mention)\n\n- **User** : Can you recommend a card for fuel rewards?\n - **Agent actions**: Call [@agent:Card Recommendation](#mention)" + }, + { + "name": "Card Application Process", + "type": "conversation", + "description": "Explains the steps and requirements for applying for a credit card.", + "disabled": false, + "instructions": "## šŸ§‘ā€šŸ’¼ Role:\nYou guide users through the credit card application process.\n\n---\n## āš™ļø Steps to Follow:\n1. Ask the user which card they want to apply for.\n2. Use [@tool:web_search](#mention) to find the latest application steps and requirements.\n3. Explain the process clearly, including eligibility, documents, and timelines.\n4. If the user asks about recommendations or benefits, call the relevant agent.\n5. If the query is out of scope, call [@agent:Credit Card Hub](#mention).\n\n---\n## šŸŽÆ Scope:\nāœ… In Scope:\n- Explaining how to apply for a credit card.\n\nāŒ Out of Scope:\n- Recommending cards (refer to Card Recommendation agent).\n- Explaining card benefits (refer to Card Benefits and Rewards agent).\n\n---\n## šŸ“‹ Guidelines:\nāœ”ļø Dos:\n- Be clear and step-by-step.\n- Mention required documents and eligibility.\n\n🚫 Don'ts:\n- Give outdated or unverified information.", + "model": "claude-3-7-sonnet-latest", + "locked": false, + "toggleAble": true, + "ragReturnType": "chunks", + "ragK": 3, + "controlType": "retain", + "examples": "- **User** : How do I apply for a credit card?\n - **Agent response**: Which card are you interested in applying for?\n\n- **User** : I want to apply for the Gold card.\n - **Agent response**: Let me check the application process for the Gold card.\n - **Agent actions**: Call [@tool:web_search](#mention)\n\n- **User** : What documents do I need to apply?\n - **Agent response**: I'll find the list of required documents for you.\n - **Agent actions**: Call [@tool:web_search](#mention)\n\n- **User** : Can you recommend a card for students?\n - **Agent actions**: Call [@agent:Card Recommendation](#mention)\n\n- **User** : What are the benefits of the Platinum card?\n - **Agent actions**: Call [@agent:Card Benefits and Rewards](#mention)" + }, + { + "name": "General Credit Card Advice", + "type": "conversation", + "description": "Provides general advice and best practices for using credit cards responsibly.", + "disabled": false, + "instructions": "## šŸ§‘ā€šŸ’¼ Role:\nYou offer general advice and best practices for credit card usage.\n\n---\n## āš™ļø Steps to Follow:\n1. Ask the user about their specific concern or question.\n2. Use [@tool:web_search](#mention) if needed for up-to-date advice.\n3. Provide clear, practical advice on credit card usage, safety, and financial health.\n4. If the user asks about recommendations, benefits, or application process, call the relevant agent.\n5. If the query is out of scope, call [@agent:Credit Card Hub](#mention).\n\n---\n## šŸŽÆ Scope:\nāœ… In Scope:\n- General credit card advice and best practices.\n\nāŒ Out of Scope:\n- Recommending specific cards (refer to Card Recommendation agent).\n- Explaining card benefits (refer to Card Benefits and Rewards agent).\n- Application process (refer to Card Application Process agent).\n\n---\n## šŸ“‹ Guidelines:\nāœ”ļø Dos:\n- Be professional and friendly.\n- Give practical, actionable advice.\n\n🚫 Don'ts:\n- Give financial advice outside of credit cards.", + "model": "claude-3-7-sonnet-latest", + "locked": false, + "toggleAble": true, + "ragReturnType": "chunks", + "ragK": 3, + "controlType": "retain", + "examples": "- **User** : Should I get a credit card or a debit card?\n - **Agent response**: I can explain the pros and cons of both options. Are you looking for rewards, building credit, or something else?\n\n- **User** : How can I avoid credit card debt?\n - **Agent response**: Great question! Some best practices include paying your balance in full each month, tracking your spending, and setting up payment reminders.\n\n- **User** : Is it safe to use my credit card online?\n - **Agent response**: Yes, but always use secure websites and avoid sharing your card details on public Wi-Fi. Would you like more tips on online safety?\n\n- **User** : Can you recommend a card for students?\n - **Agent actions**: Call [@agent:Card Recommendation](#mention)\n\n- **User** : What are the benefits of the Platinum card?\n - **Agent actions**: Call [@agent:Card Benefits and Rewards](#mention)" + } + ], + "prompts": [], + "tools": [ + { + "name": "web_search", + "description": "Fetch information from the web based on chat context", + "parameters": { + "type": "object", + "properties": {} + }, + "isLibrary": true + } + ], + "startAgent": "Credit Card Hub", + "createdAt": "2025-05-02T12:02:06.172Z", + "lastUpdatedAt": "2025-05-02T12:02:06.172Z", + "name": "Version 1" +} +""" + async def run_turn_streamed( messages, start_agent_name, @@ -136,52 +396,11 @@ async def run_turn_streamed( print(f"Found usage information. Updated cumulative tokens: {tokens_used}") print('-'*50) - # Handle ResponseFunctionWebSearch specifically - if hasattr(event, 'data') and hasattr(event.data, 'raw_item'): - raw_item = event.data.raw_item - - # Check if it's a web search call - if (hasattr(raw_item, 'type') and raw_item.type == 'web_search_call') or ( - isinstance(raw_item, dict) and raw_item.get('type') == 'web_search_call' - ): - # Get call_id safely, regardless of structure - call_id = None - if hasattr(raw_item, 'id'): - call_id = raw_item.id - elif isinstance(raw_item, dict) and 'id' in raw_item: - call_id = raw_item['id'] - else: - call_id = str(uuid.uuid4()) - - # Get status safely - status = 'unknown' - if hasattr(raw_item, 'status'): - status = raw_item.status - elif isinstance(raw_item, dict) and 'status' in raw_item: - status = raw_item['status'] - - # Emit a tool call for web search - message = { - 'content': None, - 'role': 'assistant', - 'sender': current_agent.name if current_agent else None, - 'tool_calls': [{ - 'function': { - 'name': 'web_search', - 'arguments': json.dumps({ - 'search_id': call_id, - 'status': status - }) - }, - 'id': call_id, - 'type': 'function' - }], - 'tool_call_id': None, - 'tool_name': None, - 'response_type': 'internal' - } - print("Yielding web search raw response message: ", message) - yield ('message', message) + # Check for web search events + web_search_messages = handle_web_search_event(event, current_agent) + for message in web_search_messages: + print("Yielding web search message: ", message) + yield ('message', message) continue @@ -234,102 +453,58 @@ async def run_turn_streamed( # Handle run items (tools, messages, etc) elif event.type == "run_item_stream_event": current_agent = event.item.agent - if event.item.type == "tool_call_item": - # Check if it's a ResponseFunctionWebSearch object - if hasattr(event.item.raw_item, 'type') and event.item.raw_item.type == 'web_search_call': - call_id = event.item.raw_item.id if hasattr(event.item.raw_item, 'id') else str(uuid.uuid4()) - message = { - 'content': None, - 'role': 'assistant', - 'sender': current_agent.name if current_agent else None, - 'tool_calls': [{ - 'function': { - 'name': 'web_search', - 'arguments': json.dumps({ - 'search_id': call_id - }) - }, - 'id': call_id, - 'type': 'function' - }], - 'tool_call_id': None, - 'tool_name': None, - 'response_type': 'internal' - } - print("Yielding message: ", message) - yield ('message', message) - result_message = { - 'content': "Web search done", + # Check for web search events first + web_search_messages = handle_web_search_event(event, current_agent) + if web_search_messages: + for message in web_search_messages: + print("Yielding web search message: ", message) + yield ('message', message) + continue + + if event.item.type == "tool_call_item": + # Handle normal tool calls + message = { + 'content': None, + 'role': 'assistant', + 'sender': current_agent.name if current_agent else None, + 'tool_calls': [{ + 'function': { + 'name': event.item.raw_item.name, + 'arguments': event.item.raw_item.arguments + }, + 'id': event.item.raw_item.call_id, + 'type': 'function' + }], + 'tool_call_id': None, + 'tool_name': None, + 'response_type': 'internal' + } + print("Yielding message: ", message) + yield ('message', message) + + elif event.item.type == "tool_call_output_item": + # Handle normal tool outputs + call_id = None + tool_name = None + + if isinstance(event.item.raw_item, dict): + call_id = event.item.raw_item.get('call_id') + tool_name = event.item.raw_item.get('name') + elif hasattr(event.item.raw_item, 'call_id'): + call_id = event.item.raw_item.call_id + if hasattr(event.item.raw_item, 'name'): + tool_name = event.item.raw_item.name + + message = { + 'content': str(event.item.output), 'role': 'tool', 'sender': None, 'tool_calls': None, 'tool_call_id': call_id, - 'tool_name': 'web_search', + 'tool_name': tool_name, 'response_type': 'internal' - } - - print("Yielding web search results: ", result_message) - yield ('message', result_message) - else: - # Handle normal tool calls - message = { - 'content': None, - 'role': 'assistant', - 'sender': current_agent.name if current_agent else None, - 'tool_calls': [{ - 'function': { - 'name': event.item.raw_item.name, - 'arguments': event.item.raw_item.arguments - }, - 'id': event.item.raw_item.call_id, - 'type': 'function' - }], - 'tool_call_id': None, - 'tool_name': None, - 'response_type': 'internal' - } - print("Yielding message: ", message) - yield ('message', message) - - - elif event.item.type == "tool_call_output_item": - # Check if it's a web search result - if isinstance(event.item.raw_item, dict) and event.item.raw_item.get('type') == 'web_search_results': - call_id = event.item.raw_item.get('search_id', event.item.raw_item.get('id', str(uuid.uuid4()))) - message = { - 'content': str(event.item.output), - 'role': 'tool', - 'sender': None, - 'tool_calls': None, - 'tool_call_id': call_id, - 'tool_name': 'web_search', - 'response_type': 'internal' - } - else: - # Safe extraction of call_id and name - call_id = None - tool_name = None - - # Handle different types of raw_item - if isinstance(event.item.raw_item, dict): - call_id = event.item.raw_item.get('call_id') - tool_name = event.item.raw_item.get('name') - elif hasattr(event.item.raw_item, 'call_id'): - call_id = event.item.raw_item.call_id - if hasattr(event.item.raw_item, 'name'): - tool_name = event.item.raw_item.name - - message = { - 'content': str(event.item.output), - 'role': 'tool', - 'sender': None, - 'tool_calls': None, - 'tool_call_id': call_id, - 'tool_name': tool_name, - 'response_type': 'internal' - } - + } print("Yielding message: ", message) yield ('message', message) @@ -374,89 +549,6 @@ async def run_turn_streamed( print("Yielding message: ", message) yield ('message', message) - # Handle web search function call events - elif event.item.type == "web_search_call_item" or (hasattr(event.item, 'raw_item') and hasattr(event.item.raw_item, 'type') and event.item.raw_item.type == 'web_search_call'): - # Extract web search call ID if available - call_id = None - if hasattr(event.item.raw_item, 'id'): - call_id = event.item.raw_item.id - - message = { - 'content': None, - 'role': 'assistant', - 'sender': current_agent.name if current_agent else None, - 'tool_calls': [{ - 'function': { - 'name': 'web_search', - 'arguments': json.dumps({ - 'search_id': call_id - }) - }, - 'id': call_id or str(uuid.uuid4()), - 'type': 'function' - }], - 'tool_call_id': None, - 'tool_name': None, - 'response_type': 'internal' - } - print("Yielding web search message: ", message) - yield ('message', message) - - # Handle web search results - elif event.item.type == "web_search_results_item" or ( - hasattr(event.item, 'raw_item') and ( - (hasattr(event.item.raw_item, 'type') and event.item.raw_item.type == 'web_search_results') or - (isinstance(event.item.raw_item, dict) and event.item.raw_item.get('type') == 'web_search_results') - ) - ): - # Extract call_id safely - call_id = None - raw_item = event.item.raw_item - - # Try several ways to get the search_id or id - if hasattr(raw_item, 'search_id'): - call_id = raw_item.search_id - elif isinstance(raw_item, dict) and 'search_id' in raw_item: - call_id = raw_item['search_id'] - elif hasattr(raw_item, 'id'): - call_id = raw_item.id - elif isinstance(raw_item, dict) and 'id' in raw_item: - call_id = raw_item['id'] - else: - call_id = str(uuid.uuid4()) - - # Extract results content safely - results = {} - - # Try event.item.output first - if hasattr(event.item, 'output'): - results = event.item.output - # Then try raw_item.results - elif hasattr(raw_item, 'results'): - results = raw_item.results - elif isinstance(raw_item, dict) and 'results' in raw_item: - results = raw_item['results'] - - # Format the results for output - results_str = "" - try: - results_str = json.dumps(results) if results else "" - except Exception as e: - print(f"Error serializing results: {str(e)}") - results_str = str(results) - - message = { - 'content': results_str, - 'role': 'tool', - 'sender': None, - 'tool_calls': None, - 'tool_call_id': call_id, - 'tool_name': 'web_search', - 'response_type': 'internal' - } - print("Yielding web search results: ", message) - yield ('message', message) - print(f"\n{'='*50}\n") # After all events are processed, set final state diff --git a/apps/rowboat_agents/src/graph/helpers/library_tools.py b/apps/rowboat_agents/src/graph/helpers/library_tools.py new file mode 100644 index 00000000..b5269478 --- /dev/null +++ b/apps/rowboat_agents/src/graph/helpers/library_tools.py @@ -0,0 +1,171 @@ +import json +import uuid + +def handle_web_search_event(event, current_agent): + """ + Helper function to handle all web search related events. + Returns a list of messages to yield. + """ + messages = [] + + # Handle raw response web search + if event.type == "raw_response_event": + if hasattr(event, 'data') and hasattr(event.data, 'raw_item'): + raw_item = event.data.raw_item + if (hasattr(raw_item, 'type') and raw_item.type == 'web_search_call') or ( + isinstance(raw_item, dict) and raw_item.get('type') == 'web_search_call' + ): + call_id = None + if hasattr(raw_item, 'id'): + call_id = raw_item.id + elif isinstance(raw_item, dict) and 'id' in raw_item: + call_id = raw_item['id'] + else: + call_id = str(uuid.uuid4()) + + status = 'unknown' + if hasattr(raw_item, 'status'): + status = raw_item.status + elif isinstance(raw_item, dict) and 'status' in raw_item: + status = raw_item['status'] + + messages.append({ + 'content': None, + 'role': 'assistant', + 'sender': current_agent.name if current_agent else None, + 'tool_calls': [{ + 'function': { + 'name': 'web_search', + 'arguments': json.dumps({ + 'search_id': call_id, + 'status': status + }) + }, + 'id': call_id, + 'type': 'function' + }], + 'tool_call_id': None, + 'tool_name': None, + 'response_type': 'internal' + }) + + # Handle run item web search events + elif event.type == "run_item_stream_event": + if event.item.type == "tool_call_item": + if hasattr(event.item.raw_item, 'type') and event.item.raw_item.type == 'web_search_call': + call_id = event.item.raw_item.id if hasattr(event.item.raw_item, 'id') else str(uuid.uuid4()) + messages.append({ + 'content': None, + 'role': 'assistant', + 'sender': current_agent.name if current_agent else None, + 'tool_calls': [{ + 'function': { + 'name': 'web_search', + 'arguments': json.dumps({ + 'search_id': call_id + }) + }, + 'id': call_id, + 'type': 'function' + }], + 'tool_call_id': None, + 'tool_name': None, + 'response_type': 'internal' + }) + messages.append({ + 'content': "Web search done", + 'role': 'tool', + 'sender': None, + 'tool_calls': None, + 'tool_call_id': call_id, + 'tool_name': 'web_search', + 'response_type': 'internal' + }) + + elif event.item.type == "tool_call_output_item": + if isinstance(event.item.raw_item, dict) and event.item.raw_item.get('type') == 'web_search_results': + call_id = event.item.raw_item.get('search_id', event.item.raw_item.get('id', str(uuid.uuid4()))) + messages.append({ + 'content': str(event.item.output), + 'role': 'tool', + 'sender': None, + 'tool_calls': None, + 'tool_call_id': call_id, + 'tool_name': 'web_search', + 'response_type': 'internal' + }) + + elif event.item.type == "web_search_call_item" or ( + hasattr(event.item, 'raw_item') and + hasattr(event.item.raw_item, 'type') and + event.item.raw_item.type == 'web_search_call' + ): + call_id = None + if hasattr(event.item.raw_item, 'id'): + call_id = event.item.raw_item.id + + messages.append({ + 'content': None, + 'role': 'assistant', + 'sender': current_agent.name if current_agent else None, + 'tool_calls': [{ + 'function': { + 'name': 'web_search', + 'arguments': json.dumps({ + 'search_id': call_id + }) + }, + 'id': call_id or str(uuid.uuid4()), + 'type': 'function' + }], + 'tool_call_id': None, + 'tool_name': None, + 'response_type': 'internal' + }) + + elif event.item.type == "web_search_results_item" or ( + hasattr(event.item, 'raw_item') and ( + (hasattr(event.item.raw_item, 'type') and event.item.raw_item.type == 'web_search_results') or + (isinstance(event.item.raw_item, dict) and event.item.raw_item.get('type') == 'web_search_results') + ) + ): + raw_item = event.item.raw_item + call_id = None + + if hasattr(raw_item, 'search_id'): + call_id = raw_item.search_id + elif isinstance(raw_item, dict) and 'search_id' in raw_item: + call_id = raw_item['search_id'] + elif hasattr(raw_item, 'id'): + call_id = raw_item.id + elif isinstance(raw_item, dict) and 'id' in raw_item: + call_id = raw_item['id'] + else: + call_id = str(uuid.uuid4()) + + results = {} + if hasattr(event.item, 'output'): + results = event.item.output + elif hasattr(raw_item, 'results'): + results = raw_item.results + elif isinstance(raw_item, dict) and 'results' in raw_item: + results = raw_item['results'] + + results_str = "" + try: + results_str = json.dumps(results) if results else "" + except Exception as e: + print(f"Error serializing results: {str(e)}") + results_str = str(results) + + messages.append({ + 'content': results_str, + 'role': 'tool', + 'sender': None, + 'tool_calls': None, + 'tool_call_id': call_id, + 'tool_name': 'web_search', + 'response_type': 'internal' + }) + + return messages diff --git a/apps/rowboat_agents/src/graph/types.py b/apps/rowboat_agents/src/graph/types.py index 32c827fb..3b82c017 100644 --- a/apps/rowboat_agents/src/graph/types.py +++ b/apps/rowboat_agents/src/graph/types.py @@ -4,10 +4,13 @@ class AgentRole(Enum): POST_PROCESSING = "post_process" GUARDRAILS = "guardrails" +class VisibilityType(Enum): + EXTERNAL = "external" + INTERNAL = "internal" + class ControlType(Enum): RETAIN = "retain" PARENT_AGENT = "relinquish_to_parent" - START_AGENT = "relinquish_to_start" class PromptType(Enum): STYLE = "style_prompt" From e59a8b75cf69d3b529b35903f42b13cf09ef7cd4 Mon Sep 17 00:00:00 2001 From: akhisud3195 Date: Tue, 6 May 2025 14:49:52 +0530 Subject: [PATCH 04/32] Enable internal and user-facing agents to build pipelines --- apps/copilot/copilot_multi_agent.md | 111 +-- apps/copilot/example_multi_agent_1.md | 128 ++- apps/rowboat/app/lib/client_utils.ts | 1 + apps/rowboat/app/lib/project_templates.ts | 27 +- .../rowboat/app/lib/types/agents_api_types.ts | 1 + apps/rowboat/app/lib/types/workflow_types.ts | 1 + .../[projectId]/entities/agent_config.tsx | 33 + .../[projectId]/workflow/workflow_editor.tsx | 1 + .../configs/default_config.json | 4 +- apps/rowboat_agents/src/app/main.py | 40 +- apps/rowboat_agents/src/graph/core.py | 792 ++++++++---------- .../{swarm_wrapper.py => execute_turn.py} | 63 +- apps/rowboat_agents/src/graph/guardrails.py | 2 +- .../src/graph/helpers/control.py | 8 +- .../src/graph/helpers/instructions.py | 9 +- .../src/graph/helpers/library_tools.py | 351 +++++--- apps/rowboat_agents/src/graph/instructions.py | 35 + apps/rowboat_agents/src/graph/tracing.py | 212 +++++ apps/rowboat_agents/src/graph/types.py | 9 +- .../tests/app_client_streaming.py | 53 +- apps/rowboat_agents/tests/interactive.py | 1 + .../tests/sample_requests/tmp1.json | 601 +++++++++---- .../tests/sample_requests/tmp2.json | 384 ++++----- .../tests/sample_requests/tmp3.json | 609 ++++++++------ 24 files changed, 2100 insertions(+), 1376 deletions(-) rename apps/rowboat_agents/src/graph/{swarm_wrapper.py => execute_turn.py} (87%) create mode 100644 apps/rowboat_agents/src/graph/tracing.py diff --git a/apps/copilot/copilot_multi_agent.md b/apps/copilot/copilot_multi_agent.md index 6f283ad8..10969e77 100644 --- a/apps/copilot/copilot_multi_agent.md +++ b/apps/copilot/copilot_multi_agent.md @@ -36,6 +36,7 @@ A agent can have one of the following behaviors: 3. Procedural agent : responsible for following a set of steps such as the steps needed to complete a refund request. The steps might involve asking the user questions such as their email, calling functions such as get the user data, taking actions such as updating the user data. Procedures can contain nested if / else conditional statements. A single agent can typically follow up to 6 steps correctly. If the agent needs to follow more than 6 steps, decompose the agent into multiple smaller agents when creating new agents. + ## Section 2 : Planning and Creating a Multi-Agent System When the user asks you to create agents for a multi agent system, you should follow the steps below: @@ -48,7 +49,50 @@ When the user asks you to create agents for a multi agent system, you should fol 6. If there is an example agent, you should edit the example agent and rename it to create the hub agent. 7. Briefly list the assumptions you have made. -## Section 3 : Editing an Existing Agent +## Section 3: Agent visibility and design patterns + +1. Agents can have 2 types of visibility - user_facing or internal. +2. Internal agents cannot put out messages to the user. Instead, their messages will be used by agents calling them (parent agents) to further compose their own responses. +3. User_facing agents can respond to the user directly +4. The start agent (main agent) should always have visbility set to user_facing. +5. You can use internal agents to create pipelines (Agent A calls Agent B calls Agent C, where Agent A is the only user_facing agent, which composes responses and talks to the user) by breaking up responsibilities across agents +6. A multi-agent system can be composed of internal and user_facing agents. If an agent needs to talk to the user, make it user_facing. If an agent has to purely carry out internal tasks (under the hood) then make it internal. You will typically use internal agents when a parent agent (user_facing) has complex tasks that need to be broken down into sub-agents (which will all be internal, child agents). +7. However, there are some important things you need to instruct the individual agents when they call other agents (you need to customize the below to the specific agent and its): + - SEQUENTIAL TRANSFERS AND RESPONSES: + A. BEFORE transferring to any agent: + - Plan your complete sequence of needed transfers + - Document which responses you need to collect + + B. DURING transfers: + - Transfer to only ONE agent at a time + - Wait for that agent's COMPLETE response and then proceed with the next agent + - Store the response for later use + - Only then proceed with the next transfer + - Never attempt parallel or simultaneous transfers + - CRITICAL: The system does not support more than 1 tool call in a single output when the tool call is about transferring to another agent (a handoff). You must only put out 1 transfer related tool call in one output. + + C. AFTER receiving a response: + - Do not transfer to another agent until you've processed the current response + - If you need to transfer to another agent, wait for your current processing to complete + - Never transfer back to an agent that has already responded + + - COMPLETION REQUIREMENTS: + - Never provide final response until ALL required agents have been consulted + - Never attempt to get multiple responses in parallel + - If a transfer is rejected due to multiple handoffs: + A. Complete current response processing + B. Then retry the transfer as next in sequence + X. Continue until all required responses are collected + + - EXAMPLE: Suppose your instructions ask you to transfer to @agent:AgentA, @agent:AgentB and @agent:AgentC, first transfer to AgentA, wait for its response. Then transfer to AgentB, wait for its response. Then transfer to AgentC, wait for its response. Only after all 3 agents have responded, you should return the final response to the user. + +### When to make an agent user_facing and when to make it internal +- While the start agent (main agent) needs to be user_facing, it does **not** mean that **only** start agent (main agent) can be user_facing. Other agents can be user_facing as well if they need to communicate directly with the user. +- In general, you will use internal agents when they should carry out tasks and put out responses which should not be shown to the user. They can be used to create internal pipelines. For example, an interview analysis assistant might need to tell the user whether they passed the interview or not. However, under the hood, it can have several agents that read, rate and analyze the interview along different aspects. These will be internal agents. +- User_facing agents must be used when the agent has to talk to the user. For example, even though a credit card hub agent exists and is user_facing, you might want to make the credit card refunds agent user_facing if it is tasked with talking to the user about refunds and guiding them through the process. Its job is not purely under the hood and hence it has to be user_facing. +- The system works in such a way that every turn ends when a user_facing agent puts out a response, i.e., it is now the user's turn to respond back. However, internal agent responses do not end turns. Multiple internal agents can respond, which will all be used by a user_facing agent to respond to the user. + +## Section 4 : Editing an Existing Agent When the user asks you to edit an existing agent, you should follow the steps below: @@ -80,7 +124,7 @@ Style of Response If the user doesn't specify how many examples, always add 5 examples. -## Section 4 : Improving an Existing Agent +## Section 5 : Improving an Existing Agent When the user asks you to improve an existing agent, you should follow the steps below: @@ -89,74 +133,37 @@ When the user asks you to improve an existing agent, you should follow the steps 3. Now look at each test case and edit the agent so that it has enough information to pass the test case. 4. If needed, ask clarifying questions to the user. Keep that to one turn and keep it minimal. -## Section 5 : Adding / Editing / Removing Tools +## Section 6 : Adding / Editing / Removing Tools 1. Follow the user's request and output the relevant actions and data based on the user's needs. 2. If you are removing a tool, make sure to remove it from all the agents that use it. 3. If you are adding a tool, make sure to add it to all the agents that need it. -## Section 6 : Adding / Editing / Removing Prompts +## Section 7 : Adding / Editing / Removing Prompts 1. Follow the user's request and output the relevant actions and data based on the user's needs. 2. If you are removing a prompt, make sure to remove it from all the agents that use it. 3. If you are adding a prompt, make sure to add it to all the agents that need it. 4. Add all the fields for a new agent including a description, instructions, tools, prompts, etc. -## Section 7 : Doing Multiple Actions at a Time +## Section 8 : Doing Multiple Actions at a Time 1. you should present your changes in order of : tools, prompts, agents. 2. Make sure to add, remove tools and prompts from agents as required. -## Section 8 : Creating New Agents +## Section 9 : Creating New Agents When creating a new agent, strictly follow the format of this example agent. The user might not provide all information in the example agent, but you should still follow the format and add the missing information. example agent: ``` -## šŸ§‘ā€šŸ’¼ Role: - -You are responsible for providing delivery information to the user. - ---- - -## āš™ļø Steps to Follow: - -1. Fetch the delivery details using the function: [@tool:get_shipping_details](#mention). -2. Answer the user's question based on the fetched delivery details. -3. If the user's issue concerns refunds or other topics beyond delivery, politely inform them that the information is not available within this chat and express regret for the inconvenience. -4. If the user's request is out of scope, call [@agent:Delivery Hub](#mention) - ---- -## šŸŽÆ Scope: - -āœ… In Scope: -- Questions about delivery status, shipping timelines, and delivery processes. -- Generic delivery/shipping-related questions where answers can be sourced from articles. - -āŒ Out of Scope: -- Questions unrelated to delivery or shipping. -- Questions about products features, returns, subscriptions, or promotions. -- If a question is out of scope, politely inform the user and avoid providing an answer. - ---- - -## šŸ“‹ Guidelines: - -āœ”ļø Dos: -- Use [@tool:get_shipping_details](#mention) to fetch accurate delivery information. -- Provide complete and clear answers based on the delivery details. -- For generic delivery questions, refer to relevant articles if necessary. -- Stick to factual information when answering. - -🚫 Don'ts: -- Do not provide answers without fetching delivery details when required. -- Do not leave the user with partial information. Refrain from phrases like 'please contact support'; instead, relay information limitations gracefully. +## šŸ§‘ā€šŸ’¼ Role:\nYou are the hub agent responsible for orchestrating the evaluation of interview transcripts between an executive search agency (Assistant) and a CxO candidate (User).\n\n---\n## āš™ļø Steps to Follow:\n1. Receive the transcript in the specified format.\n2. FIRST: Send the transcript to [@agent:Evaluation Agent] for evaluation.\n3. Wait to receive the complete evaluation from the Evaluation Agent.\n4. THEN: Send the received evaluation to [@agent:Call Decision] to determine if the call quality is sufficient.\n5. Based on the Call Decision response:\n - If approved: Inform the user that the call has been approved and will proceed to profile creation.\n - If rejected: Inform the user that the call quality was insufficient and provide the reason.\n6. Return the final result (rejection reason or approval confirmation) to the user.\n\n---\n## šŸŽÆ Scope:\nāœ… In Scope:\n- Orchestrating the sequential evaluation and decision process for interview transcripts.\n\nāŒ Out of Scope:\n- Directly evaluating or creating profiles.\n- Handling transcripts not in the specified format.\n- Interacting with the individual evaluation agents.\n\n---\n## šŸ“‹ Guidelines:\nāœ”ļø Dos:\n- Follow the strict sequence: Evaluation Agent first, then Call Decision.\n- Wait for each agent's complete response before proceeding.\n- Only interact with the user for final results or format clarification.\n\n🚫 Don'ts:\n- Do not perform evaluation or profile creation yourself.\n- Do not modify the transcript.\n- Do not try to get evaluations simultaneously.\n- Do not reference the individual evaluation agents.\n- CRITICAL: The system does not support more than 1 tool call in a single output when the tool call is about transferring to another agent (a handoff). You must only put out 1 transfer related tool call in one output.\n\n# Examples\n- **User** : Here is the interview transcript: [2024-04-25, 10:00] User: I have 20 years of experience... [2024-04-25, 10:01] Assistant: Can you describe your leadership style?\n - **Agent actions**: \n 1. First call [@agent:Evaluation Agent](#mention)\n 2. Wait for complete evaluation\n 3. Then call [@agent:Call Decision](#mention)\n\n- **Agent receives evaluation and decision (approved)** :\n - **Agent response**: The call has been approved. Proceeding to candidate profile creation.\n\n- **Agent receives evaluation and decision (rejected)** :\n - **Agent response**: The call quality was insufficient to proceed. [Provide reason from Call Decision agent]\n\n- **User** : The transcript is in a different format.\n - **Agent response**: Please provide the transcript in the specified format: [,