Merge pull request #46 from rowboatlabs/agent_sdk_rebased

Switch to openai agent_sdk
This commit is contained in:
Ramnique Singh 2025-03-26 17:06:51 +05:30 committed by GitHub
commit 7b8238d0dd
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
69 changed files with 7052 additions and 4694 deletions

1658
apps/agents/poetry.lock generated

File diff suppressed because it is too large Load diff

View file

@ -1,67 +0,0 @@
[tool.poetry]
name = "agents"
version = "0.1.0"
description = "RowBoat Labs Agent OS"
authors = ["Akhilesh <akhilesh@rowboatlabs.com>"]
license = "MIT"
readme = "README.md"
homepage = "https://github.com/rowboatlabs/agents"
package-mode = false
[tool.poetry.dependencies]
# Python
python = ">=3.10,<4.0"
# Dependencies
annotated-types = "^0.7.0"
anyio = "^4.6.2"
beautifulsoup4 = "^4.12.3"
blinker = "^1.8.2"
certifi = "^2024.8.30"
charset-normalizer = "^3.4.0"
click = "^8.1.7"
distro = "^1.9.0"
dnspython = "^2.7.0"
et_xmlfile = "^2.0.0"
eval_type_backport = "^0.2.0"
firecrawl = "^1.4.0"
Flask = "^3.0.3"
h11 = "^0.14.0"
httpcore = "^1.0.6"
httpx = "^0.27.2"
idna = "^3.10"
itsdangerous = "^2.2.0"
Jinja2 = "^3.1.4"
jiter = "^0.6.1"
jsonpath-python = "^1.0.6"
lxml = "^5.3.0"
markdownify = "^0.13.1"
MarkupSafe = "^3.0.2"
mypy-extensions = "^1.0.0"
nest-asyncio = "^1.6.0"
numpy = "^2.1.2"
openai = "^1.52.2"
openpyxl = "^3.1.5"
pandas = "^2.2.3"
pydantic = "^2.9.2"
pydantic_core = "^2.23.4"
pymongo = "^4.10.1"
python-dateutil = "^2.8.2"
python-docx = "^1.1.2"
python-dotenv = "^1.0.1"
pytz = "^2024.2"
requests = "^2.32.3"
setuptools = "^75.1.0"
six = "^1.16.0"
sniffio = "^1.3.1"
soupsieve = "^2.6"
tabulate = "^0.9.0"
tqdm = "^4.66.5"
typing-inspect = "^0.9.0"
typing_extensions = "^4.12.2"
tzdata = "^2024.2"
urllib3 = "^2.2.3"
websockets = "^13.1"
Werkzeug = "^3.0.5"
wheel = "^0.44.0"
gunicorn = "^23.0.0"

View file

@ -1,106 +0,0 @@
from flask import Flask, request, jsonify
from datetime import datetime
from functools import wraps
import os
from src.graph.core import run_turn
from src.graph.tools import RAG_TOOL, CLOSE_CHAT_TOOL
from src.utils.common import common_logger, read_json_from_file
logger = common_logger
app = Flask(__name__)
@app.route("/health", methods=["GET"])
def health():
return jsonify({"status": "ok"})
@app.route("/")
def home():
return "Hello, World!"
def require_api_key(f):
@wraps(f)
def decorated(*args, **kwargs):
auth_header = request.headers.get('Authorization')
if not auth_header or not auth_header.startswith('Bearer '):
return jsonify({'error': 'Missing or invalid authorization header'}), 401
token = auth_header.split('Bearer ')[1]
actual = os.environ.get('API_KEY', '').strip()
if actual and token != actual:
return jsonify({'error': 'Invalid API key'}), 403
return f(*args, **kwargs)
return decorated
@app.route("/chat", methods=["POST"])
@require_api_key
def chat():
print('='*200)
logger.info('='*200)
try:
data = request.get_json()
print('Complete request:')
logger.info('Complete request')
print(data)
logger.info(data)
print('-'*200)
logger.info('-'*200)
start_time = datetime.now()
config = read_json_from_file("./configs/default_config.json")
resp_messages, resp_tokens_used, resp_state = run_turn(
messages=data.get("messages", []),
start_agent_name=data.get("startAgent", ""),
agent_configs=data.get("agents", []),
tool_configs=data.get("tools", []),
localize_history=config.get("localize_history", True),
return_diff_messages=config.get("return_diff_messages", True),
prompt_configs=data.get("prompts", []),
start_turn_with_start_agent=config.get("start_turn_with_start_agent", False),
children_aware_of_parent=config.get("children_aware_of_parent", False),
parent_has_child_history=config.get("parent_has_child_history", True),
state=data.get("state", {}),
additional_tool_configs=[RAG_TOOL, CLOSE_CHAT_TOOL],
max_messages_per_turn=config.get("max_messages_per_turn", 2),
max_messages_per_error_escalation_turn=config.get("max_messages_per_error_escalation_turn", 2),
escalate_errors=config.get("escalate_errors", True),
max_overall_turns=config.get("max_overall_turns", 10)
)
print('-'*200)
logger.info('-'*200)
out = {
"messages": resp_messages,
"tokens_used": resp_tokens_used,
"state": resp_state,
}
print("Output: ")
logger.info(f"Output: ")
for k, v in out.items():
print(f"{k}: {v}")
print('*'*200)
logger.info(f"{k}: {v}")
logger.info('*'*200)
print("Processing time:")
print('='*200)
logger.info('='*200)
print(f"Processing time: {datetime.now() - start_time}")
logger.info(f"Processing time: {datetime.now() - start_time}")
return jsonify(out)
except Exception as e:
print(e)
logger.error(f"Error: {e}")
return jsonify({"error": str(e)}), 500
if __name__ == "__main__":
print("Starting Flask server...")
app.run(port=4040, debug=True)

View file

@ -1,561 +0,0 @@
import os
import sys
from copy import deepcopy
from src.swarm.types import Agent
from src.swarm.core import Swarm
from .guardrails import post_process_response
from .tools import create_error_tool_call
from .types import AgentRole, PromptType, ErrorType
from .helpers.access import get_agent_data_by_name, get_agent_by_name, get_agent_config_by_name, get_tool_config_by_name, get_tool_config_by_type, get_external_tools, get_prompt_by_type, pop_agent_config_by_type, get_agent_by_type
from .helpers.transfer import create_transfer_function_to_agent, create_transfer_function_to_parent_agent
from .helpers.state import add_recent_messages_to_history, construct_state_from_response, reset_current_turn, reset_current_turn_agent_history
from .helpers.instructions import add_transfer_instructions_to_child_agents, add_transfer_instructions_to_parent_agents, add_rag_instructions_to_agent, add_error_escalation_instructions, get_universal_system_message, add_universal_system_message_to_agent
from .helpers.control import get_latest_assistant_msg, get_latest_non_assistant_messages, get_last_agent_name
from src.swarm.types import Response
from datetime import datetime
from src.utils.common import common_logger
logger = common_logger
def order_messages(messages):
# Arrange keys in specified order
ordered_messages = []
for msg in messages:
ordered = {}
msg = {k: v for k, v in msg.items() if v is not None}
# Add keys in specified order if they exist
for key in ['role', 'sender', 'content', 'created_at', 'timestamp']:
if key in msg:
ordered[key] = msg[key]
# Add remaining keys in alphabetical order
for key in sorted(msg.keys()):
if key not in ['role', 'sender', 'content', 'created_at', 'timestamp']:
ordered[key] = msg[key]
ordered_messages.append(ordered)
return ordered_messages
def clean_up_history(agent_data):
for data in agent_data:
data["history"] = order_messages(data["history"])
return agent_data
def clear_agent_fields(agent):
agent.children = {}
agent.parent_function = None
agent.candidate_parent_functions = {}
agent.child_functions = {}
if agent.most_recent_parent:
agent.history = []
return agent
def get_agents(agent_configs, tool_configs, localize_history, available_tool_mappings, agent_data, start_turn_with_start_agent, children_aware_of_parent, universal_sys_msg):
# Create Agent objects
agents = []
if not isinstance(agent_configs, list):
raise ValueError("Agents config is not a list in get_agents")
if not isinstance(tool_configs, list):
raise ValueError("Tools config is not a list in get_agents")
for agent_config in agent_configs:
logger.debug(f"Processing config for agent: {agent_config['name']}")
# Get tools for this agent
external_tools = []
internal_tools = []
candidate_parent_functions = {}
child_functions = {}
logger.debug(f"Finding tools for agent {agent_config['name']}")
logger.debug(f"Agent {agent_config['name']} has {len(agent_config['tools'])} configured tools")
if agent_config.get("hasRagSources", False):
rag_tool_name = get_tool_config_by_type(tool_configs, "rag").get("name", "")
agent_config["tools"].append(rag_tool_name)
agent_config = add_rag_instructions_to_agent(agent_config, rag_tool_name)
for tool_name in agent_config["tools"]:
logger.debug(f"Looking for tool config: {tool_name}")
tool_config = get_tool_config_by_name(tool_configs, tool_name)
if tool_config:
if tool_name in available_tool_mappings:
internal_tools.append(available_tool_mappings[tool_name])
else:
external_tools.append({
"type": "function",
"function": tool_config
})
logger.debug(f"Added tool {tool_name} to agent {agent_config['name']}")
else:
logger.warning(f"Tool {tool_name} not found in tool_configs")
history = []
this_agent_data = get_agent_data_by_name(agent_config["name"], agent_data)
if this_agent_data:
if localize_history:
history = this_agent_data.get("history", [])
# Create agent
logger.debug(f"Creating Agent object for {agent_config['name']}")
logger.debug(f"Using model: {agent_config['model']}")
logger.debug(f"Number of tools being added: Internal - {len(internal_tools)} | External - {len(external_tools)}")
try:
agent = Agent(
name=agent_config["name"],
type=agent_config.get("type", "default"),
instructions=agent_config["instructions"],
description=agent_config.get("description", ""),
internal_tools=internal_tools,
external_tools=external_tools,
candidate_parent_functions=candidate_parent_functions,
child_functions=child_functions,
model=agent_config["model"],
respond_to_user=agent_config.get("respond_to_user", False),
history=history,
children_names=agent_config.get("connectedAgents", []),
most_recent_parent=None
)
agents.append(agent)
logger.debug(f"Successfully created agent: {agent_config['name']}")
except Exception as e:
logger.error(f"Failed to create agent {agent_config['name']}: {str(e)}")
raise
# Adding most recent parents to agents
for agent in agents:
most_recent_parent = None
this_agent_data = get_agent_data_by_name(agent.name, agent_data)
if this_agent_data:
most_recent_parent_name = this_agent_data.get("most_recent_parent_name", "")
if most_recent_parent_name:
most_recent_parent = get_agent_by_name(most_recent_parent_name, agents) if most_recent_parent_name else None
if most_recent_parent:
agent.most_recent_parent = most_recent_parent
# Adding children agents to parent agents
logger.info("Adding children agents to parent agents")
for agent in agents:
agent.children = {agent_.name: agent_ for agent_ in agents if agent_.name in agent.children_names}
# Generate transfer functions for transferring to children agents
logger.info("Generating transfer functions for transferring to children agents")
transfer_functions = {
agent.name: create_transfer_function_to_agent(agent)
for agent in agents
}
# Add transfer functions for parents to transfer to children
logger.info("Adding transfer functions for parents to transfer to children")
for agent in agents:
for child in agent.children.values():
agent.child_functions[child.name] = transfer_functions[child.name]
# Add transfer-related instructions to parent agents
logger.info("Adding child transfer-related instructions to parent agents")
for agent in agents:
if agent.children:
agent = add_transfer_instructions_to_parent_agents(agent, agent.children, transfer_functions)
# Generate and append duplicate transfer functions for children to transfer to parent agents
logger.info("Generating duplicate transfer functions for children to transfer to parent agents")
for agent in agents:
for child in agent.children.values():
func = create_transfer_function_to_parent_agent(
parent_agent=agent,
children_aware_of_parent=children_aware_of_parent,
transfer_functions=transfer_functions
)
child.candidate_parent_functions[agent.name] = func
for agent in agents:
if agent.candidate_parent_functions and agent.type != "escalation":
agent = add_transfer_instructions_to_child_agents(
child=agent,
children_aware_of_parent=children_aware_of_parent
)
for agent in agents:
if agent.most_recent_parent:
assert agent.most_recent_parent.name in agent.candidate_parent_functions, f"Most recent parent {agent.most_recent_parent.name} not found in candidate parent functions for agent {agent.name}"
agent.parent_function = agent.candidate_parent_functions[agent.most_recent_parent.name]
for agent in agents:
agent = add_universal_system_message_to_agent(agent, universal_sys_msg)
return agents
def check_request_validity(messages, agent_configs, tool_configs, prompt_configs, max_overall_turns):
error_msg = ""
error_type = ErrorType.ESCALATE.value
# Limits checks
external_messages_count = sum(1 for msg in messages if msg.get("response_type") == "external")
if external_messages_count >= max_overall_turns:
error_msg = f"Max overall turns reached: {max_overall_turns}"
# Empty checks
if not messages:
error_msg = "Messages list is empty"
# Empty checks --> Fatal
if not agent_configs:
error_msg = "Agent configs list is empty"
error_type = ErrorType.FATAL.value
# Type checks --> Fatal
for arg in [messages, agent_configs, tool_configs, prompt_configs]:
if not isinstance(arg, list):
error_msg = f"{arg} is not a list"
error_type = ErrorType.FATAL.value
# Post processing agent, guardrails and escalation agent check - there should be at max one agent with type "post_processing_agent", "guardrails_agent" and "escalation_agent" respectively --> Fatal
post_processing_agent_count = sum(1 for ac in agent_configs if ac.get("type", "") == AgentRole.POST_PROCESSING.value)
guardrails_agent_count = sum(1 for ac in agent_configs if ac.get("type", "") == AgentRole.GUARDRAILS.value)
escalation_agent_count = sum(1 for ac in agent_configs if ac.get("type", "") == AgentRole.ESCALATION.value)
if post_processing_agent_count > 1 or guardrails_agent_count > 1 or escalation_agent_count > 1:
error_msg = "Invalid post processing agent or guardrails agent count - expected at most 1"
error_type = ErrorType.FATAL.value
# All agent config should have: name, instructions, model --> Fatal
for agent_config in agent_configs:
if not all(key in agent_config for key in ["name", "instructions", "model"]):
missing_keys = [key for key in ["name", "instructions", "tools", "model"] if key not in agent_config]
error_msg = f"Invalid agent config - missing keys: {missing_keys}"
error_type = ErrorType.FATAL.value
# All tool configs should have: name, parameters --> Fatal
for tool_config in tool_configs:
if not all(key in tool_config for key in ["name", "parameters"]):
missing_keys = [key for key in ["name", "parameters"] if key not in tool_config]
error_msg = f"Invalid tool config - missing keys: {missing_keys}"
error_type = ErrorType.FATAL.value
# Check for cycles in the agent config graph. Raise error if cycle is found, along with the agents involved in the cycle.
def find_cycles(agent_name, agent_configs, visited=None, path=None):
if visited is None:
visited = set()
if path is None:
path = []
visited.add(agent_name)
path.append(agent_name)
agent_config = get_agent_config_by_name(agent_name, agent_configs)
if not agent_config:
return None
for child_name in agent_config.get("connectedAgents", []):
if child_name in path:
cycle = path[path.index(child_name):]
cycle.append(child_name)
return cycle
if child_name not in visited:
cycle = find_cycles(child_name, agent_configs, visited, path)
if cycle:
return cycle
path.pop()
return None
for agent_config in agent_configs:
if agent_config.get("name") in agent_config.get("connectedAgents", []):
error_msg = f"Cycle detected in agent config graph - agent {agent_config.get('name')} is connected to itself"
cycle = find_cycles(agent_config.get("name"), agent_configs)
if cycle:
cycle_str = " -> ".join(cycle)
error_msg = f"Cycle detected in agent config graph: {cycle_str}"
return error_msg, error_type
def handle_error(error_tool_call, error_msg, return_diff_messages, messages, turn_messages, state, tokens_used):
resp_messages = turn_messages if return_diff_messages else messages + turn_messages
resp_messages.extend([create_error_tool_call(error_msg)])
if error_tool_call:
return resp_messages, tokens_used, state
else:
raise ValueError(error_msg)
def create_final_response(response, turn_messages, messages, tokens_used, all_agents, return_diff_messages):
response.messages = turn_messages if return_diff_messages else messages + turn_messages
response.tokens_used = tokens_used
new_state = construct_state_from_response(response, all_agents)
return response.messages, response.tokens_used, new_state
def run_turn(messages, start_agent_name, agent_configs, tool_configs, available_tool_mappings={}, localize_history=True, return_diff_messages=True, prompt_configs=[], start_turn_with_start_agent=False, children_aware_of_parent=False, parent_has_child_history=True, state={}, additional_tool_configs=[], error_tool_call=True, max_messages_per_turn=10, max_messages_per_error_escalation_turn=4, escalate_errors=True, max_overall_turns=10):
greeting_turn = True if not any(msg.get("role") != "system" for msg in messages) else False
logger.info("Running stateless turn")
turn_messages = []
tokens_used = {}
messages = order_messages(messages)
tool_configs = tool_configs + additional_tool_configs
validation_error_msg, validation_error_type = check_request_validity(
messages=messages,
agent_configs=agent_configs,
tool_configs=tool_configs,
prompt_configs=prompt_configs,
max_overall_turns=max_overall_turns
)
if validation_error_msg and validation_error_type == ErrorType.FATAL.value:
logger.error(validation_error_msg)
return handle_error(
error_tool_call=error_tool_call,
error_msg=validation_error_msg,
return_diff_messages=return_diff_messages,
messages=messages,
turn_messages=turn_messages,
state=state,
tokens_used=tokens_used
)
post_processing_agent_config, agent_configs = pop_agent_config_by_type(agent_configs, AgentRole.POST_PROCESSING.value)
guardrails_agent_config, agent_configs = pop_agent_config_by_type(agent_configs, AgentRole.GUARDRAILS.value)
agent_data = state.get("agent_data", [])
universal_sys_msg = ""
if not greeting_turn:
latest_assistant_msg = get_latest_assistant_msg(messages)
universal_sys_msg = get_universal_system_message(messages)
latest_non_assistant_msgs = get_latest_non_assistant_messages(messages)
msg_type = latest_non_assistant_msgs[-1]["role"]
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
)
logger.info("Localizing message history")
if msg_type == "user":
messages = reset_current_turn(messages)
agent_data = reset_current_turn_agent_history(agent_data, [last_agent_name])
agent_data = clean_up_history(agent_data)
agent_data = add_recent_messages_to_history(
recent_messages=latest_non_assistant_msgs,
last_agent_name=last_agent_name,
agent_data=agent_data,
messages=messages,
parent_has_child_history=parent_has_child_history
)
state["agent_data"] = agent_data
logger.info("Initializing agents")
all_agents = get_agents(
agent_configs=agent_configs,
tool_configs=tool_configs,
available_tool_mappings=available_tool_mappings,
agent_data=state.get("agent_data", []),
localize_history=localize_history,
start_turn_with_start_agent=start_turn_with_start_agent,
children_aware_of_parent=children_aware_of_parent,
universal_sys_msg=universal_sys_msg
)
if not all_agents:
logger.error("No agents initialized")
return handle_error(
error_tool_call=error_tool_call,
error_msg="No agents initialized"
)
if greeting_turn:
greeting_msg = get_prompt_by_type(prompt_configs, PromptType.GREETING.value)
if not greeting_msg:
logger.error("Greeting prompt not found and messages is empty")
return handle_error(
error_tool_call=error_tool_call,
error_msg="Greeting prompt not found and messages is empty",
return_diff_messages=return_diff_messages,
messages=messages,
turn_messages=turn_messages,
state=state,
tokens_used=tokens_used
)
greeting_msg_internal = {
"content": greeting_msg,
"role": "assistant",
"sender": start_agent_name,
"response_type": "internal",
"created_at": datetime.now().isoformat(),
"current_turn": True
}
greeting_msg_external = deepcopy(greeting_msg_internal)
greeting_msg_external["response_type"] = "external"
greeting_msg_external["sender"] = greeting_msg_external["sender"] + ' >> External'
turn_messages.extend([greeting_msg_internal, greeting_msg_external])
response = Response(
messages=turn_messages,
tokens_used={},
agent=get_agent_by_name(start_agent_name, all_agents),
error_msg=''
)
return create_final_response(
response=response,
turn_messages=turn_messages,
messages=messages,
tokens_used=tokens_used,
all_agents=all_agents,
return_diff_messages=return_diff_messages
)
error_escalation_agent = deepcopy(get_agent_by_type(all_agents, AgentRole.ESCALATION.value))
if not error_escalation_agent:
logger.error("Escalation agent not found")
return handle_error(
error_tool_call=error_tool_call,
error_msg="Escalation agent not found",
return_diff_messages=return_diff_messages,
messages=messages,
turn_messages=turn_messages,
state=state,
tokens_used=tokens_used
)
error_escalation_agent = clear_agent_fields(error_escalation_agent)
error_escalation_agent = add_error_escalation_instructions(error_escalation_agent)
logger.info(f"Initialized {len(all_agents)} agents")
logger.debug("Getting last agent")
last_agent = get_agent_by_name(last_agent_name, all_agents)
if not last_agent:
logger.error("Last agent not found")
return handle_error(
error_tool_call=error_tool_call,
error_msg="Last agent not found",
return_diff_messages=return_diff_messages,
messages=messages,
state=state
)
external_tools = get_external_tools(tool_configs)
logger.info(f"Found {len(external_tools)} external tools")
logger.debug("Initializing Swarm client")
swarm_client = Swarm()
if not validation_error_msg:
response = swarm_client.run(
agent=last_agent,
messages=messages,
execute_tools=True,
external_tools=external_tools,
localize_history=localize_history,
parent_has_child_history=parent_has_child_history,
max_messages_per_turn=max_messages_per_turn,
tokens_used=tokens_used
)
tokens_used = response.tokens_used
last_agent = response.agent
response.messages = order_messages(response.messages)
turn_messages.extend(response.messages)
logger.info(f"Completed run of agent: {last_agent.name}")
if validation_error_msg and validation_error_type == ErrorType.ESCALATE.value or response.error_msg:
logger.info(f"Error raised in turn: {response.error_msg}")
response_sender_agent_name = response.agent.name
if escalate_errors and response_sender_agent_name != error_escalation_agent.name:
response = client.run(
agent=error_escalation_agent,
messages=[],
execute_tools=True,
external_tools=external_tools,
localize_history=False,
parent_has_child_history=False,
max_messages_per_turn=max_messages_per_error_escalation_turn,
tokens_used=tokens_used
)
tokens_used = response.tokens_used
last_agent = response.agent
response.messages = order_messages(response.messages)
turn_messages.extend(response.messages)
logger.info(f"Completed run of escalation agent: {error_escalation_agent.name}")
if response.error_msg:
logger.info(f"Error raised in escalation turn: {response.error_msg}")
return handle_error(
error_tool_call=error_tool_call,
error_msg=response.error_msg,
return_diff_messages=return_diff_messages,
messages=messages,
turn_messages=turn_messages,
state=state,
tokens_used=tokens_used
)
else:
logger.info(f"Error raised in turn: {response.error_msg}")
return handle_error(
error_tool_call=error_tool_call,
error_msg=response.error_msg,
return_diff_messages=return_diff_messages,
messages=messages,
turn_messages=turn_messages,
state=state,
tokens_used=tokens_used
)
if post_processing_agent_config:
response = post_process_response(
messages=turn_messages,
post_processing_agent_name=post_processing_agent_config.get("name", "Post Processing agent"),
post_process_instructions=post_processing_agent_config.get("instructions", ""),
style_prompt=get_prompt_by_type(prompt_configs, PromptType.STYLE.value),
context='',
model=post_processing_agent_config.get("model", "gpt-4o"),
tokens_used=tokens_used,
last_agent=last_agent
)
tokens_used = response.tokens_used
response.messages = order_messages(response.messages)
turn_messages.extend(response.messages)
logger.info("Response post-processed")
else:
logger.info("No post-processing agent found. Duplicating last response and setting to external.")
duplicate_msg = deepcopy(turn_messages[-1])
duplicate_msg["response_type"] = "external"
duplicate_msg["sender"] = duplicate_msg["sender"] + ' >> External'
response = Response(
messages=[duplicate_msg],
tokens_used=tokens_used,
agent=last_agent,
error_msg=''
)
response.messages = order_messages(response.messages)
turn_messages.extend(response.messages)
logger.info("Last response duplicated and set to external")
if guardrails_agent_config:
logger.info("Guardrails agent not implemented (ignoring)")
pass
if not state or not state.get("last_agent_name"):
logger.error("State is empty or last agent name is not set")
raise ValueError("State is empty or last agent name is not set")
return create_final_response(
response=response,
turn_messages=turn_messages,
messages=messages,
tokens_used=tokens_used,
all_agents=all_agents,
return_diff_messages=return_diff_messages
)

View file

@ -1,4 +0,0 @@
from .core import Swarm
from .types import Agent, Response
__all__ = ["Swarm", "Agent", "Response"]

View file

@ -1,275 +0,0 @@
# Standard library imports
import copy
import json
from collections import defaultdict
from typing import List, Callable, Union
from datetime import datetime
# Package/library imports
from openai import OpenAI
import random
# Local imports
from .util import *
from .types import (
Agent,
AgentFunction,
ChatCompletionMessage,
ChatCompletionMessageToolCall,
Function,
Response,
Result,
)
__CTX_VARS_NAME__ = "context_variables"
class Swarm:
def __init__(self, client=None):
if not client:
client = OpenAI(api_key=OPENAI_API_KEY)
self.client = client
self.history = defaultdict(lambda : [])
def get_chat_completion(
self,
agent: Agent,
history: List,
context_variables: dict,
model_override: str,
stream: bool,
debug: bool,
temperature: float
) -> ChatCompletionMessage:
context_variables = defaultdict(str, context_variables)
instructions = (
agent.instructions(context_variables)
if callable(agent.instructions)
else agent.instructions
)
messages = [{"role": "system", "content": instructions}] + history
debug_print(debug, "Getting chat completion for...:", messages)
all_functions = list(agent.child_functions.values()) + ([agent.parent_function] if agent.parent_function else [])
all_tools = agent.external_tools + agent.internal_tools
funcs_and_tools = [function_to_json(f) for f in all_functions] + [t for t in all_tools]
# hide context_variables from model
for tool in funcs_and_tools:
params = tool["function"]["parameters"]
params["properties"].pop(__CTX_VARS_NAME__, None)
if __CTX_VARS_NAME__ in params.get("required", []):
params["required"].remove(__CTX_VARS_NAME__)
create_params = {
"model": model_override or agent.model,
"messages": messages,
"tools": funcs_and_tools or None,
"tool_choice": agent.tool_choice,
"stream": stream,
"temperature": temperature
}
if funcs_and_tools:
create_params["parallel_tool_calls"] = agent.parallel_tool_calls
return self.client.chat.completions.create(**create_params)
def handle_function_result(self, result, debug) -> Result:
# Check if result is already a Result instance
if isinstance(result, Result):
return result
# Check if result is an Agent instance
if isinstance(result, Agent):
return Result(
value=json.dumps({"assistant": result.name}),
agent=result,
)
# Handle all other cases
try:
return Result(value=str(result))
except Exception as e:
error_message = f"Failed to cast response to string: {result}. Make sure agent functions return a string or Result object. Error: {str(e)}"
debug_print(debug, error_message)
raise TypeError(error_message)
def handle_function_calls(
self,
tool_calls: List[ChatCompletionMessageToolCall],
functions: List[AgentFunction],
context_variables: dict,
debug: bool,
) -> Response:
function_map = {f.__name__: f for f in functions}
partial_response = Response(
messages=[], agent=None, context_variables={})
for tool_call in tool_calls:
name = tool_call.function.name
# handle missing tool case, skip to next tool
if name not in function_map:
debug_print(debug, f"Tool {name} not found in function map.")
partial_response.messages.append(
{
"role": "tool",
"tool_call_id": tool_call.id,
"tool_name": name,
"content": f"Error: Tool {name} not found.",
}
)
continue
args = json.loads(tool_call.function.arguments)
debug_print(
debug, f"Processing tool call: {name} with arguments {args}")
func = function_map[name]
# pass context_variables to agent functions
if __CTX_VARS_NAME__ in func.__code__.co_varnames:
args[__CTX_VARS_NAME__] = context_variables
raw_result = function_map[name](**args)
result: Result = self.handle_function_result(raw_result, debug)
partial_response.messages.append(
{
"role": "tool",
"tool_call_id": tool_call.id,
"tool_name": name,
"content": result.value,
}
)
partial_response.context_variables.update(result.context_variables)
if result.agent:
partial_response.agent = result.agent
return partial_response
def run(
self,
agent: Agent,
messages: List,
context_variables: dict = {},
model_override: str = None,
stream: bool = False,
debug: bool = False,
max_messages_per_turn: int = 10,
execute_tools: bool = True,
external_tools: List[str] = [],
localize_history: bool = True,
parent_has_child_history: bool = True,
tokens_used: dict = {},
temperature: float = 0.0
) -> Response:
active_agent = agent
context_variables = copy.deepcopy(context_variables)
global_history = copy.deepcopy(messages)
init_len = len(messages)
while len(global_history) - init_len < max_messages_per_turn and active_agent:
history = active_agent.history if localize_history else global_history
history = arrange_messages_keys_in_order(history)
parent = active_agent.most_recent_parent
children_names_backup, children_backup, child_functions_backup = copy.deepcopy(active_agent.children_names), copy.deepcopy(active_agent.children), copy.deepcopy(active_agent.child_functions)
active_agent = check_and_remove_repeat_tool_call_to_child(active_agent, history)
# get completion with current history, agent
completion = self.get_chat_completion(
agent=active_agent,
history=history,
context_variables=context_variables,
model_override=model_override,
stream=stream,
debug=debug,
temperature=temperature
)
tokens_used = update_tokens_used(provider="openai", model=model_override or active_agent.model, tokens_used=tokens_used, completion=completion)
# Restore children and child functions
active_agent.children_names, active_agent.children, active_agent.child_functions = children_names_backup, children_backup, child_functions_backup
message = completion.choices[0].message
debug_print(debug, "Received completion:", message)
message.sender = active_agent.name
message_json = json.loads(message.model_dump_json())
message_json = add_message_metadata(message_json, active_agent)
if localize_history:
active_agent = update_histories(active_agent, message_json)
if parent and parent_has_child_history:
parent = update_histories(parent, message_json)
global_history.append(message_json)
external_tool_calls = []
internal_tool_calls = []
if message.tool_calls:
message_json["response_type"] = "internal"
for tool_call in message.tool_calls:
tool_name = tool_call.function.name
if tool_name in external_tools:
external_tool_calls.append(tool_call)
else:
internal_tool_calls.append(tool_call)
message.tool_calls = internal_tool_calls
if not message.tool_calls or not execute_tools:
if external_tool_calls:
message.tool_calls.extend(external_tool_calls)
debug_print(debug, "Ending turn.")
break
# handle function calls, updating context_variables, and switching agents
all_functions = list(active_agent.child_functions.values()) + ([active_agent.parent_function] if active_agent.parent_function else [])
partial_response = self.handle_function_calls(
message.tool_calls, all_functions, context_variables, debug
)
for msg in partial_response.messages:
msg = add_message_metadata(msg, active_agent)
if localize_history:
active_agent = update_histories(active_agent, msg)
if parent and parent_has_child_history:
parent = update_histories(parent, msg)
global_history.extend(partial_response.messages)
context_variables.update(partial_response.context_variables)
# Parent to child transfer
if partial_response.agent:
prev_agent = active_agent
active_agent = partial_response.agent
# Parent to child transfer
if active_agent.name in prev_agent.children_names:
active_agent.most_recent_parent = prev_agent
active_agent.parent_function = active_agent.candidate_parent_functions[active_agent.most_recent_parent.name]
if localize_history:
if not parent_has_child_history:
prev_agent.history = remove_irrelevant_messages(prev_agent.history)
new_active_agent_history = get_current_turn_messages(global_history, only_user = True)
active_agent.history.extend(new_active_agent_history)
# Child to parent transfer
else:
assert parent == active_agent, "Parent and active agent do not match when active agent is not a child of previous agent"
child = prev_agent
if localize_history:
child.history = remove_irrelevant_messages(child.history)
return_messages = global_history[init_len:]
error_msg = ""
if len(global_history) - init_len >= max_messages_per_turn:
error_msg = "Max messages per turn reached"
return Response(
messages=return_messages,
agent=active_agent,
context_variables=context_variables,
error_msg=error_msg,
tokens_used=tokens_used
)

View file

@ -1 +0,0 @@
from .repl import run_demo_loop

View file

@ -1,87 +0,0 @@
import json
from swarm import Swarm
def process_and_print_streaming_response(response):
content = ""
last_sender = ""
for chunk in response:
if "sender" in chunk:
last_sender = chunk["sender"]
if "content" in chunk and chunk["content"] is not None:
if not content and last_sender:
print(f"\033[94m{last_sender}:\033[0m", end=" ", flush=True)
last_sender = ""
print(chunk["content"], end="", flush=True)
content += chunk["content"]
if "tool_calls" in chunk and chunk["tool_calls"] is not None:
for tool_call in chunk["tool_calls"]:
f = tool_call["function"]
name = f["name"]
if not name:
continue
print(f"\033[94m{last_sender}: \033[95m{name}\033[0m()")
if "delim" in chunk and chunk["delim"] == "end" and content:
print() # End of response message
content = ""
if "response" in chunk:
return chunk["response"]
def pretty_print_messages(messages) -> None:
for message in messages:
if message["role"] != "assistant":
continue
# print agent name in blue
print(f"\033[94m{message['sender']}\033[0m:", end=" ")
# print response, if any
if message["content"]:
print(message["content"])
# print tool calls in purple, if any
tool_calls = message.get("tool_calls") or []
if len(tool_calls) > 1:
print()
for tool_call in tool_calls:
f = tool_call["function"]
name, args = f["name"], f["arguments"]
arg_str = json.dumps(json.loads(args)).replace(":", "=")
print(f"\033[95m{name}\033[0m({arg_str[1:-1]})")
def run_demo_loop(
starting_agent, context_variables=None, stream=False, debug=False
) -> None:
client = Swarm()
print("Starting Swarm CLI 🐝")
messages = []
agent = starting_agent
while True:
user_input = input("\033[90mUser\033[0m: ")
messages.append({"role": "user", "content": user_input})
response = client.run(
agent=agent,
messages=messages,
context_variables=context_variables or {},
stream=stream,
debug=debug,
)
if stream:
response = process_and_print_streaming_response(response)
else:
pretty_print_messages(response.messages)
messages.extend(response.messages)
agent = response.agent

View file

@ -1,54 +0,0 @@
from __future__ import annotations
from openai.types.chat import ChatCompletionMessage
from openai.types.chat.chat_completion_message_tool_call import (
ChatCompletionMessageToolCall,
Function,
)
from typing import List, Callable, Union, Optional, Dict
# Third-party imports
from pydantic import BaseModel
AgentFunction = Callable[[], Union[str, "Agent", dict]]
class Agent(BaseModel):
name: str = "Agent"
model: str = "gpt-4o"
type: str = ""
instructions: Union[str, Callable[[], str]] = "You are a helpful agent.",
description: str = "This is a helpful agent."
candidate_parent_functions: Dict[str, AgentFunction] = {}
parent_function: AgentFunction = None
child_functions: Dict[str, AgentFunction] = {}
internal_tools: List[Dict] = []
external_tools: List[Dict] = []
tool_choice: str = None
parallel_tool_calls: bool = True
respond_to_user: bool = True
history: List[Dict] = []
children_names: List[str] = []
children: Dict[str, "Agent"] = {}
most_recent_parent: Optional["Agent"] = None
parent: "Agent" = None
class Response(BaseModel):
messages: List = []
agent: Optional[Agent] = None
context_variables: dict = {}
error_msg: Optional[str] = ""
tokens_used: dict = {}
class Result(BaseModel):
"""
Encapsulates the possible return values for an agent function.
Attributes:
value (str): The result value as a string.
agent (Agent): The agent instance, if applicable.
context_variables (dict): A dictionary of context variables.
"""
value: str = ""
agent: Optional[Agent] = None
context_variables: dict = {}

View file

@ -1,175 +0,0 @@
import inspect
import json
from datetime import datetime
import os
from dotenv import load_dotenv
from src.utils.common import read_json_from_file, get_api_key
load_dotenv()
OPENAI_API_KEY = get_api_key("OPENAI_API_KEY")
def debug_print(debug: bool, *args: str) -> None:
if not debug:
return
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
message = " ".join(map(str, args))
print(f"\033[97m[\033[90m{timestamp}\033[97m]\033[90m {message}\033[0m")
def merge_fields(target, source):
for key, value in source.items():
if isinstance(value, str):
target[key] += value
elif value is not None and isinstance(value, dict):
merge_fields(target[key], value)
def merge_chunk(final_response: dict, delta: dict) -> None:
delta.pop("role", None)
merge_fields(final_response, delta)
tool_calls = delta.get("tool_calls")
if tool_calls and len(tool_calls) > 0:
index = tool_calls[0].pop("index")
merge_fields(final_response["tool_calls"][index], tool_calls[0])
def function_to_json(func) -> dict:
"""
Converts a Python function into a JSON-serializable dictionary
that describes the function's signature, including its name,
description, and parameters.
Args:
func: The function to be converted.
Returns:
A dictionary representing the function's signature in JSON format.
"""
type_map = {
str: "string",
int: "integer",
float: "number",
bool: "boolean",
list: "array",
dict: "object",
type(None): "null",
}
try:
signature = inspect.signature(func)
except ValueError as e:
raise ValueError(
f"Failed to get signature for function {func.__name__}: {str(e)}"
)
parameters = {}
for param in signature.parameters.values():
try:
param_type = type_map.get(param.annotation, "string")
except KeyError as e:
raise KeyError(
f"Unknown type annotation {param.annotation} for parameter {param.name}: {str(e)}"
)
parameters[param.name] = {"type": param_type}
required = [
param.name
for param in signature.parameters.values()
if param.default == inspect._empty
]
return {
"type": "function",
"function": {
"name": func.__name__,
"description": func.__doc__ or "",
"parameters": {
"type": "object",
"properties": parameters,
"required": required,
},
},
}
def get_current_turn_messages(messages, only_user = False):
if only_user:
return [msg for msg in messages if msg.get("current_turn") and msg.get("role") == "user"]
else:
return [msg for msg in messages if msg.get("current_turn")]
def arrange_messages_keys_in_order(messages):
"""Arranges message keys in a specific order: id, role, sender, relevant_agents, content, created_at, timestamp, followed by rest alphabetically"""
key_order = ['role', 'sender', 'content', 'created_at']
def sort_keys(message):
# Create new dict with specified key order
ordered = {}
# Add keys in specified order if they exist
for key in key_order:
if key in message:
ordered[key] = message[key]
# Add remaining keys in alphabetical order
for key in sorted(message.keys()):
if key not in key_order:
ordered[key] = message[key]
return ordered
return [sort_keys(message) for message in messages]
def remove_irrelevant_messages(messages):
"""Removes all messages from and including the latest user message"""
for i in range(len(messages)-1, -1, -1):
if messages[i].get("role") == "user":
return messages[:i]
return messages
def update_histories(active_agent, message):
active_agent.history.append(message)
return active_agent
def remove_none_fields(message):
return {k: v for k, v in message.items() if v is not None}
def add_message_metadata(message, active_agent):
message = remove_none_fields(message)
message["created_at"] = datetime.now().isoformat()
message["current_turn"] = True
if active_agent.respond_to_user:
message["response_type"] = "external"
else:
message["response_type"] = "internal"
return message
def check_and_remove_repeat_tool_call_to_child(agent, messages):
# If in the current turn, the most recent assistant message (need not be the last message overall, just needs to be the last message with role as assistant) is a tool call from a child agent, which transfers control to the agent using its parent function, then remove the tool call to transfer to that child again from this agent. This is to prevent back and forth between this agent and the child agent.
for message in reversed(messages):
if message.get("role") == "assistant" and message.get("sender") in agent.children_names and message.get("tool_calls"):
tool_call = message.get("tool_calls")[0]
child_agent = agent.children.get(message.get("sender"), None)
if not child_agent:
continue
child_agent_name = child_agent.name
if tool_call.get("function").get("name") == child_agent.parent_function:
agent.children_names.remove(child_agent_name)
agent.children.pop(child_agent_name)
agent.child_functions.pop(child_agent_name)
break
return agent
def update_tokens_used(provider, model, tokens_used, completion):
provider_model = f"{provider}/{model}"
input_tokens = completion.usage.prompt_tokens
output_tokens = completion.usage.completion_tokens
if provider_model not in tokens_used:
tokens_used[provider_model] = {
'input_tokens': 0,
'output_tokens': 0,
}
tokens_used[provider_model]['input_tokens'] += input_tokens
tokens_used[provider_model]['output_tokens'] += output_tokens
return tokens_used

View file

@ -1,424 +0,0 @@
{
"lastRequest": {
"messages": [
{
"content": "hi",
"role": "user",
"sender": null,
"tool_calls": null,
"tool_call_id": null,
"tool_name": null
},
{
"content": "Hello! How can I assist you today with your XYZ Bike?",
"role": "assistant",
"sender": "Main agent",
"tool_calls": null,
"tool_call_id": null,
"tool_name": null,
"response_type": "internal"
},
{
"content": "Hello! How can I assist you today with your XYZ Bike?",
"role": "assistant",
"sender": "Main agent >> Post process",
"tool_calls": null,
"tool_call_id": null,
"tool_name": null,
"response_type": "external"
},
{
"content": "i want to know about the range",
"role": "user",
"sender": null,
"tool_calls": null,
"tool_call_id": null,
"tool_name": null
},
{
"content": null,
"role": "assistant",
"sender": "Main agent",
"tool_calls": [
{
"function": {
"arguments": "{\"args\":\"\",\"kwargs\":\"\"}",
"name": "transfer_to_product_info_agent"
},
"id": "call_0MJHin0XCMyEJjA7T2FTJLZL",
"type": "function"
}
],
"tool_call_id": null,
"tool_name": null,
"response_type": "internal"
},
{
"content": "{\"assistant\": \"Product info agent\"}",
"role": "tool",
"sender": null,
"tool_calls": null,
"tool_call_id": "call_0MJHin0XCMyEJjA7T2FTJLZL",
"tool_name": "transfer_to_product_info_agent"
},
{
"content": null,
"role": "assistant",
"sender": "Product info agent",
"tool_calls": [
{
"function": {
"arguments": "{\"question\":\"XYZ Bike travel range\"}",
"name": "getArticleInfo"
},
"id": "call_CcNzb2N3lBt4JOCVrzyHdpdL",
"type": "function"
}
],
"tool_call_id": null,
"tool_name": null,
"response_type": "internal"
},
{
"content": "{\"results\":[{\"title\":\"XYZ Electric Bike\",\"content\":\"# XYZ Electric Bike\\n\\n### Transforming Transportation with the XYZ Electric Bike ### Revolutionizing Urban Mobility XYZ Electric Bike reimagines how we navigate cities, offering a seamless, stress-free alternative to traffic jams, pricey rideshares, rigid schedules, and the hassle of finding parking. --- #### **Instant Foldability** With a single press, XYZ's proprietary hinge mechanism folds the bike smoothly and securely in one swift motion. This innovation makes carrying and storing the bike effortless—outperforming the competition in both speed and ease of use. --- #### **Exceptional Handlebars** The sleek magnesium alloy handlebars are a marvel of design, housing intuitive controls for acceleration, braking, the horn, and LED lights, all within a streamlined, wire-free structure. Magnesium's lightweight properties—33% lighter than aluminum—make XYZ one of the most portable electric bikes available. --- #### **Unmatched Frame Design** Crafted with precision using TORAY carbon fiber, the frame achieves the perfect balance between strength and minimal weight. The material, meticulously layered for durability, is the same advanced composite used in aerospace engineering. --- #### **Impressive Range** Powered by premium electric batteries, XYZ bikes are designed for extended use with fast charging times. Their energy management system ensures long-lasting performance, providing ranges of up to 25 miles per charge, depending on riding conditions. --- #### **Dynamic Power** Equipped with dual motors delivering up to 1,000 watts at peak output, XYZ effortlessly handles steep inclines and challenging terrains. Rare-earth magnets and thermal regulation technology ensure high efficiency and reliability. --- #### **Puncture-Proof Tires** Say goodbye to flat tires. XYZ's solid rubber tires incorporate innovative air pockets for built-in shock absorption, delivering a smooth yet responsive ride across various surfaces. --- #### **Advanced Braking System** XYZ's braking system combines electronic anti-lock functionality with a user-friendly friction brake. Riders can enjoy a customizable braking experience, whether relying on fingertip controls or a traditional foot brake. --- #### **Durable and Comfortable Deck** The single-piece aluminum deck integrates a silicon surface for superior grip, eliminating unnecessary bulk or harsh finishes for a clean, modern look. --- #### **Invisible Kickstand** XYZ's custom-designed kickstand is seamlessly integrated, providing stability without disrupting the bike's sleek aesthetics. --- ### Models Comparison #### **XYZ Classic** - Price: $990 - Range: Up to 12 miles - Charge Time: 3.5 hours (80%) - Weight: 28.5 lbs #### **XYZ Voyager** - Price: $1,490 - Range: Up to 25 miles - Charge Time: 2 hours (80%) - Weight: 29.6 lbs - Features: App integration for enhanced control and ride stats --- XYZ Electric Bike is not just a mode of transport—it's the future of urban mobility, combining cutting-edge technology, top-tier materials, and unparalleled design for a ride that's as stylish as it is functional.\"}]}",
"role": "tool",
"sender": null,
"tool_calls": null,
"tool_call_id": "call_CcNzb2N3lBt4JOCVrzyHdpdL",
"tool_name": "getArticleInfo"
}
],
"state": {
"agent_data": [
{
"child_functions": [
"transfer_to_product_info_agent",
"transfer_to_delivery_info_agent",
"transfer_to_subscriptions_agent"
],
"external_tools": [],
"history": [
{
"content": "hi",
"current_turn": false,
"role": "user"
},
{
"content": "Hello! How can I assist you today with your XYZ Bike?",
"created_at": "2024-12-18T07:45:03.670088",
"current_turn": false,
"response_type": "internal",
"role": "assistant",
"sender": "Main agent"
},
{
"content": "i want to know about the range",
"current_turn": true,
"role": "user"
},
{
"created_at": "2024-12-18T07:45:13.240846",
"current_turn": true,
"response_type": "internal",
"role": "assistant",
"sender": "Main agent",
"tool_calls": [
{
"function": {
"arguments": "{\"args\":\"\",\"kwargs\":\"\"}",
"name": "transfer_to_product_info_agent"
},
"id": "call_0MJHin0XCMyEJjA7T2FTJLZL",
"type": "function"
}
]
},
{
"content": "{\"assistant\": \"Product info agent\"}",
"created_at": "2024-12-18T07:45:13.241184",
"current_turn": true,
"response_type": "internal",
"role": "tool",
"tool_call_id": "call_0MJHin0XCMyEJjA7T2FTJLZL",
"tool_name": "transfer_to_product_info_agent"
},
{
"created_at": "2024-12-18T07:45:13.821351",
"current_turn": true,
"response_type": "internal",
"role": "assistant",
"sender": "Product info agent",
"tool_calls": [
{
"function": {
"arguments": "{\"question\":\"XYZ Bike travel range\"}",
"name": "getArticleInfo"
},
"id": "call_CcNzb2N3lBt4JOCVrzyHdpdL",
"type": "function"
}
]
}
],
"instructions": "Role:\nYou are a customer support agent for XYZ Bikes. Your primary task is to facilitate conversations by passing control to specialized worker agents when needed.\n\n---\n\nTasks to Follow:\n- Engage in small talk if no specific question is asked.\n- Pass control to the appropriate worker agents for specialized conversations.\n\n---\n\nSmall Talk:\nYou are welcome to engage in basic small talk to build rapport.\n\n---\n\nExamples:\n\n---\nIn Scope Example 1:\nUser: How are you?\nAnswer: \"I'm doing well, thank you! How can I assist you today?\"\n\n---\nIn Scope Example 2:\nUser: What can you do?\nAnswer: \"I can help with customer support-related issues for XYZ Bikes. Let me know if you have any questions.\"\n\n---\nIn Scope Example 3:\nUser: I want a XYZ Bike.\nAnswer: \"What would you like to know about XYZ Bikes?\"\n\n---\nPass Control Example 1:\nUser: Tell me about the product features.\nAction: Pass control to the Product info agent.\n\n---\nPass Control Example 2:\nUser: Where is my scooter?\nAction: Pass control to the Delivery info agent.\n\n---\nPass Control Example 3:\nUser: I need help with my return.\nAction: Pass control to the Returns agent.\n\n---\nPass Control Example 4:\nUser: How does the Unagi subscription work?\nAction: Pass control to the Subscriptions agent.\n\n---\n✅ Dos:\n- Engage in small talk when necessary.\n- Pass control to the appropriate agent based on the user's query.\n\n---\n❌ Don'ts:\n- Do not focus excessively on greetings during ongoing conversations.\n- Do not continue the conversation if you suspect the user is confused or uninterested in Unagi support.\n\nSelf Support Guidance:\n\nThe bot should not suggest phrases like 'let me connect you to support' or 'you can reach out to support'. Instead, the agent is the customer support. It can say 'I apologize, but I don't have the right information'.",
"internal_tools": [],
"most_recent_parent_name": "",
"name": "Main agent",
"parent_function": null
},
{
"child_functions": [],
"external_tools": [
"getArticleInfo"
],
"history": [
{
"content": "i want to know about the range",
"current_turn": true,
"role": "user"
},
{
"created_at": "2024-12-18T07:45:13.821351",
"current_turn": true,
"response_type": "internal",
"role": "assistant",
"sender": "Product info agent",
"tool_calls": [
{
"function": {
"arguments": "{\"question\":\"XYZ Bike travel range\"}",
"name": "getArticleInfo"
},
"id": "call_CcNzb2N3lBt4JOCVrzyHdpdL",
"type": "function"
}
]
}
],
"instructions": "🧑‍💼 Role:\nYou are a product information agent for XYZ Bikes. Your job is to answer search for the right article and answer questions strictly based on the article about Unagi products. Feel free to ask the user clarification questions if needed.\n\n---\n\n📜 Instructions:\n\n✅ In Scope:\n- Answer questions strictly about Unagi product information.\n\n❌ Out of Scope:\n- Questions about delivery, returns, subscriptions, and promotions.\n- Any topic unrelated to Unagi products.\n- If a question is out of scope, call give_up_control and do not attempt to answer it.\n\n---\n\n✔ Dos:\n- Stick to the facts provided in the articles.\n- Provide complete and direct answers to the user's questions.\n- Call the Greeting agent after each interaction.\n\n---\n\n🚫 Don'ts:\n- Do not partially answer questions or direct users to a URL for more information.\n- Do not provide information outside of the given context.\n\n---\n\n📝 Examples:\n\n---\nIn Scope Example 1:\nUser: What is the maximum speed of the Unagi E500?\nAction: Call get_article_info followed by <answer based on the retrieved context about the maximum speed of the Unagi E500>.\n\n---\nIn Scope Example 2:\nUser: How long does it take to charge a XYZ Bike fully?\nAction: Call get_article_info followed by <answer based on the retrieved context about the charging duration>.\n\n---\nIn Scope Example 3:\nUser: Can you tell me about the weight-carrying capacity of XYZ Bikes?\nAction: Call get_article_info followed by <answer based on the retrieved context about weight capacity>.\n\n---\nIn Scope Example 4:\nUser: What are the differences between the E250 and E500 models?\nAction: Call get_article_info followed by <answer based on the retrieved context about model differences>.\n\n---\nIn Scope Example 5:\nUser: How far can I travel on a single charge with the E500?\nAction: Call get_article_info followed by <answer based on the retrieved context about travel range>.\n\n---\nIn Scope Example 6:\nUser: Is the scooter waterproof?\nAction: Call get_article_info followed by <answer based on the retrieved context about waterproofing>.\n\n---\nIn Scope Example 7:\nUser: Does the scooter have any safety features?\nAction: Call get_article_info followed by <answer based on the retrieved context about safety features>.\n\n---\nIn Scope Example 8:\nUser: What materials are used to make XYZ Bikes?\nAction: Call get_article_info followed by <answer based on the retrieved context about materials>.\n\n---\nIn Scope Example 9:\nUser: Can the scooter be used off-road?\nAction: Call get_article_info followed by <answer based on the retrieved context about off-road capability>.\n\n---\nIn Scope Example 10:\nUser: Are spare parts available for purchase?\nAction: Call get_article_info followed by <answer based on the retrieved context about spare parts availability>.\n\n---\nOut of Scope Example 1:\nUser: What is the status of my order delivery?\nAction: Call give_up_control.\n\n---\nOut of Scope Example 2:\nUser: How do I process a return?\nAction: Call give_up_control.\n\n---\nOut of Scope Example 3:\nUser: Can you tell me more about the subscription plans?\nAction: Call give_up_control.\n\n---\nOut of Scope Example 4:\nUser: Are there any promotions or discounts?\nAction: Call give_up_control.\n\n---\nOut of Scope Example 5:\nUser: Who won the last election?\nAction: Call give_up_control.\n\nProvide your output in the following structured JSON format:\n\n{\n \"steps_completed\": <number of steps completed, e.g., 1, 2, etc.>,\n \"current_step\": <current step number, e.g., 1>,\n \"reasoning\": \"<reasoning behind the response>\",\n \"error_count\": <number of errors encountered>,\n \"response_to_user\": \"<response to the user, ensure any detailed information such as tables or lists is included within this field>\"\n}\n\nAlways ensure that all pertinent details, including tables or structured lists, are contained within the response_to_user field to maintain clarity and a comprehensive response for the user.\n\nRetrieval instructions:\n\nIn every turn, retrieve a relevant article and use the information from that article to answer the user's question.\n\nSelf Support Guidance:\n\nThe bot should not suggest phrases like 'let me connect you to support' or 'you can reach out to support'. Instead, the agent is the customer support. It can say 'I apologize, but I don't have the right information'.",
"internal_tools": [],
"most_recent_parent_name": "Main agent",
"name": "Product info agent",
"parent_function": "give_up_chat_control"
},
{
"child_functions": [],
"external_tools": [
"get_delivery_details",
"getArticleInfo"
],
"history": [],
"instructions": "Role:\nYou are responsible for providing delivery information to the user.\n\n---\n\n⚙ Steps to Follow:\n1. Fetch the delivery details using the function: get_shipping_details.\n2. Answer the user's question based on the fetched delivery details.\n3. 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.\n\n---\n\n✅ In Scope:\nQuestions about delivery status, shipping timelines, and delivery processes.\nGeneric delivery/shipping-related questions where answers can be sourced from articles.\n\n---\n\n❌ Out of Scope:\nQuestions unrelated to delivery or shipping.\nQuestions about products features, returns, subscriptions, or promotions.\nIf a question is out of scope, politely inform the user and avoid providing an answer.\n\n---\n\nExample 1:\nUser: What is the status of my delivery?\nAction: Call get_delivery_details to fetch the current delivery status and inform the user.\n\nExample 2:\nUser: Can you explain the delivery process?\nAction: Provide a detailed answer and clarify any user questions based on the articles.\n\nExample 3:\nUser: I have a question about product features such as range, durability etc.\nAction: give_up_control as this is not in your scope.\n\n---\n\n✅ Dos:\nUse get_shipping_details to fetch accurate delivery information.\nProvide complete and clear answers based on the delivery details.\nFor generic delivery questions, refer to relevant articles if necessary.\nStick to factual information when answering.\n\n---\n\n❌ Don'ts:\nDo not provide answers without fetching delivery details when required.\nDo not leave the user with partial information.\nRefrain from phrases like 'please contact support'; instead, relay information limitations gracefully.\n\nProvide your output in the following structured JSON format:\n\n{\n \"steps_completed\": <number of steps completed, e.g., 1, 2, etc.>,\n \"current_step\": <current step number, e.g., 1>,\n \"reasoning\": \"<reasoning behind the response>\",\n \"error_count\": <number of errors encountered>,\n \"response_to_user\": \"<response to the user, ensure any detailed information such as tables or lists is included within this field>\"\n}\n\nAlways ensure that all pertinent details, including tables or structured lists, are contained within the response_to_user field to maintain clarity and a comprehensive response for the user.\n\nRetrieval instructions:\n\nIn every turn, retrieve a relevant article and use the information from that article to answer the user's question.\n\nSelf Support Guidance:\n\nThe bot should not suggest phrases like 'let me connect you to support' or 'you can reach out to support'. Instead, the agent is the customer support. It can say 'I apologize, but I don't have the right information'.",
"internal_tools": [],
"most_recent_parent_name": "",
"name": "Delivery info agent",
"parent_function": null
},
{
"child_functions": [],
"external_tools": [],
"history": [],
"instructions": "talk about returns\n\nSelf Support Guidance:\n\nThe bot should not suggest phrases like 'let me connect you to support' or 'you can reach out to support'. Instead, the agent is the customer support. It can say 'I apologize, but I don't have the right information'.",
"internal_tools": [],
"most_recent_parent_name": "",
"name": "Returns agent",
"parent_function": null
},
{
"child_functions": [],
"external_tools": [],
"history": [],
"instructions": "talk about subscriptions\n\nSelf Support Guidance:\n\nThe bot should not suggest phrases like 'let me connect you to support' or 'you can reach out to support'. Instead, the agent is the customer support. It can say 'I apologize, but I don't have the right information'.",
"internal_tools": [],
"most_recent_parent_name": "",
"name": "Subscriptions agent",
"parent_function": null
},
{
"child_functions": [],
"external_tools": [],
"history": [],
"instructions": "Talk about promotions\n\nSelf Support Guidance:\n\nThe bot should not suggest phrases like 'let me connect you to support' or 'you can reach out to support'. Instead, the agent is the customer support. It can say 'I apologize, but I don't have the right information'.",
"internal_tools": [],
"most_recent_parent_name": "",
"name": "Promotions agent",
"parent_function": null
},
{
"child_functions": [],
"external_tools": [],
"history": [],
"instructions": "Role:\nYou are a test agent for XYZ Bikes. Your job is to help test the functionality of different operations within the system.\n\n---\n\nTasks to Follow:\n- Assist in simulating various scenarios and operations to ensure smooth functioning.\n- Report any discrepancies or issues observed during testing.\n\n---\n\nIn Scope:\n- Conduct user interaction tests.\n- Evaluate agent response accuracy.\n- Validate agent transition accuracy.\n\n---\n\nOut of Scope:\n- Direct customer interactions outside of test scenarios.\n- Handling of live customer support queries.\n\n---\n\nDos:\n- Conduct comprehensive tests to cover all expected operations and scenarios.\n- Document test outcomes clearly.\n\n---\n\nDon'ts:\n- Do not intervene in live interactions unless part of a test scenario.\n- Ensure test operations do not affect live customer service functions.\n\nSelf Support Guidance:\n\nThe bot should not suggest phrases like 'let me connect you to support' or 'you can reach out to support'. Instead, the agent is the customer support. It can say 'I apologize, but I don't have the right information'.",
"internal_tools": [],
"most_recent_parent_name": "",
"name": "Test agent",
"parent_function": null
}
],
"last_agent_name": "Product info agent"
},
"agents": [
{
"name": "Main agent",
"type": "conversation",
"description": "The Main agent orchestrates interactions between various specialized worker agents to ensure efficient handling of user queries and support needs.",
"instructions": "Role:\nYou are a customer support agent for XYZ Bikes. Your primary task is to facilitate conversations by passing control to specialized worker agents when needed.\n\n---\n\nTasks to Follow:\n- Engage in small talk if no specific question is asked.\n- Pass control to the appropriate worker agents for specialized conversations.\n\n---\n\nSmall Talk:\nYou are welcome to engage in basic small talk to build rapport.\n\n---\n\nExamples:\n\n---\nIn Scope Example 1:\nUser: How are you?\nAnswer: \"I'm doing well, thank you! How can I assist you today?\"\n\n---\nIn Scope Example 2:\nUser: What can you do?\nAnswer: \"I can help with customer support-related issues for XYZ Bikes. Let me know if you have any questions.\"\n\n---\nIn Scope Example 3:\nUser: I want a XYZ Bike.\nAnswer: \"What would you like to know about XYZ Bikes?\"\n\n---\nPass Control Example 1:\nUser: Tell me about the product features.\nAction: Pass control to the Product info agent.\n\n---\nPass Control Example 2:\nUser: Where is my scooter?\nAction: Pass control to the Delivery info agent.\n\n---\nPass Control Example 3:\nUser: I need help with my return.\nAction: Pass control to the Returns agent.\n\n---\nPass Control Example 4:\nUser: How does the Unagi subscription work?\nAction: Pass control to the Subscriptions agent.\n\n---\n✅ Dos:\n- Engage in small talk when necessary.\n- Pass control to the appropriate agent based on the user's query.\n\n---\n❌ Don'ts:\n- Do not focus excessively on greetings during ongoing conversations.\n- Do not continue the conversation if you suspect the user is confused or uninterested in Unagi support.\n\nSelf Support Guidance:\n\nThe bot should not suggest phrases like 'let me connect you to support' or 'you can reach out to support'. Instead, the agent is the customer support. It can say 'I apologize, but I don't have the right information'.",
"tools": [],
"model": "gpt-4o-mini",
"hasRagSources": false,
"connectedAgents": [
"Product info agent",
"Delivery info agent",
"Subscriptions agent"
],
"controlType": "retain"
},
{
"name": "Post process",
"type": "post_process",
"instructions": "- Extract the response_to_user field from the provided structured JSON and ensure that this is the only content you use for the final output.\n- Ensure that the agent response covers all the details the user asked for.\n- When providing long details, use bullets to distinguish the different points. \n- Focus specifically on the response_to_user field in its input.\n\nSelf Support Guidance:\n\nThe bot should not suggest phrases like 'let me connect you to support' or 'you can reach out to support'. Instead, the agent is the customer support. It can say 'I apologize, but I don't have the right information'.",
"tools": [],
"model": "gpt-4o",
"hasRagSources": false,
"connectedAgents": []
},
{
"name": "Product info agent",
"type": "conversation",
"description": "You assist with product-related questions by retrieving relevant articles and information.",
"instructions": "🧑‍💼 Role:\nYou are a product information agent for XYZ Bikes. Your job is to answer search for the right article and answer questions strictly based on the article about Unagi products. Feel free to ask the user clarification questions if needed.\n\n---\n\n📜 Instructions:\n\n✅ In Scope:\n- Answer questions strictly about Unagi product information.\n\n❌ Out of Scope:\n- Questions about delivery, returns, subscriptions, and promotions.\n- Any topic unrelated to Unagi products.\n- If a question is out of scope, call give_up_control and do not attempt to answer it.\n\n---\n\n✔ Dos:\n- Stick to the facts provided in the articles.\n- Provide complete and direct answers to the user's questions.\n- Call the Greeting agent after each interaction.\n\n---\n\n🚫 Donts:\n- Do not partially answer questions or direct users to a URL for more information.\n- Do not provide information outside of the given context.\n\n---\n\n📝 Examples:\n\n---\nIn Scope Example 1:\nUser: What is the maximum speed of the Unagi E500?\nAction: Call get_article_info followed by <answer based on the retrieved context about the maximum speed of the Unagi E500>.\n\n---\nIn Scope Example 2:\nUser: How long does it take to charge a XYZ Bike fully?\nAction: Call get_article_info followed by <answer based on the retrieved context about the charging duration>.\n\n---\nIn Scope Example 3:\nUser: Can you tell me about the weight-carrying capacity of XYZ Bikes?\nAction: Call get_article_info followed by <answer based on the retrieved context about weight capacity>.\n\n---\nIn Scope Example 4:\nUser: What are the differences between the E250 and E500 models?\nAction: Call get_article_info followed by <answer based on the retrieved context about model differences>.\n\n---\nIn Scope Example 5:\nUser: How far can I travel on a single charge with the E500?\nAction: Call get_article_info followed by <answer based on the retrieved context about travel range>.\n\n---\nIn Scope Example 6:\nUser: Is the scooter waterproof?\nAction: Call get_article_info followed by <answer based on the retrieved context about waterproofing>.\n\n---\nIn Scope Example 7:\nUser: Does the scooter have any safety features?\nAction: Call get_article_info followed by <answer based on the retrieved context about safety features>.\n\n---\nIn Scope Example 8:\nUser: What materials are used to make XYZ Bikes?\nAction: Call get_article_info followed by <answer based on the retrieved context about materials>.\n\n---\nIn Scope Example 9:\nUser: Can the scooter be used off-road?\nAction: Call get_article_info followed by <answer based on the retrieved context about off-road capability>.\n\n---\nIn Scope Example 10:\nUser: Are spare parts available for purchase?\nAction: Call get_article_info followed by <answer based on the retrieved context about spare parts availability>.\n\n---\nOut of Scope Example 1:\nUser: What is the status of my order delivery?\nAction: Call give_up_control.\n\n---\nOut of Scope Example 2:\nUser: How do I process a return?\nAction: Call give_up_control.\n\n---\nOut of Scope Example 3:\nUser: Can you tell me more about the subscription plans?\nAction: Call give_up_control.\n\n---\nOut of Scope Example 4:\nUser: Are there any promotions or discounts?\nAction: Call give_up_control.\n\n---\nOut of Scope Example 5:\nUser: Who won the last election?\nAction: Call give_up_control.\n\nProvide your output in the following structured JSON format:\n\n{\n \"steps_completed\": <number of steps completed, e.g., 1, 2, etc.>,\n \"current_step\": <current step number, e.g., 1>,\n \"reasoning\": \"<reasoning behind the response>\",\n \"error_count\": <number of errors encountered>,\n \"response_to_user\": \"<response to the user, ensure any detailed information such as tables or lists is included within this field>\"\n}\n\nAlways ensure that all pertinent details, including tables or structured lists, are contained within the response_to_user field to maintain clarity and a comprehensive response for the user.\n\nRetrieval instructions:\n\nIn every turn, retrieve a relevant article and use the information from that article to answer the user's question.\n\nSelf Support Guidance:\n\nThe bot should not suggest phrases like 'let me connect you to support' or 'you can reach out to support'. Instead, the agent is the customer support. It can say 'I apologize, but I don't have the right information'.",
"tools": [],
"model": "gpt-4o-mini",
"hasRagSources": true,
"connectedAgents": [],
"controlType": "relinquish_to_parent"
},
{
"name": "Delivery info agent",
"type": "conversation",
"description": "You are responsible for providing accurate delivery status and shipping details for orders.",
"instructions": "Role:\nYou are responsible for providing delivery information to the user.\n\n---\n\n⚙ Steps to Follow:\n1. Fetch the delivery details using the function: get_shipping_details.\n2. Answer the user's question based on the fetched delivery details.\n3. 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.\n\n---\n\n✅ In Scope:\nQuestions about delivery status, shipping timelines, and delivery processes.\nGeneric delivery/shipping-related questions where answers can be sourced from articles.\n\n---\n\n❌ Out of Scope:\nQuestions unrelated to delivery or shipping.\nQuestions about products features, returns, subscriptions, or promotions.\nIf a question is out of scope, politely inform the user and avoid providing an answer.\n\n---\n\nExample 1:\nUser: What is the status of my delivery?\nAction: Call get_delivery_details to fetch the current delivery status and inform the user.\n\nExample 2:\nUser: Can you explain the delivery process?\nAction: Provide a detailed answer and clarify any user questions based on the articles.\n\nExample 3:\nUser: I have a question about product features such as range, durability etc.\nAction: give_up_control as this is not in your scope.\n\n---\n\n✅ Dos:\nUse get_shipping_details to fetch accurate delivery information.\nProvide complete and clear answers based on the delivery details.\nFor generic delivery questions, refer to relevant articles if necessary.\nStick to factual information when answering.\n\n---\n\n❌ Donts:\nDo not provide answers without fetching delivery details when required.\nDo not leave the user with partial information.\nRefrain from phrases like 'please contact support'; instead, relay information limitations gracefully.\n\nProvide your output in the following structured JSON format:\n\n{\n \"steps_completed\": <number of steps completed, e.g., 1, 2, etc.>,\n \"current_step\": <current step number, e.g., 1>,\n \"reasoning\": \"<reasoning behind the response>\",\n \"error_count\": <number of errors encountered>,\n \"response_to_user\": \"<response to the user, ensure any detailed information such as tables or lists is included within this field>\"\n}\n\nAlways ensure that all pertinent details, including tables or structured lists, are contained within the response_to_user field to maintain clarity and a comprehensive response for the user.\n\nRetrieval instructions:\n\nIn every turn, retrieve a relevant article and use the information from that article to answer the user's question.\n\nSelf Support Guidance:\n\nThe bot should not suggest phrases like 'let me connect you to support' or 'you can reach out to support'. Instead, the agent is the customer support. It can say 'I apologize, but I don't have the right information'.",
"tools": [
"get_delivery_details"
],
"model": "gpt-4o-mini",
"hasRagSources": true,
"connectedAgents": [],
"controlType": "retain"
},
{
"name": "Returns agent",
"type": "conversation",
"description": "You provide assistance for inquiries and processes related to product returns.",
"instructions": "talk about returns\n\nSelf Support Guidance:\n\nThe bot should not suggest phrases like 'let me connect you to support' or 'you can reach out to support'. Instead, the agent is the customer support. It can say 'I apologize, but I don't have the right information'.",
"tools": [],
"model": "gpt-4o-mini",
"hasRagSources": false,
"connectedAgents": []
},
{
"name": "Subscriptions agent",
"type": "conversation",
"description": "You handle all subscription-related queries from customers.",
"instructions": "talk about subscriptions\n\nSelf Support Guidance:\n\nThe bot should not suggest phrases like 'let me connect you to support' or 'you can reach out to support'. Instead, the agent is the customer support. It can say 'I apologize, but I don't have the right information'.",
"tools": [],
"model": "gpt-4o-mini",
"hasRagSources": false,
"connectedAgents": []
},
{
"name": "Promotions agent",
"type": "conversation",
"description": "You provide current promotions and discounts details to the customers.",
"instructions": "Talk about promotions\n\nSelf Support Guidance:\n\nThe bot should not suggest phrases like 'let me connect you to support' or 'you can reach out to support'. Instead, the agent is the customer support. It can say 'I apologize, but I don't have the right information'.",
"tools": [],
"model": "gpt-4o-mini",
"hasRagSources": false,
"connectedAgents": []
},
{
"name": "Test agent",
"type": "conversation",
"description": "Your job is to simulate various customer interactions and test system operations for quality assurance purposes.",
"instructions": "Role:\nYou are a test agent for XYZ Bikes. Your job is to help test the functionality of different operations within the system.\n\n---\n\nTasks to Follow:\n- Assist in simulating various scenarios and operations to ensure smooth functioning.\n- Report any discrepancies or issues observed during testing.\n\n---\n\nIn Scope:\n- Conduct user interaction tests.\n- Evaluate agent response accuracy.\n- Validate agent transition accuracy.\n\n---\n\nOut of Scope:\n- Direct customer interactions outside of test scenarios.\n- Handling of live customer support queries.\n\n---\n\nDos:\n- Conduct comprehensive tests to cover all expected operations and scenarios.\n- Document test outcomes clearly.\n\n---\n\nDonts:\n- Do not intervene in live interactions unless part of a test scenario.\n- Ensure test operations do not affect live customer service functions.\n\nSelf Support Guidance:\n\nThe bot should not suggest phrases like 'let me connect you to support' or 'you can reach out to support'. Instead, the agent is the customer support. It can say 'I apologize, but I don't have the right information'.",
"tools": [],
"model": "gpt-4o-mini",
"hasRagSources": false,
"connectedAgents": []
},
{
"name": "Escalation",
"type": "escalation",
"description": "",
"instructions": "Get the user's contact information and let them know that their request has been escalated.\n\n",
"tools": [],
"model": "gpt-4o-mini",
"hasRagSources": false,
"connectedAgents": [],
"controlType": "retain"
}
],
"tools": [
{
"name": "get_delivery_details",
"description": "Return a estimated delivery date for the XYZ Bike.",
"parameters": {
"type": "object",
"properties": {},
"required": []
}
},
{
"name": "get_subscription_plan_details",
"description": "Return details of the available subscription plans for XYZ Bikes.",
"parameters": {
"type": "object",
"properties": {},
"required": []
}
},
{
"name": "get_current_date",
"description": "Return the current date.",
"parameters": {
"type": "object",
"properties": {},
"required": []
}
}
],
"prompts": [
{
"name": "Style prompt",
"type": "style_prompt",
"prompt": "You should be empathetic and helpful."
},
{
"name": "reasoning_output",
"type": "base_prompt",
"prompt": "Give your output in the following format:\n\nreason : <a single sentence reasoning behind your response>\n\nresponse_to_user : <response to user>"
},
{
"name": "get_delivery_details",
"type": "base_prompt",
"prompt": "Return a estimated delivery date for XYZ Bike."
},
{
"name": "structured_output",
"type": "base_prompt",
"prompt": "Provide your output in the following structured JSON format:\n\n{\n \"steps_completed\": <number of steps completed, e.g., 1, 2, etc.>,\n \"current_step\": <current step number, e.g., 1>,\n \"reasoning\": \"<reasoning behind the response>\",\n \"error_count\": <number of errors encountered>,\n \"response_to_user\": \"<response to the user, ensure any detailed information such as tables or lists is included within this field>\"\n}\n\nAlways ensure that all pertinent details, including tables or structured lists, are contained within the response_to_user field to maintain clarity and a comprehensive response for the user."
},
{
"name": "rag_article_prompt",
"type": "base_prompt",
"prompt": "Retrieval instructions:\n\nIn every turn, retrieve a relevant article and use the information from that article to answer the user's question."
},
{
"name": "self_support_prompt",
"type": "base_prompt",
"prompt": "Self Support Guidance:\n\nThe bot should not suggest phrases like 'let me connect you to support' or 'you can reach out to support'. Instead, the agent is the customer support. It can say 'I apologize, but I don't have the right information'."
}
],
"startAgent": "Main agent"
}
}

View file

@ -1,26 +1,16 @@
'use server';
import { convertFromAgenticAPIChatMessages } from "../lib/types/agents_api_types";
import { AgenticAPIInitStreamResponse, convertFromAgenticAPIChatMessages } from "../lib/types/agents_api_types";
import { AgenticAPIChatRequest } from "../lib/types/agents_api_types";
import { WorkflowAgent } from "../lib/types/workflow_types";
import { EmbeddingRecord } from "../lib/types/datasource_types";
import { WebpageCrawlResponse } from "../lib/types/tool_types";
import { GetInformationToolResult } from "../lib/types/tool_types";
import { EmbeddingDoc } from "../lib/types/datasource_types";
import { generateObject, generateText, embed } from "ai";
import { dataSourceDocsCollection, dataSourcesCollection, embeddingsCollection, webpagesCollection } from "../lib/mongodb";
import { webpagesCollection } from "../lib/mongodb";
import { z } from 'zod';
import { openai } from "@ai-sdk/openai";
import FirecrawlApp, { ScrapeResponse } from '@mendable/firecrawl-js';
import { embeddingModel } from "../lib/embedding";
import { apiV1 } from "rowboat-shared";
import { Claims, getSession } from "@auth0/nextjs-auth0";
import { callClientToolWebhook, getAgenticApiResponse, mockToolResponse, runRAGToolCall } from "../lib/utils";
import { getAgenticApiResponse, getAgenticResponseStreamId } from "../lib/utils";
import { check_query_limit } from "../lib/rate_limiting";
import { QueryLimitError } from "../lib/client_utils";
import { projectAuthCheck } from "./project_actions";
import { qdrantClient } from "../lib/qdrant";
import { ObjectId } from "mongodb";
import { TestProfile } from "../lib/types/testing_types";
const crawler = new FirecrawlApp({ apiKey: process.env.FIRECRAWL_API_KEY || '' });
@ -76,17 +66,14 @@ export async function scrapeWebpage(url: string): Promise<z.infer<typeof Webpage
};
}
export async function getAssistantResponse(
projectId: string,
request: z.infer<typeof AgenticAPIChatRequest>,
): Promise<{
export async function getAssistantResponse(request: z.infer<typeof AgenticAPIChatRequest>): Promise<{
messages: z.infer<typeof apiV1.ChatMessage>[],
state: unknown,
rawRequest: unknown,
rawResponse: unknown,
}> {
await projectAuthCheck(projectId);
if (!await check_query_limit(projectId)) {
await projectAuthCheck(request.projectId);
if (!await check_query_limit(request.projectId)) {
throw new QueryLimitError();
}
@ -99,95 +86,12 @@ export async function getAssistantResponse(
};
}
export async function suggestToolResponse(toolId: string, projectId: string, messages: z.infer<typeof apiV1.ChatMessage>[], mockInstructions: string): Promise<string> {
await projectAuthCheck(projectId);
if (!await check_query_limit(projectId)) {
export async function getAssistantResponseStreamId(request: z.infer<typeof AgenticAPIChatRequest>): Promise<z.infer<typeof AgenticAPIInitStreamResponse>> {
await projectAuthCheck(request.projectId);
if (!await check_query_limit(request.projectId)) {
throw new QueryLimitError();
}
return await mockToolResponse(toolId, messages, mockInstructions);
}
export async function getInformationTool(
projectId: string,
query: string,
sourceIds: string[],
returnType: z.infer<typeof WorkflowAgent>['ragReturnType'],
k: number,
): Promise<z.infer<typeof GetInformationToolResult>> {
await projectAuthCheck(projectId);
return await runRAGToolCall(projectId, query, sourceIds, returnType, k);
}
export async function simulateUserResponse(
projectId: string,
messages: z.infer<typeof apiV1.ChatMessage>[],
scenario: string,
): Promise<string> {
await projectAuthCheck(projectId);
if (!await check_query_limit(projectId)) {
throw new QueryLimitError();
}
const scenarioPrompt = `
# Your Specific Task:
## Context:
Here is a scenario:
Scenario:
<START_SCENARIO>
{{scenario}}
<END_SCENARIO>
## Task definition:
Pretend to be a user reaching out to customer support. Chat with the
customer support assistant, assuming your issue is based on this scenario.
Ask follow-up questions and make it real-world like. Don't do dummy
conversations. Your conversation should be a maximum of 5 user turns.
As output, simply provide your (user) turn of conversation.
After you are done with the chat, keep replying with a single word EXIT
in all capitals.
`;
await projectAuthCheck(projectId);
// flip message assistant / user message
// roles from chat messages
// use only text response messages
const flippedMessages: { role: 'user' | 'assistant', content: string }[] = messages
.filter(m => m.role == 'assistant' || m.role == 'user')
.map(m => ({
role: m.role == 'assistant' ? 'user' : 'assistant',
content: m.content || '',
}));
// simulate user call
let prompt;
prompt = scenarioPrompt
.replace('{{scenario}}', scenario);
const { text } = await generateText({
model: openai("gpt-4o"),
system: prompt || '',
messages: flippedMessages,
});
return text.replace(/\. EXIT$/, '.');
}
export async function executeClientTool(
toolCall: z.infer<typeof apiV1.AssistantMessageWithToolCalls>['tool_calls'][number],
messages: z.infer<typeof apiV1.ChatMessage>[],
projectId: string,
): Promise<unknown> {
await projectAuthCheck(projectId);
const result = await callClientToolWebhook(toolCall, messages, projectId);
return result;
const response = await getAgenticResponseStreamId(request);
return response;
}

View file

@ -4,9 +4,9 @@ import { WorkflowTool } from "../lib/types/workflow_types";
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
import { projectAuthCheck } from "./project_actions";
import { callMcpTool } from "../lib/utils";
import { projectsCollection } from "../lib/mongodb";
import { Project } from "../lib/types/project_types";
import { MCPServer } from "../lib/types/types";
export async function fetchMcpTools(projectId: string): Promise<z.infer<typeof WorkflowTool>[]> {
await projectAuthCheck(projectId);
@ -74,9 +74,10 @@ export async function updateMcpServers(projectId: string, mcpServers: z.infer<ty
}, { $set: { mcpServers } });
}
export async function executeMcpTool(projectId: string, mcpServerName: string, toolName: string, parameters: Record<string, unknown>): Promise<unknown> {
export async function listMcpServers(projectId: string): Promise<z.infer<typeof MCPServer>[]> {
await projectAuthCheck(projectId);
const result = await callMcpTool(projectId, mcpServerName, toolName, parameters);
return result;
const project = await projectsCollection.findOne({
_id: projectId,
});
return project?.mcpServers ?? [];
}

View file

@ -4,10 +4,9 @@ import { z } from "zod";
import { ObjectId } from "mongodb";
import { authCheck } from "../../utils";
import { ApiRequest, ApiResponse } from "../../../../lib/types/types";
import { AgenticAPIChatRequest, AgenticAPIChatMessage, convertFromAgenticApiToApiMessages, convertFromApiToAgenticApiMessages, convertWorkflowToAgenticAPI } from "../../../../lib/types/agents_api_types";
import { getAgenticApiResponse, callClientToolWebhook, runRAGToolCall, mockToolResponse, callMcpTool } from "../../../../lib/utils";
import { AgenticAPIChatRequest, convertFromAgenticApiToApiMessages, convertFromApiToAgenticApiMessages, convertWorkflowToAgenticAPI } from "../../../../lib/types/agents_api_types";
import { getAgenticApiResponse } from "../../../../lib/utils";
import { check_query_limit } from "../../../../lib/rate_limiting";
import { apiV1 } from "rowboat-shared";
import { PrefixLogger } from "../../../../lib/utils";
import { TestProfile } from "@/app/lib/types/testing_types";
@ -70,7 +69,7 @@ export async function POST(
logger.log(`Workflow ${workflowId} not found for project ${projectId}`);
return Response.json({ error: "Workflow not found" }, { status: 404 });
}
// if test profile is provided in the request, use it
let testProfile: z.infer<typeof TestProfile> | null = null;
if (result.data.testProfileId) {
@ -84,138 +83,30 @@ export async function POST(
}
}
// if profile has a context available, overwrite the system message in the request (if there is one)
let currentMessages = reqMessages;
if (testProfile?.context) {
// if there is a system message, overwrite it
const systemMessageIndex = reqMessages.findIndex(m => m.role === "system");
if (systemMessageIndex !== -1) {
currentMessages[systemMessageIndex].content = testProfile.context;
} else {
// if there is no system message, add one
currentMessages.unshift({ role: "system", content: testProfile.context });
}
}
const MAX_TURNS = result.data.maxTurns ?? 3;
let currentState: unknown = reqState ?? { last_agent_name: workflow.agents[0].name };
let turns = 0;
let hasToolCalls = false;
do {
hasToolCalls = false;
// get assistant response
const { agents, tools, prompts, startAgent } = convertWorkflowToAgenticAPI(workflow);
const request: z.infer<typeof AgenticAPIChatRequest> = {
messages: convertFromApiToAgenticApiMessages(currentMessages),
state: currentState,
agents,
tools,
prompts,
startAgent,
};
// get assistant response
const { agents, tools, prompts, startAgent } = convertWorkflowToAgenticAPI(workflow);
const request: z.infer<typeof AgenticAPIChatRequest> = {
projectId,
messages: convertFromApiToAgenticApiMessages(reqMessages),
state: currentState,
agents,
tools,
prompts,
startAgent,
testProfile: testProfile ?? undefined,
mcpServers: project.mcpServers ?? undefined,
toolWebhookUrl: project.webhookUrl ?? undefined,
};
console.log(`turn ${turns}: sending agentic request from /chat api`, JSON.stringify(request, null, 2));
logger.log(`Processing turn ${turns} for conversation`);
const { messages: agenticMessages, state } = await getAgenticApiResponse(request);
const newMessages = convertFromAgenticApiToApiMessages(agenticMessages);
currentState = state;
// if tool calls are to be skipped, return immediately
if (result.data.skipToolCalls) {
logger.log('Skipping tool calls as requested');
const responseBody: z.infer<typeof ApiResponse> = {
messages: newMessages,
state: currentState,
};
return Response.json(responseBody);
}
// get last message to check for tool calls
const lastMessage = newMessages[newMessages.length - 1];
if (lastMessage?.role === "assistant" &&
'tool_calls' in lastMessage &&
lastMessage.tool_calls?.length > 0) {
hasToolCalls = true;
const toolCallResultMessages: z.infer<typeof apiV1.ToolMessage>[] = [];
// Process tool calls
for (const toolCall of lastMessage.tool_calls) {
let result: unknown;
if (toolCall.function.name === "getArticleInfo") {
logger.log(`Running RAG tool call for agent ${lastMessage.agenticSender}`);
// find the source ids attached to this agent in the workflow
const agent = workflow.agents.find(a => a.name === lastMessage.agenticSender);
if (!agent) {
return Response.json({ error: "Agent not found" }, { status: 404 });
}
const sourceIds = agent.ragDataSources;
if (!sourceIds) {
return Response.json({ error: "Agent has no data sources" }, { status: 404 });
}
try {
result = await runRAGToolCall(projectId, toolCall.function.arguments, sourceIds, agent.ragReturnType, agent.ragK);
logger.log(`RAG tool call completed for agent ${lastMessage.agenticSender}`);
} catch (e) {
logger.log(`Error running RAG tool call: ${e}`);
return Response.json({ error: "Error running RAG tool call" }, { status: 500 });
}
} else {
logger.log(`Processing tool call ${toolCall.function.name}`);
try {
// if tool is supposed to be mocked, mock it
const workflowTool = workflow.tools.find(t => t.name === toolCall.function.name);
if (testProfile?.mockTools || workflowTool?.mockTool) {
logger.log(`Mocking tool call ${toolCall.function.name}`);
result = await mockToolResponse(toolCall.id, currentMessages, testProfile?.mockPrompt || workflowTool?.mockInstructions || '');
} else if (workflowTool?.isMcp) {
// else run the tool call by calling the MCP tool
logger.log(`Calling MCP tool: ${toolCall.function.name}`);
result = await callMcpTool(projectId, workflowTool.mcpServerName ?? 'default', toolCall.function.name, JSON.parse(toolCall.function.arguments));
} else {
// else run the tool call by calling the client tool webhook
logger.log(`Running client tool webhook for tool ${toolCall.function.name}`);
result = await callClientToolWebhook(
toolCall,
currentMessages,
projectId,
);
}
} catch (e) {
logger.log(`Error in tool call ${toolCall.function.name}: ${e}`);
return Response.json({ error: `Error in tool call ${toolCall.function.name}` }, { status: 500 });
}
logger.log(`Tool call ${toolCall.function.name} completed`);
}
toolCallResultMessages.push({
role: "tool",
tool_call_id: toolCall.id,
content: JSON.stringify(result),
tool_name: toolCall.function.name,
});
}
// Add new messages to the conversation
currentMessages = [...currentMessages, ...newMessages, ...toolCallResultMessages];
} else {
// No tool calls, just add the new messages
currentMessages = [...currentMessages, ...newMessages];
}
turns++;
if (turns >= MAX_TURNS && hasToolCalls) {
logger.log(`Max turns (${MAX_TURNS}) reached for conversation`);
return Response.json({ error: "Max turns reached" }, { status: 429 });
}
} while (hasToolCalls);
const { messages: agenticMessages, state } = await getAgenticApiResponse(request);
const newMessages = convertFromAgenticApiToApiMessages(agenticMessages);
const newState = state;
const responseBody: z.infer<typeof ApiResponse> = {
messages: currentMessages.slice(reqMessages.length),
state: currentState,
messages: newMessages,
state: newState,
};
return Response.json(responseBody);
});

View file

@ -0,0 +1,45 @@
export async function GET(request: Request, { params }: { params: { streamId: string } }) {
// Replace with your actual upstream SSE endpoint.
const upstreamUrl = `${process.env.AGENTS_API_URL}/chat_stream/${params.streamId}`;
console.log('upstreamUrl', upstreamUrl);
// Fetch the upstream SSE stream.
const upstreamResponse = await fetch(upstreamUrl, {
headers: {
'Authorization': `Bearer ${process.env.AGENTS_API_KEY || 'test'}`,
},
cache: 'no-store',
});
// If the upstream request fails, return a 502 Bad Gateway.
if (!upstreamResponse.ok || !upstreamResponse.body) {
return new Response("Error connecting to upstream SSE stream", { status: 502 });
}
const reader = upstreamResponse.body.getReader();
const stream = new ReadableStream({
async start(controller) {
try {
// Read from the upstream stream continuously.
while (true) {
const { done, value } = await reader.read();
if (done) break;
// Immediately enqueue each received chunk.
controller.enqueue(value);
}
controller.close();
} catch (error) {
controller.error(error);
}
},
});
return new Response(stream, {
headers: {
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache",
"Connection": "keep-alive",
},
});
}

View file

@ -8,13 +8,10 @@ import { convertFromAgenticAPIChatMessages } from "../../../../../../lib/types/a
import { convertToAgenticAPIChatMessages } from "../../../../../../lib/types/agents_api_types";
import { convertWorkflowToAgenticAPI } from "../../../../../../lib/types/agents_api_types";
import { AgenticAPIChatRequest } from "../../../../../../lib/types/agents_api_types";
import { callClientToolWebhook, getAgenticApiResponse, runRAGToolCall, mockToolResponse, callMcpTool } from "../../../../../../lib/utils";
import { getAgenticApiResponse } from "../../../../../../lib/utils";
import { check_query_limit } from "../../../../../../lib/rate_limiting";
import { PrefixLogger } from "../../../../../../lib/utils";
// Add max turns constant at the top with other constants
const MAX_TURNS = 3;
// get next turn / agent response
export async function POST(
req: NextRequest,
@ -23,7 +20,7 @@ export async function POST(
return await authCheck(req, async (session) => {
const { chatId } = await params;
const logger = new PrefixLogger(`widget-chat:${chatId}`);
logger.log(`Processing turn request for chat ${chatId}`);
// check query limit
@ -95,109 +92,33 @@ export async function POST(
// get assistant response
const { agents, tools, prompts, startAgent } = convertWorkflowToAgenticAPI(workflow);
const unsavedMessages: z.infer<typeof apiV1.ChatMessage>[] = [userMessage];
let resolvingToolCalls = true;
let state: unknown = chat.agenticState ?? {last_agent_name: startAgent};
let turns = 0; // Add turns counter
let state: unknown = chat.agenticState ?? { last_agent_name: startAgent };
while (resolvingToolCalls) {
if (turns >= MAX_TURNS) {
logger.log(`Max turns (${MAX_TURNS}) reached for chat ${chatId}`);
throw new Error("Max turns reached");
}
turns++;
const request: z.infer<typeof AgenticAPIChatRequest> = {
messages: convertToAgenticAPIChatMessages([systemMessage, ...messages, ...unsavedMessages]),
state,
agents,
tools,
prompts,
startAgent,
};
logger.log(`Turn ${turns}: sending agentic request`);
const response = await getAgenticApiResponse(request);
state = response.state;
if (response.messages.length === 0) {
throw new Error("No messages returned from assistant");
}
const convertedMessages = convertFromAgenticAPIChatMessages(response.messages);
unsavedMessages.push(...convertedMessages.map(m => ({
...m,
version: 'v1' as const,
chatId,
createdAt: new Date().toISOString(),
})));
// if the last messages is tool call, execute them
const lastMessage = convertedMessages[convertedMessages.length - 1];
if (lastMessage.role === 'assistant' && 'tool_calls' in lastMessage) {
logger.log(`Processing ${lastMessage.tool_calls.length} tool calls`);
const toolCallResults = await Promise.all(lastMessage.tool_calls.map(async toolCall => {
logger.log(`Executing tool call: ${toolCall.function.name}`);
try {
if (toolCall.function.name === "getArticleInfo") {
logger.log(`Processing RAG tool call for agent ${lastMessage.agenticSender}`);
const agent = workflow.agents.find(a => a.name === lastMessage.agenticSender);
if (!agent || !agent.ragDataSources) {
throw new Error("Agent not found or has no data sources");
}
return await runRAGToolCall(
session.projectId,
toolCall.function.arguments,
agent.ragDataSources,
agent.ragReturnType,
agent.ragK
);
}
const workflowTool = workflow.tools.find(t => t.name === toolCall.function.name);
if (workflowTool?.mockTool) {
logger.log(`Using mock response for tool: ${toolCall.function.name}`);
return await mockToolResponse(
toolCall.id,
[...messages, ...unsavedMessages],
workflowTool.mockInstructions || ''
);
} else if (workflowTool?.isMcp) {
logger.log(`Calling MCP tool: ${toolCall.function.name}`);
return await callMcpTool(
session.projectId,
workflowTool.mcpServerName ?? 'default',
toolCall.function.name,
JSON.parse(toolCall.function.arguments)
);
} else {
logger.log(`Calling webhook for tool: ${toolCall.function.name}`);
return await callClientToolWebhook(
toolCall,
[...messages, ...unsavedMessages],
session.projectId,
);
}
} catch (error) {
logger.log(`Error executing tool call ${toolCall.id}: ${error}`);
return { error: "Tool execution failed" };
}
}));
unsavedMessages.push(...toolCallResults.map((result, index) => ({
version: 'v1' as const,
chatId,
createdAt: new Date().toISOString(),
role: 'tool' as const,
tool_call_id: lastMessage.tool_calls[index].id,
tool_name: lastMessage.tool_calls[index].function.name,
content: JSON.stringify(result),
})));
} else {
// ensure that the last message is from an assistant
// and is of an external type
if (lastMessage.role !== 'assistant' || lastMessage.agenticResponseType !== 'external') {
throw new Error("Last message is not from an assistant and is not of an external type");
}
resolvingToolCalls = false;
break;
}
const request: z.infer<typeof AgenticAPIChatRequest> = {
projectId: session.projectId,
messages: convertToAgenticAPIChatMessages([systemMessage, ...messages, ...unsavedMessages]),
state,
agents,
tools,
prompts,
startAgent,
mcpServers: projectSettings.mcpServers ?? undefined,
toolWebhookUrl: projectSettings.webhookUrl ?? undefined,
testProfile: undefined,
};
logger.log(`Sending agentic request`);
const response = await getAgenticApiResponse(request);
state = response.state;
if (response.messages.length === 0) {
throw new Error("No messages returned from assistant");
}
const convertedMessages = convertFromAgenticAPIChatMessages(response.messages);
unsavedMessages.push(...convertedMessages.map(m => ({
...m,
version: 'v1' as const,
chatId,
createdAt: new Date().toISOString(),
})));
logger.log(`Saving ${unsavedMessages.length} new messages and updating chat state`);
await chatMessagesCollection.insertMany(unsavedMessages);

View file

@ -1,7 +1,9 @@
import { z } from "zod";
import { ConnectedEntity, sanitizeTextWithMentions, Workflow, WorkflowAgent, WorkflowPrompt, WorkflowTool } from "./workflow_types";
import { sanitizeTextWithMentions, Workflow, WorkflowAgent, WorkflowPrompt, WorkflowTool } from "./workflow_types";
import { apiV1 } from "rowboat-shared";
import { ApiMessage } from "./types";
import { TestProfile } from "./testing_types";
import { MCPServer } from "./types";
export const AgenticAPIChatMessage = z.object({
role: z.union([z.literal('user'), z.literal('assistant'), z.literal('tool'), z.literal('system')]),
@ -30,12 +32,8 @@ export const AgenticAPIAgent = WorkflowAgent
locked: true,
toggleAble: true,
global: true,
ragDataSources: true,
ragReturnType: true,
ragK: true,
})
.extend({
hasRagSources: z.boolean().default(false).optional(),
tools: z.array(z.string()),
prompts: z.array(z.string()),
connectedAgents: z.array(z.string()),
@ -43,18 +41,22 @@ export const AgenticAPIAgent = WorkflowAgent
export const AgenticAPIPrompt = WorkflowPrompt;
export const AgenticAPITool = WorkflowTool.omit({
mockTool: true,
autoSubmitMockedResponse: true,
});
export const AgenticAPITool = WorkflowTool
.omit({
autoSubmitMockedResponse: true,
})
export const AgenticAPIChatRequest = z.object({
projectId: z.string(),
messages: z.array(AgenticAPIChatMessage),
state: z.unknown(),
agents: z.array(AgenticAPIAgent),
tools: z.array(AgenticAPITool),
prompts: z.array(WorkflowPrompt),
startAgent: z.string(),
testProfile: TestProfile.optional(),
mcpServers: z.array(MCPServer).optional(),
toolWebhookUrl: z.string().optional(),
});
export const AgenticAPIChatResponse = z.object({
@ -62,6 +64,10 @@ export const AgenticAPIChatResponse = z.object({
state: z.unknown(),
});
export const AgenticAPIInitStreamResponse = z.object({
streamId: z.string(),
});
export function convertWorkflowToAgenticAPI(workflow: z.infer<typeof Workflow>): {
agents: z.infer<typeof AgenticAPIAgent>[];
tools: z.infer<typeof AgenticAPITool>[];
@ -82,8 +88,10 @@ export function convertWorkflowToAgenticAPI(workflow: z.infer<typeof Workflow>):
description: agent.description,
instructions: sanitized,
model: agent.model,
hasRagSources: agent.ragDataSources ? agent.ragDataSources.length > 0 : false,
controlType: agent.controlType,
ragDataSources: agent.ragDataSources,
ragK: agent.ragK,
ragReturnType: agent.ragReturnType,
tools: entities.filter(e => e.type == 'tool').map(e => e.name),
prompts: entities.filter(e => e.type == 'prompt').map(e => e.name),
connectedAgents: entities.filter(e => e.type === 'agent').map(e => e.name),
@ -91,10 +99,8 @@ export function convertWorkflowToAgenticAPI(workflow: z.infer<typeof Workflow>):
return agenticAgent;
}),
tools: workflow.tools.map(tool => {
const { mockTool, autoSubmitMockedResponse, ...rest } = tool;
return {
...rest,
};
const { autoSubmitMockedResponse, ...rest } = tool;
return rest;
}),
prompts: workflow.prompts
.map(p => {

View file

@ -1,4 +1,5 @@
import { z } from "zod";
import { MCPServer } from "./types";
export const Project = z.object({
_id: z.string().uuid(),
@ -12,10 +13,7 @@ export const Project = z.object({
publishedWorkflowId: z.string().optional(),
nextWorkflowNumber: z.number().optional(),
testRunCounter: z.number().default(0),
mcpServers: z.array(z.object({
name: z.string(),
url: z.string(),
})).optional(),
mcpServers: z.array(MCPServer).optional(),
});
export const ProjectMember = z.object({

View file

@ -2,6 +2,11 @@ import { CoreMessage, ToolCallPart } from "ai";
import { z } from "zod";
import { apiV1 } from "rowboat-shared";
export const MCPServer = z.object({
name: z.string(),
url: z.string(),
});
export const PlaygroundChat = z.object({
createdAt: z.string().datetime(),
projectId: z.string(),

View file

@ -1,136 +1,8 @@
import { convertFromAgenticAPIChatMessages } from "./types/agents_api_types";
import { ClientToolCallRequest } from "./types/tool_types";
import { ClientToolCallJwt, GetInformationToolResult } from "./types/tool_types";
import { ClientToolCallRequestBody } from "./types/tool_types";
import { AgenticAPIChatResponse } from "./types/agents_api_types";
import { AgenticAPIChatRequest } from "./types/agents_api_types";
import { Workflow, WorkflowAgent } from "./types/workflow_types";
import { AgenticAPIChatMessage } from "./types/agents_api_types";
import { AgenticAPIChatResponse, AgenticAPIChatRequest, AgenticAPIChatMessage, AgenticAPIInitStreamResponse } from "./types/agents_api_types";
import { z } from "zod";
import { dataSourceDocsCollection, dataSourcesCollection, projectsCollection } from "./mongodb";
import { apiV1 } from "rowboat-shared";
import { SignJWT } from "jose";
import crypto from "crypto";
import { ObjectId } from "mongodb";
import { embeddingModel } from "./embedding";
import { embed, generateObject } from "ai";
import { qdrantClient } from "./qdrant";
import { EmbeddingRecord } from "./types/datasource_types";
import { generateObject } from "ai";
import { ApiMessage } from "./types/types";
import { openai } from "@ai-sdk/openai";
import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
export async function callMcpTool(
projectId: string,
mcpServerName: string,
toolName: string,
parameters: Record<string, unknown>,
): Promise<unknown> {
const project = await projectsCollection.findOne({
"_id": projectId,
});
if (!project) {
throw new Error('Project not found');
}
const mcpServer = project.mcpServers?.find(s => s.name === mcpServerName);
if (!mcpServer) {
throw new Error('MCP server not found');
}
const transport = new SSEClientTransport(new URL(mcpServer.url));
const client = new Client(
{
name: "rowboat-client",
version: "1.0.0"
},
{
capabilities: {
prompts: {},
resources: {},
tools: {}
}
}
);
await client.connect(transport);
const result = await client.callTool({
name: toolName,
arguments: parameters,
});
await client.close();
return result;
}
export async function callClientToolWebhook(
toolCall: z.infer<typeof apiV1.AssistantMessageWithToolCalls>['tool_calls'][number],
messages: z.infer<typeof ApiMessage>[],
projectId: string,
): Promise<unknown> {
const project = await projectsCollection.findOne({
"_id": projectId,
});
if (!project) {
throw new Error('Project not found');
}
if (!project.webhookUrl) {
throw new Error('Webhook URL not found');
}
// prepare request body
const content = JSON.stringify({
toolCall,
messages,
} as z.infer<typeof ClientToolCallRequestBody>);
const requestId = crypto.randomUUID();
const bodyHash = crypto
.createHash('sha256')
.update(content, 'utf8')
.digest('hex');
// sign request
const jwt = await new SignJWT({
requestId,
projectId,
bodyHash,
} as z.infer<typeof ClientToolCallJwt>)
.setProtectedHeader({
alg: 'HS256',
typ: 'JWT',
})
.setIssuer('rowboat')
.setAudience(project.webhookUrl)
.setSubject(`tool-call-${toolCall.id}`)
.setJti(requestId)
.setIssuedAt()
.setExpirationTime("5 minutes")
.sign(new TextEncoder().encode(project.secret));
// make request
const request: z.infer<typeof ClientToolCallRequest> = {
requestId,
content,
};
const response = await fetch(project.webhookUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-signature-jwt': jwt,
},
body: JSON.stringify(request),
});
if (!response.ok) {
throw new Error(`Failed to call webhook: ${response.status}: ${response.statusText}`);
}
const responseBody = await response.json();
return responseBody;
}
export async function getAgenticApiResponse(
request: z.infer<typeof AgenticAPIChatRequest>,
@ -163,85 +35,29 @@ export async function getAgenticApiResponse(
};
}
export async function runRAGToolCall(
projectId: string,
query: string,
sourceIds: string[],
returnType: z.infer<typeof WorkflowAgent>['ragReturnType'],
k: number,
): Promise<z.infer<typeof GetInformationToolResult>> {
// create embedding for question
const embedResult = await embed({
model: embeddingModel,
value: query,
});
// fetch all data sources for this project
const sources = await dataSourcesCollection.find({
projectId: projectId,
active: true,
}).toArray();
const validSourceIds = sources
.filter(s => sourceIds.includes(s._id.toString())) // id should be in sourceIds
.filter(s => s.active) // should be active
.map(s => s._id.toString());
// if no sources found, return empty response
if (validSourceIds.length === 0) {
return {
results: [],
};
}
// perform qdrant vector search
const qdrantResults = await qdrantClient.query("embeddings", {
query: embedResult.embedding,
filter: {
must: [
{ key: "projectId", match: { value: projectId } },
{ key: "sourceId", match: { any: validSourceIds } },
],
export async function getAgenticResponseStreamId(
request: z.infer<typeof AgenticAPIChatRequest>,
): Promise<z.infer<typeof AgenticAPIInitStreamResponse>> {
// call agentic api
console.log(`sending agentic api init stream request`, JSON.stringify(request));
const response = await fetch(process.env.AGENTS_API_URL + '/chat_stream_init', {
method: 'POST',
body: JSON.stringify(request),
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${process.env.AGENTS_API_KEY || 'test'}`,
},
limit: k,
with_payload: true,
});
// if return type is chunks, return the chunks
let results = qdrantResults.points.map((point) => {
const { title, name, content, docId, sourceId } = point.payload as z.infer<typeof EmbeddingRecord>['payload'];
return {
title,
name,
content,
docId,
sourceId,
};
});
if (returnType === 'chunks') {
return {
results,
};
if (!response.ok) {
console.error('Failed to call agentic init stream api', response);
throw new Error(`Failed to call agentic init stream api: ${response.statusText}`);
}
// otherwise, fetch the doc contents from mongodb
const docs = await dataSourceDocsCollection.find({
_id: { $in: results.map(r => new ObjectId(r.docId)) },
}).toArray();
// map the results to the docs
results = results.map(r => {
const doc = docs.find(d => d._id.toString() === r.docId);
return {
...r,
content: doc?.content || '',
};
});
return {
results,
};
const responseJson = await response.json();
console.log(`received agentic api init stream response`, JSON.stringify(responseJson));
const result: z.infer<typeof AgenticAPIInitStreamResponse> = responseJson;
return result;
}
// create a PrefixLogger class that wraps console.log with a prefix
// and allows chaining with a parent logger
export class PrefixLogger {

View file

@ -1,7 +1,7 @@
'use client';
import { useState } from "react";
import { z } from "zod";
import { PlaygroundChat } from "../../../lib/types/types";
import { MCPServer, PlaygroundChat } from "../../../lib/types/types";
import { Workflow } from "../../../lib/types/workflow_types";
import { Chat } from "./chat";
import { ActionButton, Pane } from "../workflow/pane";
@ -17,11 +17,15 @@ export function App({
projectId,
workflow,
messageSubscriber,
mcpServerUrls,
toolWebhookUrl,
}: {
hidden?: boolean;
projectId: string;
workflow: z.infer<typeof Workflow>;
messageSubscriber?: (messages: z.infer<typeof apiV1.ChatMessage>[]) => void;
mcpServerUrls: Array<z.infer<typeof MCPServer>>;
toolWebhookUrl: string;
}) {
const [counter, setCounter] = useState<number>(0);
const [testProfile, setTestProfile] = useState<z.infer<typeof TestProfile> | null>(null);
@ -44,10 +48,6 @@ export function App({
setCounter(counter + 1);
}
if (hidden) {
return <></>;
}
function handleNewChatButtonClick() {
setCounter(counter + 1);
setChat({
@ -59,6 +59,10 @@ export function App({
});
}
if (hidden) {
return <></>;
}
return (
<Pane
title="PLAYGROUND"
@ -84,6 +88,8 @@ export function App({
onTestProfileChange={handleTestProfileChange}
systemMessage={systemMessage}
onSystemMessageChange={handleSystemMessageChange}
mcpServerUrls={mcpServerUrls}
toolWebhookUrl={toolWebhookUrl}
/>
</div>
</Pane>

View file

@ -1,10 +1,10 @@
'use client';
import { getAssistantResponse, simulateUserResponse } from "../../../actions/actions";
import { useEffect, useState } from "react";
import { getAssistantResponseStreamId } from "../../../actions/actions";
import { useEffect, useOptimistic, useState } from "react";
import { Messages } from "./messages";
import z from "zod";
import { PlaygroundChat } from "../../../lib/types/types";
import { convertToAgenticAPIChatMessages } from "../../../lib/types/agents_api_types";
import { MCPServer, PlaygroundChat } from "../../../lib/types/types";
import { AgenticAPIChatMessage, convertFromAgenticAPIChatMessages, convertToAgenticAPIChatMessages } from "../../../lib/types/agents_api_types";
import { convertWorkflowToAgenticAPI } from "../../../lib/types/agents_api_types";
import { AgenticAPIChatRequest } from "../../../lib/types/agents_api_types";
import { Workflow } from "../../../lib/types/workflow_types";
@ -15,17 +15,19 @@ import { CopyAsJsonButton } from "./copy-as-json-button";
import { TestProfile } from "@/app/lib/types/testing_types";
import { ProfileSelector } from "@/app/projects/[projectId]/test/[[...slug]]/components/selectors/profile-selector";
import { WithStringId } from "@/app/lib/types/types";
import { XCircleIcon, XIcon } from "lucide-react";
import { XIcon } from "lucide-react";
export function Chat({
chat,
projectId,
workflow,
messageSubscriber,
testProfile=null,
testProfile = null,
onTestProfileChange,
systemMessage,
onSystemMessageChange,
mcpServerUrls,
toolWebhookUrl,
}: {
chat: z.infer<typeof PlaygroundChat>;
projectId: string;
@ -35,11 +37,11 @@ export function Chat({
onTestProfileChange: (profile: WithStringId<z.infer<typeof TestProfile>> | null) => void;
systemMessage: string;
onSystemMessageChange: (message: string) => void;
mcpServerUrls: Array<z.infer<typeof MCPServer>>;
toolWebhookUrl: string;
}) {
const [messages, setMessages] = useState<z.infer<typeof apiV1.ChatMessage>[]>(chat.messages);
const [loadingAssistantResponse, setLoadingAssistantResponse] = useState<boolean>(false);
const [loadingUserResponse, setLoadingUserResponse] = useState<boolean>(false);
const [simulationComplete, setSimulationComplete] = useState<boolean>(chat.simulationComplete || false);
const [agenticState, setAgenticState] = useState<unknown>(chat.agenticState || {
last_agent_name: workflow.startAgent,
});
@ -47,6 +49,12 @@ export function Chat({
const [lastAgenticRequest, setLastAgenticRequest] = useState<unknown | null>(null);
const [lastAgenticResponse, setLastAgenticResponse] = useState<unknown | null>(null);
const [isProfileSelectorOpen, setIsProfileSelectorOpen] = useState(false);
const [optimisticMessages, setOptimisticMessages] = useState<z.infer<typeof apiV1.ChatMessage>[]>(chat.messages);
// reset optimistic messages when messages change
useEffect(() => {
setOptimisticMessages(messages);
}, [messages]);
// collect published tool call results
const toolCallResults: Record<string, z.infer<typeof apiV1.ToolMessage>> = {};
@ -55,6 +63,7 @@ export function Chat({
.forEach((message) => {
toolCallResults[message.tool_call_id] = message;
});
console.log('toolCallResults', toolCallResults);
function handleUserMessage(prompt: string) {
const updatedMessages: z.infer<typeof apiV1.ChatMessage>[] = [...messages, {
@ -68,15 +77,6 @@ export function Chat({
setFetchResponseError(null);
}
function handleToolCallResults(results: z.infer<typeof apiV1.ToolMessage>[]) {
setMessages([...messages, ...results.map((result) => ({
...result,
version: 'v1' as const,
chatId: '',
createdAt: new Date().toISOString(),
}))]);
}
// reset state when workflow changes
useEffect(() => {
setMessages([]);
@ -92,15 +92,19 @@ export function Chat({
}
}, [messages, messageSubscriber]);
// get agent response
// get assistant response
useEffect(() => {
console.log('stream useEffect called');
let ignore = false;
let eventSource: EventSource | null = null;
let msgs: z.infer<typeof apiV1.ChatMessage>[] = [];
async function process() {
setLoadingAssistantResponse(true);
setFetchResponseError(null);
const { agents, tools, prompts, startAgent } = convertWorkflowToAgenticAPI(workflow);
const request: z.infer<typeof AgenticAPIChatRequest> = {
projectId,
messages: convertToAgenticAPIChatMessages([{
role: 'system',
content: systemMessage || '',
@ -113,43 +117,83 @@ export function Chat({
tools,
prompts,
startAgent,
mcpServers: mcpServerUrls,
toolWebhookUrl: toolWebhookUrl,
testProfile: testProfile ?? undefined,
};
setLastAgenticRequest(null);
setLastAgenticResponse(null);
let streamId: string | null = null;
try {
const response = await getAssistantResponse(projectId, request);
const response = await getAssistantResponseStreamId(request);
if (ignore) {
return;
}
if (simulationComplete) {
return;
}
setLastAgenticRequest(response.rawRequest);
setLastAgenticResponse(response.rawResponse);
setMessages([...messages, ...response.messages.map((message) => ({
...message,
version: 'v1' as const,
chatId: '',
createdAt: new Date().toISOString(),
}))]);
setAgenticState(response.state);
streamId = response.streamId;
} catch (err) {
if (!ignore) {
setFetchResponseError(`Failed to get assistant response: ${err instanceof Error ? err.message : 'Unknown error'}`);
}
} finally {
if (!ignore) {
setLoadingAssistantResponse(false);
}
}
if (ignore || !streamId) {
console.log('almost there', ignore, streamId);
return;
}
// log the stream id
console.log('🔄 got assistant response', streamId);
// read from SSE stream
eventSource = new EventSource(`/api/v1/stream-response/${streamId}`);
eventSource.addEventListener("message", (event) => {
if (ignore) {
return;
}
try {
const data = JSON.parse(event.data);
const msg = AgenticAPIChatMessage.parse(data);
const parsedMsg = convertFromAgenticAPIChatMessages([msg])[0];
console.log('🔄 got assistant response chunk', parsedMsg);
msgs.push(parsedMsg);
setOptimisticMessages(prev => [...prev, parsedMsg]);
} catch (err) {
console.error('Failed to parse SSE message:', err);
setFetchResponseError(`Failed to parse SSE message: ${err instanceof Error ? err.message : 'Unknown error'}`);
setOptimisticMessages(messages);
}
});
eventSource.addEventListener('done', (event) => {
if (eventSource) {
eventSource.close();
}
console.log('🔄 got assistant response done', event.data);
const parsed: {state: unknown} = JSON.parse(event.data);
setAgenticState(parsed.state);
setMessages([...messages, ...msgs]);
setLoadingAssistantResponse(false);
});
eventSource.onerror = (error) => {
console.error('SSE Error:', error);
if (!ignore) {
setLoadingAssistantResponse(false);
setFetchResponseError('Stream connection failed');
setOptimisticMessages(messages);
}
};
}
// if last message is not from role user
// or tool, return
// if last message is not a user message, return
if (messages.length > 0) {
const last = messages[messages.length - 1];
if (last.role !== 'user' && last.role !== 'tool') {
if (last.role !== 'user') {
return;
}
}
@ -163,106 +207,22 @@ export function Chat({
return () => {
ignore = true;
};
}, [chat.simulated, messages, projectId, agenticState, workflow, fetchResponseError, systemMessage, simulationComplete]);
// simulate user turn
useEffect(() => {
let ignore = false;
async function process() {
if (chat.simulationScenario === undefined) {
return;
console.log('stream useEffect cleanup called');
if (eventSource) {
eventSource.close();
}
// fetch next user prompt
setLoadingUserResponse(true);
try {
const response = await simulateUserResponse(projectId, messages, chat.simulationScenario)
if (ignore) {
return;
}
if (simulationComplete) {
return;
}
if (response.trim() === 'EXIT') {
setSimulationComplete(true);
return;
}
setMessages([...messages, {
role: 'user',
content: response,
version: 'v1' as const,
chatId: '',
createdAt: new Date().toISOString(),
}]);
setFetchResponseError(null);
} catch (err) {
setFetchResponseError(`Failed to simulate user response: ${err instanceof Error ? err.message : 'Unknown error'}`);
} finally {
setLoadingUserResponse(false);
}
}
// proceed only if chat is simulated
if (!chat.simulated) {
return;
}
// dont proceed if simulation is complete
if (chat.simulated && simulationComplete) {
return;
}
// check if there are no messages yet OR
// check if the last message is an assistant
// message containing a text response. If so,
// call the simulate user turn api to fetch
// user response
let last = messages[messages.length - 1];
if (last && last.role !== 'assistant') {
return;
}
if (last && 'tool_calls' in last) {
return;
}
process();
return () => {
ignore = true;
};
}, [chat.simulated, messages, projectId, simulationComplete, chat.simulationScenario]);
// save chat on every assistant message
// useEffect(() => {
// let ignore = false;
// function process() {
// savePlaygroundChat(projectId, {
// ...chat,
// messages,
// simulationComplete,
// agenticState,
// }, chatId)
// .then((insertedChatId) => {
// if (!chatId) {
// setChatId(insertedChatId);
// }
// });
// }
// if (messages.length === 0) {
// return;
// }
// const lastMessage = messages[messages.length - 1];
// if (lastMessage && lastMessage.role !== 'assistant') {
// return;
// }
// process();
// }, [chatId, chat, messages, projectId, simulationComplete, agenticState]);
}, [
messages,
projectId,
agenticState,
workflow,
systemMessage,
mcpServerUrls,
toolWebhookUrl,
testProfile,
fetchResponseError,
]);
const handleCopyChat = () => {
const jsonString = JSON.stringify({
@ -301,11 +261,9 @@ export function Chat({
/>
<Messages
projectId={projectId}
messages={messages}
messages={optimisticMessages}
toolCallResults={toolCallResults}
handleToolCallResults={handleToolCallResults}
loadingAssistantResponse={loadingAssistantResponse}
loadingUserResponse={loadingUserResponse}
workflow={workflow}
testProfile={testProfile}
systemMessage={systemMessage}
@ -326,26 +284,12 @@ export function Chat({
</Button>
</div>
)}
{!chat.simulated && <div className="max-w-[768px] mx-auto">
<div className="max-w-[768px] mx-auto">
<ComposeBox
handleUserMessage={handleUserMessage}
messages={messages}
/>
</div>}
{chat.simulated && !simulationComplete && <div className="p-2 bg-gray-50 border border-gray-200 flex items-center justify-center gap-2">
<Spinner size="sm" />
<div className="text-sm text-gray-500 animate-pulse">Simulating...</div>
<Button
size="sm"
color="danger"
onPress={() => {
setSimulationComplete(true);
}}
>
Stop
</Button>
</div>}
{chat.simulated && simulationComplete && <p className="text-center text-sm">Simulation complete.</p>}
</div>
</div>
</div>;
}

View file

@ -1,17 +1,14 @@
'use client';
import { Button, Spinner, Textarea } from "@heroui/react";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { Spinner } from "@heroui/react";
import { useEffect, useMemo, useRef, useState } from "react";
import z from "zod";
import { Workflow } from "../../../lib/types/workflow_types";
import { WorkflowTool } from "../../../lib/types/workflow_types";
import { GetInformationToolResult } from "../../../lib/types/tool_types";
import { executeClientTool, getInformationTool, suggestToolResponse } from "../../../actions/actions";
import MarkdownContent from "../../../lib/components/markdown-content";
import { apiV1 } from "rowboat-shared";
import { EditableField } from "../../../lib/components/editable-field";
import { MessageSquareIcon, EllipsisIcon, CircleCheckIcon, ChevronRightIcon, ChevronDownIcon, XIcon } from "lucide-react";
import { TestProfile } from "@/app/lib/types/testing_types";
import { executeMcpTool } from "@/app/actions/mcp_actions";
function UserMessage({ content }: { content: string }) {
return <div className="self-end ml-[30%] flex flex-col">
@ -74,21 +71,9 @@ function AssistantMessageLoading() {
</div>;
}
function UserMessageLoading() {
return <div className="self-end ml-[30%] flex flex-col">
<div className="text-right text-gray-500 dark:text-gray-400 text-sm mr-3">
User
</div>
<div className="bg-gray-100 dark:bg-gray-800 p-3 rounded-lg rounded-br-none animate-pulse w-20 text-gray-800 dark:text-gray-200">
<Spinner size="sm" />
</div>
</div>;
}
function ToolCalls({
toolCalls,
results,
handleResults,
projectId,
messages,
sender,
@ -98,7 +83,6 @@ function ToolCalls({
}: {
toolCalls: z.infer<typeof apiV1.AssistantMessageWithToolCalls>['tool_calls'];
results: Record<string, z.infer<typeof apiV1.ToolMessage>>;
handleResults: (results: z.infer<typeof apiV1.ToolMessage>[]) => void;
projectId: string;
messages: z.infer<typeof apiV1.ChatMessage>[];
sender: string | null | undefined;
@ -106,29 +90,14 @@ function ToolCalls({
testProfile: z.infer<typeof TestProfile> | null;
systemMessage: string | undefined;
}) {
const resultsMap: Record<string, z.infer<typeof apiV1.ToolMessage>> = {};
function handleToolCallResult(result: z.infer<typeof apiV1.ToolMessage>) {
resultsMap[result.tool_call_id] = result;
if (Object.keys(resultsMap).length === toolCalls.length) {
const results = Object.values(resultsMap);
handleResults(results);
}
}
return <div className="flex flex-col gap-4">
{toolCalls.map(toolCall => {
return <ToolCall
key={toolCall.id}
toolCall={toolCall}
result={results[toolCall.id]}
handleResult={handleToolCallResult}
projectId={projectId}
messages={messages}
sender={sender}
workflow={workflow}
testProfile={testProfile}
systemMessage={systemMessage}
/>
})}
</div>;
@ -137,23 +106,13 @@ function ToolCalls({
function ToolCall({
toolCall,
result,
handleResult,
projectId,
messages,
sender,
workflow,
testProfile = null,
systemMessage,
}: {
toolCall: z.infer<typeof apiV1.AssistantMessageWithToolCalls>['tool_calls'][number];
result: z.infer<typeof apiV1.ToolMessage> | undefined;
handleResult: (result: z.infer<typeof apiV1.ToolMessage>) => void;
projectId: string;
messages: z.infer<typeof apiV1.ChatMessage>[];
sender: string | null | undefined;
workflow: z.infer<typeof Workflow>;
testProfile: z.infer<typeof TestProfile> | null;
systemMessage: string | undefined;
}) {
let matchingWorkflowTool: z.infer<typeof WorkflowTool> | undefined;
for (const tool of workflow.tools) {
@ -163,182 +122,24 @@ function ToolCall({
}
}
switch (toolCall.function.name) {
case 'getArticleInfo':
return <GetInformationToolCall
toolCall={toolCall}
result={result}
handleResult={handleResult}
projectId={projectId}
messages={messages}
sender={sender}
workflow={workflow}
/>;
default:
if (toolCall.function.name.startsWith('transfer_to_')) {
return <TransferToAgentToolCall
toolCall={toolCall}
result={result}
handleResult={handleResult}
projectId={projectId}
messages={messages}
sender={sender}
/>;
}
if (!matchingWorkflowTool ||
matchingWorkflowTool.mockTool ||
(testProfile && testProfile.mockTools)) {
return <MockToolCall
toolCall={toolCall}
result={result}
handleResult={handleResult}
projectId={projectId}
messages={messages}
sender={sender}
testProfile={testProfile}
workflowTool={matchingWorkflowTool}
systemMessage={systemMessage}
/>;
}
if (matchingWorkflowTool?.isMcp) {
return <McpToolCall
toolCall={toolCall}
workflowTool={matchingWorkflowTool}
result={result}
handleResult={handleResult}
projectId={projectId}
messages={messages}
sender={sender}
/>;
}
return <ClientToolCall
toolCall={toolCall}
result={result}
handleResult={handleResult}
projectId={projectId}
messages={messages}
sender={sender}
/>;
if (toolCall.function.name.startsWith('transfer_to_')) {
return <TransferToAgentToolCall
result={result}
sender={sender}
/>;
}
}
function ToolCallHeader({
toolCall,
result,
}: {
toolCall: z.infer<typeof apiV1.AssistantMessageWithToolCalls>['tool_calls'][number];
result: z.infer<typeof apiV1.ToolMessage> | undefined;
}) {
return <div className="flex flex-col gap-1">
<div className='shrink-0 flex gap-2 items-center'>
{!result && <Spinner size="sm" />}
{result && <CircleCheckIcon size={16} />}
<div className='font-semibold text-sm'>
Function Call: <code className='bg-gray-100 dark:bg-neutral-800 px-2 py-0.5 rounded font-mono'>{toolCall.function.name}</code>
</div>
</div>
</div>;
}
function GetInformationToolCall({
toolCall,
result: availableResult,
handleResult,
projectId,
messages,
sender,
workflow,
}: {
toolCall: z.infer<typeof apiV1.AssistantMessageWithToolCalls>['tool_calls'][number];
result: z.infer<typeof apiV1.ToolMessage> | undefined;
handleResult: (result: z.infer<typeof apiV1.ToolMessage>) => void;
projectId: string;
messages: z.infer<typeof apiV1.ChatMessage>[];
sender: string | null | undefined;
workflow: z.infer<typeof Workflow>;
}) {
const [result, setResult] = useState<z.infer<typeof apiV1.ToolMessage> | undefined>(availableResult);
const args = JSON.parse(toolCall.function.arguments) as { question: string };
let typedResult: z.infer<typeof GetInformationToolResult> | undefined;
if (result) {
typedResult = JSON.parse(result.content) as z.infer<typeof GetInformationToolResult>;
}
useEffect(() => {
if (result) {
return;
}
let ignore = false;
async function process() {
const result: z.infer<typeof apiV1.ToolMessage> = {
role: 'tool',
tool_call_id: toolCall.id,
tool_name: toolCall.function.name,
content: '',
};
// find target agent
const agent = workflow.agents.find(agent => agent.name == sender);
if (!agent || !agent.ragDataSources) {
result.content = JSON.stringify({
results: [],
});
} else {
const matches = await getInformationTool(projectId, args.question, agent.ragDataSources, agent.ragReturnType, agent.ragK);
if (ignore) {
return;
}
result.content = JSON.stringify(matches);
}
setResult(result);
handleResult(result);
}
process();
return () => {
ignore = true;
};
}, [result, toolCall.id, toolCall.function.name, projectId, args.question, workflow.agents, sender, handleResult]);
return <div className="flex flex-col gap-1">
{sender && <div className='text-gray-500 text-sm ml-3'>{sender}</div>}
<div className='border border-gray-300 p-2 rounded-lg rounded-bl-none flex flex-col gap-2 mr-[30%]'>
<ToolCallHeader toolCall={toolCall} result={result} />
<div className='mt-1'>
{result ? 'Fetched' : 'Fetch'} information for question: <span className='font-mono font-semibold'>{args['question']}</span>
{result && <div className='flex flex-col gap-2 mt-2 pt-2 border-t border-t-gray-200'>
{typedResult && typedResult.results.length === 0 && <div>No matches found.</div>}
{typedResult && typedResult.results.length > 0 && <ul className="list-disc ml-6">
{typedResult.results.map((result, index) => {
return <li key={'' + index} className="mb-2">
<ExpandableContent
label={result.title || result.name}
content={result.content}
expanded={false}
/>
</li>
})}
</ul>}
</div>}
</div>
</div>
</div>;
return <ClientToolCall
toolCall={toolCall}
result={result}
sender={sender}
/>;
}
function TransferToAgentToolCall({
toolCall,
result: availableResult,
handleResult,
projectId,
messages,
sender,
}: {
toolCall: z.infer<typeof apiV1.AssistantMessageWithToolCalls>['tool_calls'][number];
result: z.infer<typeof apiV1.ToolMessage> | undefined;
handleResult: (result: z.infer<typeof apiV1.ToolMessage>) => void;
projectId: string;
messages: z.infer<typeof apiV1.ChatMessage>[];
sender: string | null | undefined;
}) {
const typedResult = availableResult ? JSON.parse(availableResult.content) as { assistant: string } : undefined;
@ -355,278 +156,32 @@ function TransferToAgentToolCall({
</div>;
}
function McpToolCall({
toolCall,
result: availableResult,
handleResult,
projectId,
messages,
sender,
workflowTool,
}: {
toolCall: z.infer<typeof apiV1.AssistantMessageWithToolCalls>['tool_calls'][number];
result: z.infer<typeof apiV1.ToolMessage> | undefined;
handleResult: (result: z.infer<typeof apiV1.ToolMessage>) => void;
projectId: string;
messages: z.infer<typeof apiV1.ChatMessage>[];
sender: string | null | undefined;
workflowTool: z.infer<typeof WorkflowTool>;
}) {
const [result, setResult] = useState<z.infer<typeof apiV1.ToolMessage> | undefined>(availableResult);
useEffect(() => {
if (result) {
return;
}
let ignore = false;
async function process() {
let response;
try {
response = await executeMcpTool(
projectId,
workflowTool.mcpServerName || '',
workflowTool.name,
JSON.parse(toolCall.function.arguments),
);
} catch (e) {
response = {
error: (e as Error).message,
};
}
if (ignore) {
return;
}
const result: z.infer<typeof apiV1.ToolMessage> = {
role: 'tool',
tool_call_id: toolCall.id,
tool_name: toolCall.function.name,
content: JSON.stringify(response),
};
setResult(result);
handleResult(result);
}
process();
return () => {
ignore = true;
};
}, [result, toolCall, projectId, messages, handleResult, workflowTool.mcpServerName, workflowTool.name]);
return <div className="flex flex-col gap-1">
{sender && <div className='text-gray-500 text-sm ml-3'>{sender}</div>}
<div className='border border-gray-300 p-2 pt-2 rounded-lg rounded-bl-none flex flex-col gap-2 mr-[30%]'>
<ToolCallHeader toolCall={toolCall} result={result} />
<div className='flex flex-col gap-2'>
<ExpandableContent label='Params' content={toolCall.function.arguments} expanded={false} />
{result && <ExpandableContent label='Result' content={result.content} expanded={false} />}
</div>
</div>
</div>;
}
function ClientToolCall({
toolCall,
result: availableResult,
handleResult,
projectId,
messages,
sender,
}: {
toolCall: z.infer<typeof apiV1.AssistantMessageWithToolCalls>['tool_calls'][number];
result: z.infer<typeof apiV1.ToolMessage> | undefined;
handleResult: (result: z.infer<typeof apiV1.ToolMessage>) => void;
projectId: string;
messages: z.infer<typeof apiV1.ChatMessage>[];
sender: string | null | undefined;
}) {
const [result, setResult] = useState<z.infer<typeof apiV1.ToolMessage> | undefined>(availableResult);
useEffect(() => {
if (result) {
return;
}
let ignore = false;
async function process() {
let response;
try {
response = await executeClientTool(
toolCall,
messages,
projectId,
);
} catch (e) {
response = {
error: (e as Error).message,
};
}
if (ignore) {
return;
}
const result: z.infer<typeof apiV1.ToolMessage> = {
role: 'tool',
tool_call_id: toolCall.id,
tool_name: toolCall.function.name,
content: JSON.stringify(response),
};
setResult(result);
handleResult(result);
}
process();
return () => {
ignore = true;
};
}, [result, toolCall, projectId, messages, handleResult]);
return <div className="flex flex-col gap-1">
{sender && <div className='text-gray-500 text-sm ml-3'>{sender}</div>}
<div className='border border-gray-300 p-2 pt-2 rounded-lg rounded-bl-none flex flex-col gap-2 mr-[30%]'>
<ToolCallHeader toolCall={toolCall} result={result} />
<div className='flex flex-col gap-2'>
<ExpandableContent label='Params' content={toolCall.function.arguments} expanded={false} />
{result && <ExpandableContent label='Result' content={result.content} expanded={false} />}
</div>
</div>
</div>;
}
function MockToolCall({
toolCall,
result: availableResult,
handleResult,
projectId,
messages,
sender,
testProfile = null,
workflowTool,
systemMessage,
}: {
toolCall: z.infer<typeof apiV1.AssistantMessageWithToolCalls>['tool_calls'][number];
result: z.infer<typeof apiV1.ToolMessage> | undefined;
handleResult: (result: z.infer<typeof apiV1.ToolMessage>) => void;
projectId: string;
messages: z.infer<typeof apiV1.ChatMessage>[];
sender: string | null | undefined;
testProfile: z.infer<typeof TestProfile> | null;
workflowTool: z.infer<typeof WorkflowTool> | undefined;
systemMessage: string | undefined;
}) {
const [result, setResult] = useState<z.infer<typeof apiV1.ToolMessage> | undefined>(availableResult);
const [response, setResponse] = useState('');
const [generatingResponse, setGeneratingResponse] = useState(false);
const handleSubmit = useCallback(() => {
let parsed;
try {
parsed = JSON.parse(response);
} catch (e) {
alert('Invalid JSON');
return;
}
const result: z.infer<typeof apiV1.ToolMessage> = {
role: 'tool',
tool_call_id: toolCall.id,
tool_name: toolCall.function.name,
content: JSON.stringify(parsed),
};
setResult(result);
handleResult(result);
}, [toolCall.id, toolCall.function.name, handleResult, response]);
useEffect(() => {
if (result) {
return;
}
if (response) {
return;
}
let ignore = false;
async function process() {
setGeneratingResponse(true);
const response = await suggestToolResponse(
toolCall.id,
projectId,
[{
role: 'system',
content: systemMessage || '',
createdAt: new Date().toISOString(),
version: 'v1',
chatId: '',
}, ...messages],
testProfile?.mockPrompt || workflowTool?.mockInstructions || '',
);
if (ignore) {
return;
}
setResponse(response);
setGeneratingResponse(false);
}
process();
return () => {
ignore = true;
};
}, [result, response, toolCall.id, projectId, messages, testProfile, systemMessage, workflowTool?.mockInstructions]);
// auto submit if autoSubmitMockedResponse is true
useEffect(() => {
if (!workflowTool?.autoSubmitMockedResponse) {
return;
}
if (result) {
return;
}
if (response) {
handleSubmit();
}
}, [workflowTool?.autoSubmitMockedResponse, response, handleSubmit, result]);
return <div className="flex flex-col gap-1">
{sender && <div className='text-gray-500 dark:text-gray-400 text-xs ml-3'>{sender}</div>}
<div className='border border-gray-300 dark:border-gray-700 p-2 pt-2 rounded-lg rounded-bl-none flex flex-col gap-2 mr-[30%] bg-white dark:bg-gray-900'>
<div className="flex items-center gap-2">
{!result && <Spinner size="sm" />}
{result && <CircleCheckIcon size={16} className="text-gray-500 dark:text-gray-400" />}
<span className="text-sm text-gray-700 dark:text-gray-300">
Function Call: <code className='bg-gray-100 dark:bg-neutral-800 px-2 py-0.5 rounded font-mono'>{toolCall.function.name}</code>
</span>
<div className="flex flex-col gap-1">
<div className='shrink-0 flex gap-2 items-center'>
{!availableResult && <Spinner size="sm" />}
{availableResult && <CircleCheckIcon size={16} />}
<div className='font-semibold text-sm'>
Function Call: <code className='bg-gray-100 dark:bg-neutral-800 px-2 py-0.5 rounded font-mono'>{toolCall.function.name}</code>
</div>
</div>
</div>
<div className='flex flex-col gap-2'>
<ExpandableContent label='Params' content={toolCall.function.arguments} expanded={false} />
{result && <ExpandableContent label='Result' content={result.content} expanded={false} />}
{availableResult && <ExpandableContent label='Result' content={availableResult.content} expanded={false} />}
</div>
{!result && !workflowTool?.autoSubmitMockedResponse && <div className='flex flex-col gap-2 mt-2'>
<div>Response:</div>
<Textarea
maxRows={10}
placeholder='{}'
variant="bordered"
value={response}
disabled={generatingResponse}
onValueChange={(value) => setResponse(value)}
className='font-mono'
size="sm"
>
</Textarea>
<Button
onPress={handleSubmit}
disabled={generatingResponse}
isLoading={generatingResponse}
size="sm"
>
Submit result
</Button>
</div>}
</div>
</div>;
}
@ -701,9 +256,7 @@ export function Messages({
projectId,
messages,
toolCallResults,
handleToolCallResults,
loadingAssistantResponse,
loadingUserResponse,
workflow,
testProfile = null,
systemMessage,
@ -712,9 +265,7 @@ export function Messages({
projectId: string;
messages: z.infer<typeof apiV1.ChatMessage>[];
toolCallResults: Record<string, z.infer<typeof apiV1.ToolMessage>>;
handleToolCallResults: (results: z.infer<typeof apiV1.ToolMessage>[]) => void;
loadingAssistantResponse: boolean;
loadingUserResponse: boolean;
workflow: z.infer<typeof Workflow>;
testProfile: z.infer<typeof TestProfile> | null;
systemMessage: string | undefined;
@ -726,7 +277,7 @@ export function Messages({
// scroll to bottom on new messages
useEffect(() => {
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" })
}, [messages, loadingAssistantResponse, loadingUserResponse]);
}, [messages, loadingAssistantResponse]);
return <div className="grow pt-4 overflow-auto">
<div className="max-w-[768px] mx-auto flex flex-col gap-8">
@ -742,7 +293,6 @@ export function Messages({
key={index}
toolCalls={message.tool_calls}
results={toolCallResults}
handleResults={handleToolCallResults}
projectId={projectId}
messages={messages}
sender={message.agenticSender}
@ -781,7 +331,6 @@ export function Messages({
return <></>;
})}
{loadingAssistantResponse && <AssistantMessageLoading key="assistant-loading" />}
{loadingUserResponse && <UserMessageLoading key="user-loading" />}
<div ref={messagesEndRef} />
</div>
</div>;

View file

@ -1,5 +1,5 @@
"use client";
import { WithStringId } from "../../../lib/types/types";
import { MCPServer, WithStringId } from "../../../lib/types/types";
import { Workflow } from "../../../lib/types/workflow_types";
import { DataSource } from "../../../lib/types/datasource_types";
import { z } from "zod";
@ -9,6 +9,8 @@ import { WorkflowSelector } from "./workflow_selector";
import { Spinner } from "@heroui/react";
import { cloneWorkflow, createWorkflow, fetchPublishedWorkflowId, fetchWorkflow } from "../../../actions/workflow_actions";
import { listDataSources } from "../../../actions/datasource_actions";
import { listMcpServers } from "@/app/actions/mcp_actions";
import { getProjectConfig } from "@/app/actions/project_actions";
export function App({
projectId,
@ -23,17 +25,23 @@ export function App({
const [dataSources, setDataSources] = useState<WithStringId<z.infer<typeof DataSource>>[] | null>(null);
const [loading, setLoading] = useState(false);
const [autoSelectIfOnlyOneWorkflow, setAutoSelectIfOnlyOneWorkflow] = useState(true);
const [mcpServerUrls, setMcpServerUrls] = useState<Array<z.infer<typeof MCPServer>>>([]);
const [toolWebhookUrl, setToolWebhookUrl] = useState<string>('');
const handleSelect = useCallback(async (workflowId: string) => {
setLoading(true);
const workflow = await fetchWorkflow(projectId, workflowId);
const publishedWorkflowId = await fetchPublishedWorkflowId(projectId);
const dataSources = await listDataSources(projectId);
const mcpServers = await listMcpServers(projectId);
const projectConfig = await getProjectConfig(projectId);
// Store the selected workflow ID in local storage
localStorage.setItem(`lastWorkflowId_${projectId}`, workflowId);
setWorkflow(workflow);
setPublishedWorkflowId(publishedWorkflowId);
setDataSources(dataSources);
setMcpServerUrls(mcpServers);
setToolWebhookUrl(projectConfig.webhookUrl ?? '');
setLoading(false);
}, [projectId]);
@ -108,6 +116,8 @@ export function App({
handleShowSelector={handleShowSelector}
handleCloneVersion={handleCloneVersion}
useRag={useRag}
mcpServerUrls={mcpServerUrls}
toolWebhookUrl={toolWebhookUrl}
/>}
</>
}

View file

@ -1,8 +1,8 @@
import { Metadata } from "next";
import { App } from "./app";
import { USE_RAG } from "@/app/lib/feature_flags";
export const dynamic = 'force-dynamic';
import { projectsCollection } from "@/app/lib/mongodb";
import { notFound } from "next/navigation";
export const metadata: Metadata = {
title: "Workflow"
@ -13,6 +13,14 @@ export default async function Page({
}: {
params: { projectId: string };
}) {
console.log('->>> workflow page being rendered');
const project = await projectsCollection.findOne({
_id: params.projectId,
});
if (!project) {
notFound();
}
return <App
projectId={params.projectId}
useRag={USE_RAG}

View file

@ -1,5 +1,5 @@
"use client";
import { WithStringId } from "../../../lib/types/types";
import { MCPServer, WithStringId } from "../../../lib/types/types";
import { Workflow } from "../../../lib/types/workflow_types";
import { WorkflowTool } from "../../../lib/types/workflow_types";
import { WorkflowPrompt } from "../../../lib/types/workflow_types";
@ -559,6 +559,8 @@ export function WorkflowEditor({
handleShowSelector,
handleCloneVersion,
useRag,
mcpServerUrls,
toolWebhookUrl,
}: {
dataSources: WithStringId<z.infer<typeof DataSource>>[];
workflow: WithStringId<z.infer<typeof Workflow>>;
@ -566,6 +568,8 @@ export function WorkflowEditor({
handleShowSelector: () => void;
handleCloneVersion: (workflowId: string) => void;
useRag: boolean;
mcpServerUrls: Array<z.infer<typeof MCPServer>>;
toolWebhookUrl: string;
}) {
const [state, dispatch] = useReducer<Reducer<State, Action>>(reducer, {
patches: [],
@ -911,6 +915,8 @@ export function WorkflowEditor({
projectId={state.present.workflow.projectId}
workflow={state.present.workflow}
messageSubscriber={updateChatMessages}
mcpServerUrls={mcpServerUrls}
toolWebhookUrl={toolWebhookUrl}
/>
{state.present.selection?.type === "agent" && <AgentConfig
key={state.present.selection.name}

View file

@ -2,3 +2,4 @@
.env*
__pycache__/
venv/
.venv/

View file

@ -20,9 +20,9 @@ RUN poetry install --no-interaction --no-ansi
COPY . .
# Set environment variables
ENV FLASK_APP=src.app.main
ENV QUART_APP=src.app.main
ENV PYTHONUNBUFFERED=1
ENV PYTHONPATH=/app
# Command to run Flask development server
CMD ["flask", "run", "--host=0.0.0.0", "--port=3001"]
CMD ["quart", "run", "--host=0.0.0.0", "--port=3001"]

4134
apps/rowboat_agents/poetry.lock generated Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,110 @@
[tool.poetry]
name = "agents"
version = "0.1.0"
description = "RowBoat Labs Agent OS"
authors = ["Akhilesh <akhilesh@rowboatlabs.com>"]
license = "MIT"
readme = "README.md"
homepage = "https://github.com/rowboatlabs/agents"
package-mode = false
[tool.poetry.dependencies]
python = ">=3.10,<4.0"
# Dependencies
aiohttp = "^3.9.3"
annotated-types = "^0.7.0"
anyio = "^4.8.0"
asgiref = "*"
beautifulsoup4 = "^4.12.3"
blinker = "^1.9.0"
build = "1.2.2.post1"
CacheControl = "^0.14.2"
certifi = "^2024.12.14"
cffi = "^1.17.1"
charset-normalizer = "^3.4.1"
cleo = "^2.1.0"
click = "^8.1.8"
crashtest = "^0.4.1"
distlib = "^0.3.9"
distro = "^1.9.0"
dnspython = "^2.7.0"
dulwich = "^0.22.7"
et_xmlfile = "^2.0.0"
eval_type_backport = "^0.2.2"
fastjsonschema = "^2.21.1"
filelock = "^3.17.0"
firecrawl = "^1.9.0"
Flask = "^3.1.0"
gunicorn = "^23.0.0"
h11 = "^0.14.0"
httpcore = "^1.0.7"
httpx = "^0.27.2"
hypercorn = "*"
idna = "^3.10"
installer = "^0.7.0"
itsdangerous = "^2.2.0"
"jaraco.classes" = "^3.4.0"
"jaraco.context" = "^6.0.1"
"jaraco.functools" = "^4.1.0"
Jinja2 = "^3.1.5"
jiter = "^0.6.1"
jsonpath-python = "^1.0.6"
keyring = "^25.6.0"
lxml = "^5.3.0"
markdownify = "^0.13.1"
MarkupSafe = "^3.0.2"
mcp = "*"
more-itertools = "^10.6.0"
motor = "*"
msgpack = "^1.1.0"
mypy-extensions = "^1.0.0"
nest-asyncio = "^1.6.0"
numpy = "^2.2.1"
openai = "*"
openai-agents = "*"
openpyxl = "^3.1.5"
packaging = "^24.2"
pandas = "^2.2.3"
pkginfo = "^1.12.0"
platformdirs = "^4.3.6"
poetry = "^2.0.1"
poetry-core = "^2.0.1"
pycparser = "^2.22"
pydantic = "^2.10.5"
pydantic_core = "^2.27.2"
pymongo = "^4.10.1"
pyproject_hooks = "^1.2.0"
python-dateutil = "^2.9.0.post0"
python-docx = "^1.1.2"
python-dotenv = "^1.0.1"
pytz = "^2024.2"
qdrant-client = "*"
Quart = "^0.20.0"
RapidFuzz = "^3.11.0"
redis = "^5.2.1"
requests = "^2.32.3"
requests-toolbelt = "^1.0.0"
setuptools = "^75.8.0"
shellingham = "^1.5.4"
six = "^1.17.0"
sniffio = "^1.3.1"
soupsieve = "^2.6"
tabulate = "^0.9.0"
tomlkit = "^0.13.2"
tqdm = "^4.67.1"
trove-classifiers = "^2025.1.15.22"
typing-inspect = "^0.9.0"
typing_extensions = "^4.12.2"
tzdata = "^2024.2"
urllib3 = "^2.3.0"
virtualenv = "^20.29.1"
waitress = "^2.1.2"
websockets = "^13.1"
Werkzeug = "^3.1.3"
wheel = "^0.44.0"
xattr = "^1.1.4"
[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"

View file

@ -1,5 +1,7 @@
aiohttp==3.9.3
annotated-types==0.7.0
anyio==4.8.0
asgiref
beautifulsoup4==4.12.3
blinker==1.9.0
build==1.2.2.post1
@ -24,6 +26,7 @@ gunicorn==23.0.0
h11==0.14.0
httpcore==1.0.7
httpx==0.27.2
hypercorn
idna==3.10
installer==0.7.0
itsdangerous==2.2.0
@ -37,12 +40,15 @@ keyring==25.6.0
lxml==5.3.0
markdownify==0.13.1
MarkupSafe==3.0.2
mcp
more-itertools==10.6.0
motor
msgpack==1.1.0
mypy-extensions==1.0.0
nest-asyncio==1.6.0
numpy==2.2.1
openai==1.59.7
openai
openai-agents
openpyxl==3.1.5
packaging==24.2
pandas==2.2.3
@ -59,7 +65,10 @@ python-dateutil==2.9.0.post0
python-docx==1.1.2
python-dotenv==1.0.1
pytz==2024.2
qdrant-client
Quart==0.20.0
RapidFuzz==3.11.0
redis==5.2.1
requests==2.32.3
requests-toolbelt==1.0.0
setuptools==75.8.0
@ -76,7 +85,8 @@ typing_extensions==4.12.2
tzdata==2024.2
urllib3==2.3.0
virtualenv==20.29.1
waitress==2.1.2
websockets==13.1
Werkzeug==3.1.3
wheel==0.44.0
xattr==1.1.4
xattr==1.1.4

View file

@ -0,0 +1,190 @@
from quart import Quart, request, jsonify, Response
from datetime import datetime
from functools import wraps
import os
import redis
import uuid
import json
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.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
redis_client = redis.from_url(os.environ.get('REDIS_URL', 'redis://localhost:6379'))
app = Quart(__name__)
# filter out agent transfer messages using a function
def is_agent_transfer_message(msg):
if (msg.get("role") == "assistant" and
msg.get("content") is None and
msg.get("tool_calls") is not None and
len(msg.get("tool_calls")) > 0 and
msg.get("tool_calls")[0].get("function").get("name") == "transfer_to_agent"):
return True
if (msg.get("role") == "tool" and
msg.get("tool_calls") is None and
msg.get("tool_call_id") is not None and
msg.get("tool_name") == "transfer_to_agent"):
return True
return False
@app.route("/health", methods=["GET"])
async def health():
return jsonify({"status": "ok"})
@app.route("/")
async def home():
return "Hello, World!"
def require_api_key(f):
@wraps(f)
async def decorated(*args, **kwargs):
auth_header = request.headers.get('Authorization')
if not auth_header or not auth_header.startswith('Bearer '):
return jsonify({'error': 'Missing or invalid authorization header'}), 401
token = auth_header.split('Bearer ')[1]
actual = os.environ.get('API_KEY', '').strip()
if actual and token != actual:
return jsonify({'error': 'Invalid API key'}), 403
return await f(*args, **kwargs)
return decorated
@app.route("/chat", methods=["POST"])
@require_api_key
async def chat():
logger.info('='*100)
logger.info(f"{'*'*100}Running server mode{'*'*100}")
try:
data = await request.get_json()
logger.info('Complete request:')
logger.info(data)
logger.info('-'*100)
start_time = datetime.now()
config = read_json_from_file("./configs/default_config.json")
# filter out agent transfer messages
input_messages = [msg for msg in data.get("messages", []) if not is_agent_transfer_message(msg)]
logger.info('Beginning turn')
resp_messages, resp_tokens_used, resp_state = run_turn(
messages=input_messages,
start_agent_name=data.get("startAgent", ""),
agent_configs=data.get("agents", []),
tool_configs=data.get("tools", []),
start_turn_with_start_agent=config.get("start_turn_with_start_agent", False),
state=data.get("state", {}),
additional_tool_configs=[RAG_TOOL, CLOSE_CHAT_TOOL],
complete_request=data
)
logger.info('-'*100)
logger.info('Raw output:')
logger.info((resp_messages, resp_tokens_used, resp_state))
out = {
"messages": resp_messages,
"tokens_used": resp_tokens_used,
"state": resp_state,
}
logger.info("Output:")
for k, v in out.items():
logger.info(f"{k}: {v}")
logger.info('*'*100)
logger.info('='*100)
logger.info(f"Processing time: {datetime.now() - start_time}")
return jsonify(out)
except Exception as e:
logger.error(f"Error: {e}")
return jsonify({"error": str(e)}), 500
@app.route("/chat_stream_init", methods=["POST"])
@require_api_key
async def chat_stream_init():
# create a uuid for the stream
stream_id = str(uuid.uuid4())
# store the request data in redis with 10 minute TTL
data = await request.get_json()
redis_client.setex(f"stream_request_{stream_id}", 600, json.dumps(data))
return jsonify({"streamId": stream_id})
def format_sse(data: dict, event: str = None) -> str:
msg = f"data: {json.dumps(data)}\n\n"
if event is not None:
msg = f"event: {event}\n{msg}"
return msg
@app.route("/chat_stream/<stream_id>", methods=["GET"])
@require_api_key
async def chat_stream(stream_id):
# get the request data from redis
request_data = redis_client.get(f"stream_request_{stream_id}")
if not request_data:
return jsonify({"error": "Stream not found"}), 404
request_data = json.loads(request_data)
config = read_json_from_file("./configs/default_config.json")
# filter out agent transfer messages
input_messages = [msg for msg in request_data["messages"] if not is_agent_transfer_message(msg)]
# Preprocess messages to handle null content and role issues
for msg in input_messages:
if (msg.get("role") == "assistant" and
msg.get("content") is None and
msg.get("tool_calls") is not None and
len(msg.get("tool_calls")) > 0):
msg["content"] = "Calling tool"
if msg.get("role") == "tool":
msg["role"] = "developer"
elif not msg.get("role"):
msg["role"] = "user"
print("Request:")
pprint(request_data)
async def generate():
try:
async for event_type, event_data in run_turn_streamed(
messages=input_messages,
start_agent_name=request_data.get("startAgent", ""),
agent_configs=request_data.get("agents", []),
tool_configs=request_data.get("tools", []),
start_turn_with_start_agent=config.get("start_turn_with_start_agent", False),
state=request_data.get("state", {}),
additional_tool_configs=[RAG_TOOL, CLOSE_CHAT_TOOL],
complete_request=request_data
):
if event_type == 'message':
print("Yielding message:")
yield format_sse(event_data, "message")
elif event_type == 'done':
print("Yielding done:")
yield format_sse(event_data, "done")
except Exception as e:
logger.error(f"Streaming error: {str(e)}")
yield format_sse({"error": str(e)}, "error")
return Response(generate(), mimetype='text/event-stream')
if __name__ == "__main__":
print("Starting async server...")
config = Config()
config.bind = ["0.0.0.0:4040"]
asyncio.run(serve(app, config))

View file

@ -0,0 +1,397 @@
from copy import deepcopy
from datetime import datetime
import json
import uuid
import logging
from .helpers.access import (
get_agent_by_name,
get_external_tools,
)
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 src.utils.common import common_logger as logger
import asyncio
# Create a dedicated logger for swarm wrapper
logger.setLevel(logging.INFO)
print("Logger level set to INFO")
def order_messages(messages):
"""
Sorts each message's keys in a specified order and returns a new list of ordered messages.
"""
ordered_messages = []
for msg in messages:
# Filter out None values
msg = {k: v for k, v in msg.items() if v is not None}
# Specify the exact order
ordered = {}
for key in ['role', 'sender', 'content', 'created_at', 'timestamp']:
if key in msg:
ordered[key] = msg[key]
# Add remaining keys in alphabetical order
remaining_keys = sorted(k for k in msg if k not in ordered)
for key in remaining_keys:
ordered[key] = msg[key]
ordered_messages.append(ordered)
return ordered_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": 100, "prompt": 50, "completion": 50} # 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
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 = 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 instead of a hard-coded integer
tokens_used = {"total": 100, "prompt": 50, "completion": 50} # Dummy 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": 100, "prompt": 50, "completion": 50} # 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,
agent_configs,
tool_configs,
start_turn_with_start_agent,
state={},
additional_tool_configs=[],
complete_request={}
):
final_state = None # Initialize outside try block
try:
# Initialize agents and get external tools
new_agents = get_agents(agent_configs=agent_configs, tool_configs=tool_configs, complete_request=complete_request)
last_agent_name = get_last_agent_name(
state=state,
agent_configs=agent_configs,
start_agent_name=start_agent_name,
msg_type="user",
latest_assistant_msg=None,
start_turn_with_start_agent=start_turn_with_start_agent
)
last_new_agent = get_agent_by_name(last_agent_name, new_agents)
external_tools = get_external_tools(tool_configs)
current_agent = last_new_agent
tokens_used = {"total": 0, "prompt": 0, "completion": 0}
stream_result = await swarm_run_streamed(
agent=last_new_agent,
messages=messages,
external_tools=external_tools,
tokens_used=tokens_used
)
# Process streaming events
async for event in stream_result.stream_events():
print('='*50)
print("Received event: ", event)
print('-'*50)
# Handle raw response events and accumulate tokens
if event.type == "raw_response_event":
if hasattr(event.data, 'type') and event.data.type == "response.completed":
if hasattr(event.data.response, 'usage'):
tokens_used["total"] += event.data.response.usage.total_tokens
tokens_used["prompt"] += event.data.response.usage.input_tokens
tokens_used["completion"] += event.data.response.usage.output_tokens
print('-'*50)
print(f"Found usage information. Updated cumulative tokens: {tokens_used}")
print('-'*50)
continue
# Update current agent when it changes
elif event.type == "agent_updated_stream_event":
current_agent = event.new_agent
tool_call_id = str(uuid.uuid4())
# yield the transfer invocation
message = {
'content': None,
'role': 'assistant',
'sender': current_agent.name,
'tool_calls': [{
'function': {
'name': 'transfer_to_agent',
'arguments': json.dumps({
'assistant': event.new_agent.name
})
},
'id': tool_call_id,
'type': 'function'
}],
'tool_call_id': None,
'tool_name': None,
'response_type': 'internal'
}
print("Yielding message: ", message)
yield ('message', message)
# yield the transfer result
message = {
'content': json.dumps({
'assistant': event.new_agent.name
}),
'role': 'tool',
'sender': None,
'tool_calls': None,
'tool_call_id': tool_call_id,
'tool_name': 'transfer_to_agent',
}
print("Yielding message: ", message)
yield ('message', message)
current_agent = event.new_agent
continue
# Handle run items (tools, messages, etc)
elif event.type == "run_item_stream_event":
if event.item.type == "tool_call_item":
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":
message = {
'content': str(event.item.output),
'role': 'tool',
'sender': None,
'tool_calls': None,
'tool_call_id': event.item.raw_item['call_id'],
'tool_name': event.item.raw_item.get('name', None),
'response_type': 'internal'
}
print("Yielding message: ", message)
yield ('message', message)
elif event.item.type == "message_output_item":
content = ""
if hasattr(event.item.raw_item, 'content'):
for content_item in event.item.raw_item.content:
if hasattr(content_item, 'text'):
content += content_item.text
message = {
'content': content,
'role': 'assistant',
'sender': current_agent.name,
'tool_calls': None,
'tool_call_id': None,
'tool_name': None,
'response_type': 'external'
}
print("Yielding message: ", message)
yield ('message', message)
print(f"\n{'='*50}\n")
# After all events are processed, set final state
final_state = {
"last_agent_name": current_agent.name if current_agent else None,
"tokens": tokens_used
}
yield ('done', {'state': final_state})
except Exception as e:
import traceback
print(traceback.format_exc())
print(f"Error in stream processing: {str(e)}")
yield ('error', {'error': str(e), 'state': final_state}) # Include final_state in error response

View file

@ -3,7 +3,7 @@ from src.utils.common import generate_llm_output
import os
import copy
from src.swarm.types import Response, Agent
from .swarm_wrapper import Agent, Response, create_response
from src.utils.common import common_logger, generate_openai_output, update_tokens_used
logger = common_logger
@ -20,12 +20,12 @@ def classify_hallucination(context: str, assistant_response: str, chat_history:
Returns:
str: Verdict indicating level of hallucination:
'yes-absolute' - completely supported by context
'yes-common-sensical' - supported with common sense interpretation
'yes-common-sensical' - supported with common sense interpretation
'no-absolute' - not supported by context
'no-subtle' - not supported but difference is subtle
"""
chat_history_str = "\n".join([f"{message['role']}: {message['content']}" for message in chat_history])
prompt = f"""
You are a guardrail agent. Your job is to check if the response is hallucinating.
@ -51,40 +51,40 @@ def classify_hallucination(context: str, assistant_response: str, chat_history:
no-absolute: not supported by the context
no-subtle: not supported by the context but the difference is subtle
Output of of the classes:
verdict : yes-absolute/yes-common-sensical/no-absolute/no-subtle
Output of of the classes:
verdict : yes-absolute/yes-common-sensical/no-absolute/no-subtle
Example 1: The response is completely supported by the context.
User Input:
User Input:
Context: "Our airline provides complimentary meals and beverages on all international flights. Passengers are allowed one carry-on bag and one personal item."
Chat History:
Chat History:
User: "Do international flights with your airline offer free meals?"
Response: "Yes, all international flights with our airline offer free meals and beverages."
Output: verdict: yes-absolute
Example 2: The response is generally true and could be deduced with common sense interpretation, though not explicitly stated in the context.
User Input:
User Input:
Context: "Flights may experience delays due to weather conditions. In such cases, the airline staff will provide updates at the airport."
Chat History:
Chat History:
User: "Will there be announcements if my flight is delayed?"
Response: "Yes, if your flight is delayed, there will be announcements at the airport."
Output: verdict: yes-common-sensical
Output: verdict: yes-common-sensical
Example 3: The response is not supported by the context and contains glaring inaccuracies.
User Input:
User Input:
Context: "You can cancel your ticket online up to 24 hours before the flight's departure time and receive a full refund."
Chat History:
Chat History:
User: "Can I get a refund if I cancel 12 hours before the flight?"
Response: "Yes, you can get a refund if you cancel 12 hours before the flight."
Output: verdict: no-absolute
Example 4: The response is not supported by the context but the difference is subtle.
User Input:
User Input:
Context: "Our frequent flyer program offers discounts on checked bags for members who have achieved Gold status."
Chat History:
Chat History:
User: "As a member, do I get discounts on checked bags?"
Response: "Yes, members of our frequent flyer program get discounts on checked bags."
Output: verdict: no-subtle
Output: verdict: no-subtle
"""
messages = [
{
@ -105,7 +105,7 @@ def post_process_response(messages: list, post_processing_agent_name: str, post_
logger.debug(f"Pending message keys: {pending_msg.keys()}")
skip = False
if pending_msg.get("tool_calls"):
logger.info("Last message is a tool call, skipping post processing and setting last message to external")
skip = True
@ -113,11 +113,11 @@ def post_process_response(messages: list, post_processing_agent_name: str, post_
elif not pending_msg['response_type'] == "internal":
logger.info("Last message is not internal, skipping post processing and setting last message to external")
skip = True
elif not pending_msg['content']:
logger.info("Last message has no content, skipping post processing and setting last message to external")
skip = True
elif not post_process_instructions:
logger.info("No post process instructions, skipping post processing and setting last message to external")
skip = True
@ -131,7 +131,7 @@ def post_process_response(messages: list, post_processing_agent_name: str, post_
error_msg=''
)
return response
agent_history_str = f"\n{'*'*100}\n".join([f"Role: {message['role']} | Content: {message.get('content', 'None')} | Tool Calls: {message.get('tool_calls', 'None')}" for message in agent_history[:-1]])
logger.debug(f"Agent history: {agent_history_str}")
@ -147,7 +147,7 @@ def post_process_response(messages: list, post_processing_agent_name: str, post_
{post_process_instructions}
------------------------------------------------------------------------
# CHAT HISTORY
Here is the chat history:
@ -186,7 +186,7 @@ def post_process_response(messages: list, post_processing_agent_name: str, post_
Here is the response that the agent has generated:
{pending_msg['content']}
"""
prompt += agent_response_and_instructions

View file

@ -49,13 +49,7 @@ def construct_state_from_response(response, agents):
for agent in agents:
agent_data.append({
"name": agent.name,
"instructions": agent.instructions,
"parent_function": agent.parent_function.__name__ if agent.parent_function else None,
"child_functions": [f.__name__ for f in agent.child_functions.values()] if agent.child_functions else [],
"internal_tools": [t.get("function").get("name") for t in agent.internal_tools] if agent.internal_tools else [],
"external_tools": [t.get("function").get("name") for t in agent.external_tools] if agent.external_tools else [],
"history": agent.history,
"most_recent_parent_name": agent.most_recent_parent.name if agent.most_recent_parent else ""
"instructions": agent.instructions
})
state = {

View file

@ -0,0 +1,391 @@
import logging
import json
import aiohttp
# Import helper functions needed for get_agents
from .helpers.access import (
get_tool_config_by_name,
get_tool_config_by_type
)
from .helpers.instructions import (
add_rag_instructions_to_agent
)
from agents import Agent as NewAgent, Runner, FunctionTool, RunContextWrapper
# Add import for OpenAI functionality
from src.utils.common import common_logger as logger, generate_openai_output
from typing import Any
from dataclasses import asdict
import asyncio
from mcp import ClientSession
from mcp.client.sse import sse_client
from pydantic import BaseModel
from typing import List, Optional, Dict
from .tool_calling import call_rag_tool
class NewResponse(BaseModel):
messages: List[Dict]
agent: Optional[Any] = None
tokens_used: Optional[dict] = {}
error_msg: Optional[str] = ""
async def mock_tool(tool_name: str, args: str, tool_config: str) -> str:
"""
Handles tool execution by either using mock instructions or generating a response.
Args:
tool_name: The name of the tool
args: The arguments passed to the tool
tool_config: The configuration of the tool
Returns:
The response from the tool
"""
print(f"Mock tool called for: {tool_name}")
# For non-mocked tools, generate a realistic response
description = tool_config.get("description", "")
mock_instructions = tool_config.get("mockInstructions", "")
messages = [
{"role": "system", "content": f"You are simulating the execution of a tool called '{tool_name}'.Here is the description of the tool: {description}. Here are the instructions for the mock tool: {mock_instructions}. Generate a realistic response as if the tool was actually executed with the given parameters."},
{"role": "user", "content": f"Generate a realistic response for the tool '{tool_name}' with these parameters: {args}. The response should be concise and focused on what the tool would actually return."}
]
print(f"Generating simulated response for tool: {tool_name}")
response_content = generate_openai_output(messages, output_type='text', model="gpt-4o")
return response_content
async def call_webhook(tool_name: str, args: str, webhook_url: str) -> str:
"""
Calls the webhook with the given tool name and arguments.
Args:
tool_name (str): The name of the tool to call.
args (str): The arguments for the tool as a JSON string.
Returns:
str: The response from the webhook, or an error message if the call fails.
"""
content_dict = {
"toolCall": {
"function": {
"name": tool_name,
"arguments": args
}
}
}
request_body = {
"content": json.dumps(content_dict)
}
try:
async with aiohttp.ClientSession() as session:
async with session.post(webhook_url, json=request_body) as response:
if response.status == 200:
response_json = await response.json()
return response_json.get("result", "")
else:
error_msg = await response.text()
print(f"Webhook error: {error_msg}")
return f"Error: {error_msg}"
except Exception as e:
print(f"Exception in call_webhook: {str(e)}")
return f"Error: Failed to call webhook - {str(e)}"
async def call_mcp(tool_name: str, args: str, mcp_server_url: str) -> str:
"""
Calls the MCP with the given tool name and arguments.
"""
async with sse_client(url=mcp_server_url) as streams:
async with ClientSession(*streams) as session:
await session.initialize()
jargs = json.loads(args)
response = await session.call_tool(tool_name, arguments=jargs)
json_output = json.dumps([item.__dict__ for item in response.content], indent=2)
return json_output
async def catch_all(ctx: RunContextWrapper[Any], args: str, tool_name: str, tool_config: dict, complete_request: dict) -> str:
"""
Handles all tool calls by dispatching to appropriate functions.
"""
print(f"Catch all called for tool: {tool_name}")
print(f"Args: {args}")
print(f"Tool config: {tool_config}")
# Create event loop for async operations
try:
loop = asyncio.get_event_loop()
except RuntimeError:
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
response_content = None
if tool_config.get("mockTool", False):
# Call mock_tool to handle the response (it will decide whether to use mock instructions or generate a response)
response_content = await mock_tool(tool_name, args, tool_config)
print(response_content)
elif tool_config.get("isMcp", False):
mcp_server_name = tool_config.get("mcpServerName", "")
mcp_servers = complete_request.get("mcpServers", {})
mcp_server_url = next((server.get("url", "") for server in mcp_servers if server.get("name") == mcp_server_name), "")
response_content = await call_mcp(tool_name, args, mcp_server_url)
else:
webhook_url = complete_request.get("toolWebhookUrl", "")
response_content = await call_webhook(tool_name, args, webhook_url)
return response_content
def get_rag_tool(config: dict, complete_request: dict) -> FunctionTool:
"""
Creates a RAG tool based on the provided configuration.
"""
project_id = complete_request.get("projectId", "")
if config.get("ragDataSources", None):
print("getArticleInfo")
params = {
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "The query to search for"
}
},
"additionalProperties": False,
"required": [
"query"
]
}
tool = FunctionTool(
name="getArticleInfo",
description="Get information about an article",
params_json_schema=params,
on_invoke_tool=lambda ctx, args: call_rag_tool(project_id, json.loads(args)['query'], config.get("ragDataSources", []), "chunks", 3)
)
return tool
else:
return None
def get_agents(agent_configs, tool_configs, complete_request):
"""
Creates and initializes Agent objects based on their configurations and connections.
"""
if not isinstance(agent_configs, list):
raise ValueError("Agents config is not a list in get_agents")
if not isinstance(tool_configs, list):
raise ValueError("Tools config is not a list in get_agents")
new_agents = []
new_agent_to_children = {}
new_agent_name_to_index = {}
# Create Agent objects from config
for agent_config in agent_configs:
logger.debug(f"Processing config for agent: {agent_config['name']}")
print("="*100)
print(f"Processing config for agent: {agent_config['name']}")
# If hasRagSources, append the RAG tool to the agent's tools
if agent_config.get("hasRagSources", False):
rag_tool_name = get_tool_config_by_type(tool_configs, "rag").get("name", "")
agent_config["tools"].append(rag_tool_name)
agent_config = add_rag_instructions_to_agent(agent_config, rag_tool_name)
# Prepare tool lists for this agent
external_tools = []
logger.debug(f"Agent {agent_config['name']} has {len(agent_config['tools'])} configured tools")
print(f"Agent {agent_config['name']} has {len(agent_config['tools'])} configured tools")
new_tools = []
rag_tool = get_rag_tool(agent_config, complete_request)
if rag_tool:
new_tools.append(rag_tool)
logger.debug(f"Added rag tool to agent {agent_config['name']}")
print(f"Added rag tool to agent {agent_config['name']}")
for tool_name in agent_config["tools"]:
tool_config = get_tool_config_by_name(tool_configs, tool_name)
if tool_config:
external_tools.append({
"type": "function",
"function": tool_config
})
#TODO: Remove this once we have a way to handle the additionalProperties
tool_config['parameters']['additionalProperties'] = False
tool = FunctionTool(
name=tool_name,
description=tool_config["description"],
params_json_schema=tool_config["parameters"],
on_invoke_tool=lambda ctx, args, _tool_name=tool_name, _tool_config=tool_config, _complete_request=complete_request:
catch_all(ctx, args, _tool_name, _tool_config, _complete_request)
)
new_tools.append(tool)
logger.debug(f"Added tool {tool_name} to agent {agent_config['name']}")
print(f"Added tool {tool_name} to agent {agent_config['name']}")
else:
logger.warning(f"Tool {tool_name} not found in tool_configs")
print(f"WARNING: Tool {tool_name} not found in tool_configs")
# Create the agent object
logger.debug(f"Creating Agent object for {agent_config['name']}")
print(f"Creating Agent object for {agent_config['name']}")
try:
new_agent = NewAgent(
name=agent_config["name"],
instructions=agent_config["instructions"],
handoff_description=agent_config["description"],
tools=new_tools,
model=agent_config["model"]
)
new_agent_to_children[agent_config["name"]] = agent_config.get("connectedAgents", [])
new_agent_name_to_index[agent_config["name"]] = len(new_agents)
new_agents.append(new_agent)
logger.debug(f"Successfully created agent: {agent_config['name']}")
print(f"Successfully created agent: {agent_config['name']}")
except Exception as e:
logger.error(f"Failed to create agent {agent_config['name']}: {str(e)}")
print(f"ERROR: Failed to create agent {agent_config['name']}: {str(e)}")
raise
for new_agent in new_agents:
# Initialize the handoffs attribute if it doesn't exist
if not hasattr(new_agent, 'handoffs'):
new_agent.handoffs = []
# Look up the agent's children from the old agent and create a list called handoffs in new_agent with pointers to the children in new_agents
new_agent.handoffs = [new_agents[new_agent_name_to_index[child]] for child in new_agent_to_children[new_agent.name]]
print("Returning created agents")
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
)
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)
})
# Create a new event loop for this thread
try:
loop = asyncio.get_event_loop()
except RuntimeError:
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
# Run the agent with the formatted messages
logger.info("Beginning Swarm run with run_sync")
print("Beginning Swarm run with run_sync")
try:
response = loop.run_until_complete(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,
external_tools=None,
tokens_used=None
):
"""
Wrapper function for initializing and running the Swarm client in streaming mode.
"""
logger.info(f"Initializing Swarm streaming client for agent: {agent.name}")
print(f"Initializing Swarm streaming 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 streaming run")
print("Beginning Swarm streaming run")
try:
# Use the Runner.run_streamed method
stream_result = Runner.run_streamed(agent, formatted_messages)
return stream_result
except Exception as e:
logger.error(f"Error during streaming run: {str(e)}")
print(f"Error during streaming run: {str(e)}")
raise

View file

@ -0,0 +1,143 @@
from bson.objectid import ObjectId
from openai import OpenAI
import os
from motor.motor_asyncio import AsyncIOMotorClient
import asyncio
from dataclasses import dataclass
from typing import Dict, List, Any
from qdrant_client import QdrantClient
import json
# Initialize MongoDB client
mongo_uri = os.environ.get("MONGODB_URI", "mongodb://localhost:27017")
mongo_client = AsyncIOMotorClient(mongo_uri)
db = mongo_client.rowboat
data_sources_collection = db['sources']
data_source_docs_collection = db['source_docs']
qdrant_client = QdrantClient(url=os.environ.get("QDRANT_URL"))
# Initialize OpenAI client
client = OpenAI(api_key=os.environ.get("OPENAI_API_KEY"))
# Define embedding model
embedding_model = "text-embedding-3-small"
async def embed(model: str, value: str) -> dict:
"""
Generate embeddings using OpenAI's embedding models.
Args:
model (str): The embedding model to use (e.g., "text-embedding-3-small").
value (str): The text to embed.
Returns:
dict: A dictionary containing the embedding.
"""
response = client.embeddings.create(
model=model,
input=value
)
return {"embedding": response.data[0].embedding}
async def call_rag_tool(
project_id: str,
query: str,
source_ids: list[str],
return_type: str,
k: int,
) -> dict:
"""
Runs the RAG tool call to retrieve information based on the query and source IDs.
Args:
project_id (str): The ID of the project.
query (str): The query string to search for.
source_ids (list[str]): List of source IDs to filter the search.
return_type (str): The type of return, e.g., 'chunks' or other.
k (int): The number of results to return.
Returns:
dict: A dictionary containing the results of the search.
"""
print("\n\n calling rag tool \n\n")
print(query)
# Create embedding for the query
embed_result = await embed(model=embedding_model, value=query)
print(embed_result)
# Fetch all active data sources for this project
sources = await data_sources_collection.find({
"projectId": project_id,
"active": True
}).to_list(length=None)
print(sources)
# Filter sources to those in source_ids
valid_source_ids = [
str(s["_id"]) for s in sources if str(s["_id"]) in source_ids
]
print(valid_source_ids)
# If no valid sources are found, return empty results
if not valid_source_ids:
return ''
# Perform Qdrant vector search
qdrant_results = qdrant_client.search(
collection_name="embeddings",
query_vector=embed_result["embedding"],
query_filter={
"must": [
{"key": "projectId", "match": {"value": project_id}},
{"key": "sourceId", "match": {"any": valid_source_ids}},
]
},
limit=k,
with_payload=True
)
# Map the Qdrant results to the desired format
results = [
{
"title": point.payload["title"],
"name": point.payload["name"],
"content": point.payload["content"],
"docId": point.payload["docId"],
"sourceId": point.payload["sourceId"],
}
for point in qdrant_results
]
print(return_type)
print(results)
# If return_type is 'chunks', return the results directly
if return_type == "chunks":
return json.dumps({"Information": results}, indent=2)
# Otherwise, fetch the full document contents from MongoDB
doc_ids = [ObjectId(r["docId"]) for r in results]
docs = await data_source_docs_collection.find({"_id": {"$in": doc_ids}}).to_list(length=None)
# Create a dictionary for quick lookup of documents by their string ID
doc_dict = {str(doc["_id"]): doc for doc in docs}
# Update the results with the full document content
results = [
{**r, "content": doc_dict.get(r["docId"], {}).get("content", "")}
for r in results
]
# Convert results to a JSON string
formatted_string = json.dumps({"Information": results}, indent=2)
print(formatted_string)
return formatted_string
if __name__ == "__main__":
asyncio.run(call_rag_tool(
project_id="faf2bfb3-41d4-4299-b0d2-048581ea9bd8",
query="What is the range on your scooter",
source_ids=["67e102c9fab4514d7aaeb5a4"],
return_type="docs",
k=3))

View file

@ -4,7 +4,6 @@ import os
import subprocess
import sys
import time
from collections import defaultdict
from dotenv import load_dotenv
from openai import OpenAI

View file

@ -0,0 +1,174 @@
from src.utils.common import common_logger, read_json_from_file
import requests
import json
import argparse
from datetime import datetime
logger = common_logger
logger.info("Running app_client_streaming.py")
def preprocess_messages(messages):
# Preprocess messages to handle null content and role issues
for msg in messages:
# Handle null content in assistant messages with tool calls
if (msg.get("role") == "assistant" and
msg.get("content") is None and
msg.get("tool_calls") is not None and
len(msg.get("tool_calls")) > 0):
msg["content"] = "Calling tool"
# Handle role issues
if msg.get("role") == "tool":
msg["role"] = "developer"
elif not msg.get("role"):
msg["role"] = "user"
return messages
def stream_chat(host, request_data, api_key):
start_time = datetime.now()
print("\n" + "="*80)
print(f"Starting streaming chat at {start_time}")
print(f"Host: {host}")
print("="*80 + "\n")
# First, initialize the stream
try:
print("\n" + "-"*80)
print("Initializing stream...")
init_response = requests.post(
f"{host}/chat_stream_init",
json=request_data,
headers={'Authorization': f'Bearer {api_key}'}
)
print(f"Init Response Status: {init_response.status_code}")
print(f"Init Response Text: {init_response.text}")
print("-"*80 + "\n")
if init_response.status_code != 200:
logger.error(f"Error initializing stream. Status code: {init_response.status_code}")
logger.error(f"Response: {init_response.text}")
return
init_data = init_response.json()
if 'error' in init_data:
logger.error(f"Error initializing stream: {init_data['error']}")
return
stream_id = init_data['stream_id']
print(f"Stream initialized successfully with ID: {stream_id}")
except requests.exceptions.RequestException as e:
logger.error(f"Request error during stream initialization: {e}")
return
except json.JSONDecodeError as e:
logger.error(f"Failed to decode JSON response: {e}")
logger.error(f"Raw response: {init_response.text}")
return
# Now connect to the stream
try:
print("\n" + "-"*80)
print(f"Connecting to stream {stream_id}...")
stream_response = requests.get(
f"{host}/chat_stream/{stream_id}",
headers={
'Authorization': f'Bearer {api_key}',
'Accept': 'text/event-stream'
},
stream=True
)
if stream_response.status_code != 200:
logger.error(f"Error connecting to stream. Status code: {stream_response.status_code}")
logger.error(f"Response: {stream_response.text}")
return
print(f"Successfully connected to stream")
print("-"*80 + "\n")
event_count = 0
current_data = []
try:
print("\n" + "-"*80)
print("Starting to process events...")
print("-"*80 + "\n")
for line in stream_response.iter_lines(decode_unicode=True):
if line:
if line.startswith('data: '):
data = line[6:] # Remove 'data: ' prefix
try:
event_data = json.loads(data)
event_count += 1
print("\n" + "*"*80)
print(f"Event #{event_count}")
if isinstance(event_data, dict):
# Pretty print the event data
print("Event Data:")
print(json.dumps(event_data, indent=2))
# Special handling for message events
if 'content' in event_data:
print("\nMessage Content:", event_data['content'])
if event_data.get('tool_calls'):
print("Tool Calls:", json.dumps(event_data['tool_calls'], indent=2))
else:
print("Event Data:", event_data)
print("*"*80 + "\n")
except json.JSONDecodeError as e:
print(f"Error decoding event data: {e}")
print(f"Raw data: {data}")
except Exception as e:
print(f"Error processing stream: {e}")
import traceback
traceback.print_exc()
finally:
print("\n" + "-"*80)
print(f"Closing stream after processing {event_count} events")
print("-"*80 + "\n")
stream_response.close()
except requests.exceptions.RequestException as e:
print(f"Request error during streaming: {e}")
import traceback
traceback.print_exc()
end_time = datetime.now()
duration = end_time - start_time
print("\n" + "="*80)
print(f"Streaming session completed at {end_time}")
print(f"Total duration: {duration}")
print("="*80 + "\n")
if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument('--sample_request', type=str, required=False, default='tmp.json',
help='Sample request JSON file name under tests/sample_requests/')
parser.add_argument('--api_key', type=str, required=False, default='test',
help='API key to use for authentication')
parser.add_argument('--host', type=str, default='http://localhost:4040',
help='Host to use for the request')
args = parser.parse_args()
try:
print("\n" + "="*80)
print("Loading request data...")
request = read_json_from_file(f"./tests/sample_requests/{args.sample_request}").get("lastRequest", {})
print("Request data:")
print(json.dumps(request, indent=2))
print("Starting streaming request...")
print("="*80 + "\n")
stream_chat(args.host, request, args.api_key)
except Exception as e:
print("\n" + "!"*80)
print(f"Error in main: {e}")
import traceback
traceback.print_exc()
print("!"*80 + "\n")

View file

@ -2,12 +2,76 @@ import copy
from datetime import datetime
import json
import sys
import asyncio
from src.graph.core import run_turn, order_messages
from src.graph.core import order_messages, run_turn_streamed
from src.graph.tools import respond_to_tool_raise_error, respond_to_tool_close_chat, RAG_TOOL, CLOSE_CHAT_TOOL
from src.utils.common import common_logger, read_json_from_file
logger = common_logger
def preprocess_messages(messages):
# Preprocess messages to handle null content and role issues
for msg in messages:
# Handle null content in assistant messages with tool calls
if (msg.get("role") == "assistant" and
msg.get("content") is None and
msg.get("tool_calls") is not None and
len(msg.get("tool_calls")) > 0):
msg["content"] = "Calling tool"
# Handle role issues
if msg.get("role") == "tool":
msg["role"] = "developer"
elif not msg.get("role"):
msg["role"] = "user"
return messages
async def process_turn(messages, agent_configs, tool_configs, prompt_configs, start_agent_name, state, config, complete_request):
"""Processes a single turn using streaming API"""
print(f"\n{'*'*50}\nLatest Request:\n{'*'*50}")
request_json = {
"messages": [{k: v for k, v in msg.items() if k != 'current_turn'} for msg in messages],
"state": state,
"agents": agent_configs,
"tools": tool_configs,
"prompts": prompt_configs,
"startAgent": start_agent_name
}
print(json.dumps(request_json, indent=2))
collected_messages = []
async for event_type, event_data in run_turn_streamed(
messages=messages,
start_agent_name=start_agent_name,
agent_configs=agent_configs,
tool_configs=tool_configs,
start_turn_with_start_agent=config.get("start_turn_with_start_agent", False),
state=state,
additional_tool_configs=[RAG_TOOL, CLOSE_CHAT_TOOL],
complete_request=complete_request
):
if event_type == "message":
# Add each message to collected_messages
collected_messages.append(event_data)
elif event_type == "done":
print(f"\n\n{'*'*50}\nLatest Response:\n{'*'*50}")
response_json = {
"messages": collected_messages,
"state": event_data.get('state', {}),
}
print("Turn completed. Here are the streamed messages and final state:")
print(json.dumps(response_json, indent=2))
print('='*50)
return collected_messages, event_data.get('state', {})
elif event_type == "error":
print(f"\nError: {event_data.get('error', 'Unknown error')}")
return [], state
if __name__ == "__main__":
logger.info(f"{'*'*50}Running interactive mode{'*'*50}")
@ -27,6 +91,9 @@ if __name__ == "__main__":
config_file = sys.argv[sys.argv.index("--config") + 1] if "--config" in sys.argv else "default_config.json"
sample_request_file = sys.argv[sys.argv.index("--sample_request") + 1] if "--sample_request" in sys.argv else "default_example.json"
print(f"Config file: {config_file}")
print(f"Sample request file: {sample_request_file}")
config = read_json_from_file(f"./configs/{config_file}")
example_request = read_json_from_file(f"./tests/sample_requests/{sample_request_file}").get("lastRequest", {})
@ -71,48 +138,25 @@ if __name__ == "__main__":
break
logger.info("Added user message to conversation")
print(f"\n{'*'*50}\nLatest Request:\n{'*'*50}")
request_json = {
"messages": [{k: v for k, v in msg.items() if k != 'current_turn'} for msg in messages],
"state": state,
"agents": agent_configs,
"tools": tool_configs,
"prompts": prompt_configs,
"startAgent": start_agent_name
}
print(json.dumps(request_json, indent=2))
# Preprocess messages to replace role tool with role developer and add role user to empty roles
print("Preprocessing messages to replace role tool with role developer and add role user to empty roles")
messages = preprocess_messages(messages)
complete_request["messages"] = preprocess_messages(complete_request["messages"])
resp_messages, resp_tokens_used, resp_state = run_turn(
# Run the streaming turn
resp_messages, resp_state = asyncio.run(process_turn(
messages=messages,
start_agent_name=start_agent_name,
agent_configs=agent_configs,
tool_configs=tool_configs,
return_diff_messages=config.get("return_diff_messages", True),
prompt_configs=prompt_configs,
start_turn_with_start_agent=config.get("start_turn_with_start_agent", False),
children_aware_of_parent=config.get("children_aware_of_parent", False),
parent_has_child_history=config.get("parent_has_child_history", True),
start_agent_name=start_agent_name,
state=state,
additional_tool_configs=[RAG_TOOL, CLOSE_CHAT_TOOL],
error_tool_call=config.get("error_tool_call", True),
max_messages_per_turn=config.get("max_messages_per_turn", 10),
max_messages_per_error_escalation_turn=config.get("max_messages_per_error_escalation_turn", 4),
escalate_errors=config.get("escalate_errors", True),
max_overall_turns=config.get("max_overall_turns", 10)
)
state = resp_state
resp_messages = order_messages(resp_messages)
print(f"\n{'*'*50}\nLatest Response:\n{'*'*50}")
response_json = {
"messages": resp_messages,
"state": state,
"tokens_used": resp_tokens_used
}
print(json.dumps(response_json, indent=2))
config=config,
complete_request=complete_request
))
last_msg = resp_messages[-1]
print(f"\nBOT: {last_msg}\n")
state = resp_state
last_msg = resp_messages[-1] if resp_messages else {}
tool_calls = last_msg.get("tool_calls", [])
sender = last_msg.get("sender", "")
@ -153,7 +197,9 @@ if __name__ == "__main__":
else:
user_input_needed = True
print(f"Turn Duration: {round((datetime.now() - turn_start_time).total_seconds() * 10) / 10:.1f}s\n")
print(f"Tool Response Duration: {round(tool_duration * 10) / 10:.1f}s\n")
print("Quick stats")
print(f"Turn Duration: {round((datetime.now() - turn_start_time).total_seconds() * 10) / 10:.1f}s")
print(f"Tool Response Duration: {round(tool_duration * 10) / 10:.1f}s")
print('='*50)
print("\n" + "-" * 80)

View file

@ -0,0 +1,235 @@
{
"lastRequest": {
"messages": [
{
"content": "",
"role": "system",
"sender": null,
"tool_calls": null,
"tool_call_id": null,
"tool_name": null
},
{
"content": "hi",
"role": "user",
"sender": null,
"tool_calls": null,
"tool_call_id": null,
"tool_name": null
}
],
"state": {
"last_agent_name": "Door Dash Hub"
},
"agents": [
{
"name": "Door Dash Hub",
"type": "conversation",
"description": "Hub agent to manage Door Dash-related queries.",
"instructions": "## 🧑‍💼 Role:\nYou are responsible for directing Door Dash-related queries to appropriate agents.\n\n---\n## ⚙️ Steps to Follow:\n1. Greet the user and ask which Door Dash-related query they need help with (e.g., 'Are you facing issues with your order items or delivery timing?').\n2. If the query matches a specific task, direct the user to the corresponding agent:\n - Order Issue → [@agent:Order Issue]\n - Delayed Delivery → [@agent:Delayed Delivery]\n3. If the query doesn't match any specific task, respond with 'I'm sorry, I didn't understand. Could you clarify your request?' or escalate to human support.\n\n---\n## 🎯 Scope:\n✅ In Scope:\n- Issues with order items\n- Delayed delivery issues\n\n❌ Out of Scope:\n- Issues unrelated to Door Dash\n- General knowledge queries\n\n---\n## 📋 Guidelines:\n✔ Dos:\n- Direct queries to specific Door Dash agents promptly.\n- Call [@agent:Escalation] agent for unrecognized queries.\n\n🚫 Don'ts:\n- Engage in detailed support.\n- Extend the conversation beyond Door Dash.\n- Provide user-facing text such as 'I will connect you now...' when calling another agent\n\n# Examples\n- **User** : I need help with my order items.\n - **Agent actions**: [@agent:Order Issue](#mention)\n\n- **User** : My delivery is delayed.\n - **Agent actions**: Call [@agent:Delayed Delivery](#mention)\n\n- **User** : I'm not sure where my order is.\n - **Agent actions**: Call [@agent:Delayed Delivery](#mention)\n\n- **User** : Can you reset my order settings?\n - **Agent actions**: [@agent:Escalation](#mention)\n\n- **User** : How are you today?\n - **Agent response**: I'm doing great. What would like help with today?",
"model": "gpt-4o-mini",
"hasRagSources": false,
"controlType": "retain",
"tools": [],
"prompts": [],
"connectedAgents": [
"Order Issue",
"Delayed Delivery",
"Escalation"
]
},
{
"name": "Post process",
"type": "post_process",
"description": "",
"instructions": "Ensure that the agent response is terse and to the point.",
"model": "gpt-4o-mini",
"hasRagSources": false,
"controlType": "retain",
"tools": [],
"prompts": [],
"connectedAgents": []
},
{
"name": "Escalation",
"type": "escalation",
"description": "",
"instructions": "Get the user's contact information and let them know that their request has been escalated.",
"model": "gpt-4o-mini",
"hasRagSources": false,
"controlType": "retain",
"tools": [],
"prompts": [],
"connectedAgents": []
},
{
"name": "Order Issue",
"type": "conversation",
"description": "Agent to assist users with missing or incorrect order items.",
"instructions": "## 🧑‍💼 Role:\nAssist users with issues related to missing or incorrect order items.\n\n---\n## ⚙️ Steps to Follow:\n1. Fetch the order details using the [@tool:get_order_details] tool.\n2. Confirm the issue with the user.\n3. Provide solutions or escalate if unresolved.\n\n---\n## 🎯 Scope:\n✅ In Scope:\n- Handling missing or incorrect order items\n\n❌ Out of Scope:\n- Delayed delivery issues\n- General knowledge queries\n\n---\n## 📋 Guidelines:\n✔ Dos:\n- Ensure the user is aware of the order details before proceeding.\n\n🚫 Don'ts:\n- Extend the conversation beyond order issues.\n\n# Examples\n- **User** : I received the wrong item in my order.\n - **Agent response**: I can help with that. Let me fetch your order details first.\n - **Agent actions**: Call [@tool:get_order_details](#mention)\n\n- **User** : My order is missing an item.\n - **Agent response**: Let's check your order details and resolve this issue.\n - **Agent actions**: Call [@tool:get_order_details](#mention)\n\n- **User** : I got someone else's order.\n - **Agent response**: I apologize for the mix-up. I'll fetch your order details to sort this out.\n - **Agent actions**: Call [@tool:get_order_details](#mention)\n\n- **User** : Can you help me with a missing item?\n - **Agent response**: Certainly, I'll look into your order details right away.\n - **Agent actions**: Call [@tool:get_order_details](#mention)\n\n- **User** : There's an issue with my order items.\n - **Agent response**: Let's verify your order details to address this issue.\n - **Agent actions**: Call [@tool:get_order_details](#mention)",
"model": "gpt-4o",
"hasRagSources": false,
"controlType": "retain",
"tools": [
"get_order_details"
],
"prompts": [],
"connectedAgents": []
},
{
"name": "Delayed Delivery",
"type": "conversation",
"description": "Agent to assist users with delayed delivery issues.",
"instructions": "## 🧑‍💼 Role:\nAssist users with issues related to delayed delivery.\n\n---\n## ⚙️ Steps to Follow:\n1. Fetch the delivery status using the [@tool:get_delivery_status] tool.\n2. Confirm the delay with the user.\n3. Provide solutions or escalate if unresolved.\n\n---\n## 🎯 Scope:\n✅ In Scope:\n- Handling delayed delivery issues\n\n❌ Out of Scope:\n- Missing or incorrect order items\n- General knowledge queries\n\n---\n## 📋 Guidelines:\n✔ Dos:\n- Ensure the user is aware of the delivery status before proceeding.\n\n🚫 Don'ts:\n- Extend the conversation beyond delivery issues.\n\n# Examples\n- **User** : My delivery is late.\n - **Agent response**: I can help with that. Let me fetch your delivery status first.\n - **Agent actions**: Call [@tool:get_delivery_status](#mention)\n\n- **User** : Where is my order? It's delayed.\n - **Agent response**: Let's check your delivery status and resolve this issue.\n - **Agent actions**: Call [@tool:get_delivery_status](#mention)\n\n- **User** : My order hasn't arrived yet.\n - **Agent response**: I apologize for the delay. I'll fetch your delivery status to sort this out.\n - **Agent actions**: Call [@tool:get_delivery_status](#mention)\n\n- **User** : Can you help me with a delayed delivery?\n - **Agent response**: Certainly, I'll look into your delivery status right away.\n - **Agent actions**: Call [@tool:get_delivery_status](#mention)\n\n- **User** : There's an issue with my delivery timing.\n - **Agent response**: Let's verify your delivery status to address this issue.\n - **Agent actions**: Call [@tool:get_delivery_status](#mention)",
"model": "gpt-4o",
"hasRagSources": false,
"controlType": "retain",
"tools": [
"get_delivery_status"
],
"prompts": [],
"connectedAgents": []
}
],
"tools": [
{
"name": "get_order_details",
"description": "Tool to fetch details about the user's order.",
"parameters": {
"type": "object",
"properties": {
"order_id": {
"type": "string",
"description": "The unique identifier for the order."
}
},
"required": [
"order_id"
]
},
"mockTool": true,
"mockInstructions": "Return a mock response for Door Dash order details."
},
{
"name": "get_delivery_status",
"description": "Tool to fetch the current status of the delivery.",
"parameters": {
"type": "object",
"properties": {
"order_id": {
"type": "string",
"description": "The unique identifier for the order."
}
},
"required": [
"order_id"
]
}
}
],
"prompts": [
{
"name": "Style prompt",
"type": "style_prompt",
"prompt": "You should be empathetic and helpful."
}
],
"startAgent": "Door Dash Hub"
},
"lastResponse": {
"messages": [
{
"annotations": [],
"content": "Hello! How can I assist you today? Are you facing issues with your order items or delivery timing?",
"created_at": "2025-03-19T12:29:06.547196",
"current_turn": true,
"response_type": "internal",
"role": "assistant",
"sender": "Door Dash Hub"
},
{
"annotations": [],
"content": "Hi! How can I help you today? Are you having issues with your order items or delivery timing?",
"created_at": "2025-03-19T12:29:06.547196",
"current_turn": true,
"response_type": "external",
"role": "assistant",
"sender": "Door Dash Hub >> Post process"
}
],
"state": {
"agent_data": [
{
"child_functions": [
"transfer_to_escalation",
"transfer_to_order_issue",
"transfer_to_delayed_delivery"
],
"external_tools": [],
"history": [
{
"content": "hi",
"current_turn": true,
"role": "user"
},
{
"annotations": [],
"content": "Hello! How can I assist you today? Are you facing issues with your order items or delivery timing?",
"created_at": "2025-03-19T12:29:06.547196",
"current_turn": true,
"response_type": "internal",
"role": "assistant",
"sender": "Door Dash Hub"
}
],
"instructions": "## 🧑‍💼 Role:\nYou are responsible for directing Door Dash-related queries to appropriate agents.\n\n---\n## ⚙️ Steps to Follow:\n1. Greet the user and ask which Door Dash-related query they need help with (e.g., 'Are you facing issues with your order items or delivery timing?').\n2. If the query matches a specific task, direct the user to the corresponding agent:\n - Order Issue → [@agent:Order Issue]\n - Delayed Delivery → [@agent:Delayed Delivery]\n3. If the query doesn't match any specific task, respond with 'I'm sorry, I didn't understand. Could you clarify your request?' or escalate to human support.\n\n---\n## 🎯 Scope:\n✅ In Scope:\n- Issues with order items\n- Delayed delivery issues\n\n❌ Out of Scope:\n- Issues unrelated to Door Dash\n- General knowledge queries\n\n---\n## 📋 Guidelines:\n✔ Dos:\n- Direct queries to specific Door Dash agents promptly.\n- Call [@agent:Escalation] agent for unrecognized queries.\n\n🚫 Don'ts:\n- Engage in detailed support.\n- Extend the conversation beyond Door Dash.\n- Provide user-facing text such as 'I will connect you now...' when calling another agent\n\n# Examples\n- **User** : I need help with my order items.\n - **Agent actions**: [@agent:Order Issue](#mention)\n\n- **User** : My delivery is delayed.\n - **Agent actions**: Call [@agent:Delayed Delivery](#mention)\n\n- **User** : I'm not sure where my order is.\n - **Agent actions**: Call [@agent:Delayed Delivery](#mention)\n\n- **User** : Can you reset my order settings?\n - **Agent actions**: [@agent:Escalation](#mention)\n\n- **User** : How are you today?\n - **Agent response**: I'm doing great. What would like help with today?\n\n----------------------------------------------------------------------------------------------------\n\n\n# Instructions about using other specialized agents\nYou have the following specialized agents that you can transfer the chat to, using the appropriate tool calls for the transfer: \nName: Escalation\nDescription: \nTool for transfer: transfer_to_escalation\n----------------------------------------------------------------------------------------------------\nName: Order Issue\nDescription: Agent to assist users with missing or incorrect order items.\nTool for transfer: transfer_to_order_issue\n----------------------------------------------------------------------------------------------------\nName: Delayed Delivery\nDescription: Agent to assist users with delayed delivery issues.\nTool for transfer: transfer_to_delayed_delivery\n\n## Notes:\n- Transfer the chat to the appropriate agent, based on the chat history and / or the user's request.\n- When you transfer the chat to another agent, you should not provide any response to the user. For example, do not say 'Transferring chat to X agent' or anything like that. Just invoke the tool call to transfer to the other agent.\n- Do NOT ever mention the existence of other agents. For example, do not say 'Please check with X agent for details regarding processing times.' or anything like that.\n- If any other agent transfers the chat to you without responding to the user, it means that they don't know how to help. Do not transfer the chat to back to the same agent in this case. In such cases, you should transfer to the escalation agent using the appropriate tool call. Never ask the user to contact support.\n\n\n----------------------------------------------------------------------------------------------------\n\n\n# Additional System-Wide Context or Instructions:\n\n",
"internal_tools": [],
"most_recent_parent_name": "",
"name": "Door Dash Hub",
"parent_function": null
},
{
"child_functions": [],
"external_tools": [],
"history": [],
"instructions": "Get the user's contact information and let them know that their request has been escalated.\n\n----------------------------------------------------------------------------------------------------\n\n\n# Additional System-Wide Context or Instructions:\n\n",
"internal_tools": [],
"most_recent_parent_name": "",
"name": "Escalation",
"parent_function": null
},
{
"child_functions": [],
"external_tools": [
"get_order_details"
],
"history": [],
"instructions": "## 🧑‍💼 Role:\nAssist users with issues related to missing or incorrect order items.\n\n---\n## ⚙️ Steps to Follow:\n1. Fetch the order details using the [@tool:get_order_details] tool.\n2. Confirm the issue with the user.\n3. Provide solutions or escalate if unresolved.\n\n---\n## 🎯 Scope:\n✅ In Scope:\n- Handling missing or incorrect order items\n\n❌ Out of Scope:\n- Delayed delivery issues\n- General knowledge queries\n\n---\n## 📋 Guidelines:\n✔ Dos:\n- Ensure the user is aware of the order details before proceeding.\n\n🚫 Don'ts:\n- Extend the conversation beyond order issues.\n\n# Examples\n- **User** : I received the wrong item in my order.\n - **Agent response**: I can help with that. Let me fetch your order details first.\n - **Agent actions**: Call [@tool:get_order_details](#mention)\n\n- **User** : My order is missing an item.\n - **Agent response**: Let's check your order details and resolve this issue.\n - **Agent actions**: Call [@tool:get_order_details](#mention)\n\n- **User** : I got someone else's order.\n - **Agent response**: I apologize for the mix-up. I'll fetch your order details to sort this out.\n - **Agent actions**: Call [@tool:get_order_details](#mention)\n\n- **User** : Can you help me with a missing item?\n - **Agent response**: Certainly, I'll look into your order details right away.\n - **Agent actions**: Call [@tool:get_order_details](#mention)\n\n- **User** : There's an issue with my order items.\n - **Agent response**: Let's verify your order details to address this issue.\n - **Agent actions**: Call [@tool:get_order_details](#mention)\n\n----------------------------------------------------------------------------------------------------\n\n\n# Instructions about giving up chat control\nIf you are unable to handle the chat (e.g. if it is not in your scope of instructions), you should use the tool call provided to give up control of the chat.\nTool for transfer: give_up_chat_control\n\n## Notes:\n- When you give up control of the chat, you should not provide any response to the user. Just invoke the tool call to give up control.\n\n\n----------------------------------------------------------------------------------------------------\n\n\n# Additional System-Wide Context or Instructions:\n\n",
"internal_tools": [],
"most_recent_parent_name": "",
"name": "Order Issue",
"parent_function": null
},
{
"child_functions": [],
"external_tools": [
"get_delivery_status"
],
"history": [],
"instructions": "## 🧑‍💼 Role:\nAssist users with issues related to delayed delivery.\n\n---\n## ⚙️ Steps to Follow:\n1. Fetch the delivery status using the [@tool:get_delivery_status] tool.\n2. Confirm the delay with the user.\n3. Provide solutions or escalate if unresolved.\n\n---\n## 🎯 Scope:\n✅ In Scope:\n- Handling delayed delivery issues\n\n❌ Out of Scope:\n- Missing or incorrect order items\n- General knowledge queries\n\n---\n## 📋 Guidelines:\n✔ Dos:\n- Ensure the user is aware of the delivery status before proceeding.\n\n🚫 Don'ts:\n- Extend the conversation beyond delivery issues.\n\n# Examples\n- **User** : My delivery is late.\n - **Agent response**: I can help with that. Let me fetch your delivery status first.\n - **Agent actions**: Call [@tool:get_delivery_status](#mention)\n\n- **User** : Where is my order? It's delayed.\n - **Agent response**: Let's check your delivery status and resolve this issue.\n - **Agent actions**: Call [@tool:get_delivery_status](#mention)\n\n- **User** : My order hasn't arrived yet.\n - **Agent response**: I apologize for the delay. I'll fetch your delivery status to sort this out.\n - **Agent actions**: Call [@tool:get_delivery_status](#mention)\n\n- **User** : Can you help me with a delayed delivery?\n - **Agent response**: Certainly, I'll look into your delivery status right away.\n - **Agent actions**: Call [@tool:get_delivery_status](#mention)\n\n- **User** : There's an issue with my delivery timing.\n - **Agent response**: Let's verify your delivery status to address this issue.\n - **Agent actions**: Call [@tool:get_delivery_status](#mention)\n\n----------------------------------------------------------------------------------------------------\n\n\n# Instructions about giving up chat control\nIf you are unable to handle the chat (e.g. if it is not in your scope of instructions), you should use the tool call provided to give up control of the chat.\nTool for transfer: give_up_chat_control\n\n## Notes:\n- When you give up control of the chat, you should not provide any response to the user. Just invoke the tool call to give up control.\n\n\n----------------------------------------------------------------------------------------------------\n\n\n# Additional System-Wide Context or Instructions:\n\n",
"internal_tools": [],
"most_recent_parent_name": "",
"name": "Delayed Delivery",
"parent_function": null
}
],
"last_agent_name": "Door Dash Hub"
},
"tokens_used": {
"openai/gpt-4o-mini": {
"input_tokens": 1731,
"output_tokens": 45
}
}
}
}

View file

@ -0,0 +1,176 @@
{
"lastRequest": {
"messages": [
{
"role": "system",
"content": ""
},
{
"role": "user",
"content": "hi"
},
{
"role": "assistant",
"sender": "Door Dash Hub",
"content": "Hello! Are you facing issues with your order items or delivery timing? How can I assist you today?",
"created_at": "2025-03-24T17:33:27.564940"
},
{
"role": "user",
"content": "my order is missing fries"
},
{
"content": "Agent changed to Door Dash Hub",
"role": "assistant",
"sender": "Door Dash Hub",
"tool_calls": null,
"tool_call_id": null,
"response_type": "internal"
},
{
"content": "Agent changed to Order Issue",
"role": "assistant",
"sender": "Order Issue",
"tool_calls": null,
"tool_call_id": null,
"response_type": "internal"
},
{
"content": "I can help with that. Could you please provide your order ID so I can check the details?",
"role": "assistant",
"sender": "Order Issue",
"tool_calls": null,
"tool_call_id": null,
"tool_name": null,
"response_type": "external"
},
{
"role": "user",
"content": "12312"
}
],
"state": {
"last_agent_name": "Order Issue",
"tokens": {
"total": 1521,
"prompt": 1486,
"completion": 35
}
},
"agents": [
{
"name": "Door Dash Hub",
"type": "conversation",
"description": "Hub agent to manage Door Dash-related queries.",
"instructions": "## \ud83e\uddd1\u200d\ud83d\udcbc Role:\nYou are responsible for directing Door Dash-related queries to appropriate agents.\n\n---\n## \u2699\ufe0f Steps to Follow:\n1. Greet the user and ask which Door Dash-related query they need help with (e.g., 'Are you facing issues with your order items or delivery timing?').\n2. If the query matches a specific task, direct the user to the corresponding agent:\n - Order Issue \u2192 [@agent:Order Issue]\n - Delayed Delivery \u2192 [@agent:Delayed Delivery]\n3. If the query doesn't match any specific task, respond with 'I'm sorry, I didn't understand. Could you clarify your request?' or escalate to human support.\n\n---\n## \ud83c\udfaf Scope:\n\u2705 In Scope:\n- Issues with order items\n- Delayed delivery issues\n\n\u274c Out of Scope:\n- Issues unrelated to Door Dash\n- General knowledge queries\n\n---\n## \ud83d\udccb Guidelines:\n\u2714\ufe0f Dos:\n- Direct queries to specific Door Dash agents promptly.\n- Call [@agent:Escalation] agent for unrecognized queries.\n\n\ud83d\udeab Don'ts:\n- Engage in detailed support.\n- Extend the conversation beyond Door Dash.\n- Provide user-facing text such as 'I will connect you now...' when calling another agent\n\n# Examples\n- **User** : I need help with my order items.\n - **Agent actions**: [@agent:Order Issue](#mention)\n\n- **User** : My delivery is delayed.\n - **Agent actions**: Call [@agent:Delayed Delivery](#mention)\n\n- **User** : I'm not sure where my order is.\n - **Agent actions**: Call [@agent:Delayed Delivery](#mention)\n\n- **User** : Can you reset my order settings?\n - **Agent actions**: [@agent:Escalation](#mention)\n\n- **User** : How are you today?\n - **Agent response**: I'm doing great. What would like help with today?",
"model": "gpt-4o-mini",
"hasRagSources": false,
"controlType": "retain",
"tools": [],
"prompts": [],
"connectedAgents": [
"Order Issue",
"Delayed Delivery",
"Escalation"
]
},
{
"name": "Post process",
"type": "post_process",
"description": "",
"instructions": "Ensure that the agent response is terse and to the point.",
"model": "gpt-4o-mini",
"hasRagSources": false,
"controlType": "retain",
"tools": [],
"prompts": [],
"connectedAgents": []
},
{
"name": "Escalation",
"type": "escalation",
"description": "",
"instructions": "Get the user's contact information and let them know that their request has been escalated.",
"model": "gpt-4o-mini",
"hasRagSources": false,
"controlType": "retain",
"tools": [],
"prompts": [],
"connectedAgents": []
},
{
"name": "Order Issue",
"type": "conversation",
"description": "Agent to assist users with missing or incorrect order items.",
"instructions": "## \ud83e\uddd1\u200d\ud83d\udcbc Role:\nAssist users with issues related to missing or incorrect order items.\n\n---\n## \u2699\ufe0f Steps to Follow:\n1. Fetch the order details using the [@tool:get_order_details] tool.\n2. Confirm the issue with the user.\n3. Provide solutions or escalate if unresolved.\n\n---\n## \ud83c\udfaf Scope:\n\u2705 In Scope:\n- Handling missing or incorrect order items\n\n\u274c Out of Scope:\n- Delayed delivery issues\n- General knowledge queries\n\n---\n## \ud83d\udccb Guidelines:\n\u2714\ufe0f Dos:\n- Ensure the user is aware of the order details before proceeding.\n\n\ud83d\udeab Don'ts:\n- Extend the conversation beyond order issues.\n\n# Examples\n- **User** : I received the wrong item in my order.\n - **Agent response**: I can help with that. Let me fetch your order details first.\n - **Agent actions**: Call [@tool:get_order_details](#mention)\n\n- **User** : My order is missing an item.\n - **Agent response**: Let's check your order details and resolve this issue.\n - **Agent actions**: Call [@tool:get_order_details](#mention)\n\n- **User** : I got someone else's order.\n - **Agent response**: I apologize for the mix-up. I'll fetch your order details to sort this out.\n - **Agent actions**: Call [@tool:get_order_details](#mention)\n\n- **User** : Can you help me with a missing item?\n - **Agent response**: Certainly, I'll look into your order details right away.\n - **Agent actions**: Call [@tool:get_order_details](#mention)\n\n- **User** : There's an issue with my order items.\n - **Agent response**: Let's verify your order details to address this issue.\n - **Agent actions**: Call [@tool:get_order_details](#mention)",
"model": "gpt-4o",
"hasRagSources": false,
"controlType": "retain",
"tools": [
"get_order_details"
],
"prompts": [],
"connectedAgents": []
},
{
"name": "Delayed Delivery",
"type": "conversation",
"description": "Agent to assist users with delayed delivery issues.",
"instructions": "## \ud83e\uddd1\u200d\ud83d\udcbc Role:\nAssist users with issues related to delayed delivery.\n\n---\n## \u2699\ufe0f Steps to Follow:\n1. Fetch the delivery status using the [@tool:get_delivery_status] tool.\n2. Confirm the delay with the user.\n3. Provide solutions or escalate if unresolved.\n\n---\n## \ud83c\udfaf Scope:\n\u2705 In Scope:\n- Handling delayed delivery issues\n\n\u274c Out of Scope:\n- Missing or incorrect order items\n- General knowledge queries\n\n---\n## \ud83d\udccb Guidelines:\n\u2714\ufe0f Dos:\n- Ensure the user is aware of the delivery status before proceeding.\n\n\ud83d\udeab Don'ts:\n- Extend the conversation beyond delivery issues.\n\n# Examples\n- **User** : My delivery is late.\n - **Agent response**: I can help with that. Let me fetch your delivery status first.\n - **Agent actions**: Call [@tool:get_delivery_status](#mention)\n\n- **User** : Where is my order? It's delayed.\n - **Agent response**: Let's check your delivery status and resolve this issue.\n - **Agent actions**: Call [@tool:get_delivery_status](#mention)\n\n- **User** : My order hasn't arrived yet.\n - **Agent response**: I apologize for the delay. I'll fetch your delivery status to sort this out.\n - **Agent actions**: Call [@tool:get_delivery_status](#mention)\n\n- **User** : Can you help me with a delayed delivery?\n - **Agent response**: Certainly, I'll look into your delivery status right away.\n - **Agent actions**: Call [@tool:get_delivery_status](#mention)\n\n- **User** : There's an issue with my delivery timing.\n - **Agent response**: Let's verify your delivery status to address this issue.\n - **Agent actions**: Call [@tool:get_delivery_status](#mention)",
"model": "gpt-4o",
"hasRagSources": false,
"controlType": "retain",
"tools": [
"get_delivery_status"
],
"prompts": [],
"connectedAgents": []
}
],
"tools": [
{
"name": "get_order_details",
"description": "Tool to fetch details about the user's order.",
"parameters": {
"type": "object",
"properties": {
"order_id": {
"type": "string",
"description": "The unique identifier for the order."
}
},
"required": [
"order_id"
]
},
"mockTool": true,
"mockInstructions": "Return a mock response for Door Dash order details."
},
{
"name": "get_delivery_status",
"description": "Tool to fetch the current status of the delivery.",
"parameters": {
"type": "object",
"properties": {
"order_id": {
"type": "string",
"description": "The unique identifier for the order."
}
},
"required": [
"order_id"
]
}
}
],
"prompts": [
{
"name": "Style prompt",
"type": "style_prompt",
"prompt": "You should be empathetic and helpful."
}
],
"startAgent": "Door Dash Hub"
}
}

View file

@ -0,0 +1,223 @@
{
"lastRequest": {
"messages": [
{
"role": "system",
"content": ""
},
{
"role": "user",
"content": "hi"
},
{
"role": "assistant",
"sender": "Door Dash Hub",
"content": "Hello! Are you facing issues with your order items or delivery timing? How can I assist you today?",
"created_at": "2025-03-24T17:33:27.564940"
},
{
"role": "user",
"content": "my order is missing fries"
},
{
"content": "Agent changed to Door Dash Hub",
"role": "assistant",
"sender": "Door Dash Hub",
"tool_calls": null,
"tool_call_id": null,
"response_type": "internal"
},
{
"content": "Agent changed to Order Issue",
"role": "assistant",
"sender": "Order Issue",
"tool_calls": null,
"tool_call_id": null,
"response_type": "internal"
},
{
"content": "Let's resolve this issue by checking your order details. Could you please provide the order ID?",
"role": "assistant",
"sender": "Order Issue",
"tool_calls": null,
"tool_call_id": null,
"tool_name": null,
"response_type": "external"
},
{
"role": "user",
"content": "123412"
},
{
"content": "Agent changed to Order Issue",
"role": "assistant",
"sender": "Order Issue",
"tool_calls": null,
"tool_call_id": null,
"response_type": "internal"
},
{
"content": null,
"role": "assistant",
"sender": "Order Issue",
"tool_calls": [
{
"function": {
"name": "get_order_details",
"arguments": "{\"order_id\":\"123412\"}"
},
"id": "fc_67e37c622f208192aceec557fbd3125609b4eca638eb3571",
"type": "function"
}
],
"tool_call_id": null,
"tool_name": null,
"response_type": "internal"
},
{
"content": "Order Details for Order ID 123412:\n\n- **Restaurant**: Luigi's Pizzeria \n- **Items Ordered**: \n - 2x Margherita Pizza \n - 1x Garlic Bread \n - 1x Caesar Salad \n- **Estimated Delivery Time**: 7:45 PM \n- **Delivery Address**: 123 Elm Street, Apt 4B \n- **Order Status**: Out for delivery \n- **Total Amount**: $32.75 \n- **Payment Method**: Credit Card (**** **** **** 5678) \n- **Contact**: (123) 456-7890 \n- **Special Instructions**: Leave at the door.",
"role": "tool",
"sender": null,
"tool_calls": null,
"tool_call_id": "call_dqCMC5oreOoS9znDDJ7PqWha",
"tool_name": null,
"response_type": "internal"
},
{
"content": "I checked your order details, and it seems the fries were not included. Could you confirm if there was an oversight in placing the order, or were they supposed to be included?",
"role": "assistant",
"sender": "Order Issue",
"tool_calls": null,
"tool_call_id": null,
"tool_name": null,
"response_type": "external"
},
{
"role": "user",
"content": "Fries were supposed to be in"
}
],
"state": {
"last_agent_name": "Order Issue",
"tokens": {
"total": 1699,
"prompt": 1643,
"completion": 56
}
},
"agents": [
{
"name": "Door Dash Hub",
"type": "conversation",
"description": "Hub agent to manage Door Dash-related queries.",
"instructions": "## \ud83e\uddd1\u200d\ud83d\udcbc Role:\nYou are responsible for directing Door Dash-related queries to appropriate agents.\n\n---\n## \u2699\ufe0f Steps to Follow:\n1. Greet the user and ask which Door Dash-related query they need help with (e.g., 'Are you facing issues with your order items or delivery timing?').\n2. If the query matches a specific task, direct the user to the corresponding agent:\n - Order Issue \u2192 [@agent:Order Issue]\n - Delayed Delivery \u2192 [@agent:Delayed Delivery]\n3. If the query doesn't match any specific task, respond with 'I'm sorry, I didn't understand. Could you clarify your request?' or escalate to human support.\n\n---\n## \ud83c\udfaf Scope:\n\u2705 In Scope:\n- Issues with order items\n- Delayed delivery issues\n\n\u274c Out of Scope:\n- Issues unrelated to Door Dash\n- General knowledge queries\n\n---\n## \ud83d\udccb Guidelines:\n\u2714\ufe0f Dos:\n- Direct queries to specific Door Dash agents promptly.\n- Call [@agent:Escalation] agent for unrecognized queries.\n\n\ud83d\udeab Don'ts:\n- Engage in detailed support.\n- Extend the conversation beyond Door Dash.\n- Provide user-facing text such as 'I will connect you now...' when calling another agent\n\n# Examples\n- **User** : I need help with my order items.\n - **Agent actions**: [@agent:Order Issue](#mention)\n\n- **User** : My delivery is delayed.\n - **Agent actions**: Call [@agent:Delayed Delivery](#mention)\n\n- **User** : I'm not sure where my order is.\n - **Agent actions**: Call [@agent:Delayed Delivery](#mention)\n\n- **User** : Can you reset my order settings?\n - **Agent actions**: [@agent:Escalation](#mention)\n\n- **User** : How are you today?\n - **Agent response**: I'm doing great. What would like help with today?",
"model": "gpt-4o-mini",
"hasRagSources": false,
"controlType": "retain",
"tools": [],
"prompts": [],
"connectedAgents": [
"Order Issue",
"Delayed Delivery",
"Escalation"
]
},
{
"name": "Post process",
"type": "post_process",
"description": "",
"instructions": "Ensure that the agent response is terse and to the point.",
"model": "gpt-4o-mini",
"hasRagSources": false,
"controlType": "retain",
"tools": [],
"prompts": [],
"connectedAgents": []
},
{
"name": "Escalation",
"type": "escalation",
"description": "",
"instructions": "Get the user's contact information and let them know that their request has been escalated.",
"model": "gpt-4o-mini",
"hasRagSources": false,
"controlType": "retain",
"tools": [],
"prompts": [],
"connectedAgents": []
},
{
"name": "Order Issue",
"type": "conversation",
"description": "Agent to assist users with missing or incorrect order items.",
"instructions": "## \ud83e\uddd1\u200d\ud83d\udcbc Role:\nAssist users with issues related to missing or incorrect order items.\n\n---\n## \u2699\ufe0f Steps to Follow:\n1. Fetch the order details using the [@tool:get_order_details] tool.\n2. Confirm the issue with the user.\n3. Provide solutions or escalate if unresolved.\n\n---\n## \ud83c\udfaf Scope:\n\u2705 In Scope:\n- Handling missing or incorrect order items\n\n\u274c Out of Scope:\n- Delayed delivery issues\n- General knowledge queries\n\n---\n## \ud83d\udccb Guidelines:\n\u2714\ufe0f Dos:\n- Ensure the user is aware of the order details before proceeding.\n\n\ud83d\udeab Don'ts:\n- Extend the conversation beyond order issues.\n\n# Examples\n- **User** : I received the wrong item in my order.\n - **Agent response**: I can help with that. Let me fetch your order details first.\n - **Agent actions**: Call [@tool:get_order_details](#mention)\n\n- **User** : My order is missing an item.\n - **Agent response**: Let's check your order details and resolve this issue.\n - **Agent actions**: Call [@tool:get_order_details](#mention)\n\n- **User** : I got someone else's order.\n - **Agent response**: I apologize for the mix-up. I'll fetch your order details to sort this out.\n - **Agent actions**: Call [@tool:get_order_details](#mention)\n\n- **User** : Can you help me with a missing item?\n - **Agent response**: Certainly, I'll look into your order details right away.\n - **Agent actions**: Call [@tool:get_order_details](#mention)\n\n- **User** : There's an issue with my order items.\n - **Agent response**: Let's verify your order details to address this issue.\n - **Agent actions**: Call [@tool:get_order_details](#mention)",
"model": "gpt-4o",
"hasRagSources": false,
"controlType": "retain",
"tools": [
"get_order_details"
],
"prompts": [],
"connectedAgents": []
},
{
"name": "Delayed Delivery",
"type": "conversation",
"description": "Agent to assist users with delayed delivery issues.",
"instructions": "## \ud83e\uddd1\u200d\ud83d\udcbc Role:\nAssist users with issues related to delayed delivery.\n\n---\n## \u2699\ufe0f Steps to Follow:\n1. Fetch the delivery status using the [@tool:get_delivery_status] tool.\n2. Confirm the delay with the user.\n3. Provide solutions or escalate if unresolved.\n\n---\n## \ud83c\udfaf Scope:\n\u2705 In Scope:\n- Handling delayed delivery issues\n\n\u274c Out of Scope:\n- Missing or incorrect order items\n- General knowledge queries\n\n---\n## \ud83d\udccb Guidelines:\n\u2714\ufe0f Dos:\n- Ensure the user is aware of the delivery status before proceeding.\n\n\ud83d\udeab Don'ts:\n- Extend the conversation beyond delivery issues.\n\n# Examples\n- **User** : My delivery is late.\n - **Agent response**: I can help with that. Let me fetch your delivery status first.\n - **Agent actions**: Call [@tool:get_delivery_status](#mention)\n\n- **User** : Where is my order? It's delayed.\n - **Agent response**: Let's check your delivery status and resolve this issue.\n - **Agent actions**: Call [@tool:get_delivery_status](#mention)\n\n- **User** : My order hasn't arrived yet.\n - **Agent response**: I apologize for the delay. I'll fetch your delivery status to sort this out.\n - **Agent actions**: Call [@tool:get_delivery_status](#mention)\n\n- **User** : Can you help me with a delayed delivery?\n - **Agent response**: Certainly, I'll look into your delivery status right away.\n - **Agent actions**: Call [@tool:get_delivery_status](#mention)\n\n- **User** : There's an issue with my delivery timing.\n - **Agent response**: Let's verify your delivery status to address this issue.\n - **Agent actions**: Call [@tool:get_delivery_status](#mention)",
"model": "gpt-4o",
"hasRagSources": false,
"controlType": "retain",
"tools": [
"get_delivery_status"
],
"prompts": [],
"connectedAgents": []
}
],
"tools": [
{
"name": "get_order_details",
"description": "Tool to fetch details about the user's order.",
"parameters": {
"type": "object",
"properties": {
"order_id": {
"type": "string",
"description": "The unique identifier for the order."
}
},
"required": [
"order_id"
]
},
"mockTool": true,
"mockInstructions": "Return a mock response for Door Dash order details."
},
{
"name": "get_delivery_status",
"description": "Tool to fetch the current status of the delivery.",
"parameters": {
"type": "object",
"properties": {
"order_id": {
"type": "string",
"description": "The unique identifier for the order."
}
},
"required": [
"order_id"
]
}
}
],
"prompts": [
{
"name": "Style prompt",
"type": "style_prompt",
"prompt": "You should be empathetic and helpful."
}
],
"startAgent": "Door Dash Hub"
}
}

View file

@ -0,0 +1,270 @@
{
"lastRequest": {
"projectId": "4ebd5e81-010a-4bc6-91e0-0aa98173dbac",
"messages": [
{
"content": "",
"role": "system",
"sender": null,
"tool_calls": null,
"tool_call_id": null,
"tool_name": null
},
{
"content": "Hello! How can I assist you with your DoorDash query today? Are you inquiring about a missing item, delivery status, or subscription details?",
"role": "assistant",
"sender": "DoorDash Support Hub",
"tool_calls": null,
"tool_call_id": null,
"tool_name": null,
"response_type": "internal"
},
{
"content": "Hello! How can I assist you with your DoorDash query today? Are you inquiring about a missing item, delivery status, or subscription details?",
"role": "assistant",
"sender": "DoorDash Support Hub >> External",
"tool_calls": null,
"tool_call_id": null,
"tool_name": null,
"response_type": "external"
},
{
"content": "hi",
"role": "user",
"sender": null,
"tool_calls": null,
"tool_call_id": null,
"tool_name": null
}
],
"state": {
"agent_data": [
{
"instructions": "## 🧑‍💼 Role:\nYou are responsible for directing DoorDash-related queries to appropriate agents.\n\n---\n## ⚙️ Steps to Follow:\n1. Greet the user and ask which DoorDash-related query they need help with (e.g., 'Are you inquiring about a missing item, delivery status, or subscription details?').\n2. If the query matches a specific task, direct the user to the corresponding agent:\n - Missing Items → [@agent:Missing Items]\n - Delivery Status → [@agent:Delivery Status]\n - Subscription Info → [@agent:Subscription Info]\n3. If the query doesn't match any specific task, respond with 'I'm sorry, I didn't understand. Could you clarify your request?' or escalate to human support.\n\n---\n## 🎯 Scope:\n✅ In Scope:\n- Missing items queries\n- Delivery status queries\n- Subscription-related queries\n\n❌ Out of Scope:\n- Issues unrelated to DoorDash\n- General knowledge queries\n\n---\n## 📋 Guidelines:\n✔ Dos:\n- Direct queries to specific DoorDash agents promptly.\n- Call [@agent:Escalation] agent for unrecognized queries.\n\n🚫 Don'ts:\n- Engage in detailed support.\n- Extend the conversation beyond DoorDash.\n- Provide user-facing text such as 'I will connect you now...' when calling another agent\n\n# Examples\n- **User** : I need help with a missing item in my order.\n - **Agent actions**: Call [@agent:Missing Items](#mention)\n\n- **User** : Can you tell me the status of my delivery?\n - **Agent actions**: Call [@agent:Delivery Status](#mention)\n\n- **User** : How are you today?\n - **Agent response**: I'm doing great. What would you like help with today?\n\n- **User** : Can you reset my account settings?\n - **Agent actions**: Call [@agent:Escalation](#mention)\n\n- **User** : I have a question about my order.\n - **Agent response**: Could you specify if it's about a missing item, delivery status, or subscription details?\n\n- **User** : What are the available subscription plans?\n - **Agent actions**: Call [@agent:Subscription Info](#mention)",
"name": "DoorDash Support Hub"
},
{
"instructions": "Ensure that the agent response is terse and to the point.",
"name": "Post process"
},
{
"instructions": "Get the user's contact information and let them know that their request has been escalated.",
"name": "Escalation"
},
{
"instructions": "## 🧑‍💼 Role:\nHelp users resolve issues with missing items in their orders.\n\n---\n## ⚙️ Steps to Follow:\n1. Fetch the order details using the [@tool:get_order_details] tool.\n2. Confirm the missing items with the user.\n3. Provide resolution options or escalate if unresolved.\n\n---\n## 🎯 Scope:\n✅ In Scope:\n- Resolving missing items issues\n\n❌ Out of Scope:\n- Delivery status queries\n- General knowledge queries\n\n---\n## 📋 Guidelines:\n✔ Dos:\n- Use [@tool:get_order_details](#mention) to fetch accurate order information.\n- Provide clear resolution options.\n\n🚫 Don'ts:\n- Assume missing items without user confirmation.\n- Extend the conversation beyond missing items.\n\n# Examples\n- **User** : I didn't receive my fries with my order.\n - **Agent response**: Let me check your order details. Could you please provide your order ID?\n - **Agent actions**: Call [@tool:get_order_details](#mention)\n\n- **User** : My order is missing a drink.\n - **Agent response**: I apologize for the inconvenience. Let's verify your order details. Could you provide your order ID?\n - **Agent actions**: Call [@tool:get_order_details](#mention)\n\n- **User** : I didn't get the extra sauce I ordered.\n - **Agent response**: Let's check your order details to confirm. Could you provide your order ID?\n - **Agent actions**: Call [@tool:get_order_details](#mention)\n\n- **User** : My burger is missing from the order.\n - **Agent response**: I'm sorry to hear that. Let's verify your order details. Could you provide your order ID?\n - **Agent actions**: Call [@tool:get_order_details](#mention)\n\n- **User** : I didn't get my dessert.\n - **Agent response**: Let's check your order details to confirm. Could you provide your order ID?\n - **Agent actions**: Call [@tool:get_order_details](#mention)",
"name": "Missing Items"
},
{
"instructions": "## 🧑‍💼 Role:\nHelp users with queries related to the delivery status of their orders.\n\n---\n## ⚙️ Steps to Follow:\n1. Fetch the delivery status using the [@tool:get_delivery_status] tool.\n2. Provide the user with the current delivery status.\n3. Offer additional assistance if needed.\n\n---\n## 🎯 Scope:\n✅ In Scope:\n- Providing delivery status updates\n\n❌ Out of Scope:\n- Resolving missing items issues\n- General knowledge queries\n\n---\n## 📋 Guidelines:\n✔ Dos:\n- Use [@tool:get_delivery_status](#mention) to fetch accurate delivery information.\n- Provide clear and concise status updates.\n\n🚫 Don'ts:\n- Provide status updates without fetching current information.\n- Extend the conversation beyond delivery status.\n\n# Examples\n- **User** : Where is my order?\n - **Agent response**: Let me check the delivery status for you. Could you please provide your order ID?\n - **Agent actions**: Call [@tool:get_delivery_status](#mention)\n\n- **User** : Is my order on the way?\n - **Agent response**: I'll check the delivery status for you. Could you provide your order ID?\n - **Agent actions**: Call [@tool:get_delivery_status](#mention)\n\n- **User** : When will my order arrive?\n - **Agent response**: Let me find out the delivery status. Could you provide your order ID?\n - **Agent actions**: Call [@tool:get_delivery_status](#mention)\n\n- **User** : How long until my order gets here?\n - **Agent response**: I'll check the delivery status for you. Could you provide your order ID?\n - **Agent actions**: Call [@tool:get_delivery_status](#mention)\n\n- **User** : Is my order delayed?\n - **Agent response**: Let me check the delivery status for you. Could you provide your order ID?\n - **Agent actions**: Call [@tool:get_delivery_status](#mention)",
"name": "Delivery Status"
},
{
"instructions": "## 🧑‍💼 Role:\nProvide information and answer questions related to subscriptions using RAG.\n\n---\n## ⚙️ Steps to Follow:\n1. Use RAG to retrieve relevant information about subscriptions.\n2. Answer the user's questions based on the retrieved information.\n3. If the user's query is outside the scope of subscriptions, inform them politely.\n\n---\n## 🎯 Scope:\n✅ In Scope:\n- Subscription plans and pricing\n- Subscription features and benefits\n\n❌ Out of Scope:\n- Non-subscription-related queries\n- Detailed account management\n\n---\n## 📋 Guidelines:\n✔ Dos:\n- Use RAG to provide accurate and up-to-date information.\n- Be clear and concise in your responses.\n\n🚫 Don'ts:\n- Provide information without using RAG.\n- Extend the conversation beyond subscription topics.\n\n# Examples\n- **User** : What are the available subscription plans?\n - **Agent response**: Let me check the available subscription plans for you.\n\n- **User** : How much does the premium subscription cost?\n - **Agent response**: I'll find the current pricing for the premium subscription.\n\n- **User** : Can I change my subscription plan?\n - **Agent response**: I'll provide information on how to change your subscription plan.\n\n- **User** : What benefits do I get with a subscription?\n - **Agent response**: Let me retrieve the benefits associated with our subscription plans.\n\n- **User** : Is there a free trial available?\n - **Agent response**: I'll check if there's a free trial available for our subscriptions.",
"name": "Subscription Info"
}
],
"last_agent_name": "DoorDash Support Hub"
},
"agents": [
{
"name": "DoorDash Support Hub",
"type": "conversation",
"description": "Hub agent to manage DoorDash-related queries.",
"instructions": "## 🧑‍💼 Role:\nYou are responsible for directing DoorDash-related queries to appropriate agents.\n\n---\n## ⚙️ Steps to Follow:\n1. Greet the user and ask which DoorDash-related query they need help with (e.g., 'Are you inquiring about a missing item, delivery status, or subscription details?').\n2. If the query matches a specific task, direct the user to the corresponding agent:\n - Missing Items → [@agent:Missing Items]\n - Delivery Status → [@agent:Delivery Status]\n - Subscription Info → [@agent:Subscription Info]\n3. If the query doesn't match any specific task, respond with 'I'm sorry, I didn't understand. Could you clarify your request?' or escalate to human support.\n\n---\n## 🎯 Scope:\n✅ In Scope:\n- Missing items queries\n- Delivery status queries\n- Subscription-related queries\n\n❌ Out of Scope:\n- Issues unrelated to DoorDash\n- General knowledge queries\n\n---\n## 📋 Guidelines:\n✔ Dos:\n- Direct queries to specific DoorDash agents promptly.\n- Call [@agent:Escalation] agent for unrecognized queries.\n\n🚫 Don'ts:\n- Engage in detailed support.\n- Extend the conversation beyond DoorDash.\n- Provide user-facing text such as 'I will connect you now...' when calling another agent\n\n# Examples\n- **User** : I need help with a missing item in my order.\n - **Agent actions**: Call [@agent:Missing Items](#mention)\n\n- **User** : Can you tell me the status of my delivery?\n - **Agent actions**: Call [@agent:Delivery Status](#mention)\n\n- **User** : How are you today?\n - **Agent response**: I'm doing great. What would you like help with today?\n\n- **User** : Can you reset my account settings?\n - **Agent actions**: Call [@agent:Escalation](#mention)\n\n- **User** : I have a question about my order.\n - **Agent response**: Could you specify if it's about a missing item, delivery status, or subscription details?\n\n- **User** : What are the available subscription plans?\n - **Agent actions**: Call [@agent:Subscription Info](#mention)",
"model": "gpt-4o",
"controlType": "retain",
"ragK": 3,
"ragReturnType": "chunks",
"tools": [],
"prompts": [],
"connectedAgents": [
"Missing Items",
"Delivery Status",
"Subscription Info",
"Escalation"
]
},
{
"name": "Post process",
"type": "post_process",
"description": "",
"instructions": "Ensure that the agent response is terse and to the point.",
"model": "gpt-4o-mini",
"controlType": "retain",
"ragK": 3,
"ragReturnType": "chunks",
"tools": [],
"prompts": [],
"connectedAgents": []
},
{
"name": "Escalation",
"type": "escalation",
"description": "",
"instructions": "Get the user's contact information and let them know that their request has been escalated.",
"model": "gpt-4o-mini",
"controlType": "retain",
"ragK": 3,
"ragReturnType": "chunks",
"tools": [],
"prompts": [],
"connectedAgents": []
},
{
"name": "Missing Items",
"type": "conversation",
"description": "Agent to assist users with missing items in their orders.",
"instructions": "## 🧑‍💼 Role:\nHelp users resolve issues with missing items in their orders.\n\n---\n## ⚙️ Steps to Follow:\n1. Fetch the order details using the [@tool:get_order_details] tool.\n2. Confirm the missing items with the user.\n3. Provide resolution options or escalate if unresolved.\n\n---\n## 🎯 Scope:\n✅ In Scope:\n- Resolving missing items issues\n\n❌ Out of Scope:\n- Delivery status queries\n- General knowledge queries\n\n---\n## 📋 Guidelines:\n✔ Dos:\n- Use [@tool:get_order_details](#mention) to fetch accurate order information.\n- Provide clear resolution options.\n\n🚫 Don'ts:\n- Assume missing items without user confirmation.\n- Extend the conversation beyond missing items.\n\n# Examples\n- **User** : I didn't receive my fries with my order.\n - **Agent response**: Let me check your order details. Could you please provide your order ID?\n - **Agent actions**: Call [@tool:get_order_details](#mention)\n\n- **User** : My order is missing a drink.\n - **Agent response**: I apologize for the inconvenience. Let's verify your order details. Could you provide your order ID?\n - **Agent actions**: Call [@tool:get_order_details](#mention)\n\n- **User** : I didn't get the extra sauce I ordered.\n - **Agent response**: Let's check your order details to confirm. Could you provide your order ID?\n - **Agent actions**: Call [@tool:get_order_details](#mention)\n\n- **User** : My burger is missing from the order.\n - **Agent response**: I'm sorry to hear that. Let's verify your order details. Could you provide your order ID?\n - **Agent actions**: Call [@tool:get_order_details](#mention)\n\n- **User** : I didn't get my dessert.\n - **Agent response**: Let's check your order details to confirm. Could you provide your order ID?\n - **Agent actions**: Call [@tool:get_order_details](#mention)",
"model": "gpt-4o",
"controlType": "retain",
"ragK": 3,
"ragReturnType": "chunks",
"tools": [
"get_order_details"
],
"prompts": [],
"connectedAgents": []
},
{
"name": "Delivery Status",
"type": "conversation",
"description": "Agent to assist users with delivery status queries.",
"instructions": "## 🧑‍💼 Role:\nHelp users with queries related to the delivery status of their orders.\n\n---\n## ⚙️ Steps to Follow:\n1. Fetch the delivery status using the [@tool:get_delivery_status] tool.\n2. Provide the user with the current delivery status.\n3. Offer additional assistance if needed.\n\n---\n## 🎯 Scope:\n✅ In Scope:\n- Providing delivery status updates\n\n❌ Out of Scope:\n- Resolving missing items issues\n- General knowledge queries\n\n---\n## 📋 Guidelines:\n✔ Dos:\n- Use [@tool:get_delivery_status](#mention) to fetch accurate delivery information.\n- Provide clear and concise status updates.\n\n🚫 Don'ts:\n- Provide status updates without fetching current information.\n- Extend the conversation beyond delivery status.\n\n# Examples\n- **User** : Where is my order?\n - **Agent response**: Let me check the delivery status for you. Could you please provide your order ID?\n - **Agent actions**: Call [@tool:get_delivery_status](#mention)\n\n- **User** : Is my order on the way?\n - **Agent response**: I'll check the delivery status for you. Could you provide your order ID?\n - **Agent actions**: Call [@tool:get_delivery_status](#mention)\n\n- **User** : When will my order arrive?\n - **Agent response**: Let me find out the delivery status. Could you provide your order ID?\n - **Agent actions**: Call [@tool:get_delivery_status](#mention)\n\n- **User** : How long until my order gets here?\n - **Agent response**: I'll check the delivery status for you. Could you provide your order ID?\n - **Agent actions**: Call [@tool:get_delivery_status](#mention)\n\n- **User** : Is my order delayed?\n - **Agent response**: Let me check the delivery status for you. Could you provide your order ID?\n - **Agent actions**: Call [@tool:get_delivery_status](#mention)",
"model": "gpt-4o",
"controlType": "retain",
"ragK": 3,
"ragReturnType": "chunks",
"tools": [
"get_delivery_status"
],
"prompts": [],
"connectedAgents": []
},
{
"name": "Subscription Info",
"type": "conversation",
"description": "Agent to assist users with subscription-related queries.",
"instructions": "## 🧑‍💼 Role:\nProvide information and answer questions related to subscriptions using RAG.\n\n---\n## ⚙️ Steps to Follow:\n1. Use RAG to retrieve relevant information about subscriptions.\n2. Answer the user's questions based on the retrieved information.\n3. If the user's query is outside the scope of subscriptions, inform them politely.\n\n---\n## 🎯 Scope:\n✅ In Scope:\n- Subscription plans and pricing\n- Subscription features and benefits\n\n❌ Out of Scope:\n- Non-subscription-related queries\n- Detailed account management\n\n---\n## 📋 Guidelines:\n✔ Dos:\n- Use RAG to provide accurate and up-to-date information.\n- Be clear and concise in your responses.\n\n🚫 Don'ts:\n- Provide information without using RAG.\n- Extend the conversation beyond subscription topics.\n\n# Examples\n- **User** : What are the available subscription plans?\n - **Agent response**: Let me check the available subscription plans for you.\n\n- **User** : How much does the premium subscription cost?\n - **Agent response**: I'll find the current pricing for the premium subscription.\n\n- **User** : Can I change my subscription plan?\n - **Agent response**: I'll provide information on how to change your subscription plan.\n\n- **User** : What benefits do I get with a subscription?\n - **Agent response**: Let me retrieve the benefits associated with our subscription plans.\n\n- **User** : Is there a free trial available?\n - **Agent response**: I'll check if there's a free trial available for our subscriptions.",
"model": "gpt-4o",
"controlType": "relinquish_to_parent",
"ragDataSources": [
"67e1612510540d9027909e10"
],
"ragK": 3,
"ragReturnType": "content",
"tools": [],
"prompts": [],
"connectedAgents": []
}
],
"tools": [
{
"name": "get_order_details",
"description": "Tool to fetch the user's order details.",
"parameters": {
"type": "object",
"properties": {
"order_id": {
"type": "string",
"description": "The unique identifier for the order."
}
},
"required": [
"order_id"
]
}
},
{
"name": "get_delivery_status",
"description": "Tool to fetch the delivery status of an order.",
"parameters": {
"type": "object",
"properties": {
"order_id": {
"type": "string",
"description": "The unique identifier for the order."
}
},
"required": [
"order_id"
]
},
"mockTool": true,
"mockInstructions": "Give a mock response for a door dash order delivery status."
}
],
"prompts": [
{
"name": "Style prompt",
"type": "style_prompt",
"prompt": "You should be empathetic and helpful."
},
{
"name": "Greeting",
"type": "greeting",
"prompt": "Hello! How can I help you?"
}
],
"startAgent": "DoorDash Support Hub",
"mcpServers": [
{
"name": "delivery",
"url": "http://localhost:8000/sse"
}
],
"toolWebhookUrl": "http://127.0.0.1:4020/tool_call"
},
"lastResponse": {
"messages": [
{
"content": "Hi there! How can I help you with your DoorDash query? Are you inquiring about a missing item, delivery status, or subscription details?",
"created_at": null,
"response_type": "internal",
"role": "assistant",
"sender": "DoorDash Support Hub"
},
{
"content": "Hi there! How can I help you with your DoorDash query? Are you inquiring about a missing item, delivery status, or subscription details?",
"created_at": null,
"response_type": "external",
"role": "assistant",
"sender": "DoorDash Support Hub >> External"
}
],
"state": {
"agent_data": [
{
"instructions": "## 🧑‍💼 Role:\nYou are responsible for directing DoorDash-related queries to appropriate agents.\n\n---\n## ⚙️ Steps to Follow:\n1. Greet the user and ask which DoorDash-related query they need help with (e.g., 'Are you inquiring about a missing item, delivery status, or subscription details?').\n2. If the query matches a specific task, direct the user to the corresponding agent:\n - Missing Items → [@agent:Missing Items]\n - Delivery Status → [@agent:Delivery Status]\n - Subscription Info → [@agent:Subscription Info]\n3. If the query doesn't match any specific task, respond with 'I'm sorry, I didn't understand. Could you clarify your request?' or escalate to human support.\n\n---\n## 🎯 Scope:\n✅ In Scope:\n- Missing items queries\n- Delivery status queries\n- Subscription-related queries\n\n❌ Out of Scope:\n- Issues unrelated to DoorDash\n- General knowledge queries\n\n---\n## 📋 Guidelines:\n✔ Dos:\n- Direct queries to specific DoorDash agents promptly.\n- Call [@agent:Escalation] agent for unrecognized queries.\n\n🚫 Don'ts:\n- Engage in detailed support.\n- Extend the conversation beyond DoorDash.\n- Provide user-facing text such as 'I will connect you now...' when calling another agent\n\n# Examples\n- **User** : I need help with a missing item in my order.\n - **Agent actions**: Call [@agent:Missing Items](#mention)\n\n- **User** : Can you tell me the status of my delivery?\n - **Agent actions**: Call [@agent:Delivery Status](#mention)\n\n- **User** : How are you today?\n - **Agent response**: I'm doing great. What would you like help with today?\n\n- **User** : Can you reset my account settings?\n - **Agent actions**: Call [@agent:Escalation](#mention)\n\n- **User** : I have a question about my order.\n - **Agent response**: Could you specify if it's about a missing item, delivery status, or subscription details?\n\n- **User** : What are the available subscription plans?\n - **Agent actions**: Call [@agent:Subscription Info](#mention)",
"name": "DoorDash Support Hub"
},
{
"instructions": "Ensure that the agent response is terse and to the point.",
"name": "Post process"
},
{
"instructions": "Get the user's contact information and let them know that their request has been escalated.",
"name": "Escalation"
},
{
"instructions": "## 🧑‍💼 Role:\nHelp users resolve issues with missing items in their orders.\n\n---\n## ⚙️ Steps to Follow:\n1. Fetch the order details using the [@tool:get_order_details] tool.\n2. Confirm the missing items with the user.\n3. Provide resolution options or escalate if unresolved.\n\n---\n## 🎯 Scope:\n✅ In Scope:\n- Resolving missing items issues\n\n❌ Out of Scope:\n- Delivery status queries\n- General knowledge queries\n\n---\n## 📋 Guidelines:\n✔ Dos:\n- Use [@tool:get_order_details](#mention) to fetch accurate order information.\n- Provide clear resolution options.\n\n🚫 Don'ts:\n- Assume missing items without user confirmation.\n- Extend the conversation beyond missing items.\n\n# Examples\n- **User** : I didn't receive my fries with my order.\n - **Agent response**: Let me check your order details. Could you please provide your order ID?\n - **Agent actions**: Call [@tool:get_order_details](#mention)\n\n- **User** : My order is missing a drink.\n - **Agent response**: I apologize for the inconvenience. Let's verify your order details. Could you provide your order ID?\n - **Agent actions**: Call [@tool:get_order_details](#mention)\n\n- **User** : I didn't get the extra sauce I ordered.\n - **Agent response**: Let's check your order details to confirm. Could you provide your order ID?\n - **Agent actions**: Call [@tool:get_order_details](#mention)\n\n- **User** : My burger is missing from the order.\n - **Agent response**: I'm sorry to hear that. Let's verify your order details. Could you provide your order ID?\n - **Agent actions**: Call [@tool:get_order_details](#mention)\n\n- **User** : I didn't get my dessert.\n - **Agent response**: Let's check your order details to confirm. Could you provide your order ID?\n - **Agent actions**: Call [@tool:get_order_details](#mention)",
"name": "Missing Items"
},
{
"instructions": "## 🧑‍💼 Role:\nHelp users with queries related to the delivery status of their orders.\n\n---\n## ⚙️ Steps to Follow:\n1. Fetch the delivery status using the [@tool:get_delivery_status] tool.\n2. Provide the user with the current delivery status.\n3. Offer additional assistance if needed.\n\n---\n## 🎯 Scope:\n✅ In Scope:\n- Providing delivery status updates\n\n❌ Out of Scope:\n- Resolving missing items issues\n- General knowledge queries\n\n---\n## 📋 Guidelines:\n✔ Dos:\n- Use [@tool:get_delivery_status](#mention) to fetch accurate delivery information.\n- Provide clear and concise status updates.\n\n🚫 Don'ts:\n- Provide status updates without fetching current information.\n- Extend the conversation beyond delivery status.\n\n# Examples\n- **User** : Where is my order?\n - **Agent response**: Let me check the delivery status for you. Could you please provide your order ID?\n - **Agent actions**: Call [@tool:get_delivery_status](#mention)\n\n- **User** : Is my order on the way?\n - **Agent response**: I'll check the delivery status for you. Could you provide your order ID?\n - **Agent actions**: Call [@tool:get_delivery_status](#mention)\n\n- **User** : When will my order arrive?\n - **Agent response**: Let me find out the delivery status. Could you provide your order ID?\n - **Agent actions**: Call [@tool:get_delivery_status](#mention)\n\n- **User** : How long until my order gets here?\n - **Agent response**: I'll check the delivery status for you. Could you provide your order ID?\n - **Agent actions**: Call [@tool:get_delivery_status](#mention)\n\n- **User** : Is my order delayed?\n - **Agent response**: Let me check the delivery status for you. Could you provide your order ID?\n - **Agent actions**: Call [@tool:get_delivery_status](#mention)",
"name": "Delivery Status"
},
{
"instructions": "## 🧑‍💼 Role:\nProvide information and answer questions related to subscriptions using RAG.\n\n---\n## ⚙️ Steps to Follow:\n1. Use RAG to retrieve relevant information about subscriptions.\n2. Answer the user's questions based on the retrieved information.\n3. If the user's query is outside the scope of subscriptions, inform them politely.\n\n---\n## 🎯 Scope:\n✅ In Scope:\n- Subscription plans and pricing\n- Subscription features and benefits\n\n❌ Out of Scope:\n- Non-subscription-related queries\n- Detailed account management\n\n---\n## 📋 Guidelines:\n✔ Dos:\n- Use RAG to provide accurate and up-to-date information.\n- Be clear and concise in your responses.\n\n🚫 Don'ts:\n- Provide information without using RAG.\n- Extend the conversation beyond subscription topics.\n\n# Examples\n- **User** : What are the available subscription plans?\n - **Agent response**: Let me check the available subscription plans for you.\n\n- **User** : How much does the premium subscription cost?\n - **Agent response**: I'll find the current pricing for the premium subscription.\n\n- **User** : Can I change my subscription plan?\n - **Agent response**: I'll provide information on how to change your subscription plan.\n\n- **User** : What benefits do I get with a subscription?\n - **Agent response**: Let me retrieve the benefits associated with our subscription plans.\n\n- **User** : Is there a free trial available?\n - **Agent response**: I'll check if there's a free trial available for our subscriptions.",
"name": "Subscription Info"
}
],
"last_agent_name": "DoorDash Support Hub"
},
"tokens_used": {
"completion": 50,
"prompt": 50,
"total": 100
}
}
}

View file

@ -0,0 +1,166 @@
{
"lastRequest": {
"projectId": "4ebd5e81-010a-4bc6-91e0-0aa98173dbac",
"messages": [
{
"content": "",
"role": "system",
"sender": null,
"tool_calls": null,
"tool_call_id": null,
"tool_name": null
}
],
"state": {
"last_agent_name": "DoorDash Support Hub"
},
"agents": [
{
"name": "DoorDash Support Hub",
"type": "conversation",
"description": "Hub agent to manage DoorDash-related queries.",
"instructions": "## 🧑‍💼 Role:\nYou are responsible for directing DoorDash-related queries to appropriate agents.\n\n---\n## ⚙️ Steps to Follow:\n1. Greet the user and ask which DoorDash-related query they need help with (e.g., 'Are you inquiring about a missing item, delivery status, or subscription details?').\n2. If the query matches a specific task, direct the user to the corresponding agent:\n - Missing Items → [@agent:Missing Items]\n - Delivery Status → [@agent:Delivery Status]\n - Subscription Info → [@agent:Subscription Info]\n3. If the query doesn't match any specific task, respond with 'I'm sorry, I didn't understand. Could you clarify your request?' or escalate to human support.\n\n---\n## 🎯 Scope:\n✅ In Scope:\n- Missing items queries\n- Delivery status queries\n- Subscription-related queries\n\n❌ Out of Scope:\n- Issues unrelated to DoorDash\n- General knowledge queries\n\n---\n## 📋 Guidelines:\n✔ Dos:\n- Direct queries to specific DoorDash agents promptly.\n- Call [@agent:Escalation] agent for unrecognized queries.\n\n🚫 Don'ts:\n- Engage in detailed support.\n- Extend the conversation beyond DoorDash.\n- Provide user-facing text such as 'I will connect you now...' when calling another agent\n\n# Examples\n- **User** : I need help with a missing item in my order.\n - **Agent actions**: Call [@agent:Missing Items](#mention)\n\n- **User** : Can you tell me the status of my delivery?\n - **Agent actions**: Call [@agent:Delivery Status](#mention)\n\n- **User** : How are you today?\n - **Agent response**: I'm doing great. What would you like help with today?\n\n- **User** : Can you reset my account settings?\n - **Agent actions**: Call [@agent:Escalation](#mention)\n\n- **User** : I have a question about my order.\n - **Agent response**: Could you specify if it's about a missing item, delivery status, or subscription details?\n\n- **User** : What are the available subscription plans?\n - **Agent actions**: Call [@agent:Subscription Info](#mention)",
"model": "gpt-4o",
"controlType": "retain",
"ragK": 3,
"ragReturnType": "chunks",
"tools": [],
"prompts": [],
"connectedAgents": [
"Missing Items",
"Delivery Status",
"Subscription Info",
"Escalation"
]
},
{
"name": "Post process",
"type": "post_process",
"description": "",
"instructions": "Ensure that the agent response is terse and to the point.",
"model": "gpt-4o-mini",
"controlType": "retain",
"ragK": 3,
"ragReturnType": "chunks",
"tools": [],
"prompts": [],
"connectedAgents": []
},
{
"name": "Escalation",
"type": "escalation",
"description": "",
"instructions": "Get the user's contact information and let them know that their request has been escalated.",
"model": "gpt-4o-mini",
"controlType": "retain",
"ragK": 3,
"ragReturnType": "chunks",
"tools": [],
"prompts": [],
"connectedAgents": []
},
{
"name": "Missing Items",
"type": "conversation",
"description": "Agent to assist users with missing items in their orders.",
"instructions": "## 🧑‍💼 Role:\nHelp users resolve issues with missing items in their orders.\n\n---\n## ⚙️ Steps to Follow:\n1. Fetch the order details using the [@tool:get_order_details] tool.\n2. Confirm the missing items with the user.\n3. Provide resolution options or escalate if unresolved.\n\n---\n## 🎯 Scope:\n✅ In Scope:\n- Resolving missing items issues\n\n❌ Out of Scope:\n- Delivery status queries\n- General knowledge queries\n\n---\n## 📋 Guidelines:\n✔ Dos:\n- Use [@tool:get_order_details](#mention) to fetch accurate order information.\n- Provide clear resolution options.\n\n🚫 Don'ts:\n- Assume missing items without user confirmation.\n- Extend the conversation beyond missing items.\n\n# Examples\n- **User** : I didn't receive my fries with my order.\n - **Agent response**: Let me check your order details. Could you please provide your order ID?\n - **Agent actions**: Call [@tool:get_order_details](#mention)\n\n- **User** : My order is missing a drink.\n - **Agent response**: I apologize for the inconvenience. Let's verify your order details. Could you provide your order ID?\n - **Agent actions**: Call [@tool:get_order_details](#mention)\n\n- **User** : I didn't get the extra sauce I ordered.\n - **Agent response**: Let's check your order details to confirm. Could you provide your order ID?\n - **Agent actions**: Call [@tool:get_order_details](#mention)\n\n- **User** : My burger is missing from the order.\n - **Agent response**: I'm sorry to hear that. Let's verify your order details. Could you provide your order ID?\n - **Agent actions**: Call [@tool:get_order_details](#mention)\n\n- **User** : I didn't get my dessert.\n - **Agent response**: Let's check your order details to confirm. Could you provide your order ID?\n - **Agent actions**: Call [@tool:get_order_details](#mention)",
"model": "gpt-4o",
"controlType": "retain",
"ragK": 3,
"ragReturnType": "chunks",
"tools": [
"get_order_details"
],
"prompts": [],
"connectedAgents": []
},
{
"name": "Delivery Status",
"type": "conversation",
"description": "Agent to assist users with delivery status queries.",
"instructions": "## 🧑‍💼 Role:\nHelp users with queries related to the delivery status of their orders.\n\n---\n## ⚙️ Steps to Follow:\n1. Fetch the delivery status using the [@tool:get_delivery_status] tool.\n2. Provide the user with the current delivery status.\n3. Offer additional assistance if needed.\n\n---\n## 🎯 Scope:\n✅ In Scope:\n- Providing delivery status updates\n\n❌ Out of Scope:\n- Resolving missing items issues\n- General knowledge queries\n\n---\n## 📋 Guidelines:\n✔ Dos:\n- Use [@tool:get_delivery_status](#mention) to fetch accurate delivery information.\n- Provide clear and concise status updates.\n\n🚫 Don'ts:\n- Provide status updates without fetching current information.\n- Extend the conversation beyond delivery status.\n\n# Examples\n- **User** : Where is my order?\n - **Agent response**: Let me check the delivery status for you. Could you please provide your order ID?\n - **Agent actions**: Call [@tool:get_delivery_status](#mention)\n\n- **User** : Is my order on the way?\n - **Agent response**: I'll check the delivery status for you. Could you provide your order ID?\n - **Agent actions**: Call [@tool:get_delivery_status](#mention)\n\n- **User** : When will my order arrive?\n - **Agent response**: Let me find out the delivery status. Could you provide your order ID?\n - **Agent actions**: Call [@tool:get_delivery_status](#mention)\n\n- **User** : How long until my order gets here?\n - **Agent response**: I'll check the delivery status for you. Could you provide your order ID?\n - **Agent actions**: Call [@tool:get_delivery_status](#mention)\n\n- **User** : Is my order delayed?\n - **Agent response**: Let me check the delivery status for you. Could you provide your order ID?\n - **Agent actions**: Call [@tool:get_delivery_status](#mention)",
"model": "gpt-4o",
"controlType": "retain",
"ragK": 3,
"ragReturnType": "chunks",
"tools": [
"get_delivery_status"
],
"prompts": [],
"connectedAgents": []
},
{
"name": "Subscription Info",
"type": "conversation",
"description": "Agent to assist users with subscription-related queries.",
"instructions": "## 🧑‍💼 Role:\nProvide information and answer questions related to subscriptions using RAG.\n\n---\n## ⚙️ Steps to Follow:\n1. Use RAG to retrieve relevant information about subscriptions.\n2. Answer the user's questions based on the retrieved information.\n3. If the user's query is outside the scope of subscriptions, inform them politely.\n\n---\n## 🎯 Scope:\n✅ In Scope:\n- Subscription plans and pricing\n- Subscription features and benefits\n\n❌ Out of Scope:\n- Non-subscription-related queries\n- Detailed account management\n\n---\n## 📋 Guidelines:\n✔ Dos:\n- Use RAG to provide accurate and up-to-date information.\n- Be clear and concise in your responses.\n\n🚫 Don'ts:\n- Provide information without using RAG.\n- Extend the conversation beyond subscription topics.\n\n# Examples\n- **User** : What are the available subscription plans?\n - **Agent response**: Let me check the available subscription plans for you.\n\n- **User** : How much does the premium subscription cost?\n - **Agent response**: I'll find the current pricing for the premium subscription.\n\n- **User** : Can I change my subscription plan?\n - **Agent response**: I'll provide information on how to change your subscription plan.\n\n- **User** : What benefits do I get with a subscription?\n - **Agent response**: Let me retrieve the benefits associated with our subscription plans.\n\n- **User** : Is there a free trial available?\n - **Agent response**: I'll check if there's a free trial available for our subscriptions.",
"model": "gpt-4o",
"controlType": "relinquish_to_parent",
"ragDataSources": [
"67e1612510540d9027909e10"
],
"ragK": 3,
"ragReturnType": "content",
"tools": [],
"prompts": [],
"connectedAgents": []
}
],
"tools": [
{
"name": "get_order_details",
"description": "Tool to fetch the user's order details.",
"parameters": {
"type": "object",
"properties": {
"order_id": {
"type": "string",
"description": "The unique identifier for the order."
}
},
"required": [
"order_id"
]
}
},
{
"name": "get_delivery_status",
"description": "Tool to fetch the delivery status of an order.",
"parameters": {
"type": "object",
"properties": {
"order_id": {
"type": "string",
"description": "The unique identifier for the order."
}
},
"required": [
"order_id"
]
},
"mockTool": true,
"mockInstructions": "Give a mock response for a door dash order delivery status."
}
],
"prompts": [
{
"name": "Style prompt",
"type": "style_prompt",
"prompt": "You should be empathetic and helpful."
},
{
"name": "Greeting",
"type": "greeting",
"prompt": "Hello! How can I help you?"
}
],
"startAgent": "DoorDash Support Hub",
"mcpServers": [
{
"name": "delivery",
"url": "http://localhost:8000/sse"
}
],
"toolWebhookUrl": "http://127.0.0.1:4020/tool_call"
}
}

View file

@ -15,7 +15,7 @@ services:
- AUTH0_ISSUER_BASE_URL=${AUTH0_ISSUER_BASE_URL}
- AUTH0_CLIENT_ID=${AUTH0_CLIENT_ID}
- AUTH0_CLIENT_SECRET=${AUTH0_CLIENT_SECRET}
- AGENTS_API_URL=http://agents:3001
- AGENTS_API_URL=http://rowboat_agents:3001
- AGENTS_API_KEY=${AGENTS_API_KEY}
- COPILOT_API_URL=http://copilot:3002
- COPILOT_API_KEY=${COPILOT_API_KEY}
@ -37,15 +37,16 @@ services:
- MAX_PROJECTS_PER_USER=${MAX_PROJECTS_PER_USER}
restart: unless-stopped
agents:
rowboat_agents:
build:
context: ./apps/agents
context: ./apps/rowboat_agents
dockerfile: Dockerfile
ports:
- "3001:3001"
environment:
- OPENAI_API_KEY=${OPENAI_API_KEY}
- API_KEY=${AGENTS_API_KEY}
- REDIS_URL=redis://redis:6379
restart: unless-stopped
copilot: