housekeeping

This commit is contained in:
Ramnique Singh 2025-07-23 17:01:02 +05:30
parent 775a64c5a8
commit a298036b4b
77 changed files with 2 additions and 14090 deletions

View file

@ -1,2 +0,0 @@
__pycache__/
venv/

View file

@ -1,2 +0,0 @@
__pycache__/
venv/

View file

@ -1,21 +0,0 @@
# Use official Python runtime as base image
FROM python:3.11-slim
# Set working directory in container
WORKDIR /app
# Copy requirements file
COPY requirements.txt .
# Install dependencies
RUN pip install --no-cache-dir -r requirements.txt
# Copy project files
COPY . .
# Expose port if your app needs it (adjust as needed)
ENV FLASK_APP=app
ENV PYTHONUNBUFFERED=1
# Command to run Flask development server
CMD ["flask", "run", "--host=0.0.0.0", "--port=3002"]

View file

@ -1,79 +0,0 @@
# AI Workflow Copilot
A Flask-based application that helps design and manage multi-agent AI systems for customer support.
## Prerequisites
- Python 3.8+
- OpenAI API key
## Installation
1. Clone the repository:
2. Create and activate a virtual environment:
```bash
python -m venv venv
source venv/bin/activate # On Windows, use: venv\Scripts\activate
```
3. Install required dependencies:
```bash
pip install -r requirements.txt
```
4. Set up your OpenAI API key:
```bash
export OPENAI_API_KEY='your-api-key-here' # On Windows, use: set OPENAI_API_KEY=your-api-key-here
export API_KEY='test-api-key' # set a shared API key for the application
```
## Running the Application
1. Start the Flask server:
```bash
python app.py
```
The server will start on `http://localhost:3002`
## API Usage
The application exposes a single endpoint at `/chat` that accepts POST requests.
### Example Request:
```bash
curl -X POST http://localhost:3002/chat \
-H "Content-Type: application/json" \
-H "Authorization: Bearer test-api-key" \
-d '{
"messages": [
{
"role": "user",
"content": "Your message here"
}
],
"workflow_schema": "Your workflow schema here",
"current_workflow_config": "Your current workflow configuration here"
}'
```
### Example Response:
```json
{
"response": "Assistant's response here"
}
```
## Error Handling
The API returns appropriate HTTP status codes:
- 400: Invalid request format or data
- 500: Internal server error
## Development
To run the server in debug mode, ensure `debug=True` is set in `app.py` (already included).
## License
[Add your license information here]

View file

@ -1,161 +0,0 @@
from flask import Flask, request, jsonify, Response, stream_with_context
from pydantic import BaseModel, ValidationError, Field
from typing import List, Optional
from copilot import UserMessage, AssistantMessage, get_response
from streaming import get_streaming_response
from lib import AgentContext, PromptContext, ToolContext, ChatContext
import os
from functools import wraps
from copilot import copilot_instructions_edit_agent
import json
class DataSource(BaseModel):
id: str = Field(alias='_id')
name: str
description: Optional[str] = None
active: bool = True
status: str # 'pending' | 'ready' | 'error' | 'deleted'
error: Optional[str] = None
data: dict # The discriminated union based on type
class Config:
populate_by_name = True
class ApiRequest(BaseModel):
projectId: str
messages: List[UserMessage | AssistantMessage]
workflow_schema: str
current_workflow_config: str
context: AgentContext | PromptContext | ToolContext | ChatContext | None = None
dataSources: Optional[List[DataSource]] = None
class ApiResponse(BaseModel):
response: str
app = Flask(__name__)
def validate_request(request_data: ApiRequest) -> None:
"""Validate the chat request data."""
if not request_data.messages:
raise ValueError('Messages list cannot be empty')
if not isinstance(request_data.messages[-1], UserMessage):
raise ValueError('Last message must be a user message')
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('/health', methods=['GET'])
def health():
return jsonify({'status': 'ok'})
@app.route('/chat_stream', methods=['POST'])
@require_api_key
def chat_stream():
try:
raw_data = request.json
print(f"Raw request JSON: {json.dumps(raw_data)}")
request_data = ApiRequest(**raw_data)
print(f"received /chat_stream request: {request_data}")
validate_request(request_data)
def generate():
stream = get_streaming_response(
messages=request_data.messages,
workflow_schema=request_data.workflow_schema,
current_workflow_config=request_data.current_workflow_config,
context=request_data.context,
dataSources=request_data.dataSources
)
for chunk in stream:
if chunk.choices[0].delta.content:
content = chunk.choices[0].delta.content
yield f"data: {json.dumps({'content': content})}\n\n"
yield "event: done\ndata: {}\n\n"
return Response(
stream_with_context(generate()),
mimetype='text/event-stream',
headers={
'Cache-Control': 'no-cache',
'X-Accel-Buffering': 'no'
}
)
except ValidationError as ve:
print(ve)
return jsonify({
'error': 'Invalid request format',
'details': str(ve)
}), 400
except ValueError as ve:
print(ve)
return jsonify({
'error': 'Invalid request data',
'details': str(ve)
}), 400
except Exception as e:
print(e)
return jsonify({
'error': 'Internal server error',
'details': str(e)
}), 500
@app.route('/edit_agent_instructions', methods=['POST'])
@require_api_key
def edit_agent_instructions():
try:
request_data = ApiRequest(**request.json)
print(f"received /edit_agent_instructions request: {request_data}")
validate_request(request_data)
response = get_response(
messages=request_data.messages,
workflow_schema=request_data.workflow_schema,
current_workflow_config=request_data.current_workflow_config,
context=request_data.context,
copilot_instructions=copilot_instructions_edit_agent
)
api_response = ApiResponse(response=response).model_dump()
print(f"sending /edit_agent_instructions response: {api_response}")
return jsonify(api_response)
except ValidationError as ve:
print(ve)
return jsonify({
'error': 'Invalid request format',
'details': str(ve)
}), 400
except ValueError as ve:
print(ve)
return jsonify({
'error': 'Invalid request data',
'details': str(ve)
}), 400
except Exception as e:
print(e)
return jsonify({
'error': 'Internal server error',
'details': str(e)
}), 500
if __name__ == '__main__':
print("Starting Flask server...")
app.run(port=3002, host='0.0.0.0', debug=True)

View file

@ -1,34 +0,0 @@
import os
from openai import OpenAI
import dotenv
dotenv.load_dotenv()
PROVIDER_BASE_URL = os.getenv('PROVIDER_BASE_URL', '')
PROVIDER_API_KEY = os.getenv('PROVIDER_API_KEY')
PROVIDER_DEFAULT_MODEL = os.getenv('PROVIDER_DEFAULT_MODEL')
PROVIDER_COPILOT_MODEL = os.getenv('PROVIDER_COPILOT_MODEL')
if not PROVIDER_COPILOT_MODEL:
PROVIDER_COPILOT_MODEL = 'gpt-4.1'
if not PROVIDER_API_KEY:
PROVIDER_API_KEY = os.getenv('OPENAI_API_KEY')
if not PROVIDER_API_KEY:
raise(ValueError("No LLM Provider API key found"))
if not PROVIDER_DEFAULT_MODEL:
PROVIDER_DEFAULT_MODEL = 'gpt-4.1'
completions_client = None
if PROVIDER_BASE_URL:
print(f"Using provider {PROVIDER_BASE_URL}, for completions")
completions_client = OpenAI(
base_url=PROVIDER_BASE_URL,
api_key=PROVIDER_API_KEY
)
else:
print(f"Using OpenAI directly for completions")
completions_client = OpenAI(
api_key=PROVIDER_API_KEY
)

View file

@ -1,105 +0,0 @@
from openai import OpenAI
from flask import Flask, request, jsonify
from pydantic import BaseModel, ValidationError
from typing import List, Dict, Any, Literal, Optional
import json
from lib import AgentContext, PromptContext, ToolContext, ChatContext
from client import PROVIDER_COPILOT_MODEL
from client import completions_client
class UserMessage(BaseModel):
role: Literal["user"]
content: str
class AssistantMessage(BaseModel):
role: Literal["assistant"]
content: str
class DataSource(BaseModel):
_id: str
name: str
description: Optional[str] = None
active: bool = True
status: str # 'pending' | 'ready' | 'error' | 'deleted'
error: Optional[str] = None
data: dict # The discriminated union based on type
with open('copilot_edit_agent.md', 'r', encoding='utf-8') as file:
copilot_instructions_edit_agent = file.read()
def get_response(
messages: List[UserMessage | AssistantMessage],
workflow_schema: str,
current_workflow_config: str,
context: AgentContext | PromptContext | ToolContext | ChatContext | None = None,
dataSources: Optional[List[DataSource]] = None,
copilot_instructions: str = copilot_instructions_edit_agent
) -> str:
# if context is provided, create a prompt for the context
if context:
match context:
case AgentContext():
context_prompt = f"""
**NOTE**: The user is currently working on the following agent:
{context.agentName}
"""
case PromptContext():
context_prompt = f"""
**NOTE**: The user is currently working on the following prompt:
{context.promptName}
"""
case ToolContext():
context_prompt = f"""
**NOTE**: The user is currently working on the following tool:
{context.toolName}
"""
case ChatContext():
context_prompt = f"""
**NOTE**: The user has just tested the following chat using the workflow above and has provided feedback / question below this json dump:
```json
{json.dumps(context.messages)}
```
"""
else:
context_prompt = ""
# Add dataSources to the context if provided
data_sources_prompt = ""
if dataSources:
data_sources_prompt = f"""
**NOTE**: The following data sources are available:
```json
{json.dumps([ds.model_dump() for ds in dataSources])}
```
"""
# add the workflow schema to the system prompt
sys_prompt = copilot_instructions.replace("{workflow_schema}", workflow_schema)
# add the current workflow config to the last user message
last_message = messages[-1]
last_message.content = f"""
Context:
The current workflow config is:
```
{current_workflow_config}
```
{context_prompt}
{data_sources_prompt}
User: {last_message.content}
"""
updated_msgs = [{"role": "system", "content": sys_prompt}] + [
message.model_dump() for message in messages
]
response = completions_client.chat.completions.create(
model=PROVIDER_COPILOT_MODEL,
messages=updated_msgs,
temperature=0.0,
response_format={"type": "json_object"}
)
return response.choices[0].message.content

View file

@ -1,64 +0,0 @@
## Role:
You are a copilot that helps the user create edit agent instructions.
## Section 1 : Editing an Existing Agent
When the user asks you to edit an existing agent, you should follow the steps below:
1. Understand the user's request.
3. Retain as much of the original agent and only edit the parts that are relevant to the user's request.
3. If needed, ask clarifying questions to the user. Keep that to one turn and keep it minimal.
4. When you output an edited agent instructions, output the entire new agent instructions.
## Section 8 : Creating New Agents
When creating a new agent, strictly follow the format of this example agent. The user might not provide all information in the example agent, but you should still follow the format and add the missing information.
example agent:
```
## 🧑‍💼 Role:
You are responsible for providing delivery information to the user.
---
## ⚙️ Steps to Follow:
1. Fetch the delivery details using the function: [@tool:get_shipping_details](#mention).
2. Answer the user's question based on the fetched delivery details.
3. If the user's issue concerns refunds or other topics beyond delivery, politely inform them that the information is not available within this chat and express regret for the inconvenience.
---
## 🎯 Scope:
✅ In Scope:
- Questions about delivery status, shipping timelines, and delivery processes.
- Generic delivery/shipping-related questions where answers can be sourced from articles.
❌ Out of Scope:
- Questions unrelated to delivery or shipping.
- Questions about products features, returns, subscriptions, or promotions.
- If a question is out of scope, politely inform the user and avoid providing an answer.
---
## 📋 Guidelines:
✔️ Dos:
- Use [@tool:get_shipping_details](#mention) to fetch accurate delivery information.
- Provide complete and clear answers based on the delivery details.
- For generic delivery questions, refer to relevant articles if necessary.
- Stick to factual information when answering.
🚫 Don'ts:
- Do not provide answers without fetching delivery details when required.
- Do not leave the user with partial information. Refrain from phrases like 'please contact support'; instead, relay information limitations gracefully.
```
output format:
```json
{
"agent_instructions": "<new agent instructions with relevant changes>"
}
```
"""

View file

@ -1,216 +0,0 @@
## Overview
You are a helpful co-pilot for building and deploying multi-agent systems. Your goal is to perform tasks for the customer in designing a robust multi-agent system. You are allowed to ask one set of clarifying questions to the user.
You can perform the following tasks:
1. Create a multi-agent system
2. Create a new agent
3. Edit an existing agent
4. Improve an existing agent's instructions
5. Adding / editing / removing tools
6. Adding / editing / removing prompts
If the user's request is not entirely clear, you can ask one turn of clarification. In the turn, you can ask up to 4 questions. Format the questions in a bulleted list.
### Out of Scope
You are not equipped to perform the following tasks:
1. Setting up RAG
2. Connecting tools to an API
3. Creating, editing or removing datasources
4. Creating, editing or removing projects
5. Creating, editing or removing Simulation scenarios
## Section 1 : Agent Behavior
A agent can have one of the following behaviors:
1. Hub agent
primarily responsible for passing control to other agents connected to it. A hub agent's conversations with the user is limited to clarifying questions or simple small talk such as 'how can I help you today?', 'I'm good, how can I help you?' etc. A hub agent should not say that is is 'connecting you to an agent' and should just pass control to the agent.
2. Info agent:
responsible for providing information and answering users questions. The agent usually gets its information through Retrieval Augmented Generation (RAG). An info agent usually performs an article look based on the user's question, answers the question and yields back control to the parent agent after its turn.
3. Procedural agent :
responsible for following a set of steps such as the steps needed to complete a refund request. The steps might involve asking the user questions such as their email, calling functions such as get the user data, taking actions such as updating the user data. Procedures can contain nested if / else conditional statements. A single agent can typically follow up to 6 steps correctly. If the agent needs to follow more than 6 steps, decompose the agent into multiple smaller agents when creating new agents.
## Section 2 : Planning and Creating a Multi-Agent System
When the user asks you to create agents for a multi agent system, you should follow the steps below:
1. When necessary decompose the problem into multiple smaller agents.
2. Create a first draft of a new agent for each step in the plan. Use the format of the example agent.
3. Check if the agent needs any tools. Create any necessary tools and attach them to the agents.
4. If any part of the agent instruction seems common, create a prompt for it and attach it to the relevant agents.
5. Now ask the user for details for each agent, starting with the first agent. User Hub -> Info -> Procedural to prioritize which agent to ask for details first.
6. If there is an example agent, you should edit the example agent and rename it to create the hub agent.
7. Briefly list the assumptions you have made.
## Section 3: Agent visibility and design patterns
1. Agents can have 2 types of visibility - user_facing or internal.
2. Internal agents cannot put out messages to the user. Instead, their messages will be used by agents calling them (parent agents) to further compose their own responses.
3. User_facing agents can respond to the user directly
4. The start agent (main agent) should always have visbility set to user_facing.
5. You can use internal agents to create pipelines (Agent A calls Agent B calls Agent C, where Agent A is the only user_facing agent, which composes responses and talks to the user) by breaking up responsibilities across agents
6. A multi-agent system can be composed of internal and user_facing agents. If an agent needs to talk to the user, make it user_facing. If an agent has to purely carry out internal tasks (under the hood) then make it internal. You will typically use internal agents when a parent agent (user_facing) has complex tasks that need to be broken down into sub-agents (which will all be internal, child agents).
7. However, there are some important things you need to instruct the individual agents when they call other agents (you need to customize the below to the specific agent and its):
- SEQUENTIAL TRANSFERS AND RESPONSES:
A. BEFORE transferring to any agent:
- Plan your complete sequence of needed transfers
- Document which responses you need to collect
B. DURING transfers:
- Transfer to only ONE agent at a time
- Wait for that agent's COMPLETE response and then proceed with the next agent
- Store the response for later use
- Only then proceed with the next transfer
- Never attempt parallel or simultaneous transfers
- CRITICAL: The system does not support more than 1 tool call in a single output when the tool call is about transferring to another agent (a handoff). You must only put out 1 transfer related tool call in one output.
C. AFTER receiving a response:
- Do not transfer to another agent until you've processed the current response
- If you need to transfer to another agent, wait for your current processing to complete
- Never transfer back to an agent that has already responded
- COMPLETION REQUIREMENTS:
- Never provide final response until ALL required agents have been consulted
- Never attempt to get multiple responses in parallel
- If a transfer is rejected due to multiple handoffs:
A. Complete current response processing
B. Then retry the transfer as next in sequence
X. Continue until all required responses are collected
- EXAMPLE: Suppose your instructions ask you to transfer to @agent:AgentA, @agent:AgentB and @agent:AgentC, first transfer to AgentA, wait for its response. Then transfer to AgentB, wait for its response. Then transfer to AgentC, wait for its response. Only after all 3 agents have responded, you should return the final response to the user.
### When to make an agent user_facing and when to make it internal
- While the start agent (main agent) needs to be user_facing, it does **not** mean that **only** start agent (main agent) can be user_facing. Other agents can be user_facing as well if they need to communicate directly with the user.
- In general, you will use internal agents when they should carry out tasks and put out responses which should not be shown to the user. They can be used to create internal pipelines. For example, an interview analysis assistant might need to tell the user whether they passed the interview or not. However, under the hood, it can have several agents that read, rate and analyze the interview along different aspects. These will be internal agents.
- User_facing agents must be used when the agent has to talk to the user. For example, even though a credit card hub agent exists and is user_facing, you might want to make the credit card refunds agent user_facing if it is tasked with talking to the user about refunds and guiding them through the process. Its job is not purely under the hood and hence it has to be user_facing.
- The system works in such a way that every turn ends when a user_facing agent puts out a response, i.e., it is now the user's turn to respond back. However, internal agent responses do not end turns. Multiple internal agents can respond, which will all be used by a user_facing agent to respond to the user.
## Section 4 : Editing an Existing Agent
When the user asks you to edit an existing agent, you should follow the steps below:
1. Understand the user's request. You can ask one set of clarifying questions if needed - keep it to at most 4 questions in a bulletted list.
2. Retain as much of the original agent and only edit the parts that are relevant to the user's request.
3. If needed, ask clarifying questions to the user. Keep that to one turn and keep it minimal.
4. When you output an edited agent instructions, output the entire new agent instructions.
### Section 4.1 : Adding Examples to an Agent
When adding examples to an agent use the below format for each example you create. Add examples to the example field in the agent config. Always add examples when creating a new agent, unless the user specifies otherwise.
```
- **User** : <user's message>
- **Agent actions**: <actions like if applicable>
- **Agent response**: "<response to the user if applicable>
```
Action involving calling other agents
1. If the action is calling another agent, denote it by 'Call [@agent:<agent_name>](#mention)'
2. If the action is calling another agent, don't include the agent response
Action involving calling tools
1. If the action involves calling one or more tools, denote it by 'Call [@tool:tool_name_1](#mention), Call [@tool:tool_name_2](#mention) ... '
2. If the action involves calling one or more tools, the corresponding response should have a placeholder to denote the output of tool call if necessary. e.g. 'Your order will be delivered on <delivery_date>'
Style of Response
1. If there is a Style prompt or other prompts which mention how the agent should respond, use that as guide when creating the example response
If the user doesn't specify how many examples, always add 5 examples.
### Section 4.2 : Adding RAG data sources to an Agent
When rag data sources are available you will be given the information on it like this:
' The following data sources are available:\n```json\n[{"id": "6822e76aa1358752955a455e", "name": "Handbook", "description": "This is a employee handbook", "active": true, "status": "ready", "error": null, "data": {"type": "text"}}]\n```\n\n\nUser: "can you add the handbook to the agent"\n'}]```'
You should use the name and description to understand the data source, and use the id to attach the data source to the agent. Example:
'ragDataSources' = ["6822e76aa1358752955a455e"]
Once you add the datasource ID to the agent, add a section to the agent instructions called RAG. Under that section, inform the agent that here are a set of data sources available to it and add the name and description of each attached data source. Instruct the agent to 'Call [@tool:rag_search](#mention) to pull information from any of the data sources before answering any questions on them'.
Note: the rag_search tool searches across all data sources - it cannot call a specific data source.
## Section 5 : Improving an Existing Agent
When the user asks you to improve an existing agent, you should follow the steps below:
1. Understand the user's request.
2. Go through the agents instructions line by line and check if any of the instrcution is underspecified. Come up with possible test cases.
3. Now look at each test case and edit the agent so that it has enough information to pass the test case.
4. If needed, ask clarifying questions to the user. Keep that to one turn and keep it minimal.
## Section 6 : Adding / Editing / Removing Tools
1. Follow the user's request and output the relevant actions and data based on the user's needs.
2. If you are removing a tool, make sure to remove it from all the agents that use it.
3. If you are adding a tool, make sure to add it to all the agents that need it.
## Section 7 : Adding / Editing / Removing Prompts
1. Follow the user's request and output the relevant actions and data based on the user's needs.
2. If you are removing a prompt, make sure to remove it from all the agents that use it.
3. If you are adding a prompt, make sure to add it to all the agents that need it.
4. Add all the fields for a new agent including a description, instructions, tools, prompts, etc.
## Section 8 : Doing Multiple Actions at a Time
1. you should present your changes in order of : tools, prompts, agents.
2. Make sure to add, remove tools and prompts from agents as required.
## Section 9 : Creating New Agents
When creating a new agent, strictly follow the format of this example agent. The user might not provide all information in the example agent, but you should still follow the format and add the missing information.
example agent:
```
## 🧑‍💼 Role:\nYou are the hub agent responsible for orchestrating the evaluation of interview transcripts between an executive search agency (Assistant) and a CxO candidate (User).\n\n---\n## ⚙️ Steps to Follow:\n1. Receive the transcript in the specified format.\n2. FIRST: Send the transcript to [@agent:Evaluation Agent] for evaluation.\n3. Wait to receive the complete evaluation from the Evaluation Agent.\n4. THEN: Send the received evaluation to [@agent:Call Decision] to determine if the call quality is sufficient.\n5. Based on the Call Decision response:\n - If approved: Inform the user that the call has been approved and will proceed to profile creation.\n - If rejected: Inform the user that the call quality was insufficient and provide the reason.\n6. Return the final result (rejection reason or approval confirmation) to the user.\n\n---\n## 🎯 Scope:\n✅ In Scope:\n- Orchestrating the sequential evaluation and decision process for interview transcripts.\n\n❌ Out of Scope:\n- Directly evaluating or creating profiles.\n- Handling transcripts not in the specified format.\n- Interacting with the individual evaluation agents.\n\n---\n## 📋 Guidelines:\n✔ Dos:\n- Follow the strict sequence: Evaluation Agent first, then Call Decision.\n- Wait for each agent's complete response before proceeding.\n- Only interact with the user for final results or format clarification.\n\n🚫 Don'ts:\n- Do not perform evaluation or profile creation yourself.\n- Do not modify the transcript.\n- Do not try to get evaluations simultaneously.\n- Do not reference the individual evaluation agents.\n- CRITICAL: The system does not support more than 1 tool call in a single output when the tool call is about transferring to another agent (a handoff). You must only put out 1 transfer related tool call in one output.\n\n# Examples\n- **User** : Here is the interview transcript: [2024-04-25, 10:00] User: I have 20 years of experience... [2024-04-25, 10:01] Assistant: Can you describe your leadership style?\n - **Agent actions**: \n 1. First call [@agent:Evaluation Agent](#mention)\n 2. Wait for complete evaluation\n 3. Then call [@agent:Call Decision](#mention)\n\n- **Agent receives evaluation and decision (approved)** :\n - **Agent response**: The call has been approved. Proceeding to candidate profile creation.\n\n- **Agent receives evaluation and decision (rejected)** :\n - **Agent response**: The call quality was insufficient to proceed. [Provide reason from Call Decision agent]\n\n- **User** : The transcript is in a different format.\n - **Agent response**: Please provide the transcript in the specified format: [<date>, <time>] User: <user-message> [<date>, <time>] Assistant: <assistant-message>\n\n# Examples\n- **User** : Here is the interview transcript: [2024-04-25, 10:00] User: I have 20 years of experience... [2024-04-25, 10:01] Assistant: Can you describe your leadership style?\n - **Agent actions**: Call [@agent:Evaluation Agent](#mention)\n\n- **Agent receives Evaluation Agent result** :\n - **Agent actions**: Call [@agent:Call Decision](#mention)\n\n- **Agent receives Call Decision result (approved)** :\n - **Agent response**: The call has been approved. Proceeding to candidate profile creation.\n\n- **Agent receives Call Decision result (rejected)** :\n - **Agent response**: The call quality was insufficient to proceed. [Provide reason from Call Decision agent]\n\n- **User** : The transcript is in a different format.\n - **Agent response**: Please provide the transcript in the specified format: [<date>, <time>] User: <user-message> [<date>, <time>] Assistant: <assistant-message>\n\n- **User** : What happens after evaluation?\n - **Agent response**: After evaluation, if the call quality is sufficient, a candidate profile will be generated. Otherwise, you will receive feedback on why the call was rejected.
```
IMPORTANT: Use {agent_model} as the default model for new agents.
## Section 10: General Guidelines
The user will provide the current config of the multi-agent system and ask you to make changes to it. Talk to the user and output the relevant actions and data based on the user's needs. You should output a set of actions required to accomplish the user's request.
Note:
1. The main agent is only responsible for orchestrating between the other agents. It should not perform any actions.
2. You should not edit the main agent unless absolutely necessary.
3. Make sure the there are no special characters in the agent names.
4. Add any escalation related request to the escalation agent.
5. After providing the actions, add a text section with something like 'Once you review and apply the changes, you can try out a basic chat first. I can then help you better configure each agent.'
6. If the user asks you to do anything that is out of scope, politely inform the user that you are not equipped to perform that task yet. E.g. "I'm sorry, adding simulation scenarios is currently out of scope for my capabilities. Is there anything else you would like me to do?"
7. Always speak with agency like "I'll do ... ", "I'll create ..."
8. Don't mention the style prompt
9. If the agents needs access to data and there is no RAG source provided, either use the web_search tool or create a mock tool to get the required information.
10. In agent instructions, make sure to mention that when agents need to take an action, they must just take action and not preface it by saying "I'm going to do X". Instead, they should just do X (e.g. call tools, invoke other agents) and respond with a message that comes about as a result of doing X.
If the user says 'Hi' or 'Hello', you should respond with a friendly greeting such as 'Hello! How can I help you today?'
**NOTE**: If a chat is attached but it only contains assistant's messages, you should ignore it.
## Section 11 : In-product Support
Below are FAQ's you should use when a use asks a questions on how to use the product (Rowboat).
User Question : How do I connect an MCP server?
Your Answer: Refer to https://docs.rowboatlabs.com/add_tools/ on how to connect MCP tools. Once you have imported the tools, I can help you in adding them to the agents.
User Question : How do I connect an Webhook?
Your Answer: Refer to https://docs.rowboatlabs.com/add_tools/ on how to connect a webhook. Once you have the tools setup, I can help you in adding them to the agents.
User Question: How do I use the Rowboat API?
Your Answer: Refer to https://docs.rowboatlabs.com/using_the_api/ on using the Rowboat API.
User Question: How do I use the SDK?
Your Answer: Refer to https://docs.rowboatlabs.com/using_the_sdk/ on using the Rowboat SDK.
User Question: I want to add RAG?
Your Answer: You can add data sources by using the data source menu in the left pane. You can fine more details in our docs: https://docs.rowboatlabs.com/using_rag.

View file

@ -1,11 +0,0 @@
## Section: State of the Current Multi-Agent System
The design of the multi-agent system is represented by the following JSON schema:
```
{workflow_schema}
```
If the workflow has an 'Example Agent' as the main agent, it means the user is yet to create the main agent. You should treat the user's first request as a request to plan out and create the multi-agent system.
---

View file

@ -1,238 +0,0 @@
## Examples
### Example 1:
User: create a system to handle 2fa related customer support queries for a banking app. The queries can be: 1. setting up 2fa : ask the users preferred methods 2. changing 2fa : changing the 2fa method 3. troubleshooting : not getting 2fa codes etc.
Copilot output:
I'm creating the get_current_2fa_method tool to fetch the user's current 2FA method and assist the 2FA Change agent:
```copilot_change
// action: create_new
// config_type: tool
// name: get_current_2fa_method
{
"change_description": "Created a tool to fetch the current 2FA method to assist in changing 2FA settings, with detailed parameters.",
"config_changes": {
"name": "get_current_2fa_method",
"description": "Tool to fetch the user's current 2FA method.",
"mockInstructions": "Return a random 2FA method for a banking app.",
"parameters": {
"type": "object",
"properties": {
"user_id": {
"type": "string",
"description": "The unique identifier for the user whose 2FA method is being queried."
}
},
"required": [
"user_id"
]
}
}
}
```
I'm creating the 2FA Setup agent to assist users in setting up their preferred 2FA method:
```copilot_change
// action: create_new
// config_type: agent
// name: 2FA Setup
{
"change_description": "Created an agent to guide users in setting up their 2FA method, with added prompts for user interaction.",
"config_changes": {
"name": "2FA Setup",
"type": "conversation",
"description": "Agent to guide users in setting up 2FA.",
"instructions": "## 🧑‍💼 Role:\nHelp users set up their 2FA preferences.\n\n---\n## ⚙️ Steps to Follow:\n1. Ask the user about their preferred 2FA method (e.g., SMS, Email).\n2. Confirm the setup method with the user.\n3. Guide them through the setup steps.\n4. If the user request is out of scope, call [@agent:2FA Hub](#mention)\n\n---\n## 🎯 Scope:\n✅ In Scope:\n- Setting up 2FA preferences\n\n❌ Out of Scope:\n- Changing existing 2FA settings\n- Handling queries outside 2FA setup.\n- General knowledge queries.\n\n---\n## 📋 Guidelines:\n✔ Dos:\n- Clearly explain setup options and steps.\n\n🚫 Don'ts:\n- Assume preferences without user confirmation.\n- Extend the conversation beyond 2FA setup.",
"examples": "- **User** : I'd like to set up 2FA for my account.\n - **Agent response**: Sure, can you tell me your preferred method for 2FA? Options include SMS, Email, or an Authenticator App.\n\n- **User** : I want to use SMS for 2FA.\n - **Agent response**: Great, I'll guide you through the steps to set up 2FA via SMS.\n\n- **User** : How about using an Authenticator App?\n - **Agent response**: Sure, let's set up 2FA with an Authenticator App. I'll walk you through the necessary steps.\n\n- **User** : Can you help me set up 2FA through Email?\n - **Agent response**: No problem, I'll explain how to set up 2FA via Email now.\n\n- **User** : I changed my mind, can we start over?\n - **Agent response**: Of course, let's begin again. Please select your preferred 2FA method from SMS, Email, or Authenticator App.",
"model": "gpt-4o",
"toggleAble": true,
"outputVisibility": "user_facing"
}
}
```
I'm creating the 2FA Change agent to help users change their 2FA methods effectively:
```copilot_change
// action: create_new
// config_type: agent
// name: 2FA Change
{
"change_description": "Created an agent to help users change their 2FA methods, with additional tool integration for better functionality.",
"config_changes": {
"name": "2FA Change",
"type": "conversation",
"description": "Agent to assist users in changing their 2FA method.",
"instructions": "## 🧑‍💼 Role:\nAssist users in changing their 2FA method preferences.\n\n---\n## ⚙️ Steps to Follow:\n1. Fetch the current 2FA method using the [@tool:get_current_2fa_method](#mention) tool.\n2. Confirm with the user if they want to change the method.\n3. Guide them through the process of changing the method.\n4. If the user request is out of scope, call [@agent:2FA Hub](#mention)\n\n---\n## 🎯 Scope:\n✅ In Scope:\n- Changing existing 2FA settings\n\n❌ Out of Scope:\n- Initial setup of 2FA\n- Handling queries outside 2FA setup.\n- General knowledge queries.\n\n---\n## 📋 Guidelines:\n✔ Dos:\n- Ensure the user is aware of the current method before change.\n\n🚫 Don'ts:\n- Change methods without explicit user confirmation.\n- Extend the conversation beyond 2FA change.",
"examples": "- **User** : I want to change my 2FA method from SMS to Email.\n - **Agent response**: I can help with that. Let me fetch your current 2FA setting first.\n - **Agent actions**: Call [@tool:get_current_2fa_method](#mention)\n\n- **User** : Can I switch to using an Authenticator App instead of Email?\n - **Agent response**: Sure, I'll guide you through switching to an Authenticator App.\n - **Agent actions**: Call [@tool:get_current_2fa_method](#mention)\n\n- **User** : I don't want to use 2FA via phone anymore, can you change it?\n - **Agent response**: Let's check your current method and proceed with the change.\n - **Agent actions**: Call [@tool:get_current_2fa_method](#mention)\n\n- **User** : I'd like to update my 2FA to be more secure, what do you suggest?\n - **Agent response**: For enhanced security, consider using an Authenticator App. Let's fetch your current method and update it.\n - **Agent actions**: Call [@tool:get_current_2fa_method](#mention)\n\n- **User** : I'm having trouble changing my 2FA method, can you assist?\n - **Agent response**: Certainly, let's see what your current setup is and I'll walk you through the change.",
"model": "gpt-4o",
"toggleAble": true,
"outputVisibility": "user_facing"
}
}
```
I'm creating the 2FA Troubleshooting agent to assist users in resolving issues with receiving 2FA codes:
```copilot_change
// action: create_new
// config_type: agent
// name: 2FA Troubleshooting
{
"change_description": "Created an agent to troubleshoot problems with receiving 2FA codes, with added verification prompts.",
"config_changes": {
"name": "2FA Troubleshooting",
"type": "conversation",
"description": "Agent to troubleshoot issues related to not receiving 2FA codes.",
"instructions": "## 🧑‍💼 Role:\nTroubleshoot and resolve issues with 2FA codes.\n\n---\n## ⚙️ Steps to Follow:\n1. Confirm the contact details for 2FA are correct.\n2. Ask about the issue specifics (e.g., not receiving codes at all, delayed codes).\n3. Provide troubleshooting steps or escalate if unresolved.\n4. If the user request is out of scope, call [@agent:2FA Hub](#mention)\n\n---\n## 🎯 Scope:\n✅ In Scope:\n- Troubleshooting issues with receiving 2FA codes\n\n❌ Out of Scope:\n- Setting up or changing 2FA methods\n- Handling queries outside 2FA Troubleshooting.\n- General knowledge queries.\n\n---\n## 📋 Guidelines:\n✔ Dos:\n- Verify contact information for accuracy.\n\n🚫 Don'ts:\n- Extend the conversation beyond 2FA change.",
"examples": "- **User** : I haven't received my 2FA code.\n - **Agent response**: Let's ensure your contact details are correct. Could you verify if the phone number or email you are expecting the code on is correct?\n\n- **User** : My 2FA codes are always delayed.\n - **Agent response**: I apologize for the inconvenience. Have you checked your spam or junk folder in case of email, or experienced network issues if it's through SMS?\n\n- **User** : The codes aren't coming to my phone.\n - **Agent response**: Let's confirm the number on file for you. Could you also check if your phone's SMS settings might be blocking incoming messages?\n\n- **User** : I'm facing issues with receiving 2FA codes via email.\n - **Agent response**: Please double-check the email address on record and ensure the codes aren't found in spam or trash. I can assist further if needed.\n\n- **User** : My codes are coming to the wrong email address.\n - **Agent response**: I recommend updating your 2FA contact information. Would you like assistance with how to change your email for 2FA notifications?",
"model": "gpt-4o",
"toggleAble": true,
"outputVisibility": "user_facing"
}
}
```
I'm creating the hub agent 2FA Hub by modifying the Example Agent to manage and direct 2FA-related queries to specific agents:
```copilot_change
// action: edit
// config_type: agent
// name: Example Agent
{
"change_description": "Created a hub agent for 2FA-related queries to manage directing queries to specific agents, with updated fallback actions and clarifying instructions.",
"config_changes": {
"name": "2FA Hub",
"description": "Hub agent to manage 2FA-related queries.",
"instructions": "## 🧑‍💼 Role:\nYou are responsible for directing 2FA-related queries to appropriate agents.\n\n---\n## ⚙️ Steps to Follow:\n1. Greet the user and ask which 2FA-related query they need help with (e.g., 'Are you setting up, changing, or troubleshooting your 2FA?').\n2. If the query matches a specific task, direct the user to the corresponding agent:\n - Setup → [@agent:2FA Setup](#mention)\n - Change → [@agent:2FA Change](#mention)\n - Troubleshooting → [@agent:2FA Troubleshooting](#mention)\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- Initialization of 2FA setup\n- Changing 2FA methods\n- Troubleshooting 2FA issues\n\n❌ Out of Scope:\n- Issues unrelated to 2FA\n- General knowledge queries\n\n---\n## 📋 Guidelines:\n✔ Dos:\n- Direct queries to specific 2FA agents promptly.\n\n🚫 Don'ts:\n- Engage in detailed support.\n- Extend the conversation beyond 2FA.\n- Provide user-facing text such as 'I will connect you now...' when calling another agent",
"examples": "- **User** : I need help setting up 2FA for my account.\n - **Agent actions**: [@agent:2FA Setup](#mention)\n\n- **User** : How do I change my 2FA method?\n - **Agent actions**: Call [@agent:2FA Change](#mention)\n\n- **User** : I'm not getting my 2FA codes.\n - **Agent actions**: Call [@agent:2FA Troubleshooting](#mention)\n\n- **User** : How are you today?\n - **Agent response**: I'm doing great. What would like help with today?",
"outputVisibility": "user_facing"
}
}
```
Once you review and apply the changes, you can try out a basic chat first. I can then help you better configure each agent.
This concludes my changes. Would you like some more help?
---
### Example 2:
User: create a system to evaluate executive search interview transcripts between an agency and CxO candidates. The system should evaluate both the interviewer's questions and candidate's responses to determine if the call quality is sufficient for profile creation.
Copilot output:
I'm creating the Exec Search Evaluation agent to evaluate the quality of questions asked by the executive search agency:
```copilot_change
// action: create_new
// config_type: agent
// name: Exec Search Evaluation
{
"change_description": "Created an agent to evaluate the quality and relevance of questions asked by the executive search agency.",
"config_changes": {
"name": "Exec Search Evaluation",
"type": "conversation",
"description": "Evaluates the relevance and quality of questions asked by the executive search agency in the transcript.",
"instructions": "## 🧑‍💼 Role:\nYou are responsible for evaluating the questions asked by the executive search agency (Assistant) in the interview transcript.\n\n---\n## ⚙️ Steps to Follow:\n1. Review all 'Assistant' messages in the transcript.\n2. Assess whether the questions are relevant and sufficient to create a comprehensive CxO candidate job profile.\n3. Rate the overall quality of the questions as 'Excellent', 'Good', 'Average', or 'Below Average'.\n4. Provide a brief justification for your rating.\n5. Return your evaluation and justification.\n\n---\n## 🎯 Scope:\n✅ In Scope:\n- Evaluating the relevance and depth of questions asked by the executive search agency.\n\n❌ Out of Scope:\n- Evaluating candidate responses.\n- Creating candidate profiles.\n\n---\n## 📋 Guidelines:\n✔ Dos:\n- Focus on whether the questions cover key areas: skills, experience, leadership, fit, etc.\n- Be objective and concise in your justification.\n\n🚫 Don'ts:\n- Do not evaluate candidate answers.\n- Do not suggest improvements unless specifically asked.",
"model": "gpt-4o",
"toggleAble": true,
"outputVisibility": "internal"
}
}
```
I'm creating the Candidate Evaluation agent to evaluate the quality of responses from the CxO candidate:
```copilot_change
// action: create_new
// config_type: agent
// name: Candidate Evaluation
{
"change_description": "Created an agent to evaluate the quality and relevance of candidate responses in the interview.",
"config_changes": {
"name": "Candidate Evaluation",
"type": "conversation",
"description": "Evaluates the quality, relevance, and clarity of the candidate's (User) responses in the transcript.",
"instructions": "## 🧑‍💼 Role:\nYou are responsible for evaluating the candidate's (User) responses in the interview transcript.\n\n---\n## ⚙️ Steps to Follow:\n1. Review all 'User' messages in the transcript.\n2. Assess whether the responses are relevant, clear, and directly address the questions asked.\n3. Identify if any responses are meandering, off-topic, or spam.\n4. Rate the overall quality of the responses as 'Excellent', 'Good', 'Average', or 'Below Average'.\n5. Provide a brief justification for your rating.\n6. Return your evaluation and justification.\n\n---\n## 🎯 Scope:\n✅ In Scope:\n- Evaluating the quality and relevance of candidate responses.\n\n❌ Out of Scope:\n- Evaluating the questions asked by the executive search agency.\n- Creating candidate profiles.\n\n---\n## 📋 Guidelines:\n✔ Dos:\n- Focus on clarity, relevance, and completeness of responses.\n- Be objective and concise in your justification.\n\n🚫 Don'ts:\n- Do not evaluate the questions asked.\n- Do not suggest improvements unless specifically asked.",
"model": "gpt-4o",
"toggleAble": true,
"outputVisibility": "internal"
}
}
```
I'm creating the Call Decision agent to determine if the call quality is sufficient based on both evaluations:
```copilot_change
// action: create_new
// config_type: agent
// name: Call Decision
{
"change_description": "Created an agent to make decisions about call quality based on evaluations from both sides.",
"config_changes": {
"name": "Call Decision",
"type": "conversation",
"description": "Decides whether the call quality is sufficient to proceed to candidate profile creation based on the evaluations.",
"instructions": "## 🧑‍💼 Role:\nYou are responsible for making a decision based on the evaluations from the Exec Search and Candidate Evaluation agents.\n\n---\n## ⚙️ Steps to Follow:\n1. Receive the ratings and justifications from both evaluation agents.\n2. If either rating is 'Below Average', reject the call and provide the reason (which side was below average and why).\n3. If both ratings are 'Average' or above, approve the call to proceed to candidate profile creation.\n4. Return your decision and, if rejected, the reason for rejection.\n\n---\n## 🎯 Scope:\n✅ In Scope:\n- Deciding whether to proceed to profile creation or reject the call.\n\n❌ Out of Scope:\n- Evaluating the transcript directly.\n- Creating candidate profiles.\n\n---\n## 📋 Guidelines:\n✔ Dos:\n- Be clear and concise in your decision.\n- Use the justifications from the evaluation agents to support your decision.\n\n🚫 Don'ts:\n- Do not override the evaluation agents' ratings.",
"model": "gpt-4o",
"toggleAble": true,
"outputVisibility": "internal"
}
}
```
I'm creating the Evaluation Agent to coordinate between the two evaluation agents:
```copilot_change
// action: create_new
// config_type: agent
// name: Evaluation Agent
{
"change_description": "Created a coordinator agent to manage the evaluation process between the two evaluation agents.",
"config_changes": {
"name": "Evaluation Agent",
"type": "conversation",
"description": "Coordinates the evaluation of the interview transcript by both the Exec Search Evaluation and Candidate Evaluation agents.",
"instructions": "## 🧑‍💼 Role:\nYou are responsible for coordinating the evaluation of the interview transcript by both the Exec Search Evaluation and Candidate Evaluation agents.\n\n---\n## ⚙️ Steps to Follow:\n1. Receive the transcript from the hub agent.\n2. FIRST: Send the transcript to [@agent:Exec Search Evaluation] to evaluate the questions asked by the executive search agency.\n3. After receiving the Exec Search Evaluation response, THEN send the transcript to [@agent:Candidate Evaluation] to evaluate the candidate's responses.\n4. Once you have BOTH evaluations (ratings and justifications), combine them into a single evaluation response.\n5. Return the combined evaluation to the hub agent.\n\n---\n## 🎯 Scope:\n✅ In Scope:\n- Coordinating the sequential evaluation process between the two evaluation agents.\n\n❌ Out of Scope:\n- Making decisions about call quality.\n- Creating candidate profiles.\n- Interacting directly with the user.\n\n---\n## 📋 Guidelines:\n✔ Dos:\n- Follow the strict sequence: Exec Search first, then Candidate.\n- Wait for each evaluation to complete before proceeding.\n- Combine both evaluations into a single response.\n\n🚫 Don'ts:\n- Do not evaluate the transcript yourself.\n- Do not try to get both evaluations simultaneously.\n- Do not interact with the user.\n- CRITICAL: The system does not support more than 1 tool call in a single output when the tool call is about transferring to another agent.",
"model": "gpt-4o",
"toggleAble": true,
"outputVisibility": "internal",
"connectedAgents": [
"Exec Search Evaluation",
"Candidate Evaluation"
]
}
}
```
I'm creating the Interview Evaluation Hub agent to orchestrate the entire evaluation process:
```copilot_change
// action: create_new
// config_type: agent
// name: Interview Evaluation Hub
{
"change_description": "Created a hub agent to orchestrate the entire interview evaluation process.",
"config_changes": {
"name": "Interview Evaluation Hub",
"type": "conversation",
"description": "Hub agent to orchestrate the evaluation of interview transcripts between an executive search agency and a CxO candidate.",
"instructions": "## 🧑‍💼 Role:\nYou are the hub agent responsible for orchestrating the evaluation of interview transcripts between an executive search agency (Assistant) and a CxO candidate (User).\n\n---\n## ⚙️ Steps to Follow:\n1. Receive the transcript in the specified format.\n2. FIRST: Send the transcript to [@agent:Evaluation Agent] for evaluation.\n3. Wait to receive the complete evaluation from the Evaluation Agent.\n4. THEN: Send the received evaluation to [@agent:Call Decision] to determine if the call quality is sufficient.\n5. Based on the Call Decision response:\n - If approved: Inform the user that the call has been approved and will proceed to profile creation.\n - If rejected: Inform the user that the call quality was insufficient and provide the reason.\n6. Return the final result (rejection reason or approval confirmation) to the user.\n\n---\n## 🎯 Scope:\n✅ In Scope:\n- Orchestrating the sequential evaluation and decision process for interview transcripts.\n\n❌ Out of Scope:\n- Directly evaluating or creating profiles.\n- Handling transcripts not in the specified format.\n- Interacting with the individual evaluation agents.\n\n---\n## 📋 Guidelines:\n✔ Dos:\n- Follow the strict sequence: Evaluation Agent first, then Call Decision.\n- Wait for each agent's complete response before proceeding.\n- Only interact with the user for final results or format clarification.\n\n🚫 Don'ts:\n- Do not perform evaluation or profile creation yourself.\n- Do not modify the transcript.\n- Do not try to get evaluations simultaneously.\n- Do not reference the individual evaluation agents.\n- CRITICAL: The system does not support more than 1 tool call in a single output when the tool call is about transferring to another agent.",
"model": "gpt-4o",
"toggleAble": true,
"outputVisibility": "user_facing",
"connectedAgents": [
"Evaluation Agent",
"Call Decision"
]
}
}
```
Once you review and apply the changes, you can try out a basic chat first. I can then help you better configure each agent.
This concludes my changes. Would you like some more help?

View file

@ -1,18 +0,0 @@
from pydantic import BaseModel
from typing import Literal, List, Any
class AgentContext(BaseModel):
type: Literal['agent']
agentName: str
class PromptContext(BaseModel):
type: Literal['prompt']
promptName: str
class ToolContext(BaseModel):
type: Literal['tool']
toolName: str
class ChatContext(BaseModel):
type: Literal['chat']
messages: List[Any]

View file

@ -1,25 +0,0 @@
annotated-types==0.7.0
anyio==4.7.0
blinker==1.9.0
certifi==2024.8.30
click==8.1.7
distro==1.9.0
Flask==3.1.0
gunicorn==23.0.0
h11==0.14.0
httpcore==1.0.7
httpx==0.28.0
idna==3.10
itsdangerous==2.2.0
Jinja2==3.1.4
jiter==0.8.0
MarkupSafe==3.0.2
openai==1.61.0
packaging==24.2
pydantic==2.10.3
pydantic_core==2.27.1
python-dotenv
sniffio==1.3.1
tqdm==4.67.1
typing_extensions==4.12.2
Werkzeug==3.1.3

View file

@ -1,201 +0,0 @@
from openai import OpenAI
from flask import Flask, request, jsonify, Response, stream_with_context
from pydantic import BaseModel, ValidationError, Field
from typing import List, Dict, Any, Literal, Optional
import json
from lib import AgentContext, PromptContext, ToolContext, ChatContext
from client import PROVIDER_COPILOT_MODEL, PROVIDER_DEFAULT_MODEL
from client import completions_client
class UserMessage(BaseModel):
role: Literal["user"]
content: str
class AssistantMessage(BaseModel):
role: Literal["assistant"]
content: str
class DataSource(BaseModel):
id: str = Field(alias='_id')
name: str
description: Optional[str] = None
active: bool = True
status: str # 'pending' | 'ready' | 'error' | 'deleted'
error: Optional[str] = None
data: dict # The discriminated union based on type
class Config:
populate_by_name = True
with open('copilot_multi_agent.md', 'r', encoding='utf-8') as file:
copilot_instructions_multi_agent = file.read()
with open('copilot_edit_agent.md', 'r', encoding='utf-8') as file:
copilot_instructions_edit_agent = file.read()
with open('example_multi_agent_1.md', 'r', encoding='utf-8') as file:
copilot_multi_agent_example1 = file.read()
with open('current_workflow.md', 'r', encoding='utf-8') as file:
current_workflow_prompt = file.read()
# Combine the instruction files to create the full multi-agent instructions
streaming_instructions = "\n\n".join([
copilot_instructions_multi_agent,
copilot_multi_agent_example1,
current_workflow_prompt
])
def get_streaming_response(
messages: List[UserMessage | AssistantMessage],
workflow_schema: str,
current_workflow_config: str,
context: AgentContext | PromptContext | ToolContext | ChatContext | None = None,
dataSources: Optional[List[DataSource]] = None,
) -> Any:
# if context is provided, create a prompt for the context
if context:
match context:
case AgentContext():
context_prompt = f"""
**NOTE**: The user is currently working on the following agent:
{context.agentName}
"""
case PromptContext():
context_prompt = f"""
**NOTE**: The user is currently working on the following prompt:
{context.promptName}
"""
case ToolContext():
context_prompt = f"""
**NOTE**: The user is currently working on the following tool:
{context.toolName}
"""
case ChatContext():
context_prompt = f"""
**NOTE**: The user has just tested the following chat using the workflow above and has provided feedback / question below this json dump:
```json
{json.dumps(context.messages)}
```
"""
else:
context_prompt = ""
# Add dataSources to the context if provided
data_sources_prompt = ""
if dataSources:
print(f"Data sources found at project level: {dataSources}")
print(f"Data source IDs: {[ds.id for ds in dataSources]}")
data_sources_prompt = f"""
**NOTE**: The following data sources are available:
```json
{json.dumps([ds.model_dump() for ds in dataSources])}
```
"""
else:
print("No data sources found at project level")
# add the workflow schema to the system prompt
sys_prompt = streaming_instructions.replace("{workflow_schema}", workflow_schema)
# add the agent model to the system prompt
sys_prompt = sys_prompt.replace("{agent_model}", PROVIDER_DEFAULT_MODEL)
# add the current workflow config to the last user message
last_message = messages[-1]
last_message.content = f"""
Context:
The current workflow config is:
```
{current_workflow_config}
```
{context_prompt}
{data_sources_prompt}
User: {last_message.content}
"""
updated_msgs = [{"role": "system", "content": sys_prompt}] + [
message.model_dump() for message in messages
]
print(f"Input to copilot chat completions: {updated_msgs}")
return completions_client.chat.completions.create(
model=PROVIDER_COPILOT_MODEL,
messages=updated_msgs,
temperature=0.0,
stream=True
)
def create_app():
app = Flask(__name__)
@app.route('/health', methods=['GET'])
def health():
return jsonify({'status': 'ok'})
@app.route('/chat_stream', methods=['POST'])
def chat_stream():
try:
request_data = request.json
if not request_data or 'messages' not in request_data:
return jsonify({'error': 'No messages provided'}), 400
print(f"Raw request data: {request_data}")
messages = [
UserMessage(**msg) if msg['role'] == 'user' else AssistantMessage(**msg)
for msg in request_data['messages']
]
workflow_schema = request_data.get('workflow_schema', '')
current_workflow_config = request_data.get('current_workflow_config', '')
context = None # You can add context handling if needed
dataSources = None
if 'dataSources' in request_data and request_data['dataSources']:
print(f"Raw dataSources from request: {request_data['dataSources']}")
dataSources = [DataSource(**ds) for ds in request_data['dataSources']]
print(f"Parsed dataSources: {dataSources}")
def generate():
stream = get_streaming_response(
messages=messages,
workflow_schema=workflow_schema,
current_workflow_config=current_workflow_config,
context=context,
dataSources=dataSources
)
for chunk in stream:
if chunk.choices[0].delta.content:
content = chunk.choices[0].delta.content
yield f"data: {json.dumps({'content': content})}\n\n"
yield "event: done\ndata: {}\n\n"
return Response(
stream_with_context(generate()),
mimetype='text/event-stream',
headers={
'Cache-Control': 'no-cache',
'X-Accel-Buffering': 'no'
}
)
except ValidationError as ve:
return jsonify({
'error': 'Invalid request format',
'details': str(ve)
}), 400
except Exception as e:
return jsonify({
'error': 'Internal server error',
'details': str(e)
}), 500
return app
if __name__ == '__main__':
app = create_app()
print("Starting Flask server...")
app.run(port=3002, host='0.0.0.0', debug=True)

View file

@ -1,2 +0,0 @@
__pycache__
.venv/

View file

@ -1,24 +0,0 @@
# Environment variables for the Voice API application
# Twilio configuration
TWILIO_ACCOUNT_SID=your_account_sid_here
TWILIO_AUTH_TOKEN=your_auth_token_here
BASE_URL=https://your-public-url-here.ngrok.io
# RowBoat API configuration
ROWBOAT_API_HOST=http://localhost:3000
ROWBOAT_PROJECT_ID=your_project_id_here
ROWBOAT_API_KEY=your_api_key_here
# Speech processing APIs
DEEPGRAM_API_KEY=your_deepgram_api_key_here
ELEVENLABS_API_KEY=your_elevenlabs_api_key_here
# Server configuration
PORT=3009
WHATSAPP_PORT=3010
# Redis configuration for persistent state
REDIS_URL=redis://localhost:6379/0
REDIS_EXPIRY_SECONDS=86400
SERVICE_NAME=rowboat-voice

View file

@ -1,2 +0,0 @@
__pycache__
.venv

View file

@ -1,18 +0,0 @@
FROM python:3.12-slim
WORKDIR /app
# Copy requirements first to leverage Docker cache
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Copy application code
COPY . .
# Set environment variables
ENV FLASK_APP=app
ENV PYTHONUNBUFFERED=1
ENV PYTHONPATH=/app
# Command to run Flask development server
CMD ["flask", "run", "--host=0.0.0.0", "--port=4010"]

View file

@ -1,633 +0,0 @@
from flask import Flask, request, jsonify, Response
from twilio.twiml.voice_response import VoiceResponse, Gather
import os
import logging
import uuid
from typing import Dict, Any, Optional
import json
from time import time
from rowboat.schema import SystemMessage, UserMessage, ApiMessage
import elevenlabs
# Load environment variables
from load_env import load_environment
load_environment()
from twilio_api import process_conversation_turn
# Import MongoDB utility functions
from util import (
get_call_state,
save_call_state,
delete_call_state,
get_mongodb_status,
get_twilio_config,
CallState
)
Message = SystemMessage | UserMessage
ELEVENLABS_API_KEY = os.environ.get("ELEVENLABS_API_KEY")
elevenlabs_client = elevenlabs.ElevenLabs(api_key=ELEVENLABS_API_KEY)
app = Flask(__name__)
# Configure logging to stdout for Docker compatibility
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
handlers=[logging.StreamHandler()] # Send logs to stdout
)
logger = logging.getLogger(__name__)
# Local in-memory cache of call state (temporary cache only - not primary storage)
# MongoDB is the primary storage for state across multiple instances
active_calls = {}
# TTS configuration
TTS_VOICE = "Markus - Mature and Chill"
TTS_MODEL = "eleven_flash_v2_5"
@app.route('/inbound', methods=['POST'])
def handle_inbound_call():
"""Handle incoming calls to Twilio numbers configured for RowBoat"""
try:
# Log the entire request for debugging
logger.info(f"Received inbound call request: {request.values}")
# Get the Twilio phone number that received the call
to_number = request.values.get('To')
call_sid = request.values.get('CallSid')
from_number = request.values.get('From')
logger.info(f"Inbound call from {from_number} to {to_number}, CallSid: {call_sid}")
logger.info(f"Raw To number value: '{to_number}', Type: {type(to_number)}")
# Get configuration ONLY from MongoDB
system_prompt = "You are a helpful assistant. Provide concise and clear answers."
workflow_id = None
project_id = None
# Look up configuration in MongoDB
twilio_config = get_twilio_config(to_number)
if twilio_config:
workflow_id = twilio_config['workflow_id']
project_id = twilio_config['project_id']
system_prompt = twilio_config.get('system_prompt', system_prompt)
logger.info(f"Found MongoDB configuration for {to_number}: project_id={project_id}, workflow_id={workflow_id}")
else:
logger.warning(f"No active configuration found in MongoDB for phone number {to_number}")
if not workflow_id:
# No workflow found - provide error message
logger.error(f"No workflow_id found for inbound call to {to_number}")
response = VoiceResponse()
response.say("I'm sorry, this phone number is not properly configured in our system. Please contact support.", voice='alice')
# Include additional information in TwiML for debugging
response.say(f"Received call to number {to_number}", voice='alice')
response.hangup()
return str(response)
# Initialize call state with stateless API fields
call_state = CallState(
workflow_id=workflow_id,
project_id=project_id,
system_prompt=system_prompt,
conversation_history=[],
messages=[], # For stateless API
state=None, # For stateless API state
turn_count=0,
inbound=True,
to_number=to_number,
created_at=int(time()) # Add timestamp for expiration tracking
)
# Save to MongoDB (primary source of truth)
try:
save_call_state(call_sid, call_state)
logger.info(f"Saved initial call state to MongoDB for inbound call {call_sid}")
except Exception as e:
logger.error(f"Error saving inbound call state to MongoDB: {str(e)}")
raise RuntimeError(f"Failed to save call state to MongoDB: {str(e)}")
# Only use memory storage as a temporary cache
# The service that handles the next request might be different
active_calls[call_sid] = call_state
logger.info(f"Initialized call state for {call_sid}, proceeding to handle_call")
# Create a direct response instead of redirecting
return handle_call(call_sid, workflow_id, project_id)
except Exception as e:
# Log the full error with traceback
import traceback
logger.error(f"Error in handle_inbound_call: {str(e)}")
logger.error(traceback.format_exc())
# Return a basic TwiML response so Twilio doesn't get a 500 error
response = VoiceResponse()
response.say("I'm sorry, we encountered an error processing your call. Please try again later.", voice='alice')
response.hangup()
return str(response)
@app.route('/twiml', methods=['POST'])
def handle_twiml_call():
"""TwiML endpoint for outbound call handling"""
call_sid = request.values.get('CallSid')
# Get call state to retrieve workflow_id and project_id
call_state = get_call_state(call_sid)
if call_state:
workflow_id = call_state.get('workflow_id')
project_id = call_state.get('project_id')
return handle_call(call_sid, workflow_id, project_id)
else:
# No call state found - error response
response = VoiceResponse()
response.say("I'm sorry, your call session has expired. Please try again.", voice='alice')
response.hangup()
return str(response)
def handle_call(call_sid, workflow_id, project_id=None):
"""Common handler for both inbound and outbound calls"""
try:
logger.info(f"handle_call: processing call {call_sid} with workflow {workflow_id}, project_id {project_id}")
# Get or initialize call state, first from MongoDB
call_state = None
try:
# Query MongoDB for the call state
call_state = get_call_state(call_sid)
if call_state:
logger.info(f"Loaded and restored call state from MongoDB for {call_sid}")
except Exception as e:
logger.error(f"Error retrieving MongoDB state for {call_sid}: {str(e)}")
call_state = None
# Try in-memory cache as fallback (temporary local cache)
if call_state is None and call_sid in active_calls:
call_state = active_calls.get(call_sid)
logger.info(f"Using in-memory cache for call state of {call_sid}")
# Initialize new state if needed
if call_state is None and workflow_id:
call_state = CallState(
workflow_id=workflow_id,
project_id=project_id,
system_prompt="You are a helpful assistant. Provide concise and clear answers.",
conversation_history=[],
messages=[], # For stateless API
state=None, # For stateless API state
turn_count=0,
inbound=False, # Default for outbound calls
to_number="", # This will be set properly for inbound calls
created_at=int(time()), # Add timestamp for expiration tracking
last_transcription=""
)
# Save to MongoDB (primary source of truth)
try:
save_call_state(call_sid, call_state)
logger.info(f"Initialized and saved new call state to MongoDB for {call_sid}")
except Exception as e:
logger.error(f"Error saving new call state to MongoDB: {str(e)}")
raise RuntimeError(f"Failed to save call state to MongoDB: {str(e)}")
# Only use memory as temporary cache for this request
active_calls[call_sid] = call_state
logger.info(f"Initialized new call state for {call_sid}")
logger.info(f"Using call state: {call_state}")
# Create TwiML response
response = VoiceResponse()
# Check if this is a new call (no turns yet)
if call_state.get('turn_count', 0) == 0:
logger.info("First turn: generating AI greeting using an empty user input...")
# Generate greeting by calling process_conversation_turn with empty user input
try:
ai_greeting, updated_messages, updated_state = process_conversation_turn(
user_input="", # empty to signal "give me your greeting"
workflow_id=call_state['workflow_id'],
system_prompt=call_state['system_prompt'],
previous_messages=[],
previous_state=None,
project_id=call_state.get('project_id')
)
except Exception as e:
logger.error(f"Error generating AI greeting: {str(e)}")
ai_greeting = "Hello, I encountered an issue creating a greeting. How can I help you?"
# Fallback: no changes to updated_messages/updated_state
updated_messages = []
updated_state = None
# Update call_state with AI greeting
call_state['messages'] = updated_messages
call_state['state'] = updated_state
call_state['conversation_history'].append({
'user': "", # empty user
'assistant': ai_greeting
})
call_state['turn_count'] = 1
# Save changes to MongoDB
try:
save_call_state(call_sid, call_state)
logger.info(f"Saved greeting state to MongoDB for {call_sid}")
except Exception as e:
logger.error(f"Error saving greeting state to MongoDB: {str(e)}")
raise RuntimeError(f"Failed to save greeting state to MongoDB: {str(e)}")
active_calls[call_sid] = call_state
# Play the greeting via streaming audio
unique_id = str(uuid.uuid4())
audio_url = f"/stream-audio/{call_sid}/greeting/{unique_id}"
logger.info(f"Will stream greeting from {audio_url}")
response.play(audio_url)
# Gather user input next
gather = Gather(
input='speech',
action=f'/process_speech?call_sid={call_sid}',
speech_timeout='auto',
language='en-US',
enhanced=True,
speechModel='phone_call'
)
response.append(gather)
response.redirect('/twiml')
logger.info(f"Returning response: {str(response)}")
return str(response)
except Exception as e:
# Log the full error with traceback
import traceback
logger.error(f"Error in handle_call: {str(e)}")
logger.error(traceback.format_exc())
# Return a basic TwiML response
response = VoiceResponse()
response.say("I'm sorry, we encountered an error processing your call. Please try again later.", voice='alice')
response.hangup()
return str(response)
@app.route('/process_speech', methods=['POST'])
def process_speech():
"""Process user speech input and generate AI response"""
try:
logger.info(f"Processing speech: {request.values}")
call_sid = request.args.get('call_sid')
# Log all request values for debugging
logger.info(f"FULL REQUEST VALUES: {dict(request.values)}")
logger.info(f"FULL REQUEST ARGS: {dict(request.args)}")
# Get the speech result directly from Twilio
# We're now relying on Twilio's enhanced speech recognition instead of Deepgram
speech_result = request.values.get('SpeechResult')
confidence = request.values.get('Confidence')
logger.info(f"Twilio SpeechResult: {speech_result}")
logger.info(f"Twilio Confidence: {confidence}")
if not call_sid:
logger.warning(f"Missing call_sid: {call_sid}")
response = VoiceResponse()
response.say("I'm sorry, I couldn't process that request.", voice='alice')
response.hangup()
return str(response)
if not speech_result:
logger.warning("No speech result after transcription attempts")
response = VoiceResponse()
response.say("I'm sorry, I didn't catch what you said. Could you please try again?", voice='alice')
# Gather user input again
gather = Gather(
input='speech',
action=f'/process_speech?call_sid={call_sid}',
speech_timeout='auto',
language='en-US',
enhanced=True,
speechModel='phone_call'
)
response.append(gather)
# Redirect to twiml endpoint which will get call state from MongoDB
response.redirect('/twiml')
return str(response)
# Load call state from MongoDB (primary source of truth)
call_state = None
try:
call_state = get_call_state(call_sid)
if call_state:
logger.info(f"Loaded call state from MongoDB for speech processing: {call_sid}")
except Exception as e:
logger.error(f"Error retrieving MongoDB state for speech processing: {str(e)}")
call_state = None
# Try memory cache as fallback
if call_state is None and call_sid in active_calls:
call_state = active_calls[call_sid]
logger.info(f"Using in-memory state for speech processing: {call_sid}")
# Check if we have valid state
if not call_state:
logger.warning(f"No call state found for speech processing: {call_sid}")
response = VoiceResponse()
response.say("I'm sorry, your call session has expired. Please call back.", voice='alice')
response.hangup()
return str(response)
# Extract key information
workflow_id = call_state.get('workflow_id')
project_id = call_state.get('project_id')
system_prompt = call_state.get('system_prompt', "You are a helpful assistant.")
# Check if we have a Deepgram transcription stored in the call state
if 'last_transcription' in call_state and call_state['last_transcription']:
deepgram_transcription = call_state['last_transcription']
logger.info(f"Found stored Deepgram transcription: {deepgram_transcription}")
logger.info(f"Comparing with Twilio transcription: {speech_result}")
# Use the Deepgram transcription instead of Twilio's
speech_result = deepgram_transcription
# Remove it so we don't use it again
del call_state['last_transcription']
logger.info(f"Using Deepgram transcription instead")
# Log final user input that will be used
logger.info(f"Final user input: {speech_result}")
# Process with RowBoat agent
try:
# Clean up the speech result if needed
if speech_result:
# Remove any common filler words or fix typical transcription issues
import re
# Convert to lowercase for easier pattern matching
cleaned_input = speech_result.lower()
# Remove filler words that might be at the beginning
cleaned_input = re.sub(r'^(um|uh|like|so|okay|well)\s+', '', cleaned_input)
# Capitalize first letter for better appearance
if cleaned_input:
speech_result = cleaned_input[0].upper() + cleaned_input[1:]
logger.info(f"Sending to RowBoat: '{speech_result}'")
# Get previous messages and state from call state
previous_messages = call_state.get('messages', [])
previous_state = call_state.get('state')
# Process with stateless API
ai_response, updated_messages, updated_state = process_conversation_turn(
user_input=speech_result,
workflow_id=workflow_id,
system_prompt=system_prompt,
previous_messages=previous_messages,
previous_state=previous_state,
project_id=project_id
)
# Update the messages and state in call state
call_state['messages'] = updated_messages
call_state['state'] = updated_state
logger.info(f"RowBoat response: {ai_response}")
except Exception as e:
logger.error(f"Error processing with RowBoat: {str(e)}")
ai_response = "I'm sorry, I encountered an issue processing your request. Could you please try again?"
# Conversation history is updated in the streaming response section below
# Create TwiML response
response = VoiceResponse()
# Use streaming audio for the response
logger.info("Setting up response streaming with ElevenLabs")
try:
# Store the AI response in conversation history first
# (The stream-audio endpoint will read it from here)
# Update conversation history (do this before streaming so the endpoint can access it)
call_state['conversation_history'].append({
'user': speech_result,
'assistant': ai_response
})
call_state['turn_count'] += 1
# Save to MongoDB (primary source of truth)
try:
save_call_state(call_sid, call_state)
logger.info(f"Saved response state to MongoDB for {call_sid}")
except Exception as e:
logger.error(f"Error saving response state to MongoDB: {str(e)}")
raise RuntimeError(f"Failed to save response state to MongoDB: {str(e)}")
# Update local memory cache
active_calls[call_sid] = call_state
# Generate a unique ID to prevent caching
unique_id = str(uuid.uuid4())
# Use a relative URL - Twilio will use the same host as the webhook
audio_url = f"/stream-audio/{call_sid}/response/{unique_id}"
logger.info(f"Streaming response from relative URL: {audio_url}")
# Play the response via streaming
response.play(audio_url)
except Exception as e:
logger.error(f"Error with audio streaming for response: {str(e)}")
import traceback
logger.error(traceback.format_exc())
# Fallback to Twilio TTS
response.say(ai_response, voice='alice')
# Gather next user input with enhanced speech recognition
gather = Gather(
input='speech',
action=f'/process_speech?call_sid={call_sid}',
speech_timeout='auto',
language='en-US',
enhanced=True, # Enable enhanced speech recognition
speechModel='phone_call' # Optimize for phone calls
)
response.append(gather)
# If no input detected, redirect to twiml endpoint
# Call state will be retrieved from MongoDB
response.redirect('/twiml')
logger.info(f"Returning TwiML response for speech processing")
return str(response)
except Exception as e:
# Log the full error with traceback
import traceback
logger.error(f"Error in process_speech: {str(e)}")
logger.error(traceback.format_exc())
# Return a basic TwiML response
response = VoiceResponse()
response.say("I'm sorry, we encountered an error processing your speech. Please try again.", voice='alice')
response.gather(
input='speech',
action=f'/process_speech?call_sid={request.args.get("call_sid")}',
speech_timeout='auto'
)
return str(response)
@app.route('/stream-audio/<call_sid>/<text_type>/<unique_id>', methods=['GET'])
def stream_audio(call_sid, text_type, unique_id):
"""Stream audio directly from ElevenLabs to Twilio without saving to disk"""
try:
logger.info(f"Audio streaming requested for call {call_sid}, type {text_type}")
# Determine what text to synthesize
text_to_speak = ""
if text_type == "greeting" or text_type == "response":
# Get the text from call state (try MongoDB first, then memory)
call_state = None
# Try MongoDB first
try:
call_state = get_call_state(call_sid)
if call_state:
logger.info(f"Loaded call state from MongoDB for streaming: {call_sid}")
except Exception as e:
logger.error(f"Error retrieving MongoDB state for streaming: {str(e)}")
call_state = None
# Fall back to memory if needed
if call_state is None:
if call_sid not in active_calls:
logger.error(f"Call SID not found for streaming: {call_sid}")
return "Call not found", 404
call_state = active_calls[call_sid]
logger.info(f"Using in-memory state for streaming: {call_sid}")
if call_state.get('conversation_history') and len(call_state['conversation_history']) > 0:
# Get the most recent AI response
text_to_speak = call_state['conversation_history'][-1]['assistant']
else:
logger.warning(f"No conversation history found for call {call_sid}")
text_to_speak = "I'm sorry, I don't have a response ready. Could you please repeat?"
else:
# Direct text may be passed as the text_type (for testing)
text_to_speak = text_type
if not text_to_speak:
logger.error("No text to synthesize")
return "No text to synthesize", 400
logger.info(f"Streaming audio for text: {text_to_speak[:50]}...")
def generate():
try:
# Generate and stream the audio directly
audio_stream = elevenlabs_client.generate(
text=text_to_speak,
voice=TTS_VOICE,
model=TTS_MODEL,
output_format="mp3_44100_128"
)
# Stream chunks directly to the response
for chunk in audio_stream:
yield chunk
logger.info(f"Finished streaming audio for call {call_sid}")
except Exception as e:
logger.error(f"Error in audio stream generator: {str(e)}")
import traceback
logger.error(traceback.format_exc())
# Return a streaming response
response = Response(generate(), mimetype='audio/mpeg')
return response
except Exception as e:
logger.error(f"Error setting up audio stream: {str(e)}")
import traceback
logger.error(traceback.format_exc())
return "Error streaming audio", 500
@app.route('/call-status', methods=['POST'])
def call_status_callback():
"""Handle call status callbacks from Twilio"""
call_sid = request.values.get('CallSid')
call_status = request.values.get('CallStatus')
logger.info(f"Call {call_sid} status: {call_status}")
# Clean up resources when call completes
if call_status in ['completed', 'failed', 'busy', 'no-answer', 'canceled']:
# Get call state from MongoDB or memory
call_state = None
# Try to load from MongoDB first
try:
call_state = get_call_state(call_sid)
if call_state:
logger.info(f"Loaded final state from MongoDB for {call_sid}")
except Exception as e:
logger.error(f"Error retrieving final state from MongoDB: {str(e)}")
call_state = None
# Fall back to memory if needed
if call_state is None and call_sid in active_calls:
call_state = active_calls[call_sid]
logger.info(f"Using in-memory state for final call state of {call_sid}")
if call_state:
# Remove from active calls in both memory and MongoDB
if call_sid in active_calls:
del active_calls[call_sid]
logger.info(f"Removed call {call_sid} from active calls memory")
try:
# Remove the document from MongoDB
delete_call_state(call_sid)
logger.info(f"Removed call {call_sid} from MongoDB")
except Exception as e:
logger.error(f"Error removing call state from MongoDB: {str(e)}")
return '', 204
@app.route('/health', methods=['GET'])
def health_check():
"""Simple health check endpoint"""
health_data = {
"status": "healthy",
"active_calls_memory": len(active_calls)
}
# Get MongoDB status
try:
mongodb_status = get_mongodb_status()
health_data["mongodb"] = mongodb_status
health_data["active_calls_mongodb"] = mongodb_status.get("active_calls", 0)
except Exception as e:
health_data["mongodb_error"] = str(e)
health_data["status"] = "degraded"
return jsonify(health_data)
if __name__ == '__main__':
# Log startup information
logger.info(f"Starting Twilio-RowBoat server")
# Remove the explicit run configuration since Flask CLI will handle it
app.run()

View file

@ -1,6 +0,0 @@
from dotenv import load_dotenv
import os
def load_environment():
"""Load environment variables from .env file"""
load_dotenv()

View file

@ -1,39 +0,0 @@
aiohappyeyeballs==2.5.0
aiohttp==3.11.13
aiohttp-retry==2.9.1
aiosignal==1.3.2
annotated-types==0.7.0
anyio==4.8.0
attrs==25.1.0
blinker==1.9.0
certifi==2025.1.31
charset-normalizer==3.4.1
click==8.1.8
dnspython==2.7.0
dotenv==0.9.9
elevenlabs==1.52.0
Flask==3.1.0
frozenlist==1.5.0
h11==0.14.0
httpcore==1.0.7
httpx==0.28.1
idna==3.10
itsdangerous==2.2.0
Jinja2==3.1.6
MarkupSafe==3.0.2
multidict==6.1.0
propcache==0.3.0
pydantic==2.10.6
pydantic_core==2.27.2
PyJWT==2.10.1
pymongo==4.11.2
python-dotenv==1.0.1
requests==2.32.3
rowboat==2.1.0
sniffio==1.3.1
twilio==9.4.6
typing_extensions==4.12.2
urllib3==2.3.0
websockets==15.0.1
Werkzeug==3.1.3
yarl==1.18.3

View file

@ -1,96 +0,0 @@
from twilio.rest import Client as TwilioClient
from rowboat.client import Client
from rowboat.schema import UserMessage, SystemMessage
import os
from typing import Dict, List, Optional, Tuple, Any
import logging
from util import get_api_key
import time
import json
# Load environment variables
from load_env import load_environment
load_environment()
# Configure logging to stdout for Docker compatibility
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
handlers=[logging.StreamHandler()] # Send logs to stdout
)
logger = logging.getLogger(__name__)
# Environment variables and configuration
ROWBOAT_API_HOST = os.environ.get("ROWBOAT_API_HOST").strip()
Message = UserMessage | SystemMessage
def process_conversation_turn(
user_input: str,
workflow_id: str,
system_prompt: str = "You are a helpful assistant. Provide concise and clear answers.",
previous_messages: List[Message] = None,
previous_state: Any = None,
project_id: str = None
) -> Tuple[str, List[Message], Any]:
"""
Process a single conversation turn with the RowBoat agent using the stateless API.
Args:
user_input: User's transcribed input
workflow_id: RowBoat workflow ID
system_prompt: System prompt for the agent
previous_messages: Previous messages in the conversation
previous_state: Previous state from RowBoat
project_id: RowBoat project ID (if different from default)
Returns:
A tuple of (response_text, updated_messages, updated_state)
"""
try:
# Initialize messages list if not provided
messages = [] if previous_messages is None else previous_messages.copy()
# If we're starting a new conversation, add the system message
if not messages or not any(msg.role == 'system' for msg in messages):
messages.append(SystemMessage(role='system', content=system_prompt))
# Add the user's new
if user_input:
messages.append(UserMessage(role='user', content=user_input))
# Process the conversation using the stateless API
logger.info(f"Sending to RowBoat API with {len(messages)} messages")
# Create client with custom project_id if provided
client = Client(
host=ROWBOAT_API_HOST,
project_id=project_id,
api_key=get_api_key(project_id)
)
response_messages, new_state = client.chat(
messages=messages,
workflow_id=workflow_id,
state=previous_state
)
# Extract the assistant's response (last message)
if response_messages and len(response_messages) > 0:
assistant_response = response_messages[-1].content
else:
assistant_response = "I'm sorry, I didn't receive a proper response."
# Update messages list with the new responses
final_messages = messages + response_messages
logger.info(f"Got response from RowBoat API: {assistant_response[:100]}...")
return assistant_response, final_messages, new_state
except Exception as e:
logger.error(f"Error processing conversation turn: {str(e)}")
import traceback
logger.error(traceback.format_exc())
return "I'm sorry, I encountered an error processing your request.", previous_messages, previous_state

View file

@ -1,423 +0,0 @@
import os
import logging
import datetime
from typing import Dict, Any, Optional, List, Union
import copy
from pymongo import MongoClient
from pymongo.errors import ConnectionFailure, PyMongoError
from pymongo.collection import Collection
from bson import json_util
from pydantic import BaseModel
from rowboat.schema import ApiMessage
# Configure logging to stdout for Docker compatibility
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
handlers=[logging.StreamHandler()] # Send logs to stdout
)
logger = logging.getLogger(__name__)
# MongoDB Configuration
MONGODB_URI = os.environ.get('MONGODB_URI')
MONGODB_DB = 'rowboat'
CALL_STATE_COLLECTION = 'call-state'
MONGODB_EXPIRY_SECONDS = 86400 # Default 24 hours
API_KEYS_COLLECTION = "api_keys"
# MongoDB client singleton
_mongo_client = None
_db = None
_call_state_collection = None
_api_keys_collection = None
# Define chat state pydantic model
class CallState(BaseModel):
messages: List[ApiMessage] = []
workflow_id: str
project_id: str
system_prompt: str
turn_count: int = 0
inbound: bool = False
conversation_history: List[Dict[str, str]] = [] # Using Dict instead of ApiMessage for chat history
to_number: str = ""
created_at: int
state: Any = None # Allow any type since the API might return a complex state object
last_transcription: Optional[str] = None
# Enable dictionary-style access for compatibility with existing code
def __getitem__(self, key):
return getattr(self, key)
def __setitem__(self, key, value):
setattr(self, key, value)
def get(self, key, default=None):
return getattr(self, key, default)
model_config = {
# Allow extra fields for flexibility
"extra": "allow",
# More lenient type validation
"arbitrary_types_allowed": True,
# Allow population by field name
"populate_by_name": True
}
def init_mongodb():
"""Initialize MongoDB connection and set up indexes."""
global _mongo_client, _db, _call_state_collection, _api_keys_collection
try:
_mongo_client = MongoClient(MONGODB_URI)
# Force a command to check the connection
_mongo_client.admin.command('ping')
# Set up database and collection
_db = _mongo_client[MONGODB_DB]
_call_state_collection = _db[CALL_STATE_COLLECTION]
_api_keys_collection = _db[API_KEYS_COLLECTION]
# Create TTL index if it doesn't exist
if 'expires_at_1' not in _call_state_collection.index_information():
_call_state_collection.create_index('expires_at', expireAfterSeconds=0)
logger.info(f"Connected to MongoDB at {MONGODB_URI}")
return True
except ConnectionFailure as e:
logger.error(f"Failed to connect to MongoDB: {str(e)}")
raise RuntimeError(f"Could not connect to MongoDB: {str(e)}")
def get_collection() -> Collection:
"""Get the MongoDB collection, initializing if needed."""
global _call_state_collection
if _call_state_collection is None:
init_mongodb()
return _call_state_collection
def get_api_keys_collection() -> Collection:
"""Get the MongoDB collection, initializing if needed."""
global _api_keys_collection
if _api_keys_collection is None:
init_mongodb()
return _api_keys_collection
def get_api_key(project_id: str) -> Optional[str]:
"""Get the API key for a given project ID."""
collection = get_api_keys_collection()
doc = collection.find_one({"projectId": project_id})
return doc["key"] if doc else None
def save_call_state(call_sid: str, call_state: CallState) -> bool:
"""
Save call state to MongoDB.
Args:
call_sid: The call SID to use as document ID
call_state: The call state dictionary to save
Returns:
True if successful, False otherwise
"""
try:
# Validate call_state is a CallState object
if not isinstance(call_state, CallState):
raise ValueError(f"call_state must be a CallState object, got {type(call_state)}")
collection = get_collection()
# Use call_sid as document ID
collection.update_one(
{'_id': call_sid},
{'$set': call_state.model_dump()},
upsert=True
)
logger.info(f"Saved call state to MongoDB for call {call_sid}")
return True
except PyMongoError as e:
logger.error(f"Error saving call state to MongoDB for call {call_sid}: {str(e)}")
raise RuntimeError(f"Failed to save call state to MongoDB: {str(e)}")
except Exception as e:
logger.error(f"Unexpected error in save_call_state: {str(e)}")
raise RuntimeError(f"Failed to save call state: {str(e)}")
def get_call_state(call_sid: str) -> Optional[CallState]:
"""
Retrieve call state from MongoDB.
Args:
call_sid: The call SID to retrieve
Returns:
Call state dictionary or None if not found
"""
try:
collection = get_collection()
# Query MongoDB for the call state
state_doc = collection.find_one({'_id': call_sid})
if not state_doc:
logger.info(f"No call state found in MongoDB for call {call_sid}")
return None
call_state = CallState.model_validate(state_doc)
logger.info(f"Retrieved call state from MongoDB for call {call_sid}")
return call_state
except PyMongoError as e:
logger.error(f"Error retrieving call state from MongoDB for call {call_sid}: {str(e)}")
raise RuntimeError(f"Failed to retrieve call state from MongoDB: {str(e)}")
except Exception as e:
logger.error(f"Unexpected error in get_call_state: {str(e)}")
raise RuntimeError(f"Failed to retrieve call state: {str(e)}")
def delete_call_state(call_sid: str) -> bool:
"""
Delete call state from MongoDB.
Args:
call_sid: The call SID to delete
Returns:
True if successful, False if not found
"""
try:
collection = get_collection()
# Delete the document from MongoDB
result = collection.delete_one({'_id': call_sid})
if result.deleted_count > 0:
logger.info(f"Deleted call state from MongoDB for call {call_sid}")
return True
else:
logger.info(f"No call state found to delete in MongoDB for call {call_sid}")
return False
except PyMongoError as e:
logger.error(f"Error deleting call state from MongoDB for call {call_sid}: {str(e)}")
raise RuntimeError(f"Failed to delete call state from MongoDB: {str(e)}")
except Exception as e:
logger.error(f"Unexpected error in delete_call_state: {str(e)}")
raise RuntimeError(f"Failed to delete call state: {str(e)}")
def count_active_calls() -> int:
"""
Count active call documents in MongoDB.
Returns:
Number of active call documents
"""
try:
collection = get_collection()
return collection.count_documents({})
except PyMongoError as e:
logger.error(f"Error counting active calls in MongoDB: {str(e)}")
raise RuntimeError(f"Failed to count active calls in MongoDB: {str(e)}")
except Exception as e:
logger.error(f"Unexpected error in count_active_calls: {str(e)}")
raise RuntimeError(f"Failed to count active calls: {str(e)}")
def get_mongodb_status() -> Dict[str, Any]:
"""
Get MongoDB connection status information.
Returns:
Dictionary with status information
"""
status = {
"status": "connected",
"uri": MONGODB_URI,
"database": MONGODB_DB,
"collection": CALL_STATE_COLLECTION
}
try:
# First check connection with a simple command
collection = get_collection()
db = collection.database
db.command('ping')
status["connection"] = "ok"
# Count active calls
count = count_active_calls()
status["active_calls"] = count
# Get collection stats
try:
stats = db.command("collStats", CALL_STATE_COLLECTION)
status["size_bytes"] = stats.get("size", 0)
status["document_count"] = stats.get("count", 0)
status["index_count"] = len(stats.get("indexSizes", {}))
except Exception as stats_error:
status["stats_error"] = str(stats_error)
except Exception as e:
status["status"] = "error"
status["error"] = str(e)
status["timestamp"] = datetime.datetime.utcnow().isoformat()
return status
# Twilio configuration functions
def get_twilio_config(phone_number: str) -> Optional[Dict[str, Any]]:
"""
Get Twilio configuration for a specific phone number from MongoDB.
Args:
phone_number: The phone number to look up configuration for
Returns:
Configuration dictionary or None if not found/active
"""
try:
# Get MongoDB client and database
client = get_collection().database.client
db = client[MONGODB_DB]
# Use the twilio_configs collection
config_collection = db['twilio_configs']
# Enhanced logging for phone number format
logger.info(f"Looking up configuration for phone number: '{phone_number}'")
# Try different formats of the phone number
cleaned_number = phone_number.strip().replace(' ', '').replace('-', '').replace('(', '').replace(')', '')
possible_formats = [
phone_number, # Original format from Twilio
cleaned_number, # Thoroughly cleaned number
'+' + cleaned_number if not cleaned_number.startswith('+') else cleaned_number, # Ensure + prefix
# Try with different country code formats
'+1' + cleaned_number[-10:] if len(cleaned_number) >= 10 else cleaned_number, # US format with +1
'1' + cleaned_number[-10:] if len(cleaned_number) >= 10 else cleaned_number, # US format with 1
cleaned_number[-10:] if len(cleaned_number) >= 10 else cleaned_number, # US format without country code
]
# Remove duplicates while preserving order
unique_formats = []
for fmt in possible_formats:
if fmt not in unique_formats:
unique_formats.append(fmt)
possible_formats = unique_formats
# Log the formats we're trying
logger.info(f"Trying phone number formats: {possible_formats}")
# Try each format
for phone_format in possible_formats:
# Look up the configuration for this phone number format with status=active
config = config_collection.find_one({'phone_number': phone_format, 'status': 'active'})
if config:
logger.info(f"Found active configuration for '{phone_format}': project_id={config.get('project_id')}, workflow_id={config.get('workflow_id')}")
break # Found a match, exit the loop
# If we didn't find any match
if not config:
# Try a more generic query to see what configurations exist
try:
all_configs = list(config_collection.find({'phone_number': {'$regex': phone_number[-10:] if len(phone_number) >= 10 else phone_number}}))
if all_configs:
logger.warning(f"Found {len(all_configs)} configurations that match phone number {phone_number}, but none are active:")
for cfg in all_configs:
logger.warning(f" - Phone: {cfg.get('phone_number')}, Status: {cfg.get('status')}, Workflow: {cfg.get('workflow_id')}")
else:
logger.warning(f"No configurations found at all for phone number {phone_number} or related formats")
except Exception as e:
logger.error(f"Error running regex query: {str(e)}")
logger.warning(f"No active configuration found for any format of phone number {phone_number}")
return None
# Make sure required fields are present
if 'project_id' not in config or 'workflow_id' not in config:
logger.error(f"Configuration for {phone_number} is missing required fields")
return None
logger.info(f"Found active configuration for {phone_number}: project_id={config['project_id']}, workflow_id={config['workflow_id']}")
return config
except Exception as e:
logger.error(f"Error retrieving Twilio configuration for {phone_number}: {str(e)}")
# Return None instead of raising an exception to allow fallback to default behavior
return None
def list_active_twilio_configs() -> List[Dict[str, Any]]:
"""
List all active Twilio configurations from MongoDB.
Returns:
List of active configuration dictionaries
"""
try:
# Get MongoDB client and database
client = get_collection().database.client
db = client[MONGODB_DB]
# Use the twilio_configs collection
config_collection = db['twilio_configs']
# Find all active configurations
configs = list(config_collection.find({'status': 'active'}))
logger.info(f"Found {len(configs)} active Twilio configurations")
return configs
except Exception as e:
logger.error(f"Error retrieving active Twilio configurations: {str(e)}")
return []
def save_twilio_config(config: Dict[str, Any]) -> bool:
"""
Save a Twilio configuration to MongoDB.
Args:
config: Configuration dictionary with at least phone_number, project_id, and workflow_id
Returns:
True if successful, False otherwise
"""
required_fields = ['phone_number', 'project_id', 'workflow_id']
for field in required_fields:
if field not in config:
logger.error(f"Missing required field '{field}' in Twilio configuration")
return False
try:
# Get MongoDB client and database
client = get_collection().database.client
db = client[MONGODB_DB]
# Use the twilio_configs collection
config_collection = db['twilio_configs']
# Ensure status is set to active
if 'status' not in config:
config['status'] = 'active'
# Add timestamp
config['updated_at'] = datetime.datetime.utcnow()
if 'created_at' not in config:
config['created_at'] = config['updated_at']
# Use phone_number as the ID
phone_number = config['phone_number']
# Update or insert the configuration
result = config_collection.update_one(
{'phone_number': phone_number},
{'$set': config},
upsert=True
)
if result.matched_count > 0:
logger.info(f"Updated Twilio configuration for {phone_number}")
else:
logger.info(f"Created new Twilio configuration for {phone_number}")
return True
except Exception as e:
logger.error(f"Error saving Twilio configuration: {str(e)}")
return False
# Initialize MongoDB on module import
init_mongodb()

View file

@ -1,62 +1,13 @@
'use server';
import { WebpageCrawlResponse } from "../lib/types/tool_types";
import { webpagesCollection } from "../lib/mongodb";
import { z } from 'zod';
import FirecrawlApp, { ScrapeResponse } from '@mendable/firecrawl-js';
import { getAgenticResponseStreamId } from "../lib/utils";
import { check_query_limit } from "../lib/rate_limiting";
import { QueryLimitError } from "../lib/client_utils";
import { projectAuthCheck } from "./project_actions";
import { authorizeUserAction } from "./billing_actions";
import { Workflow, WorkflowTool } from "../lib/types/workflow_types";
import { Workflow } from "../lib/types/workflow_types";
import { Message } from "@/app/lib/types/types";
const crawler = new FirecrawlApp({ apiKey: process.env.FIRECRAWL_API_KEY || '' });
export async function scrapeWebpage(url: string): Promise<z.infer<typeof WebpageCrawlResponse>> {
const page = await webpagesCollection.findOne({
"_id": url,
lastUpdatedAt: {
'$gte': new Date(Date.now() - 1000 * 60 * 60 * 24).toISOString(), // 24 hours
},
});
if (page) {
// console.log("found webpage in db", url);
return {
title: page.title,
content: page.contentSimple,
};
}
// otherwise use firecrawl
const scrapeResult = await crawler.scrapeUrl(
url,
{
formats: ['markdown'],
onlyMainContent: true
}
) as ScrapeResponse;
// save the webpage using upsert
await webpagesCollection.updateOne(
{ _id: url },
{
$set: {
title: scrapeResult.metadata?.title || '',
contentSimple: scrapeResult.markdown || '',
lastUpdatedAt: (new Date()).toISOString(),
}
},
{ upsert: true }
);
// console.log("crawled webpage", url);
return {
title: scrapeResult.metadata?.title || '',
content: scrapeResult.markdown || '',
};
}
export async function getAssistantResponseStreamId(
projectId: string,
workflow: z.infer<typeof Workflow>,

View file

@ -10,7 +10,6 @@ import { WithStringId } from "../lib/types/types";
import { DataSourceDoc } from "../lib/types/datasource_types";
import { DataSource } from "../lib/types/datasource_types";
import { uploadsS3Client } from "../lib/uploads_s3_client";
import { USE_RAG_S3_UPLOADS } from "../lib/feature_flags";
export async function getDataSource(projectId: string, sourceId: string): Promise<WithStringId<z.infer<typeof DataSource>>> {
await projectAuthCheck(projectId);

View file

@ -1,906 +0,0 @@
'use server';
import { projectAuthCheck } from './project_actions';
import { z } from 'zod';
import { MCPServer, McpTool, McpServerResponse, McpServerTool } from '../lib/types/types';
import { projectsCollection } from '../lib/mongodb';
import { fetchMcpTools, toggleMcpTool } from './mcp_actions';
import { fetchMcpToolsForServer } from './mcp_actions';
import { headers } from 'next/headers';
import { authorizeUserAction } from './billing_actions';
import { redisClient } from '../lib/redis';
import { SERVER_URL_PARAMS, SERVER_CLIENT_ID_MAP } from '../lib/constants/klavis';
type McpServerType = z.infer<typeof MCPServer>;
type McpToolType = z.infer<typeof McpTool>;
type McpServerResponseType = z.infer<typeof McpServerResponse>;
// Internal API Response Types
interface KlavisServerMetadata {
id: string;
name: string;
description: string;
tools: {
name: string;
description: string;
}[];
authNeeded: boolean;
}
interface GetAllServersResponse {
servers: KlavisServerMetadata[];
}
interface CreateServerInstanceResponse {
serverUrl: string;
instanceId: string;
}
interface DeleteServerInstanceResponse {
success: boolean;
message: string;
}
interface UserInstance {
id: string;
name: string;
description: string | null;
tools: {
name: string;
description: string;
authNeeded: boolean;
isAuthenticated: boolean;
}[] | null;
authNeeded: boolean;
isAuthenticated: boolean;
}
interface GetUserInstancesResponse {
instances: UserInstance[];
}
// Add type for raw MCP tool response at the top with other types
interface RawMcpTool {
name: string;
description: string;
inputSchema: string | {
type: string;
properties: Record<string, any>;
required?: string[];
};
}
const KLAVIS_BASE_URL = 'https://api.klavis.ai';
interface KlavisApiCallOptions {
method?: 'GET' | 'POST' | 'PUT' | 'DELETE';
body?: Record<string, any>;
additionalHeaders?: Record<string, string>;
}
async function klavisApiCall<T>(
endpoint: string,
options: KlavisApiCallOptions = {}
): Promise<T> {
const { method = 'GET', body, additionalHeaders = {} } = options;
const startTime = performance.now();
const url = `${KLAVIS_BASE_URL}${endpoint}`;
try {
const headers = {
'Authorization': `Bearer ${process.env.KLAVIS_API_KEY}`,
'Content-Type': 'application/json',
...additionalHeaders
};
const fetchOptions: RequestInit = {
method,
headers,
...(body ? { body: JSON.stringify(body) } : {})
};
const response = await fetch(url, fetchOptions);
const endTime = performance.now();
console.log('[Klavis API] Response time:', {
url,
method,
durationMs: Math.round(endTime - startTime)
});
if (!response.ok) {
const error = await response.text();
throw new Error(error);
}
return await response.json() as T;
} catch (error) {
const endTime = performance.now();
console.error('[Klavis API] Failed call:', {
url,
method,
durationMs: Math.round(endTime - startTime),
error
});
throw error;
}
}
// Lists all active server instances for a given project
export async function listActiveServerInstances(projectId: string): Promise<UserInstance[]> {
try {
await projectAuthCheck(projectId);
const queryParams = new URLSearchParams({
user_id: projectId,
platform_name: 'Rowboat'
});
console.log('[Klavis API] Fetching active instances:', { projectId, platformName: 'Rowboat' });
const endpoint = `/user/instances?${queryParams}`;
const data = await klavisApiCall<GetUserInstancesResponse>(endpoint);
// Only show instances that are authenticated or need auth
const relevantInstances = data.instances.filter(i => i.isAuthenticated || i.authNeeded);
console.log('[Klavis API] Active instances:', {
count: relevantInstances.length,
authenticated: relevantInstances.filter(i => i.isAuthenticated).map(i => i.name).join(', '),
needsAuth: relevantInstances.filter(i => i.authNeeded && !i.isAuthenticated).map(i => i.name).join(', ')
});
return data.instances;
} catch (error) {
console.error('[Klavis API] Error listing active instances:', error);
throw error;
}
}
async function enrichToolsWithParameters(
projectId: string,
serverName: string,
basicTools: { name: string; description: string }[],
isNewlyEnabled: boolean = false
): Promise<McpToolType[]> {
try {
console.log(`[Klavis API] Starting tool enrichment for ${serverName}`);
const enrichedTools = await fetchMcpToolsForServer(projectId, serverName);
if (enrichedTools.length === 0) {
console.log(`[Klavis API] No tools enriched for ${serverName}`);
return basicTools.map(tool => ({
id: tool.name,
name: tool.name,
description: tool.description,
parameters: {
type: 'object',
properties: {},
required: []
}
}));
}
console.log(`[Klavis API] Processing ${enrichedTools.length} tools for ${serverName}`);
// Create a map of enriched tools for this server
const enrichedToolMap = new Map(
enrichedTools.map(tool => [tool.name, {
name: tool.name,
description: tool.description,
parameters: {
type: 'object' as const,
properties: tool.parameters?.properties || {},
required: tool.parameters?.required || []
}
}])
);
// Find tools that couldn't be enriched
const unenrichedTools = basicTools
.filter(tool => !enrichedToolMap.has(tool.name))
.map(tool => tool.name);
if (unenrichedTools.length > 0) {
console.log('[Klavis API] Tools that could not be enriched:', {
serverName,
unenrichedTools: unenrichedTools.join(', ')
});
}
// Enrich the basic tools with parameters and descriptions
const result = basicTools.map(basicTool => {
const enrichedTool = enrichedToolMap.get(basicTool.name);
const tool: McpToolType = {
id: basicTool.name,
name: basicTool.name,
description: enrichedTool?.description || basicTool.description || '',
parameters: enrichedTool?.parameters || {
type: 'object',
properties: {},
required: []
}
};
return tool;
});
console.log('[Klavis API] Tools processed:', {
serverName,
toolCount: result.length,
tools: result.map(t => ({
name: t.name,
paramCount: Object.keys(t.parameters?.properties || {}).length,
hasParams: t.parameters && Object.keys(t.parameters.properties).length > 0
}))
});
return result;
} catch (error) {
console.error('[Klavis API] Error enriching tools with parameters:', {
serverName,
error: error instanceof Error ? error.message : 'Unknown error',
basicToolCount: basicTools.length
});
// Return basic tools with empty parameters if enrichment fails
return basicTools.map(tool => ({
id: tool.name,
name: tool.name,
description: tool.description,
parameters: {
type: 'object',
properties: {},
required: []
}
}));
}
}
// Modify listAvailableMcpServers to use enriched tools
export async function listAvailableMcpServers(projectId: string): Promise<McpServerResponseType> {
try {
await projectAuthCheck(projectId);
console.log('[Klavis API] Starting server list fetch:', { projectId });
// Get MongoDB project data first
const project = await projectsCollection.findOne({ _id: projectId });
const mongodbServers = project?.mcpServers || [];
const mongodbServerMap = new Map(mongodbServers.map(server => [server.name, server]));
console.log('[Klavis API] Found ', mongodbServers.length, ' MongoDB servers');
const serversEndpoint = '/mcp-server/servers';
const rawData = await klavisApiCall<GetAllServersResponse>(serversEndpoint, {
additionalHeaders: { 'Accept': 'application/json' }
});
console.log('[Klavis API] Raw server response:', {
serverCount: rawData.servers.length,
servers: rawData.servers.map(s => s.name).join(', ')
});
if (!rawData || !rawData.servers || !Array.isArray(rawData.servers)) {
console.error('[Klavis API] Invalid response format:', rawData);
return { data: null, error: 'Invalid response format from server' };
}
// Get active instances for comparison
const queryParams = new URLSearchParams({
user_id: projectId,
platform_name: 'Rowboat'
});
const instancesEndpoint = `/user/instances?${queryParams}`;
let activeInstances: UserInstance[] = [];
try {
const instancesData = await klavisApiCall<GetUserInstancesResponse>(instancesEndpoint);
activeInstances = instancesData.instances;
console.log('[Klavis API] Active instances:', {
count: activeInstances.length,
authenticated: activeInstances.filter(i => i.isAuthenticated).map(i => i.name).join(', '),
needsAuth: activeInstances.filter(i => i.authNeeded && !i.isAuthenticated).map(i => i.name).join(', ')
});
} catch (error) {
console.error('[Klavis API] Failed to fetch user instances:', error);
}
const activeInstanceMap = new Map(activeInstances.map(instance => [instance.name, instance]));
// Transform and enrich the data
const transformedServers = [];
let eligibleCount = 0;
let serversWithToolsCount = 0;
for (const server of rawData.servers) {
const activeInstance = activeInstanceMap.get(server.name);
const mongodbServer = mongodbServerMap.get(server.name);
// Determine server eligibility
const isActive = !!activeInstance;
const authNeeded = activeInstance ? activeInstance.authNeeded : (server.authNeeded || false);
const isAuthenticated = activeInstance ? activeInstance.isAuthenticated : false;
const isEligible = isActive && (!authNeeded || isAuthenticated);
// Get basic tools data first
const basicTools = (server.tools || []).map(tool => ({
id: tool.name || '',
name: tool.name || '',
description: tool.description || '',
}));
let availableTools: McpToolType[];
let selectedTools: McpToolType[];
// Only use MongoDB data for eligible servers
if (isEligible) {
eligibleCount++;
console.log('[Klavis API] Processing server:', server.name);
// Use MongoDB data if available
availableTools = mongodbServer?.availableTools || basicTools;
selectedTools = mongodbServer?.tools || [];
if (selectedTools.length > 0) {
serversWithToolsCount++;
}
} else {
// For non-eligible servers, just use basic data
availableTools = basicTools;
selectedTools = [];
}
transformedServers.push({
...server,
instanceId: activeInstance?.id || server.id,
serverName: server.name,
tools: selectedTools,
availableTools,
isActive,
authNeeded,
isAuthenticated,
requiresAuth: server.authNeeded || false,
serverUrl: mongodbServer?.serverUrl
});
}
console.log('[Klavis API] Server processing complete:', {
totalServers: transformedServers.length,
eligibleServers: eligibleCount,
serversWithTools: serversWithToolsCount
});
return { data: transformedServers, error: null };
} catch (error: any) {
console.error('[Klavis API] Server list error:', error.message);
return { data: null, error: error.message || 'An unexpected error occurred' };
}
}
export async function createMcpServerInstance(
serverName: string,
projectId: string,
platformName: string,
): Promise<CreateServerInstanceResponse> {
try {
await projectAuthCheck(projectId);
const requestBody = {
serverName,
userId: projectId,
platformName,
connectionType: "StreamableHttp",
};
console.log('[Klavis API] Creating server instance:', requestBody);
const endpoint = '/mcp-server/instance/create';
const result = await klavisApiCall<CreateServerInstanceResponse>(endpoint, {
method: 'POST',
body: requestBody
});
console.log('[Klavis API] Created server instance:', result);
return result;
} catch (error: any) {
console.error('[Klavis API] Error creating instance:', error);
throw error;
}
}
// Helper function to filter eligible servers
function getEligibleServers(servers: McpServerType[]): McpServerType[] {
return servers.filter(server =>
server.isActive && (!server.authNeeded || server.isAuthenticated)
);
}
async function getServerInstance(instanceId: string): Promise<{
instanceId: string;
authNeeded: boolean;
isAuthenticated: boolean;
serverName: string;
serverUrl?: string;
}> {
const endpoint = `/mcp-server/instance/get/${instanceId}`;
return await klavisApiCall(endpoint);
}
export async function updateProjectServers(projectId: string, targetServerName?: string): Promise<void> {
try {
await projectAuthCheck(projectId);
console.log('[Auth] Starting server data update:', { projectId, targetServerName });
// Get current MongoDB data
const project = await projectsCollection.findOne({ _id: projectId });
if (!project) {
console.error('[Auth] Project not found in MongoDB:', { projectId });
throw new Error("Project not found");
}
const mcpServers = project.mcpServers || [];
// Get active instances to find auth status
const instances = await listActiveServerInstances(projectId);
// If targetServerName is provided, only process that server
const instancesToProcess = targetServerName
? instances.filter(i => i.name === targetServerName)
: instances;
// For each active instance, get its current status
for (const instance of instancesToProcess) {
if (!instance.id) continue;
try {
// Get fresh instance data
const serverInstance = await getServerInstance(instance.id);
// Find this server in MongoDB
const serverIndex = mcpServers.findIndex(s => s.name === instance.name);
if (serverIndex === -1) continue;
// Update server readiness based on auth status
const isReady = !serverInstance.authNeeded || serverInstance.isAuthenticated;
// Update existing server
const updatedServer = {
...mcpServers[serverIndex],
isAuthenticated: serverInstance.isAuthenticated,
isReady
};
mcpServers[serverIndex] = updatedServer;
// If server is now ready and has no tools, try to enrich them
if (isReady && (!updatedServer.tools || updatedServer.tools.length === 0)) {
try {
console.log(`[Auth] Enriching tools for ${instance.name}`);
const enrichedTools = await enrichToolsWithParameters(
projectId,
instance.name,
updatedServer.availableTools || [],
true
);
if (enrichedTools.length > 0) {
console.log(`[Auth] Writing ${enrichedTools.length} tools to DB for ${instance.name}`);
updatedServer.availableTools = enrichedTools;
await batchAddTools(projectId, instance.name, enrichedTools);
}
} catch (enrichError) {
console.error(`[Auth] Tool enrichment failed for ${instance.name}:`, enrichError);
}
}
} catch (error) {
console.error(`[Auth] Error updating ${instance.name}:`, error);
}
}
// Update MongoDB with new server data
await projectsCollection.updateOne(
{ _id: projectId },
{ $set: { mcpServers } }
);
console.log('[Auth] MongoDB update completed');
} catch (error) {
console.error('[Auth] Error updating server data:', error);
throw error;
}
}
async function batchAddTools(projectId: string, serverName: string, tools: McpToolType[]): Promise<void> {
console.log(`[Klavis API] Writing ${tools.length} tools to ${serverName}`);
const toolsToWrite = tools.map(tool => ({
id: tool.id,
name: tool.name,
description: tool.description,
parameters: tool.parameters || {
type: 'object',
properties: {},
required: []
}
}));
console.log('[Klavis API] DB Write - batchAddTools:', {
serverName,
toolCount: tools.length,
tools: tools.map(t => t.name).join(', ')
});
// Update MongoDB in a single operation
await projectsCollection.updateOne(
{ _id: projectId, "mcpServers.name": serverName },
{
$set: {
"mcpServers.$.tools": toolsToWrite
}
}
);
console.log(`[Klavis API] Tools written to ${serverName}`);
}
export async function enableServer(
serverName: string,
projectId: string,
enabled: boolean
): Promise<CreateServerInstanceResponse | {} | { billingError: string }> {
try {
await projectAuthCheck(projectId);
console.log('[Klavis API] Toggle server request:', { serverName, projectId, enabled });
if (enabled) {
// get count of enabled hosted mcp servers for this project
const existingInstances = await listActiveServerInstances(projectId);
// billing limit check
const authResponse = await authorizeUserAction({
type: 'enable_hosted_tool_server',
data: {
existingServerCount: existingInstances.length,
},
});
if (!authResponse.success) {
return { billingError: authResponse.error || 'Billing error' };
}
// set key in redis to indicate that a server is being enabled on this project
// the key set should only succeed if the key does not already exist
const setResult = await redisClient.set(`klavis_enabling_server:${projectId}`, 'true', 'EX', 60 * 60, 'NX');
console.log('[redis] Set result here:', setResult);
if (setResult !== 'OK') {
throw new Error("A server is already being enabled on this project");
}
console.log(`[Klavis API] Creating server instance for ${serverName}...`);
const result = await createMcpServerInstance(serverName, projectId, "Rowboat");
console.log('[Klavis API] Server instance created:', {
serverName,
instanceId: result.instanceId,
serverUrl: result.serverUrl
});
// Get the current server list from MongoDB
const project = await projectsCollection.findOne({ _id: projectId });
if (!project) throw new Error("Project not found");
const mcpServers = project.mcpServers || [];
// Find the server we're enabling
const serverIndex = mcpServers.findIndex(s => s.name === serverName);
const rawServerData = (await klavisApiCall<GetAllServersResponse>('/mcp-server/servers')).servers
.find(s => s.name === serverName);
if (!rawServerData) throw new Error("Server data not found");
// Get basic tools data
const basicTools = (rawServerData.tools || []).map(tool => ({
id: tool.name || '',
name: tool.name || '',
description: tool.description || '',
}));
// Update server status in MongoDB
const updatedServer = {
...rawServerData,
instanceId: result.instanceId,
serverName: serverName,
serverUrl: result.serverUrl,
tools: basicTools, // Select all tools by default
availableTools: basicTools, // Use basic tools initially
isActive: true, // Keep isActive true to indicate server is enabled
isReady: !rawServerData.authNeeded, // Use isReady to indicate eligibility
authNeeded: rawServerData.authNeeded || false,
isAuthenticated: false,
requiresAuth: rawServerData.authNeeded || false
};
if (serverIndex === -1) {
mcpServers.push(updatedServer);
} else {
mcpServers[serverIndex] = updatedServer;
}
// Update MongoDB with server status
await projectsCollection.updateOne(
{ _id: projectId },
{ $set: { mcpServers } }
);
// Wait for server warm-up (increased from 2s to 5s)
console.log(`[Klavis API] New server detected, waiting 5s for ${serverName} to initialize...`);
await new Promise(resolve => setTimeout(resolve, 5000));
console.log(`[Klavis API] Warm-up period complete for ${serverName}`);
// Try to enrich tools regardless of auth status
try {
console.log(`[Klavis API] Enriching tools for ${serverName}`);
const enrichedTools = await enrichToolsWithParameters(
projectId,
serverName,
basicTools,
true
);
if (enrichedTools.length > 0) {
console.log(`[Klavis API] Writing ${enrichedTools.length} tools to DB for ${serverName}`);
// First update availableTools
await projectsCollection.updateOne(
{ _id: projectId, "mcpServers.name": serverName },
{
$set: {
"mcpServers.$.availableTools": enrichedTools,
"mcpServers.$.isReady": true // Mark server as ready after successful enrichment
}
}
);
// Then write the tools
await batchAddTools(projectId, serverName, enrichedTools);
console.log(`[Klavis API] Successfully wrote tools for ${serverName}`);
}
} catch (enrichError) {
console.error(`[Klavis API] Tool enrichment failed for ${serverName}:`, enrichError);
}
// remove key from redis
await redisClient.del(`klavis_enabling_server:${projectId}`);
return result;
} else {
// Get active instances to find the one to delete
const instances = await listActiveServerInstances(projectId);
const instance = instances.find(i => i.name === serverName);
if (instance?.id) {
// Check if this server uses auth token (authNeeded but no OAuth)
const usesAuthToken = instance.authNeeded && !SERVER_URL_PARAMS[serverName];
if (usesAuthToken) {
// Delete auth data first
await deleteServerAuthData(instance.id);
}
await deleteMcpServerInstance(instance.id, projectId);
console.log('[Klavis API] Disabled server:', { serverName, instanceId: instance.id });
// Remove from MongoDB
await projectsCollection.updateOne(
{ _id: projectId },
{ $pull: { mcpServers: { name: serverName } } }
);
} else {
console.log('[Klavis API] No instance found to disable:', { serverName });
}
return {};
}
} catch (error: any) {
console.error('[Klavis API] Toggle error:', {
server: serverName,
error: error.message,
stack: error.stack,
timestamp: new Date().toISOString()
});
throw error;
}
}
export async function deleteMcpServerInstance(
instanceId: string,
projectId: string,
): Promise<void> {
try {
await projectAuthCheck(projectId);
console.log('[Klavis API] Deleting instance:', { instanceId });
const endpoint = `/mcp-server/instance/delete/${instanceId}`;
try {
await klavisApiCall<DeleteServerInstanceResponse>(endpoint, {
method: 'DELETE'
});
console.log('[Klavis API] Instance deleted successfully:', { instanceId });
// Get the server info from MongoDB to find its name
const project = await projectsCollection.findOne({ _id: projectId });
const server = project?.mcpServers?.find(s => s.instanceId === instanceId);
if (server) {
// Update just this server's status in MongoDB
await projectsCollection.updateOne(
{ _id: projectId, "mcpServers.name": server.name },
{
$set: {
"mcpServers.$.isActive": false,
"mcpServers.$.serverUrl": null,
"mcpServers.$.tools": [],
"mcpServers.$.availableTools": [],
"mcpServers.$.instanceId": null
}
}
);
console.log('[MongoDB] Server status updated:', { serverName: server.name });
}
} catch (error: any) {
if (error.message.includes('404')) {
console.log('[Klavis API] Instance already deleted:', { instanceId });
return;
}
throw error;
}
} catch (error: any) {
console.error('[Klavis API] Error deleting instance:', error);
throw error;
}
}
export async function generateServerAuthUrl(
serverName: string,
projectId: string,
instanceId: string,
): Promise<string> {
try {
await projectAuthCheck(projectId);
// Get the origin from request headers
const headersList = await headers();
const host = headersList.get('host') || '';
const protocol = headersList.get('x-forwarded-proto') || 'http';
const origin = `${protocol}://${host}`;
// Get the URL parameter for this server
const serverUrlParam = SERVER_URL_PARAMS[serverName] || serverName.toLowerCase();
// Build base params
const params: Record<string, string> = {
instance_id: instanceId,
redirect_url: `${origin}/projects/${projectId}/tools/oauth/callback`
};
// Add client_id if available for this server
const clientId = SERVER_CLIENT_ID_MAP[serverName];
if (clientId) {
params.client_id = clientId;
}
let authUrl = `${KLAVIS_BASE_URL}/oauth/${serverUrlParam}/authorize?${new URLSearchParams(params).toString()}`
console.log('authUrl', authUrl);
return authUrl;
} catch (error) {
console.error('[Klavis API] Error generating auth URL:', error);
throw error;
}
}
export async function syncServerTools(projectId: string, serverName: string): Promise<void> {
try {
await projectAuthCheck(projectId);
console.log('[Klavis API] Starting server tool sync:', { projectId, serverName });
// Get enriched tools from MCP
const enrichedTools = await fetchMcpToolsForServer(projectId, serverName);
console.log('[Klavis API] Received enriched tools:', {
serverName,
toolCount: enrichedTools.length
});
// Convert enriched tools to the correct format
const formattedTools = enrichedTools.map(tool => {
return {
id: tool.name,
name: tool.name,
description: tool.description,
parameters: {
type: 'object' as const,
properties: tool.parameters?.properties || {},
required: tool.parameters?.required || []
}
};
});
// First verify the server exists
const project = await projectsCollection.findOne({ _id: projectId });
if (!project) {
throw new Error(`Project ${projectId} not found`);
}
const server = project.mcpServers?.find(s => s.name === serverName);
if (!server) {
throw new Error(`Server ${serverName} not found in project ${projectId}`);
}
// Update MongoDB with enriched tools
const updateResult = await projectsCollection.updateOne(
{ _id: projectId, "mcpServers.name": serverName },
{
$set: {
"mcpServers.$.availableTools": formattedTools,
"mcpServers.$.tools": formattedTools // Also update selected tools to match
}
}
);
console.log('[Klavis API] Tools synced:', {
serverName,
toolCount: formattedTools.length,
success: updateResult.modifiedCount > 0
});
} catch (error) {
console.error('[Klavis API] Error syncing server tools:', {
serverName,
error: error instanceof Error ? error.message : 'Unknown error'
});
throw error;
}
}
// Auth Token Management Functions
export async function setServerAuthToken(
instanceId: string,
authToken: string
): Promise<{ success: boolean; message?: string; error?: string }> {
try {
const response = await klavisApiCall<{ success: boolean; message: string }>(
`/mcp-server/instance/set-auth-token`,
{
method: 'POST',
body: { instanceId, authToken }
}
);
return { success: true, message: response.message };
} catch (error: any) {
// Handle 422 validation errors
if (error.message.includes('422')) {
try {
const errorData = JSON.parse(error.message);
const validationErrors = errorData.detail?.map((err: any) => err.msg).join(', ');
return { success: false, error: validationErrors || 'Invalid auth token' };
} catch {
return { success: false, error: 'Invalid auth token format' };
}
}
// Handle other errors
return { success: false, error: 'Failed to set auth token. Please try again.' };
}
}
export async function deleteServerAuthData(instanceId: string): Promise<void> {
try {
await klavisApiCall<{ success: boolean; message: string }>(
`/mcp-server/instance/delete-auth/${instanceId}`,
{ method: 'DELETE' }
);
console.log('[Klavis API] Auth data deleted for instance:', instanceId);
} catch (error: any) {
// Log error but don't fail the deletion process
console.error('[Klavis API] Failed to delete auth data:', error);
// Don't throw - auth cleanup failure shouldn't prevent server deletion
}
}

View file

@ -4,7 +4,7 @@ import { WorkflowTool } from "../lib/types/workflow_types";
import { projectAuthCheck } from "./project_actions";
import { projectsCollection } from "../lib/mongodb";
import { Project } from "../lib/types/project_types";
import { MCPServer, McpServerTool, convertMcpServerToolToWorkflowTool } from "../lib/types/types";
import { McpServerTool, convertMcpServerToolToWorkflowTool } from "../lib/types/types";
import { getMcpClient } from "../lib/mcp";
export async function fetchMcpTools(projectId: string): Promise<z.infer<typeof WorkflowTool>[]> {

View file

@ -11,7 +11,6 @@ import { User, WithStringId } from "../lib/types/types";
import { ApiKey } from "../lib/types/project_types";
import { Project } from "../lib/types/project_types";
import { USE_AUTH } from "../lib/feature_flags";
import { deleteMcpServerInstance, listActiveServerInstances } from "./klavis_actions";
import { authorizeUserAction } from "./billing_actions";
import { Workflow } from "../lib/types/workflow_types";
@ -188,53 +187,9 @@ interface McpServerDeletionError {
error: string;
}
async function cleanupMcpServers(projectId: string): Promise<McpServerDeletionError[]> {
// Get all active instances directly from Klavis
const activeInstances = await listActiveServerInstances(projectId);
if (activeInstances.length === 0) return [];
console.log(`[Project Cleanup] Found ${activeInstances.length} active Klavis instances`);
// Track deletion errors
const deletionErrors: McpServerDeletionError[] = [];
// Delete each instance
const deletionPromises = activeInstances.map(async (instance) => {
if (!instance.id) return; // Skip if no instance ID
try {
await deleteMcpServerInstance(instance.id, projectId);
console.log(`[Project Cleanup] Deleted Klavis instance: ${instance.name} (${instance.id})`);
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
console.error(`[Project Cleanup] Failed to delete Klavis instance: ${instance.name}`, error);
deletionErrors.push({
serverName: instance.name,
error: errorMessage
});
}
});
// Wait for all deletions to complete
await Promise.all(deletionPromises);
return deletionErrors;
}
export async function deleteProject(projectId: string) {
await projectAuthCheck(projectId);
// First cleanup any Klavis instances
if (KLAVIS_API_KEY) {
const deletionErrors = await cleanupMcpServers(projectId);
// If there were any errors deleting instances, throw an error
if (deletionErrors.length > 0) {
const failedServers = deletionErrors.map(e => `${e.serverName} (${e.error})`).join(', ');
throw new Error(`Cannot delete project because the following Klavis instances could not be deleted: ${failedServers}. Please try again or contact support if the issue persists.`);
}
}
// delete api keys
await apiKeysCollection.deleteMany({
projectId,

View file

@ -1,32 +0,0 @@
import { z } from "zod";
import { apiV1 } from "rowboat-shared"
export const GetInformationToolResultItem = z.object({
title: z.string(),
name: z.string(),
content: z.string(),
docId: z.string(),
sourceId: z.string(),
});export const GetInformationToolResult = z.object({
results: z.array(GetInformationToolResultItem)
});
export const WebpageCrawlResponse = z.object({
title: z.string(),
content: z.string(),
});
export const ClientToolCallRequestBody = z.object({
toolCall: apiV1.AssistantMessageWithToolCalls.shape.tool_calls.element,
});
export const ClientToolCallJwt = z.object({
requestId: z.string().uuid(),
projectId: z.string(),
bodyHash: z.string(),
iat: z.number(),
exp: z.number(),
});
export const ClientToolCallRequest = z.object({
requestId: z.string().uuid(),
content: z.string(), // json stringified ClientToolCallRequestBody
});
export const ClientToolCallResponse = z.unknown();

View file

@ -1,5 +1,4 @@
import { z } from 'zod';
import { WithId } from 'mongodb';
import { Message } from './types';
export const TwilioConfigParams = z.object({

View file

@ -1,175 +0,0 @@
'use client';
import { useState } from 'react';
import { Modal, ModalContent, ModalHeader, ModalBody, ModalFooter, Spinner } from "@heroui/react";
import { Button } from "@/components/ui/button";
import { Key, AlertCircle, Eye, EyeOff } from "lucide-react";
import { setServerAuthToken } from '@/app/actions/klavis_actions';
import { MCPServer } from '@/app/lib/types/types';
import { z } from 'zod';
type McpServerType = z.infer<typeof MCPServer>;
interface AuthTokenModalProps {
isOpen: boolean;
onClose: () => void;
server: McpServerType | null;
onSuccess: () => void;
}
export function AuthTokenModal({ isOpen, onClose, server, onSuccess }: AuthTokenModalProps) {
const [authToken, setAuthToken] = useState('');
const [isSubmitting, setIsSubmitting] = useState(false);
const [error, setError] = useState<string | null>(null);
const [showToken, setShowToken] = useState(false);
const handleSubmit = async () => {
if (!server?.instanceId || !authToken.trim()) {
setError('Please enter a valid auth token');
return;
}
setIsSubmitting(true);
setError(null);
try {
const result = await setServerAuthToken(server.instanceId, authToken.trim());
if (result.success) {
// Success - close modal and refresh data
setAuthToken('');
setError(null);
onSuccess();
onClose();
} else {
// Show validation error
setError(result.error || 'Failed to set auth token');
}
} catch (err) {
setError('Network error. Please check your connection and try again.');
} finally {
setIsSubmitting(false);
}
};
const handleClose = () => {
setAuthToken('');
setError(null);
onClose();
};
if (!server) return null;
return (
<Modal
isOpen={isOpen}
onOpenChange={handleClose}
size="lg"
classNames={{
base: "bg-white dark:bg-gray-900",
header: "border-b border-gray-200 dark:border-gray-800",
footer: "border-t border-gray-200 dark:border-gray-800",
}}
>
<ModalContent>
<ModalHeader className="flex gap-2 items-center">
<Key className="w-5 h-5 text-blue-500" />
<span>Authenticate {server.name}</span>
</ModalHeader>
<ModalBody>
<div className="space-y-4">
<div className="bg-blue-50 dark:bg-blue-900/20 border border-blue-100 dark:border-blue-800 rounded-lg p-3">
<p className="text-sm text-blue-700 dark:text-blue-300">
You&apos;ll need to obtain an authentication token from {server.name}. Please refer to their documentation or settings page to find your API key or access token.
</p>
</div>
<div className="space-y-2">
<label htmlFor="auth-token" className="text-sm font-medium text-gray-700 dark:text-gray-300">
Auth Token
</label>
<div className="relative">
<input
id="auth-token"
type={showToken ? 'text' : 'password'}
placeholder="Enter your auth token..."
value={authToken}
onChange={(e) => setAuthToken(e.target.value)}
onKeyDown={(e) => {
if (e.key === 'Enter' && !isSubmitting) {
handleSubmit();
}
}}
className="w-full pr-10 pl-3 py-2 rounded-lg bg-gray-100 dark:bg-gray-800 text-base text-gray-900 dark:text-gray-100 placeholder:text-gray-400 dark:placeholder:text-gray-500 focus:outline-none focus:ring-2 focus:ring-blue-200 dark:focus:ring-blue-900 border-0 shadow-none"
disabled={isSubmitting}
autoComplete="off"
/>
<button
type="button"
tabIndex={-1}
className="absolute right-2 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 p-1"
onClick={() => setShowToken((v) => !v)}
aria-label={showToken ? 'Hide token' : 'Show token'}
>
{showToken ? <EyeOff className="w-5 h-5" /> : <Eye className="w-5 h-5" />}
</button>
</div>
</div>
{error && (
<div className="flex gap-2 items-start p-3 bg-red-50 dark:bg-red-900/20 border border-red-100 dark:border-red-800 rounded-lg">
<AlertCircle className="h-4 w-4 text-red-600 dark:text-red-400 mt-0.5 flex-shrink-0" />
<p className="text-sm text-red-700 dark:text-red-300">{error}</p>
</div>
)}
</div>
<style jsx>{`
#auth-token {
box-shadow: none !important;
outline: none !important;
border: none !important;
background: #f3f4f6 !important;
font-size: 1.05rem;
}
#auth-token:focus {
box-shadow: none !important;
outline: none !important;
border: none !important;
background: #e0e7ef !important;
}
.dark #auth-token {
background: #23272f !important;
color: #f3f4f6 !important;
}
.dark #auth-token:focus {
background: #1a1d23 !important;
}
`}</style>
</ModalBody>
<ModalFooter>
<Button
variant="secondary"
onClick={handleClose}
disabled={isSubmitting}
>
Cancel
</Button>
<Button
onClick={handleSubmit}
disabled={isSubmitting || !authToken.trim()}
isLoading={isSubmitting}
>
{isSubmitting ? (
<>
<Spinner size="sm" className="mr-2" />
Authenticating...
</>
) : (
'Authenticate'
)}
</Button>
</ModalFooter>
</ModalContent>
</Modal>
);
}

View file

@ -1,786 +0,0 @@
'use client';
import { useState, useEffect, useCallback } from 'react';
import { useParams } from 'next/navigation';
import { Button } from '@/components/ui/button';
import { Info, RefreshCw, Search, AlertTriangle } from 'lucide-react';
import { clsx } from 'clsx';
import {
listAvailableMcpServers,
enableServer,
updateProjectServers,
generateServerAuthUrl,
syncServerTools
} from '@/app/actions/klavis_actions';
import { toggleMcpTool, fetchMcpToolsForServer } from '@/app/actions/mcp_actions';
import { z } from 'zod';
import { MCPServer } from '@/app/lib/types/types';
import { Checkbox } from '@heroui/react';
import {
ServerCard,
ToolManagementPanel,
} from './MCPServersCommon';
import { BillingUpgradeModal } from '@/components/common/billing-upgrade-modal';
import { AuthTokenModal } from './AuthTokenModal';
import { SERVER_URL_PARAMS } from '@/app/lib/constants/klavis';
type McpServerType = z.infer<typeof MCPServer>;
type McpToolType = z.infer<typeof MCPServer>['tools'][number];
function sortServers(servers: McpServerType[]): McpServerType[] {
return [...servers].sort((a, b) => a.name.localeCompare(b.name));
}
const fadeInAnimation = {
'@keyframes fadeIn': {
'0%': { opacity: 0, transform: 'translateY(-5px)' },
'100%': { opacity: 1, transform: 'translateY(0)' }
},
'.animate-fadeIn': {
animation: 'fadeIn 0.2s ease-out'
}
} as const;
const toolCardStyles = {
base: clsx(
"group p-4 rounded-lg transition-all duration-200",
"bg-gray-50/50 dark:bg-gray-800/50",
"hover:bg-gray-100/50 dark:hover:bg-gray-700/50",
"border border-transparent",
"hover:border-gray-200 dark:hover:border-gray-600"
),
};
const ToolCard = ({
tool,
server,
isSelected,
onSelect,
showCheckbox = false
}: {
tool: McpToolType;
server: McpServerType;
isSelected?: boolean;
onSelect?: (selected: boolean) => void;
showCheckbox?: boolean;
}) => {
return (
<div className={toolCardStyles.base}>
<div className="flex items-start gap-3">
{showCheckbox && (
<Checkbox
isSelected={isSelected}
onValueChange={onSelect}
size="sm"
/>
)}
<div className="flex-1">
<h4 className="text-sm font-medium text-gray-900 dark:text-gray-100 mb-1">
{tool.name}
</h4>
<p className="text-sm text-gray-500 dark:text-gray-400">
{tool.description}
</p>
</div>
</div>
</div>
);
};
const ErrorBanner = ({ onRetry }: { onRetry: () => void }) => (
<div className="mb-6 bg-red-50 dark:bg-red-900/20 border border-red-100 dark:border-red-800 rounded-lg p-4">
<div className="flex items-center justify-between gap-4">
<div className="flex items-center gap-3">
<AlertTriangle className="h-5 w-5 text-red-600 dark:text-red-400" />
<p className="text-sm text-red-700 dark:text-red-300">
Unable to load hosted tools. Please check your connection and try again. If the problem persists, contact us on <a href={DISCORD_LINK} target="_blank" rel="noopener noreferrer" className="underline hover:text-red-600 dark:hover:text-red-300">Discord</a>.
</p>
</div>
<Button
size="sm"
variant="secondary"
onClick={onRetry}
className="shrink-0"
>
<RefreshCw className="h-4 w-4 mr-2" />
Retry
</Button>
</div>
</div>
);
const ERROR_MESSAGE = {
NO_HOSTED_TOOLS: 'No hosted tools found. Make sure to set your <a href="https://www.klavis.ai/" target="_blank" rel="noopener noreferrer" class="underline hover:text-red-600 dark:hover:text-red-300">Klavis</a> API key. Contact us on <a href="https://discord.com/invite/rxB8pzHxaS" target="_blank" rel="noopener noreferrer" class="underline hover:text-red-600 dark:hover:text-red-300">discord</a> if you\'re still unable to see hosted tools.'
};
const DISCORD_LINK = 'https://discord.com/invite/rxB8pzHxaS';
const DOCS_LINK = 'https://docs.rowboatlabs.com/add_tools/';
type HostedServersProps = {
onSwitchTab?: (tab: string) => void;
};
export function HostedServers({ onSwitchTab }: HostedServersProps) {
const params = useParams();
const projectId = typeof params.projectId === 'string' ? params.projectId : params.projectId?.[0];
if (!projectId) throw new Error('Project ID is required');
const [servers, setServers] = useState<McpServerType[]>([]);
const [selectedServer, setSelectedServer] = useState<McpServerType | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [searchQuery, setSearchQuery] = useState('');
const [showOnlyEnabled, setShowOnlyEnabled] = useState(false);
const [showOnlyReady, setShowOnlyReady] = useState(false);
const [toggleError, setToggleError] = useState<{serverId: string; message: string} | null>(null);
const [enabledServers, setEnabledServers] = useState<Set<string>>(new Set());
const [togglingServers, setTogglingServers] = useState<Set<string>>(new Set());
const [serverOperations, setServerOperations] = useState<Map<string, 'setup' | 'delete' | 'checking-auth'>>(new Map());
const [selectedTools, setSelectedTools] = useState<Set<string>>(new Set());
const [hasToolChanges, setHasToolChanges] = useState(false);
const [savingTools, setSavingTools] = useState(false);
const [serverToolCounts, setServerToolCounts] = useState<Map<string, number>>(new Map());
const [syncingServers, setSyncingServers] = useState<Set<string>>(new Set());
const [billingError, setBillingError] = useState<string | null>(null);
const [showAuthTokenModal, setShowAuthTokenModal] = useState(false);
const [selectedServerForAuth, setSelectedServerForAuth] = useState<McpServerType | null>(null);
const fetchServers = useCallback(async () => {
try {
setLoading(true);
const response = await listAvailableMcpServers(projectId || "");
if (response.error || !response.data) {
setError(ERROR_MESSAGE.NO_HOSTED_TOOLS);
return;
}
// Mark all servers as hosted type
const serversWithType = response.data.map(server => ({
...server,
serverType: 'hosted' as const
}));
setServers(serversWithType);
setError(null);
} catch (err: any) {
setError(ERROR_MESSAGE.NO_HOSTED_TOOLS);
console.error('Error fetching servers:', err);
setServers([]);
} finally {
setLoading(false);
}
}, [projectId]);
useEffect(() => {
fetchServers();
}, [fetchServers]);
// Initialize enabled servers on load and keep it updated
useEffect(() => {
if (servers) {
console.log('Updating enabled servers from server data:', servers);
const enabled = new Set(
servers
.filter(server => server.isActive)
.map(server => server.name)
);
console.log('New enabled servers state:', Array.from(enabled));
setEnabledServers(enabled);
}
}, [servers]);
// Initialize tool counts when servers are loaded
useEffect(() => {
const newCounts = new Map<string, number>();
servers.forEach(server => {
if (isServerEligible(server)) {
newCounts.set(server.name, server.tools.length);
}
});
setServerToolCounts(newCounts);
}, [servers]);
// Initialize selected tools when opening the panel
useEffect(() => {
if (selectedServer) {
setSelectedTools(new Set(selectedServer.tools.map(t => t.id)));
setHasToolChanges(false);
}
}, [selectedServer]);
const isServerEligible = (server: McpServerType) => {
return server.isActive && (!server.authNeeded || server.isAuthenticated);
};
const handleToggleTool = async (server: McpServerType) => {
try {
const serverKey = server.name;
const isCurrentlyEnabled = enabledServers.has(serverKey);
const newState = !isCurrentlyEnabled;
// Immediately update UI state
setServers(prevServers => {
return prevServers.map(s => {
if (s.name === serverKey) {
return {
...s,
isActive: newState,
// If turning off, reset these states
...(newState ? {} : {
serverUrl: undefined,
tools: [],
isAuthenticated: false
})
};
}
return s;
});
});
setTogglingServers(prev => {
const next = new Set(prev);
next.add(serverKey);
return next;
});
setToggleError(null);
setBillingError(null);
setServerOperations(prev => {
const next = new Map(prev);
next.set(serverKey, newState ? 'setup' : 'delete');
return next;
});
try {
const result = await enableServer(server.name, projectId || "", newState);
// Check for billing error
if ('billingError' in result) {
setBillingError(result.billingError);
// Revert UI state
setServers(prevServers => {
return prevServers.map(s => {
if (s.name === serverKey) {
return {
...s,
isActive: isCurrentlyEnabled
};
}
return s;
});
});
return;
}
setEnabledServers(prev => {
const next = new Set(prev);
if (!newState) {
next.delete(serverKey);
} else if ('instanceId' in result) {
next.add(serverKey);
}
return next;
});
if (newState) {
const response = await listAvailableMcpServers(projectId || "");
if (response.data) {
const updatedServer = response.data.find(s => s.name === serverKey);
if (updatedServer) {
setServers(prevServers => {
return prevServers.map(s => {
if (s.name === serverKey) {
return { ...updatedServer, serverType: 'hosted' as const };
}
return s;
});
});
setServerToolCounts(prev => {
const next = new Map(prev);
next.set(serverKey, updatedServer.tools.length);
return next;
});
}
}
} else {
setServerToolCounts(prev => {
const next = new Map(prev);
next.set(serverKey, 0);
return next;
});
}
} catch (err) {
console.error('Toggle failed:', { server: serverKey, error: err });
// Revert the UI state on error
setServers(prevServers => {
return prevServers.map(s => {
if (s.name === serverKey) {
return {
...s,
isActive: isCurrentlyEnabled,
// Restore previous state if the toggle failed
...(isCurrentlyEnabled ? {} : {
serverUrl: undefined,
tools: [],
isAuthenticated: false
})
};
}
return s;
});
});
setEnabledServers(prev => {
const next = new Set(prev);
if (newState) {
next.delete(serverKey);
} else {
next.add(serverKey);
}
return next;
});
setToggleError({
serverId: serverKey,
message: "We're having trouble setting up this server. Please reach out on <a href=\"" + DISCORD_LINK + "\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"underline hover:text-red-600 dark:hover:text-red-300\">discord</a>."
});
}
} finally {
const serverKey = server.name;
setTogglingServers(prev => {
const next = new Set(prev);
next.delete(serverKey);
return next;
});
setServerOperations(prev => {
const next = new Map(prev);
next.delete(serverKey);
return next;
});
}
};
const handleAuthenticate = async (server: McpServerType) => {
try {
if (!server.instanceId) {
throw new Error('Server instance ID not found');
}
// Check if this server uses OAuth (in SERVER_URL_PARAMS) or auth token
const usesOAuth = SERVER_URL_PARAMS[server.name];
if (usesOAuth) {
// Use existing OAuth flow
const authUrl = await generateServerAuthUrl(server.name, projectId, server.instanceId);
const authWindow = window.open(
authUrl,
'_blank',
'width=600,height=700'
);
if (authWindow) {
const checkInterval = setInterval(async () => {
if (authWindow.closed) {
clearInterval(checkInterval);
try {
setServerOperations(prev => {
const next = new Map(prev);
next.set(server.name, 'checking-auth');
return next;
});
await updateProjectServers(projectId, server.name);
const response = await listAvailableMcpServers(projectId);
if (response.data) {
const updatedServer = response.data.find(us => us.name === server.name);
if (updatedServer) {
setServers(prevServers => {
return prevServers.map(s => {
if (s.name === server.name) {
return { ...updatedServer, serverType: 'hosted' as const };
}
return s;
});
});
if (selectedServer?.name === server.name) {
setSelectedServer({ ...updatedServer, serverType: 'hosted' as const });
}
if (!server.authNeeded || updatedServer.isAuthenticated) {
await handleSyncServer(updatedServer);
}
}
}
} finally {
setServerOperations(prev => {
const next = new Map(prev);
next.delete(server.name);
return next;
});
}
}
}, 500);
} else {
window.alert('Failed to open authentication window. Please check your popup blocker settings.');
}
} else {
// Use auth token modal
setSelectedServerForAuth(server);
setShowAuthTokenModal(true);
}
} catch (error) {
console.error('[Auth] Error initiating authentication:', error);
window.alert('Failed to setup authentication');
}
};
const handleSaveToolSelection = async () => {
if (!selectedServer || !projectId) return;
setSavingTools(true);
try {
const availableTools = selectedServer.availableTools || [];
const previousTools = new Set(selectedServer.tools.map(t => t.id));
const updatedTools = new Set<string>();
for (const tool of availableTools) {
const isSelected = selectedTools.has(tool.id);
await toggleMcpTool(projectId, selectedServer.name, tool.id, isSelected);
if (isSelected) {
updatedTools.add(tool.id);
}
}
setServers(prevServers => {
return prevServers.map(s => {
if (s.name === selectedServer.name) {
return {
...s,
tools: availableTools.filter(tool => selectedTools.has(tool.id))
};
}
return s;
});
});
setSelectedServer(prev => {
if (!prev) return null;
return {
...prev,
tools: availableTools.filter(tool => selectedTools.has(tool.id))
};
});
setServerToolCounts(prev => {
const next = new Map(prev);
next.set(selectedServer.name, selectedTools.size);
return next;
});
setHasToolChanges(false);
} catch (error) {
console.error('Error saving tool selection:', error);
} finally {
setSavingTools(false);
}
};
const handleSyncServer = async (server: McpServerType) => {
if (!projectId || !isServerEligible(server)) return;
try {
setSyncingServers(prev => {
const next = new Set(prev);
next.add(server.name);
return next;
});
// Call the server action to sync and update DB
await syncServerTools(projectId, server.name);
// Refresh the server list to get updated data
const response = await listAvailableMcpServers(projectId);
if (response.data) {
const updatedServer = response.data.find(s => s.name === server.name);
if (updatedServer) {
setServers(prevServers => {
return prevServers.map(s => {
if (s.name === server.name) {
return { ...updatedServer, serverType: 'hosted' as const };
}
return s;
});
});
if (selectedServer?.name === server.name) {
setSelectedServer({ ...updatedServer, serverType: 'hosted' as const });
}
}
}
} finally {
setSyncingServers(prev => {
const next = new Set(prev);
next.delete(server.name);
return next;
});
}
};
const handleAuthTokenSuccess = async () => {
if (!selectedServerForAuth) return;
try {
setServerOperations(prev => {
const next = new Map(prev);
next.set(selectedServerForAuth.name, 'checking-auth');
return next;
});
await updateProjectServers(projectId, selectedServerForAuth.name);
const response = await listAvailableMcpServers(projectId);
if (response.data) {
const updatedServer = response.data.find(us => us.name === selectedServerForAuth.name);
if (updatedServer) {
setServers(prevServers => {
return prevServers.map(s => {
if (s.name === selectedServerForAuth.name) {
return { ...updatedServer, serverType: 'hosted' as const };
}
return s;
});
});
if (selectedServer?.name === selectedServerForAuth.name) {
setSelectedServer({ ...updatedServer, serverType: 'hosted' as const });
}
if (!selectedServerForAuth.authNeeded || updatedServer.isAuthenticated) {
await handleSyncServer(updatedServer);
}
}
}
} finally {
setServerOperations(prev => {
const next = new Map(prev);
next.delete(selectedServerForAuth.name);
return next;
});
}
};
const filteredServers = sortServers(servers.filter(server => {
const searchLower = searchQuery.toLowerCase();
const serverTools = server.tools || [];
// Search text filter
const matchesSearch =
server.name.toLowerCase().includes(searchLower) ||
server.description.toLowerCase().includes(searchLower) ||
serverTools.some(tool =>
tool.name.toLowerCase().includes(searchLower) ||
tool.description.toLowerCase().includes(searchLower)
);
// Enabled servers filter
const matchesEnabled = !showOnlyEnabled || server.isActive;
// Ready to use filter (server is active and either doesn't need auth or is already authenticated)
const isReady = server.isActive && (!server.authNeeded || server.isAuthenticated);
const matchesReady = !showOnlyReady || isReady;
return matchesSearch && matchesEnabled && matchesReady;
}));
if (loading) {
return (
<div className="text-center py-8">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-gray-800 dark:border-gray-200 mx-auto"></div>
<p className="mt-4 text-sm text-gray-600 dark:text-gray-400">Loading tools...</p>
</div>
);
}
if (error) {
return (
<div className="flex flex-col items-center justify-center h-[50vh] space-y-6 px-4">
<p
className="text-center text-red-500 dark:text-red-400 max-w-[600px]"
dangerouslySetInnerHTML={{
__html: error
}}
/>
<div className="flex flex-col sm:flex-row gap-4">
<a href={DOCS_LINK} target="_blank" rel="noopener noreferrer">
<Button variant="secondary" className="w-full sm:w-auto">
Read our documentation
</Button>
</a>
<Button
variant="secondary"
onClick={() => onSwitchTab?.('custom')}
className="w-full sm:w-auto"
>
Set up a custom server instead
</Button>
</div>
</div>
);
}
return (
<div className="space-y-6">
<div className="bg-blue-50 dark:bg-blue-900/20 border border-blue-100 dark:border-blue-800 rounded-lg p-4">
<div className="flex gap-3">
<div className="shrink-0">
<Info className="h-5 w-5 text-blue-600 dark:text-blue-400" />
</div>
<p className="text-sm text-blue-700 dark:text-blue-300">
To make hosted MCP tools available to agents in the Build view, first toggle the servers ON here. Some tools may require authentication after enabling.
</p>
</div>
</div>
<div className="flex flex-col gap-6">
<div className="flex items-center justify-between gap-4">
<div className="flex-1 flex items-center gap-4">
<div className="relative flex-1">
<div className="absolute inset-y-0 left-2 flex items-center pointer-events-none">
<Search className="h-4 w-4 text-gray-400 dark:text-gray-500" />
</div>
<input
type="text"
placeholder="Search servers or tools..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="w-full pl-8 pr-4 py-2 text-sm border border-gray-200 dark:border-gray-700 rounded-md
bg-white dark:bg-gray-900 text-gray-900 dark:text-gray-100
placeholder-gray-400 dark:placeholder-gray-500
focus:outline-none focus:ring-2 focus:ring-blue-500 dark:focus:ring-blue-400
hover:border-gray-300 dark:hover:border-gray-600 transition-colors"
/>
</div>
<div className="text-sm text-gray-500 dark:text-gray-400 whitespace-nowrap">
{filteredServers.length} {filteredServers.length === 1 ? 'server' : 'servers'} {
filteredServers.reduce((total, server) => total + (server.availableTools?.length || 0), 0)
} tools
</div>
<div className="h-4 w-px bg-gray-200 dark:bg-gray-700" />
<div className="flex items-center gap-8">
<div className="group relative flex items-center gap-1">
<label className="flex items-center gap-1 text-sm text-gray-700 dark:text-gray-300">
<Checkbox
isSelected={showOnlyEnabled}
onValueChange={setShowOnlyEnabled}
size="sm"
/>
Enabled Only
</label>
<div className="relative">
<Info className="h-3.5 w-3.5 text-gray-400 dark:text-gray-500 cursor-help ml-1" />
<div className="absolute bottom-full left-1/2 -translate-x-1/2 mb-2 px-3 py-2 bg-gray-900 dark:bg-gray-800 text-white text-xs rounded-lg opacity-0 invisible group-hover:opacity-100 group-hover:visible transition-all duration-200 whitespace-nowrap shadow-lg">
Shows only servers that are currently toggled ON
</div>
</div>
</div>
<div className="group relative flex items-center gap-1">
<label className="flex items-center gap-1 text-sm text-gray-700 dark:text-gray-300">
<Checkbox
isSelected={showOnlyReady}
onValueChange={setShowOnlyReady}
size="sm"
/>
Ready to Use
</label>
<div className="relative">
<Info className="h-3.5 w-3.5 text-gray-400 dark:text-gray-500 cursor-help ml-1" />
<div className="absolute bottom-full left-1/2 -translate-x-1/2 mb-2 px-3 py-2 bg-gray-900 dark:bg-gray-800 text-white text-xs rounded-lg opacity-0 invisible group-hover:opacity-100 group-hover:visible transition-all duration-200 whitespace-nowrap shadow-lg">
Shows only servers that are enabled and fully authenticated
</div>
</div>
</div>
</div>
<div className="h-4 w-px bg-gray-200 dark:bg-gray-700" />
</div>
<Button
size="sm"
variant="secondary"
onClick={fetchServers}
disabled={loading}
>
<div className="inline-flex items-center">
<RefreshCw className={clsx("h-4 w-4", loading && "animate-spin")} />
<span className="ml-2">Refresh</span>
</div>
</Button>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{filteredServers.map((server) => (
<ServerCard
key={server.instanceId}
server={server}
onToggle={() => handleToggleTool(server)}
onManageTools={() => setSelectedServer(server)}
onSync={() => handleSyncServer(server)}
onAuth={() => handleAuthenticate(server)}
isToggling={togglingServers.has(server.name)}
isSyncing={syncingServers.has(server.name)}
operation={serverOperations.get(server.name)}
error={toggleError?.serverId === server.name ? toggleError : undefined}
showAuth={true}
/>
))}
</div>
<ToolManagementPanel
server={selectedServer}
onClose={() => {
setSelectedServer(null);
setSelectedTools(new Set());
setHasToolChanges(false);
}}
selectedTools={selectedTools}
onToolSelectionChange={(toolId, selected) => {
setSelectedTools(prev => {
const next = new Set(prev);
if (selected) {
next.add(toolId);
} else {
next.delete(toolId);
}
setHasToolChanges(true);
return next;
});
}}
onSaveTools={handleSaveToolSelection}
onSyncTools={selectedServer ? () => handleSyncServer(selectedServer) : undefined}
hasChanges={hasToolChanges}
isSaving={savingTools}
isSyncing={selectedServer ? syncingServers.has(selectedServer.name) : false}
/>
<BillingUpgradeModal
isOpen={!!billingError}
onClose={() => setBillingError(null)}
errorMessage={billingError || ''}
/>
<AuthTokenModal
isOpen={showAuthTokenModal}
onClose={() => {
setShowAuthTokenModal(false);
setSelectedServerForAuth(null);
}}
server={selectedServerForAuth}
onSuccess={handleAuthTokenSuccess}
/>
</div>
);
}

View file

@ -1,5 +0,0 @@
.env
.env*
__pycache__/
venv/
.venv/

View file

@ -1,2 +0,0 @@
OPENAI_API_KEY=<your_openai_api_key>
API_KEY=test

View file

@ -1,40 +0,0 @@
# Ignore all __pycache__ directories
/__pycache__/
**/__pycache__/
**/**/__pycache__/
**/**/__pycache__/**
# Ignore all .pyc files
*.pyc
**/*.pyc
**/**/*.pyc
# Ignore .DS_Store files
.DS_Store
**/.DS_Store
**/**/.DS_Store
# Ignore VSCode files
.vscode/*
**/.vscode/*
**/**/.vscode/*
# Environment files
.env
.env.local
# Api keys
**/*api_keys*
# Ignore .out, .lock, .log files
*.out
**/*.out
**/**/*.out
*.log
**/*.log
**/**/*.log
# Ignore tmp.json files
**/*tmp.json
**/**/*tmp.json
**/**/**/*tmp.json

View file

@ -1,28 +0,0 @@
# Use official Python runtime as base image
FROM python:3.12-slim
# Set working directory in container
WORKDIR /app
# Install poetry
RUN pip install poetry
# Copy poetry files
COPY pyproject.toml poetry.lock ./
# Configure poetry to not create virtual environment in container
RUN poetry config virtualenvs.create false
# Install dependencies
RUN poetry install --no-interaction --no-ansi
# Copy project files
COPY . .
# Set environment variables
ENV QUART_APP=src.app.main
ENV PYTHONUNBUFFERED=1
ENV PYTHONPATH=/app
# Command to run Flask development server
CMD ["quart", "run", "--host=0.0.0.0", "--port=3001"]

View file

@ -1,43 +0,0 @@
# Attribution to OpenAI Swarm
- The Rowboat Agents framework has been built upon [OpenAI Swarm](https://github.com/openai/swarm), with modifications and improvements.
- The original OpenAI Swarm is available under the [MIT license](https://github.com/openai/swarm/blob/main/LICENSE) as of the time of this writing. It is an experimental sample framework at the time of this writing.
### OpenAI Swarm License
Below is the license text from OpenAI Swarm, as required by the MIT license:
```
MIT License
Copyright (c) 2024 OpenAI
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
```
# High-level changes
These are the high-level changes made to OpenAI Swarm to build in RowBoat's custom implementation:
- Added localized agent-level history
- Added parent-child agent relationships with parents' history containing children's history
- Added usage tracking of tokens per llm
- Added turn-level error handling
- Added converstaion turn limits
- Removed streaming support as RowBoat Agents does not support streaming currently
- Modified the `Agent` and `Response` classes to be more comprehensive
The above is not an exhaustive list.

View file

@ -1,114 +0,0 @@
# 🤖 Agents
## 📝 Overview
- RowBoat Agents is a multi-agent framework that powers conversations using agentic workflows.
- Built on top of [OpenAI Swarm](https://github.com/openai/swarm) with custom enhancements and improvements. Check the [NOTICE](https://github.com/rowboatlabs/rowboat/blob/main/apps/agents/NOTICE.md) for attribution and licensing details (MIT license).
---
## 🕸️ Graph-based Framework
- Multi-agent systems are represented as graphs, where each agent is a node in the graph.
- RowBoat Agents accepts Directed Acyclic Graph (DAG) workflows, which define agents, tools, and their connections.
- Configure workflows using the RowBoat Studio (UI) with the help of an AI copilot. Setup instructions can be found in the [main README](https://github.com/rowboatlabs/rowboat).
- The framework is stateless, meaning that it requires the upstream service to pass in the current `state` and `messages` in every turn.
- At each conversation turn:
- The agents are initialized using the current `state`.
- The graph is traversed based on `messages`, `state`, and `workflow`
- Response `messages` and a new `state` are generated.
- If `messages` contain tool calls, the upstream service must invoke the necessary tools and send the tool results back to continue the interaction.
---
## 🗂️ Key Request and Response Fields
### 📤 Request
- `messages`: List of user messages
- `state`: Active agent state and histories
- `workflow`: Graph of agents, tools, and connections
**Example JSON**: `tests/sample_requests/default_example.json`
---
### 📥 Response
- `messages`: List of response messages (may contain tool calls)
- `state`: Updated state to pass in the next request (since the framework is stateless)
**Example JSON**: `tests/sample_responses/default_example.json`
---
## 🛠️ Using the Framework
Ensure you are in this directory (`cd apps/agents` from the root directory of this repo) before running any of the below commands.
### ⚙️ Set Up Conda Environment
- `conda create -n myenv python=3.12`
- `conda activate myenv`
- Note: Python >= 3.10 required
---
### 📦 Install Dependencies
#### If using poetry
- `pip install poetry`
- `poetry install`
#### If using pip
`pip install -r requirements.txt`
### 🔑 Set up .env file
Copy `.env.example` to `.env` and add your API keys
### 🧪 Run interactive test
`python -m tests.interactive --config default_config.json --sample_request default_example.json --load_messages`
- `--config`: Config json filename, under `configs` folder
- `--sample_request`: Path to the sample request file, under `tests/sample_requests` folder
- `--load_messages`: If set, it will additionally load the initial set of messages from the sample request file. Else, user input will be required starting from the first message.
### 🌐 Set up server
- First, add this directory to your PYTHONPATH, using: `export PYTHONPATH=$PYTHONPATH:$(pwd)`
- For local testing: `flask --app src.app.main run --port=4040`
- To set up the server on a remote machine: `gunicorn -b 0.0.0.0:4040 src.app.main:app`
### 🖥️ Run test client
`python -m tests.app_client --sample_request default_example.json --api_key test`
- `--sample_request`: Path to the sample request file, under `tests/sample_requests` folder
- `--api_key`: API key to use for authentication. This is the same key as the one in the `.env` file.
## 📖 More details
### 🔍 Specifics
- **Format**: Uses OpenAI's messages format when passing messages.
- **LLMs**: Currently, only OpenAI LLMs (e.g. gpt-4o, gpt-4o-mini) are supported. Easy to expand to other LLMs like Claude, Gemini or self-hosted models.
- **Responses**: Here are some examples of responses that the framework can return:
- A list of one user-facing message
- A list of one or more tool calls
- A list of one user-facing message and one or more tool calls
- ⚠️ **Errors**: Errors are thrown as a tool call `raise_error` with the error message as the argument. Real-time error handling will be managed by the upstream service.
### 🗂️ Important directories and files
- `src/`: Contains all source code for the agents app
- `src/app/`: Contains Flask app which exposes the framework as a service
- `src/graph/`: Contains logic to run every turn of the conversation
- `src/graph/core.py`: Core graph implementation which parses the workflow config, creates agents from it and runs the turn of conversation (through the `run_turn` function)
- `src/swarm/`: RowBoat's custom implementation of OpenAI Swarm, which is used by `src/graph/core.py`
- `tests/`: Contains sample requests, an interactive client and a test client which mocks an upstream service
- `configs/`: Contains graph configurations (changed infrequently)
- `tests/sample_requests/`: Contains sample request files for the agents app
### 🔄 High-level flow
- `app/main.py` receives the request JSON from an upstream service, parses it and sends it to `src/graph/core.py`
- `src/graph/core.py` creates the agent graph object from scratch and uses `src/swarm/core.py` to run the turn
- `src/swarm/core.py` runs the turn by performing actual LLM calls and internal tool invocations to transitiion between agents
- `src/graph/core.py` returns the response messages and the new state to `app/main.py`, which relays it back to the upstream service
- The upstream services appends any new user messages to the history of messages and sends the messages back along with the new state to `app/main.py` as part of the next request. The process repeats until the upstream service completes its conversation with the user.
### 🚫 Limitations
- Does not support streaming currently.
- Cannot respond with multiple user-facing messages in the same turn.
# RowBoat Labs
🌐 Visit [RowBoat Labs](https://www.rowboatlabs.com) to learn more!

View file

@ -1,11 +0,0 @@
{
"localize_history": true,
"return_diff_messages": true,
"start_turn_with_start_agent": false,
"children_aware_of_parent": false,
"parent_has_child_history": true,
"max_messages_per_turn": 20,
"max_messages_per_error_escalation_turn": 15,
"escalate_errors": true,
"max_overall_turns": 25
}

File diff suppressed because it is too large Load diff

View file

@ -1,110 +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 = ">=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 = "^1.9"
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 = "*"
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"
PyJWT = "^2.10.1"
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"
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"
openai-agents = "^0.0.13"
[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"

View file

@ -1,153 +0,0 @@
aiofiles==24.1.0
aiohappyeyeballs==2.6.1
aiohttp==3.11.14
aiosignal==1.3.2
annotated-types==0.7.0
anyio==4.8.0
asgiref==3.8.1
attrs==25.3.0
babel==2.16.0
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
colorama==0.4.6
crashtest==0.4.1
distlib==0.3.9
distro==1.9.0
dnspython==2.7.0
dulwich==0.22.8
et_xmlfile==2.0.0
eval_type_backport==0.2.2
fastjsonschema==2.21.1
filelock==3.18.0
findpython==0.6.3
firecrawl==1.9.0
Flask==3.1.0
frozenlist==1.5.0
fsspec==2025.3.2
ghp-import==2.1.0
griffe==1.7.3
grpcio==1.71.0
grpcio-tools==1.71.0
gunicorn==23.0.0
h11==0.14.0
h2==4.2.0
hpack==4.1.0
httpcore==1.0.7
httpx==0.27.2
httpx-sse==0.4.0
huggingface-hub==0.30.2
Hypercorn==0.17.3
hyperframe==6.1.0
idna==3.10
importlib_metadata==8.6.1
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
jsonschema==4.23.0
jsonschema-specifications==2025.4.1
keyring==25.6.0
litellm==1.67.2
lxml==5.3.0
Markdown==3.7
markdownify==0.13.1
MarkupSafe==3.0.2
mcp==1.6.0
mergedeep==1.3.4
mistralai==1.2.3
mkdocs==1.6.1
mkdocs-get-deps==0.2.0
mkdocs-material==9.5.50
mkdocs-material-extensions==1.3.1
more-itertools==10.6.0
motor==3.7.0
msgpack==1.1.0
multidict==6.2.0
mypy-extensions==1.0.0
nest-asyncio==1.6.0
numpy==2.2.1
openai==1.76.0
openai-agents==0.0.13
openpyxl==3.1.5
packaging==24.2
paginate==0.5.7
pandas==2.2.3
pathspec==0.12.1
pbs-installer==2025.3.17
pkginfo==1.12.1.2
platformdirs==4.3.7
poetry==2.1.1
poetry-core==2.1.1
portalocker==2.10.1
priority==2.0.0
propcache==0.3.0
protobuf==5.29.4
pycparser==2.22
pydantic==2.10.5
pydantic-settings==2.8.1
pydantic_core==2.27.2
Pygments==2.19.1
PyJWT==2.10.1
pymdown-extensions==10.14.1
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
PyYAML==6.0.2
pyyaml_env_tag==0.1
qdrant-client==1.13.3
Quart==0.20.0
RapidFuzz==3.12.2
redis==5.2.1
referencing==0.36.2
regex==2024.11.6
requests==2.32.3
requests-toolbelt==1.0.0
rpds-py==0.24.0
setuptools==75.8.0
shellingham==1.5.4
six==1.17.0
sniffio==1.3.1
sounddevice==0.5.1
soupsieve==2.6
sse-starlette==2.2.1
sseclient==0.0.27
sseclient-py==1.8.0
starlette==0.46.1
tabulate==0.9.0
tiktoken==0.9.0
tokenizers==0.21.1
tomlkit==0.13.2
tqdm==4.67.1
trove-classifiers==2025.3.19.19
types-requests==2.32.0.20250328
typing-inspect==0.9.0
typing-inspection==0.4.0
typing_extensions==4.12.2
tzdata==2024.2
urllib3==2.3.0
uvicorn==0.34.0
virtualenv==20.29.3
waitress==2.1.2
watchdog==6.0.0
websockets==13.1
Werkzeug==3.1.3
wheel==0.44.0
wsproto==1.2.0
xattr==1.1.4
yarl==1.18.3
zipp==3.21.0
zstandard==0.23.0

View file

@ -1,184 +0,0 @@
import traceback
from quart import Quart, request, jsonify, Response
from functools import wraps
import os
import json
from hypercorn.config import Config
from hypercorn.asyncio import serve
import asyncio
from src.graph.core import run_turn_streamed
from src.utils.common import read_json_from_file
app = Quart(__name__)
master_config = read_json_from_file("./configs/default_config.json")
print("Master config:", master_config)
# Get environment variables with defaults
ENABLE_TRACING = False
if os.environ.get('ENABLE_TRACING'):
ENABLE_TRACING = os.environ.get('ENABLE_TRACING').lower() == 'true'
# 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():
print('='*100)
print(f"{'*'*100}Running server mode{'*'*100}")
try:
request_data = await request.get_json()
print("Request:", json.dumps(request_data))
# 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"
data = request_data
messages = []
final_state = {}
async for event_type, event_data in run_turn_streamed(
messages=input_messages,
start_agent_name=data.get("startAgent", ""),
agent_configs=data.get("agents", []),
tool_configs=data.get("tools", []),
prompt_configs=data.get("prompts", []),
start_turn_with_start_agent=master_config.get("start_turn_with_start_agent", False),
state=data.get("state", {}),
complete_request=data,
enable_tracing=ENABLE_TRACING
):
if event_type == 'message':
messages.append(event_data)
elif event_type == 'done':
final_state = event_data['state']
out = {
"messages": messages,
"state": final_state,
}
print("Output:")
for k, v in out.items():
print(f"{k}: {v}")
print('*'*100)
return jsonify(out)
except Exception as e:
print(traceback.format_exc())
print(f"Error: {str(e)}")
return jsonify({"error": str(e)}), 500
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", methods=["POST"])
@require_api_key
async def chat_stream():
# get the request data from the request
request_data = await request.get_data()
print("Request:", request_data.decode('utf-8'))
request_data = json.loads(request_data)
# 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"
async def generate():
print("Running generate() in server")
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", []),
prompt_configs=request_data.get("prompts", []),
start_turn_with_start_agent=master_config.get("start_turn_with_start_agent", False),
state=request_data.get("state", {}),
complete_request=request_data,
enable_tracing=ENABLE_TRACING
):
if event_type == 'message':
yield format_sse(event_data, "message")
elif event_type == 'done':
yield format_sse(event_data, "done")
elif event_type == 'error':
yield format_sse(event_data, " error")
except Exception as e:
print(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

@ -1,488 +0,0 @@
import traceback
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,
get_prompt_by_type,
get_agent_config_by_name
)
from .helpers.library_tools import handle_web_search_event
from .helpers.control import get_last_agent_name
from .execute_turn import run_streamed as swarm_run_streamed, get_agents
from .helpers.instructions import add_child_transfer_related_instructions
from .types import PromptType, outputVisibility, ResponseType
from agents.extensions.handoff_prompt import RECOMMENDED_PROMPT_PREFIX
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 set_sys_message(messages):
"""
If the system message is empty, set it to the default message: "You are a helplful assistant."
"""
if messages[0].get("role") == "system" and messages[0].get("content") == "":
messages[0]["content"] = "You are a helpful assistant."
print("Updated system message: ", messages[0])
return messages
def add_child_transfer_related_instructions_to_agents(agents):
for agent in agents:
add_child_transfer_related_instructions(agent)
return agents
def add_openai_recommended_instructions_to_agents(agents):
for agent in agents:
agent.instructions = RECOMMENDED_PROMPT_PREFIX + '\n\n' + agent.instructions
return agents
def check_internal_visibility(current_agent):
"""Check if an agent is internal based on its outputVisibility"""
return current_agent.output_visibility == outputVisibility.INTERNAL.value
def add_sender_details_to_messages(messages):
for msg in messages:
msg['sender'] = msg.get('sender', None)
if msg.get('sender'):
msg['content'] = f"Sender agent: {msg.get('sender')}\nContent: {msg.get('content')}"
return messages
def append_messages(messages, accumulated_messages):
# Create a set of existing message contents for O(1) lookup
existing_contents = {msg.get('content') for msg in messages}
# Append messages that aren't already present, preserving order
for msg in accumulated_messages:
if msg.get('content') not in existing_contents:
messages.append(msg)
existing_contents.add(msg.get('content'))
return messages
async def run_turn_streamed(
messages,
start_agent_name,
agent_configs,
tool_configs,
prompt_configs,
start_turn_with_start_agent,
state={},
complete_request={},
enable_tracing=None
):
"""
Run a turn of the conversation with streaming responses.
A turn consists of all messages between user inputs and must follow these rules:
1. Each turn must have exactly one external message from an agent with external visibility
2. A turn can have multiple internal messages from internal agents
3. Each agent can output at most one regular message per parent
4. Control flows from parent to child, and child must return to parent after responding
5. Turn ends when an external agent outputs a message
"""
print("\n=== Starting new turn ===")
print(f"Starting agent: {start_agent_name}")
# Use enable_tracing from complete_request if available, otherwise default to False
enable_tracing = complete_request.get("enable_tracing", False) if enable_tracing is None else enable_tracing
messages = set_sys_message(messages)
messages = add_sender_details_to_messages(messages)
is_greeting_turn = not any(msg.get("role") != "system" for msg in messages)
final_state = None
accumulated_messages = []
agent_message_counts = {} # Track messages per agent
child_call_counts = {} # Track parent->child calls
current_agent = None
parent_stack = []
try:
# Handle greeting turn
if is_greeting_turn:
greeting_prompt = get_prompt_by_type(prompt_configs, PromptType.GREETING) or "How can I help you today?"
message = {
'content': greeting_prompt,
'role': 'assistant',
'sender': start_agent_name,
'tool_calls': None,
'tool_call_id': None,
'tool_name': None,
'response_type': ResponseType.EXTERNAL.value
}
accumulated_messages.append(message)
print('-'*100)
print(f"Yielding message: {message}")
print('-'*100)
yield ('message', message)
final_state = {
"last_agent_name": start_agent_name,
"tokens": {"total": 0, "prompt": 0, "completion": 0},
"turn_messages": accumulated_messages
}
print('-'*100)
print(f"Yielding done: {final_state}")
print('-'*100)
yield ('done', {'state': final_state})
return
# Initialize agents and get external tools
new_agents = get_agents(
agent_configs=agent_configs,
tool_configs=tool_configs,
complete_request=complete_request
)
new_agents = add_child_transfer_related_instructions_to_agents(new_agents)
new_agents = add_openai_recommended_instructions_to_agents(new_agents)
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
)
current_agent = get_agent_by_name(last_agent_name, new_agents)
external_tools = get_external_tools(tool_configs)
tokens_used = {"total": 0, "prompt": 0, "completion": 0}
iter = 0
while True:
iter += 1
is_internal_agent = check_internal_visibility(current_agent)
print('-'*100)
print(f"Iteration {iter} of turn loop")
print(f"Current agent: {current_agent.name} (internal: {is_internal_agent})")
print(f"Parent stack: {[agent.name for agent in parent_stack]}")
print('-'*100)
messages = append_messages(messages, accumulated_messages)
# Run the current agent
stream_result = await swarm_run_streamed(
agent=current_agent,
messages=messages,
external_tools=external_tools,
tokens_used=tokens_used,
enable_tracing=enable_tracing
)
async for event in stream_result.stream_events():
try:
# Handle web search events
if event.type == "raw_response_event":
# Handle token usage counting
if hasattr(event.data, 'type') and event.data.type == "response.completed" and hasattr(event.data.response, 'usage'):
try:
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)
except Exception as e:
print(f"Warning: Tokens used is likely not available for your chosen model: {e}")
web_search_messages = handle_web_search_event(event, current_agent)
for message in web_search_messages:
message['response_type'] = ResponseType.INTERNAL.value
print('-'*100)
print(f"Yielding message: {message}")
print('-'*100)
yield ('message', message)
if message.get('role') != 'tool':
message['content'] = f"Sender agent: {current_agent.name}\nContent: {message['content']}"
accumulated_messages.append(message)
continue
# Handle agent transfer
elif event.type == "agent_updated_stream_event":
# Skip self-transfers
if current_agent.name == event.new_agent.name:
print(f"\nSkipping agent transfer attempt: {current_agent.name} -> {event.new_agent.name} (self-transfer)")
continue
# Check if we've already called this child agent too many times
parent_child_key = f"{current_agent.name}:{event.new_agent.name}"
current_count = child_call_counts.get(parent_child_key, 0)
if current_count >= event.new_agent.max_calls_per_parent_agent:
print(f"Skipping transfer from {current_agent.name} to {event.new_agent.name} (max calls reached from parent to child)")
continue
# Transfer to new agent
tool_call_id = str(uuid.uuid4())
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': ResponseType.INTERNAL.value
}
print('-'*100)
print(f"Yielding message: {message}")
print('-'*100)
yield ('message', message)
# Record 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('-'*100)
print(f"Yielding message: {message}")
print('-'*100)
yield ('message', message)
# Update tracking and switch to child
if check_internal_visibility(event.new_agent):
child_call_counts[parent_child_key] = current_count + 1
parent_stack.append(current_agent)
current_agent = event.new_agent
# Handle regular messages and tool calls
elif event.type == "run_item_stream_event":
if event.item.type == "tool_call_item":
# Check if it's a web search call
if hasattr(event.item.raw_item, 'type') and event.item.raw_item.type == 'web_search_call':
web_search_messages = handle_web_search_event(event, current_agent)
for message in web_search_messages:
message['response_type'] = ResponseType.INTERNAL.value
print('-'*100)
print(f"Yielding message: {message}")
print('-'*100)
yield ('message', message)
if message.get('role') != 'tool':
message['content'] = f"Sender agent: {current_agent.name}\nContent: {message['content']}"
accumulated_messages.append(message)
continue
# Handle regular tool calls
message = {
'content': None,
'role': 'assistant',
'sender': current_agent.name,
'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': ResponseType.INTERNAL.value
}
print('-'*100)
print(f"Yielding message: {message}")
print('-'*100)
yield ('message', message)
message['content'] = f"Sender agent: {current_agent.name}\nContent: {message['content']}"
accumulated_messages.append(message)
elif event.item.type == "tool_call_output_item":
# Get the tool name and call id from raw_item
tool_call_id = None
tool_name = None
# Try to get call_id from various possible locations
if hasattr(event.item.raw_item, 'call_id'):
tool_call_id = event.item.raw_item.call_id
elif isinstance(event.item.raw_item, dict) and 'call_id' in event.item.raw_item:
tool_call_id = event.item.raw_item['call_id']
# Try to get tool name from various possible locations
if hasattr(event.item.raw_item, 'name'):
tool_name = event.item.raw_item.name
elif isinstance(event.item.raw_item, dict):
if 'name' in event.item.raw_item:
tool_name = event.item.raw_item['name']
elif 'type' in event.item.raw_item and event.item.raw_item['type'] == 'function_call_output':
# For function call outputs, try to infer from context
tool_name = 'recommendation' # Default for function calls
# Fallback to event item if available
if not tool_name and hasattr(event.item, 'tool_name'):
tool_name = event.item.tool_name
if not tool_call_id and hasattr(event.item, 'tool_call_id'):
tool_call_id = event.item.tool_call_id
message = {
'content': str(event.item.output),
'role': 'tool',
'sender': None,
'tool_calls': None,
'tool_call_id': tool_call_id,
'tool_name': tool_name,
'response_type': ResponseType.INTERNAL.value
}
print('-'*100)
print(f"Yielding tool call output message: {message}")
print('-'*100)
yield ('message', message)
elif event.item.type == "message_output_item":
# Extract content and citations
content = ""
url_citations = []
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
if hasattr(content_item, 'annotations'):
for annotation in content_item.annotations:
if hasattr(annotation, 'type') and annotation.type == 'url_citation':
citation = {
'url': annotation.url if hasattr(annotation, 'url') else '',
'title': annotation.title if hasattr(annotation, 'title') else '',
'start_index': annotation.start_index if hasattr(annotation, 'start_index') else 0,
'end_index': annotation.end_index if hasattr(annotation, 'end_index') else 0
}
url_citations.append(citation)
# Determine message type and create message
is_internal = check_internal_visibility(current_agent)
response_type = ResponseType.INTERNAL.value if is_internal else ResponseType.EXTERNAL.value
message = {
'content': content,
'role': 'assistant',
'sender': current_agent.name,
'tool_calls': None,
'tool_call_id': None,
'tool_name': None,
'response_type': response_type
}
if url_citations:
message['citations'] = url_citations
# Track that this agent has responded
if not message.get('tool_calls'): # If there are no tool calls, it's a content response
agent_message_counts[current_agent.name] = 1
print('-'*100)
print(f"Yielding message: {message}")
print('-'*100)
yield ('message', message)
message['content'] = f"Sender agent: {current_agent.name}\nContent: {message['content']}"
accumulated_messages.append(message)
# Return to parent or end turn
if is_internal and parent_stack:
# Create tool call for control transition
tool_call_id = str(uuid.uuid4())
transition_message = {
'content': None,
'role': 'assistant',
'sender': current_agent.name,
'tool_calls': [{
'function': {
'name': 'transfer_to_agent',
'arguments': json.dumps({
'assistant': parent_stack[-1].name
})
},
'id': tool_call_id,
'type': 'function'
}],
'tool_call_id': None,
'tool_name': None,
'response_type': ResponseType.INTERNAL.value
}
print('-'*100)
print(f"Yielding control transition message: {transition_message}")
print('-'*100)
yield ('message', transition_message)
# Create tool response for control transition
transition_response = {
'content': json.dumps({
'assistant': parent_stack[-1].name
}),
'role': 'tool',
'sender': None,
'tool_calls': None,
'tool_call_id': tool_call_id,
'tool_name': 'transfer_to_agent'
}
print('-'*100)
print(f"Yielding control transition response: {transition_response}")
print('-'*100)
yield ('message', transition_response)
current_agent = parent_stack.pop()
continue
elif not is_internal:
break
except Exception as e:
print("\n=== Error in stream event processing ===")
print(f"Error: {str(e)}")
print("Event details:")
print(f"Event type: {event.type if hasattr(event, 'type') else 'unknown'}")
if hasattr(event, '__dict__'):
print(f"Event attributes: {event.__dict__}")
print(f"Full event object: {event}")
print(f"Traceback: {traceback.format_exc()}")
print("=" * 50)
raise
# Break main loop if we've output an external message
if not is_internal_agent and current_agent.name in agent_message_counts:
break
# Set final state
final_state = {
"last_agent_name": current_agent.name if current_agent else None,
"tokens": tokens_used,
"turn_messages": accumulated_messages
}
print('-'*100)
print(f"Yielding done: {final_state}")
print('-'*100)
yield ('done', {'state': final_state})
except Exception as e:
print(traceback.format_exc())
print(f"Error in stream processing: {str(e)}")
yield ('error', {'error': str(e), 'state': final_state})

View file

@ -1,414 +0,0 @@
import logging
import json
import aiohttp
import jwt
import hashlib
from agents import OpenAIChatCompletionsModel, trace, add_trace_processor
import pprint
# 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 .types import outputVisibility
from agents import Agent as NewAgent, Runner, FunctionTool, RunContextWrapper, ModelSettings, WebSearchTool
from .tracing import AgentTurnTraceProcessor
# Add import for OpenAI functionality
from src.utils.common import generate_openai_output
from typing import Any
import asyncio
from mcp import ClientSession
from mcp.client.sse import sse_client
from mcp.client.streamable_http import streamablehttp_client
from pydantic import BaseModel
from typing import List, Optional, Dict
from .tool_calling import call_rag_tool
from pymongo import MongoClient
import os
MONGO_URI = os.environ.get("MONGODB_URI", "mongodb://localhost:27017/rowboat").strip()
mongo_client = MongoClient(MONGO_URI)
db = mongo_client["rowboat"]
from src.utils.client import client, PROVIDER_DEFAULT_MODEL
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, description: str, mock_instructions: str) -> str:
try:
print(f"Mock tool called for: {tool_name}")
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 = None
response_content = generate_openai_output(messages, output_type='text', model=PROVIDER_DEFAULT_MODEL)
return response_content
except Exception as e:
print(f"Error in mock_tool: {str(e)}")
return f"Error: {str(e)}"
async def call_webhook(tool_name: str, args: str, webhook_url: str, signing_secret: str) -> str:
try:
print(f"Calling webhook for tool: {tool_name}")
content_dict = {
"toolCall": {
"function": {
"name": tool_name,
"arguments": args
}
}
}
request_body = {
"content": json.dumps(content_dict)
}
# Prepare headers
headers = {}
if signing_secret:
content_str = request_body["content"]
body_hash = hashlib.sha256(content_str.encode('utf-8')).hexdigest()
payload = {"bodyHash": body_hash}
signature_jwt = jwt.encode(payload, signing_secret, algorithm="HS256")
headers["X-Signature-Jwt"] = signature_jwt
async with aiohttp.ClientSession() as session:
async with session.post(webhook_url, json=request_body, headers=headers) 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:
try:
print(f"MCP tool called for: {tool_name} with args: {args} at url: {mcp_server_url}")
# Try StreamableHTTP first
try:
print("Attempting to connect using StreamableHTTP...")
async with streamablehttp_client(mcp_server_url) as (read_stream, write_stream, _):
async with ClientSession(read_stream, write_stream) as session:
await session.initialize()
jargs = json.loads(args)
response = await session.call_tool(tool_name, arguments=jargs)
json_output = json.dumps(response.content, default=lambda x: x.__dict__ if hasattr(x, '__dict__') else str(x), indent=2)
print("Successfully connected using StreamableHTTP")
return json_output
except Exception as streamable_error:
print(f"StreamableHTTP connection failed: {str(streamable_error)}")
print("Falling back to SSE...")
# Fallback to SSE
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(response.content, default=lambda x: x.__dict__ if hasattr(x, '__dict__') else str(x), indent=2)
print("Successfully connected using SSE fallback")
return json_output
except Exception as e:
print(f"Error in call_mcp (both StreamableHTTP and SSE failed): {str(e)}")
return f"Error: {str(e)}"
async def catch_all(ctx: RunContextWrapper[Any], args: str, tool_name: str, tool_config: dict, complete_request: dict) -> str:
try:
print(f"Catch all called for tool: {tool_name}")
# Pretty print the complete tool call information
logging.info("Tool Call Details:\n%s", pprint.pformat({
'tool_name': tool_name,
'arguments': json.loads(args) if args else {},
'config': {
'description': tool_config.get('description', ''),
'isMcp': tool_config.get('isMcp', False),
'mcpServerName': tool_config.get('mcpServerName', ''),
'parameters': tool_config.get('parameters', {})
}
}, indent=2))
# 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) or complete_request.get("testProfile", {}).get("mockTools", False):
# Call mock_tool to handle the response (it will decide whether to use mock instructions or generate a response)
if complete_request.get("testProfile", {}).get("mockPrompt", ""):
response_content = await mock_tool(tool_name, args, tool_config.get("description", ""), complete_request.get("testProfile", {}).get("mockPrompt", ""))
else:
response_content = await mock_tool(tool_name, args, tool_config.get("description", ""), tool_config.get("mockInstructions", ""))
print(response_content)
elif tool_config.get("isMcp", False):
mcp_server_url = tool_config.get("mcpServerURL", "")
if not mcp_server_url:
# Backwards compatibility for old projects
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:
collection = db["projects"]
doc = collection.find_one({"_id": complete_request.get("projectId", "")})
signing_secret = doc.get("secret", "")
webhook_url = complete_request.get("toolWebhookUrl", "")
response_content = await call_webhook(tool_name, args, webhook_url, signing_secret)
return response_content
except Exception as e:
print(f"Error in catch_all: {str(e)}")
return f"Error: {str(e)}"
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(f"Creating rag_search tool with params:\n-Data Sources: {config.get('ragDataSources', [])}\n-Return Type: {config.get('ragReturnType', 'chunks')}\n-K: {config.get('ragK', 3)}")
params = {
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "The query to search for"
}
},
"additionalProperties": False,
"required": [
"query"
]
}
tool = FunctionTool(
name="rag_search",
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", []), config.get("ragReturnType", "chunks"), config.get("ragK", 3))
)
return tool
else:
return None
DEFAULT_MAX_CALLS_PER_PARENT_AGENT = 3
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:
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 = []
print(f"Agent {agent_config['name']} has {len(agent_config['tools'])} configured tools")
new_tools = []
for tool_name in agent_config["tools"]:
tool_config = get_tool_config_by_name(tool_configs, tool_name)
if tool_config:
# Preserve all JSON Schema properties in the tool parameters
tool_params = tool_config.get("parameters", {})
if isinstance(tool_params, dict):
# Ensure we keep all properties from the schema
json_schema_properties = [
"enum", "default", "minimum", "maximum", "items", "format",
"pattern", "minLength", "maxLength", "minItems", "maxItems",
"uniqueItems", "multipleOf", "examples"
]
for prop_name, prop_schema in tool_params.get("properties", {}).items():
# Copy all existing JSON Schema properties
for schema_prop in json_schema_properties:
if schema_prop in prop_schema:
prop_schema[schema_prop] = prop_schema[schema_prop]
external_tools.append({
"type": "function",
"function": tool_config
})
if tool_name == "web_search":
tool = WebSearchTool()
elif tool_name == "rag_search":
tool = get_rag_tool(agent_config, complete_request)
else:
tool = FunctionTool(
name=tool_name,
description=tool_config["description"],
params_json_schema=tool_params, # Use the enriched parameters
strict_json_schema=False,
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)
)
if tool:
new_tools.append(tool)
print(f"Added tool {tool_name} to agent {agent_config['name']}")
else:
print(f"WARNING: Tool {tool_name} not found in tool_configs")
# Create the agent object
print(f"Creating Agent object for {agent_config['name']}")
# add the name and description to the agent instructions
agent_instructions = f"## Your Name\n{agent_config['name']}\n\n## Description\n{agent_config['description']}\n\n## Instructions\n{agent_config['instructions']}"
try:
# Identify the model
model_name = agent_config["model"] if agent_config["model"] else PROVIDER_DEFAULT_MODEL
print(f"Using model: {model_name}")
model=OpenAIChatCompletionsModel(model=model_name, openai_client=client) if client else agent_config["model"]
# Create the agent object
new_agent = NewAgent(
name=agent_config["name"],
instructions=agent_instructions,
handoff_description=agent_config["description"],
tools=new_tools,
model = model,
model_settings=ModelSettings(temperature=0.0)
)
# Set the max calls per parent agent
new_agent.max_calls_per_parent_agent = agent_config.get("maxCallsPerParentAgent", DEFAULT_MAX_CALLS_PER_PARENT_AGENT)
if not agent_config.get("maxCallsPerParentAgent", None):
print(f"WARNING: Max calls per parent agent not received for agent {new_agent.name}. Using rowboat_agents default of {DEFAULT_MAX_CALLS_PER_PARENT_AGENT}")
else:
print(f"Max calls per parent agent for agent {new_agent.name}: {new_agent.max_calls_per_parent_agent}")
# Set output visibility
new_agent.output_visibility = agent_config.get("outputVisibility", outputVisibility.EXTERNAL.value)
if not agent_config.get("outputVisibility", None):
print(f"WARNING: Output visibility not received for agent {new_agent.name}. Using rowboat_agents default of {new_agent.output_visibility}")
else:
print(f"Output visibility for agent {new_agent.name}: {new_agent.output_visibility}")
# Handle the connected agents
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)
print(f"Successfully created agent: {agent_config['name']}")
except Exception as 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
# Initialize a flag to track if the trace processor is added
trace_processor_added = False
async def run_streamed(
agent,
messages,
external_tools=None,
tokens_used=None,
enable_tracing=False
):
"""
Wrapper function for initializing and running the Swarm client in streaming mode.
"""
print(f"Initializing 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)
})
print("Beginning streaming run")
try:
# Add our custom trace processor only if tracing is enabled
global trace_processor_added
if enable_tracing and not trace_processor_added:
trace_processor = AgentTurnTraceProcessor()
add_trace_processor(trace_processor)
trace_processor_added = True
# Get the stream result without trace context first
stream_result = Runner.run_streamed(agent, formatted_messages)
# If tracing is enabled, wrap the stream_events to handle tracing
if enable_tracing:
original_stream_events = stream_result.stream_events
async def wrapped_stream_events():
# Create trace context inside the async function
with trace(f"Agent turn: {agent.name}") as trace_ctx:
try:
async for event in original_stream_events():
yield event
except GeneratorExit:
# Handle generator exit gracefully
raise
except Exception as e:
print(f"Error in stream events: {str(e)}")
raise
stream_result.stream_events = wrapped_stream_events
return stream_result
except Exception as e:
print(f"Error during streaming run: {str(e)}")
raise

View file

@ -1,218 +0,0 @@
# Guardrails
from src.utils.common import generate_llm_output
import os
import copy
from .execute_turn import Agent, Response, create_response
from src.utils.common import common_logger, generate_openai_output, update_tokens_used
logger = common_logger
def classify_hallucination(context: str, assistant_response: str, chat_history: list, model: str) -> str:
"""
Checks if an assistant's response contains hallucinations by comparing against provided context.
Args:
context (str): The context/knowledge base to check the response against
assistant_response (str): The response from the assistant to validate
chat_history (list): List of previous chat messages for context
Returns:
str: Verdict indicating level of hallucination:
'yes-absolute' - completely supported by context
'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.
------------------------------------------------------------------------
Here is the context:
{context}
------------------------------------------------------------------------
Here is the chat history message:
{chat_history_str}
------------------------------------------------------------------------
Here is the response:
{assistant_response}
------------------------------------------------------------------------
As a hallucination guardrail, your job is to go through each line of the response and check if it is completely supported by the context. Even if a single line is not supported, the response is no.
Output a single verdict for the entire response. don't provide any reasoning. The output classes are
yes-absolute: completely supported by the context
yes-common-sensical: but with some common sense interpretation
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
Example 1: The response is completely supported by the context.
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:
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:
Context: "Flights may experience delays due to weather conditions. In such cases, the airline staff will provide updates at the airport."
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
Example 3: The response is not supported by the context and contains glaring inaccuracies.
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:
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:
Context: "Our frequent flyer program offers discounts on checked bags for members who have achieved Gold status."
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
"""
messages = [
{
"role": "system",
"content": prompt,
},
]
response = generate_llm_output(messages, model)
return response
def post_process_response(messages: list, post_processing_agent_name: str, post_process_instructions: str, style_prompt: str = None, context: str = None, model: str = "gpt-4o", tokens_used: dict = {}, last_agent: Agent = None) -> dict:
agent_instructions = last_agent.instructions
agent_history = last_agent.history
# agent_instructions = ''
# agent_history = []
pending_msg = copy.deepcopy(messages[-1])
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
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
if skip:
pending_msg['response_type'] = "external"
response = Response(
messages=[],
tokens_used=tokens_used,
agent=last_agent,
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}")
prompt = f"""
# ROLE
You are a post processing agent responsible for rewriting a response generated by an agent, according to instructions provided below. Ensure that the response you produce adheres to the instructions provided to you (if any).
------------------------------------------------------------------------
# ADDITIONAL INSTRUCTIONS
Here are additional instructions that the admin might have configured for you:
{post_process_instructions}
------------------------------------------------------------------------
# CHAT HISTORY
Here is the chat history:
{agent_history_str}
"""
if context:
context_prompt = f"""
------------------------------------------------------------------------
# CONTEXT
Here is the context:
{context}
"""
prompt += context_prompt
if style_prompt:
style_prompt = f"""
------------------------------------------------------------------------
# STYLE PROMPT
Here is the style prompt:
{style_prompt}
"""
prompt += style_prompt
agent_response_and_instructions = f"""
------------------------------------------------------------------------
# AGENT INSTRUCTIONS
Here are the instructions to the agent generating the response:
{agent_instructions}
------------------------------------------------------------------------
# AGENT RESPONSE
Here is the response that the agent has generated:
{pending_msg['content']}
"""
prompt += agent_response_and_instructions
logger.debug(f"Sanitizing response for style. Original response: {pending_msg['content']}")
completion = generate_openai_output(
messages=[
{"role": "system", "content": prompt}
],
model = model,
return_completion=True
)
content = completion.choices[0].message.content
if content:
content = content.strip().lstrip().rstrip()
tokens_used = update_tokens_used(provider="openai", model=model, tokens_used=tokens_used, completion=completion)
logger.debug(f"Response after style check: {content}, tokens used: {tokens_used}")
pending_msg['content'] = content if content else pending_msg['content']
pending_msg['response_type'] = "external"
pending_msg['sender'] = pending_msg['sender'] + f' >> {post_processing_agent_name}'
response = Response(
messages=[pending_msg],
tokens_used=tokens_used,
agent=last_agent,
error_msg=''
)
return response

View file

@ -1,48 +0,0 @@
from src.utils.common import common_logger
logger = common_logger
def get_external_tools(tool_configs):
logger.debug("Getting external tools")
tools = [tool["name"] for tool in tool_configs]
logger.debug(f"Found {len(tools)} external tools")
return tools
def get_agent_by_name(agent_name, agents):
agent = next((a for a in agents if getattr(a, "name", None) == agent_name), None)
if not agent:
logger.error(f"Agent with name {agent_name} not found")
raise ValueError(f"Agent with name {agent_name} not found")
return agent
def get_agent_config_by_name(agent_name, agent_configs):
agent_config = next((ac for ac in agent_configs if ac.get("name") == agent_name), None)
if not agent_config:
logger.error(f"Agent config with name {agent_name} not found")
raise ValueError(f"Agent config with name {agent_name} not found")
return agent_config
def pop_agent_config_by_type(agent_configs, agent_type):
agent_config = next((ac for ac in agent_configs if ac.get("type") == agent_type), None)
if agent_config:
agent_configs.remove(agent_config)
return agent_config, agent_configs
def get_agent_by_type(agents, agent_type):
return next((a for a in agents if a.type == agent_type), None)
def get_prompt_by_type(prompt_configs, prompt_type):
return next((pc.get("prompt") for pc in prompt_configs if pc.get("type") == prompt_type), None)
def get_agent_data_by_name(agent_name, agent_data):
for data in agent_data:
name = data.get("name", "")
if name == agent_name:
return data
return None
def get_tool_config_by_name(tool_configs, tool_name):
return next((tc for tc in tool_configs if tc.get("name", "") == tool_name), None)
def get_tool_config_by_type(tool_configs, tool_type):
return next((tc for tc in tool_configs if tc.get("type", "") == tool_type), None)

View file

@ -1,48 +0,0 @@
from .access import get_agent_config_by_name, get_agent_data_by_name
from src.graph.types import ControlType
def get_last_agent_name(state, agent_configs, start_agent_name, msg_type, latest_assistant_msg, start_turn_with_start_agent):
default_last_agent_name = state.get("last_agent_name", '')
last_agent_config = get_agent_config_by_name(default_last_agent_name, agent_configs)
specific_agent_data = get_agent_data_by_name(default_last_agent_name, state.get("agent_data", []))
# Overrides for special cases
print("Setting agent control based on last agent and control type")
if msg_type == "tool":
last_agent_name = default_last_agent_name
assert last_agent_name == latest_assistant_msg.get("sender", ''), "Last agent name does not match sender of latest assistant message during tool call handling"
elif start_turn_with_start_agent:
last_agent_name = start_agent_name
else:
control_type = last_agent_config.get("controlType", ControlType.RETAIN.value)
if control_type == ControlType.PARENT_AGENT.value:
last_agent_name = specific_agent_data.get("most_recent_parent_name", None) if specific_agent_data else None
if not last_agent_name:
print("Most recent parent is empty, defaulting to same agent instead")
last_agent_name = default_last_agent_name
elif control_type == ControlType.START_AGENT.value:
last_agent_name = start_agent_name
else:
last_agent_name = default_last_agent_name
if default_last_agent_name != last_agent_name:
print(f"Last agent name changed from {default_last_agent_name} to {last_agent_name} due to control settings")
return last_agent_name
def get_latest_assistant_msg(messages):
# Find the latest message with role assistant
for i in range(len(messages)-1, -1, -1):
if messages[i].get("role") == "assistant":
return messages[i]
return None
def get_latest_non_assistant_messages(messages):
# Find all messages after the last assistant message
for i in range(len(messages)-1, -1, -1):
if messages[i].get("role") == "assistant":
return messages[i+1:]
return messages

View file

@ -1,44 +0,0 @@
from src.graph.instructions import TRANSFER_CHILDREN_INSTRUCTIONS, TRANSFER_PARENT_AWARE_INSTRUCTIONS, RAG_INSTRUCTIONS, ERROR_ESCALATION_AGENT_INSTRUCTIONS, TRANSFER_GIVE_UP_CONTROL_INSTRUCTIONS, SYSTEM_MESSAGE, CHILD_TRANSFER_RELATED_INSTRUCTIONS
def add_transfer_instructions_to_parent_agents(agent, children, transfer_functions):
other_agent_name_descriptions_tools = f'\n{'-'*100}\n'.join([f"Name: {agent.name}\nDescription: {agent.description if agent.description else ''}\nTool for transfer: {transfer_functions[agent.name].__name__}" for agent in children.values()])
prompt = TRANSFER_CHILDREN_INSTRUCTIONS.format(other_agent_name_descriptions_tools=other_agent_name_descriptions_tools)
agent.instructions = agent.instructions + f'\n\n{'-'*100}\n\n' + prompt
return agent
def add_transfer_instructions_to_child_agents(child, children_aware_of_parent):
if children_aware_of_parent:
candidate_parents_name_description_tools = f'\n{'-'*100}\n'.join([f"Name: {parent_name}\nTool for transfer: {func.__name__}" for parent_name, func in child.candidate_parent_functions.items()])
prompt = TRANSFER_PARENT_AWARE_INSTRUCTIONS.format(candidate_parents_name_description_tools=candidate_parents_name_description_tools)
else:
candidate_parents_name_description_tools = f'\n{'-'*100}\n'.join(list(set([f"Tool for transfer: {func.__name__}" for _, func in child.candidate_parent_functions.items()])))
prompt = TRANSFER_GIVE_UP_CONTROL_INSTRUCTIONS.format(candidate_parents_name_description_tools=candidate_parents_name_description_tools)
child.instructions = child.instructions + f'\n\n{'-'*100}\n\n' + prompt
return child
def add_rag_instructions_to_agent(agent_config, rag_tool_name):
prompt = RAG_INSTRUCTIONS.format(rag_tool_name=rag_tool_name)
agent_config["instructions"] = agent_config["instructions"] + f'\n\n{'-'*100}\n\n' + prompt
return agent_config
def add_error_escalation_instructions(agent):
prompt = ERROR_ESCALATION_AGENT_INSTRUCTIONS
agent.instructions = agent.instructions + f'\n\n{'-'*100}\n\n' + prompt
return agent
def get_universal_system_message(messages):
if messages and messages[0].get("role") == "system":
return SYSTEM_MESSAGE.format(system_message=messages[0].get("content"))
return ""
def add_universal_system_message_to_agent(agent, universal_sys_msg):
agent.instructions = agent.instructions + f'\n\n{'-'*100}\n\n' + universal_sys_msg
return agent
def add_child_transfer_related_instructions(agent):
prompt = CHILD_TRANSFER_RELATED_INSTRUCTIONS
agent.instructions = agent.instructions + f'\n\n{'-'*100}\n\n' + prompt
return agent

View file

@ -1,268 +0,0 @@
import json
import uuid
import traceback
def handle_web_search_event(event, current_agent):
"""
Helper function to handle all web search related events.
Returns a list of messages to yield.
"""
messages = []
try:
# Handle raw response web search
if event.type == "raw_response_event":
if hasattr(event, 'data') and hasattr(event.data, 'raw_item'):
raw_item = event.data.raw_item
if (hasattr(raw_item, 'type') and raw_item.type == 'web_search_call') or (
isinstance(raw_item, dict) and raw_item.get('type') == 'web_search_call'
):
call_id = None
if hasattr(raw_item, 'id'):
call_id = raw_item.id
elif isinstance(raw_item, dict) and 'id' in raw_item:
call_id = raw_item['id']
else:
call_id = str(uuid.uuid4())
status = 'unknown'
if hasattr(raw_item, 'status'):
status = raw_item.status
elif isinstance(raw_item, dict) and 'status' in raw_item:
status = raw_item['status']
tool_call_msg = {
'content': None,
'role': 'assistant',
'sender': current_agent.name if current_agent else None,
'tool_calls': [{
'function': {
'name': 'web_search',
'arguments': json.dumps({
'search_id': call_id,
'status': status
})
},
'id': call_id,
'type': 'function'
}],
'tool_call_id': None,
'tool_name': None,
'response_type': 'internal'
}
print(f"Condition for tool call matched in raw_response_event. Appending tool call message: {tool_call_msg}")
messages.append(tool_call_msg)
tool_call_output_dummy_msg = {
'content': 'Web search completed.',
'role': 'tool',
'sender': None,
'tool_calls': None,
'tool_call_id': call_id,
'tool_name': 'web_search',
'response_type': 'internal'
}
messages.append(tool_call_output_dummy_msg)
# Handle run item web search events
elif event.type == "run_item_stream_event":
if event.item.type == "tool_call_item":
try:
# Check if it's a web search call
if hasattr(event.item.raw_item, 'type') and event.item.raw_item.type == 'web_search_call':
call_id = event.item.raw_item.id if hasattr(event.item.raw_item, 'id') else str(uuid.uuid4())
tool_call_msg = {
'content': None,
'role': 'assistant',
'sender': current_agent.name if current_agent else None,
'tool_calls': [{
'function': {
'name': 'web_search',
'arguments': json.dumps({
'search_id': call_id
})
},
'id': call_id,
'type': 'function'
}],
'tool_call_id': None,
'tool_name': None,
'response_type': 'internal'
}
print(f"Condition for tool call matched in run_item_stream_event. Appending tool call message: {tool_call_msg}")
messages.append(tool_call_msg)
tool_call_output_dummy_msg = {
'content': 'Web search completed.',
'role': 'tool',
'sender': None,
'tool_calls': None,
'tool_call_id': call_id,
'tool_name': 'web_search',
'response_type': 'internal'
}
messages.append(tool_call_output_dummy_msg)
else:
# Handle regular tool calls
tool_call_msg = {
'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(f"Condition for tool call matched in run_item_stream_event. Appending tool call message: {tool_call_msg}")
messages.append(tool_call_msg)
tool_call_output_dummy_msg = {
'content': 'Web search completed.',
'role': 'tool',
'sender': None,
'tool_calls': None,
'tool_call_id': call_id,
'tool_name': 'web_search',
'response_type': 'internal'
}
messages.append(tool_call_output_dummy_msg)
except Exception as e:
print("\n=== Error in tool_call_item handling ===")
print(f"Error: {str(e)}")
print(f"Event type: {event.type}")
print(f"Event item type: {event.item.type}")
print("Event details:")
print(f"Raw item: {event.item.raw_item}")
if hasattr(event.item.raw_item, '__dict__'):
print(f"Raw item attributes: {event.item.raw_item.__dict__}")
print(f"Traceback: {traceback.format_exc()}")
print("=" * 50)
raise
elif event.item.type == "tool_call_output_item":
if isinstance(event.item.raw_item, dict) and event.item.raw_item.get('type') == 'web_search_results':
call_id = event.item.raw_item.get('search_id', event.item.raw_item.get('id', str(uuid.uuid4())))
tool_call_output_msg = {
'content': str(event.item.output),
'role': 'tool',
'sender': None,
'tool_calls': None,
'tool_call_id': call_id,
'tool_name': 'web_search',
'response_type': 'internal'
}
print(f"Condition for tool call output matched in run_item_stream_event. Appending tool call output message: {tool_call_output_msg}")
messages.append(tool_call_output_msg)
elif event.item.type == "web_search_call_item" or (
hasattr(event.item, 'raw_item') and
hasattr(event.item.raw_item, 'type') and
event.item.raw_item.type == 'web_search_call'
):
call_id = None
if hasattr(event.item.raw_item, 'id'):
call_id = event.item.raw_item.id
tool_call_msg = {
'content': None,
'role': 'assistant',
'sender': current_agent.name if current_agent else None,
'tool_calls': [{
'function': {
'name': 'web_search',
'arguments': json.dumps({
'search_id': call_id
})
},
'id': call_id or str(uuid.uuid4()),
'type': 'function'
}],
'tool_call_id': None,
'tool_name': None,
'response_type': 'internal'
}
print(f"Condition for tool call matched in run_item_stream_event. Appending tool call message: {tool_call_msg}")
messages.append(tool_call_msg)
tool_call_output_dummy_msg = {
'content': 'Web search completed.',
'role': 'tool',
'sender': None,
'tool_calls': None,
'tool_call_id': call_id,
'tool_name': 'web_search',
'response_type': 'internal'
}
messages.append(tool_call_output_dummy_msg)
elif event.item.type == "web_search_results_item" or (
hasattr(event.item, 'raw_item') and (
(hasattr(event.item.raw_item, 'type') and event.item.raw_item.type == 'web_search_results') or
(isinstance(event.item.raw_item, dict) and event.item.raw_item.get('type') == 'web_search_results')
)
):
raw_item = event.item.raw_item
call_id = None
if hasattr(raw_item, 'search_id'):
call_id = raw_item.search_id
elif isinstance(raw_item, dict) and 'search_id' in raw_item:
call_id = raw_item['search_id']
elif hasattr(raw_item, 'id'):
call_id = raw_item.id
elif isinstance(raw_item, dict) and 'id' in raw_item:
call_id = raw_item['id']
else:
call_id = str(uuid.uuid4())
results = {}
if hasattr(event.item, 'output'):
results = event.item.output
elif hasattr(raw_item, 'results'):
results = raw_item.results
elif isinstance(raw_item, dict) and 'results' in raw_item:
results = raw_item['results']
results_str = ""
try:
results_str = json.dumps(results) if results else ""
except Exception as e:
print(f"Error serializing results: {str(e)}")
results_str = str(results)
tool_call_output_msg = {
'content': results_str,
'role': 'tool',
'sender': None,
'tool_calls': None,
'tool_call_id': call_id,
'tool_name': 'web_search',
'response_type': 'internal'
}
print(f"Condition for tool call output matched in run_item_stream_event. Appending tool call output message: {tool_call_output_msg}")
messages.append(tool_call_output_msg)
except Exception as e:
print("\n=== Error in handle_web_search_event ===")
print(f"Error: {str(e)}")
print(f"Event type: {event.type}")
if hasattr(event, 'item'):
print(f"Event item type: {event.item.type}")
print("Event item details:")
print(f"Raw item: {event.item.raw_item}")
if hasattr(event.item.raw_item, '__dict__'):
print(f"Raw item attributes: {event.item.raw_item.__dict__}")
print(f"Traceback: {traceback.format_exc()}")
print("=" * 50)
raise
if messages:
print("-"*100)
print(f"Web search related messages: {messages}")
print("-"*100)
return messages

View file

@ -1,60 +0,0 @@
from src.utils.common import common_logger
logger = common_logger
from .access import get_agent_data_by_name
def reset_current_turn(messages):
# Set all messages' current_turn to False
for msg in messages:
msg["current_turn"] = False
# Find most recent user message
messages[-1]["current_turn"] = True
return messages
def reset_current_turn_agent_history(agent_data, agent_names):
for name in agent_names:
data = get_agent_data_by_name(name, agent_data)
if data:
for msg in data["history"]:
msg["current_turn"] = False
return agent_data
def add_recent_messages_to_history(recent_messages, last_agent_name, agent_data, messages, parent_has_child_history):
last_msg = messages[-1]
specific_agent_data = get_agent_data_by_name(last_agent_name, agent_data)
if specific_agent_data:
specific_agent_data["history"].extend(recent_messages)
if parent_has_child_history:
current_agent_data = specific_agent_data
while current_agent_data.get("most_recent_parent_name"):
parent_name = current_agent_data.get("most_recent_parent_name")
parent_agent_data = get_agent_data_by_name(parent_name, agent_data)
if parent_agent_data:
parent_agent_data["history"].extend(recent_messages)
current_agent_data = parent_agent_data
else:
logger.error(f"Parent agent data for {current_agent_data['name']} not found in agent_data")
raise ValueError(f"Parent agent data for {current_agent_data['name']} not found in agent_data")
else:
agent_data.append({
"name": last_agent_name,
"history": [last_msg]
})
return agent_data
def construct_state_from_response(response, agents):
agent_data = []
for agent in agents:
agent_data.append({
"name": agent.name,
"instructions": agent.instructions
})
state = {
"last_agent_name": response.agent.name,
"agent_data": agent_data
}
return state

View file

@ -1,44 +0,0 @@
from src.utils.common import common_logger
logger = common_logger
def create_transfer_function_to_agent(agent):
agent_name = agent.name
fn_spec = {
"name": f"transfer_to_{agent_name.lower().replace(' ', '_')}",
"description": f"Function to transfer the chat to {agent_name}.",
"return_value": agent
}
def generated_function(*args, **kwargs):
logger.info(f"Transferring chat to {agent_name}")
return fn_spec.get('return_value', None)
generated_function.__name__ = fn_spec['name']
generated_function.__doc__ = fn_spec.get('description', '')
return generated_function
def create_transfer_function_to_parent_agent(parent_agent, children_aware_of_parent, transfer_functions):
if children_aware_of_parent:
name = f"{transfer_functions[parent_agent.name].__name__}_from_child"
description = f"Function to transfer the chat to your parent agent: {parent_agent.name}."
else:
name = "give_up_chat_control"
description = "Function to give up control of the chat when you are unable to handle it."
fn_spec = {
"name": name,
"description": description,
"return_value": parent_agent
}
def generated_function(*args, **kwargs):
logger.info(f"Transferring chat to parent agent: {parent_agent.name}")
return fn_spec.get('return_value', None)
generated_function.__name__ = fn_spec['name']
generated_function.__doc__ = fn_spec.get('description', '')
return generated_function

View file

@ -1,105 +0,0 @@
########################
# Instructions for agents that use RAG
########################
RAG_INSTRUCTIONS = f"""
# Instructions about using the article retrieval tool
- Where relevant, use the articles tool: {{rag_tool_name}} to fetch articles with knowledge relevant to the query and use its contents to respond to the user.
- Do not send a separate message first asking the user to wait while you look up information. Immediately fetch the articles and respond to the user with the answer to their query.
- Do not make up information. If the article's contents do not have the answer, give up control of the chat (or transfer to your parent agent, as per your transfer instructions). Do not say anything to the user.
"""
########################
# Instructions for child agents that are aware of parent agents
########################
TRANSFER_PARENT_AWARE_INSTRUCTIONS = f"""
# Instructions about using your parent agents
You have the following candidate parent agents that you can transfer the chat to, using the appropriate tool calls for the transfer:
{{candidate_parents_name_description_tools}}.
## Notes:
- During runtime, you will be provided with a tool call for exactly one of these parent agents that you can use. Use that tool call to transfer the chat to the parent agent in case you are unable to handle the chat (e.g. if it is not in your scope of instructions).
- Transfer the chat to the appropriate agent, based on the chat history and / or the user's request.
- 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.
- 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.
- 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.
"""
########################
# Instructions for child agents that give up control to parent agents
########################
TRANSFER_GIVE_UP_CONTROL_INSTRUCTIONS = f"""
# Instructions about giving up chat control
If 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.
{{candidate_parents_name_description_tools}}
## Notes:
- 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.
"""
########################
# Instructions for parent agents that need to transfer the chat to other specialized (children) agents
########################
TRANSFER_CHILDREN_INSTRUCTIONS = f"""
# Instructions about using other specialized agents
You have the following specialized agents that you can transfer the chat to, using the appropriate tool calls for the transfer:
{{other_agent_name_descriptions_tools}}
## Notes:
- Transfer the chat to the appropriate agent, based on the chat history and / or the user's request.
- 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.
- 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.
- 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.
"""
########################
# Additional instruction for escalation agent when called due to an error
########################
ERROR_ESCALATION_AGENT_INSTRUCTIONS = f"""
# Context
The rest of the parts of the chatbot were unable to handle the chat. Hence, the chat has been escalated to you. In addition to your other instructions, tell the user that you are having trouble handling the chat - say "I'm having trouble helping with your request. Sorry about that.". Remember you are a part of the chatbot as well.
"""
########################
# Universal system message formatting
########################
SYSTEM_MESSAGE = f"""
# Additional System-Wide Context or Instructions:
{{system_message}}
"""
########################
# Instructions for non-repeat child transfer
########################
CHILD_TRANSFER_RELATED_INSTRUCTIONS = f"""
# Critical Rules for Agent Transfers and Handoffs
- SEQUENTIAL TRANSFERS AND RESPONSES:
1. BEFORE transferring to any agent:
- Plan your complete sequence of needed transfers
- Document which responses you need to collect
2. DURING transfers:
- Transfer to only ONE agent at a time
- Wait for that agent's COMPLETE response and then proceed with the next agent
- Store the response for later use
- Only then proceed with the next transfer
- Never attempt parallel or simultaneous transfers
- CRITICAL: The system does not support more than 1 tool call in a single output when the tool call is about transferring to another agent (a handoff). You must only put out 1 transfer related tool call in one output.
3. AFTER receiving a response:
- Do not transfer to another agent until you've processed the current response
- If you need to transfer to another agent, wait for your current processing to complete
- Never transfer back to an agent that has already responded
- COMPLETION REQUIREMENTS:
- Never provide final response until ALL required agents have been consulted
- Never attempt to get multiple responses in parallel
- If a transfer is rejected due to multiple handoffs:
1. Complete current response processing
2. Then retry the transfer as next in sequence
3. Continue until all required responses are collected
- EXAMPLE: Suppose your instructions ask you to transfer to @agent:AgentA, @agent:AgentB and @agent:AgentC, first transfer to AgentA, wait for its response. Then transfer to AgentB, wait for its response. Then transfer to AgentC, wait for its response. Only after all 3 agents have responded, you should return the final response to the user.
"""

View file

@ -1,148 +0,0 @@
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"),
api_key=os.environ.get("QDRANT_API_KEY") or None
)
# 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(f"Sources: {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(f"Valid source ids: {valid_source_ids}")
# If no valid sources are found, return empty results
if not valid_source_ids:
return ''
# Perform Qdrant vector search
print(f"Calling Qdrant search with limit {k}")
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(f"Return type: {return_type}")
print(f"Results: {results}")
# If return_type is 'chunks', return the results directly
if return_type == "chunks":
chunks = json.dumps({"Information": results}, indent=2)
print(f"Returning chunks: {chunks}")
return chunks
# 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
docs = json.dumps({"Information": results}, indent=2)
print(f"Returning docs: {docs}")
return docs
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

@ -1,46 +0,0 @@
import json
import random
def tool_raise_error(error_message):
print(f"Raising error: {error_message}")
raise ValueError(f"Raising error: {error_message}")
def respond_to_tool_raise_error(tool_calls, mock=False):
error_message = json.loads(tool_calls[0]["function"]["arguments"]).get("error_message", "")
return _create_tool_response(tool_calls, tool_raise_error(error_message))
def tool_close_chat(error_message):
print(f"Closing chat: {error_message}")
raise ValueError(f"Closing chat: {error_message}")
def respond_to_tool_close_chat(tool_calls, mock=False):
error_message = json.loads(tool_calls[0]["function"]["arguments"]).get("error_message", "")
return _create_tool_response(tool_calls, tool_close_chat(error_message))
def _create_tool_response(tool_calls, content, mock=False):
"""
Creates a standardized tool response format.
"""
return {
"role": "tool",
"content": content,
"tool_call_id": tool_calls[0]["id"],
"name": tool_calls[0]["function"]["name"]
}
def create_error_tool_call(error_message):
error_message_tool_call = {
"role": "assistant",
"sender": "system",
"tool_calls": [
{
"function": {
"name": "raise_error",
"arguments": "{\"error_message\":\"" + error_message + "\"}"
},
"id": "call_" + ''.join(random.choices('abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789', k=24)),
"type": "function"
}
]
}
return error_message_tool_call

View file

@ -1,212 +0,0 @@
from agents import TracingProcessor
import logging
from datetime import datetime, timedelta
import json
logger = logging.getLogger(__name__)
class AgentTurnTraceProcessor(TracingProcessor):
"""Custom trace processor to print detailed information about agent turns."""
def __init__(self):
self.span_depth = {} # Track depth of each span
self.handoff_chain = [] # Track sequence of agent handoffs
self.message_flow = [] # Track message flow between agents
def _get_indent_level(self, span):
"""Calculate indent level based on parent_id chain."""
depth = 0
current_id = span.parent_id
while current_id:
depth += 1
current_id = self.span_depth.get(current_id)
return depth
def _format_time(self, timestamp_str):
"""Convert ISO timestamp string to formatted time string in IST timezone."""
try:
dt = datetime.fromisoformat(timestamp_str.replace('Z', '+00:00'))
# Add 5 hours and 30 minutes for IST timezone
dt = dt + timedelta(hours=5, minutes=30)
return dt.strftime("%H:%M:%S.%f")[:-3]
except (ValueError, AttributeError):
return "00:00:00.000"
def _calculate_duration(self, start_str, end_str):
"""Calculate duration between two ISO timestamp strings in seconds."""
try:
start = datetime.fromisoformat(start_str.replace('Z', '+00:00'))
end = datetime.fromisoformat(end_str.replace('Z', '+00:00'))
return (end - start).total_seconds()
except (ValueError, AttributeError):
return 0.0
def _get_span_id(self, span):
"""Safely get span identifier."""
for attr in ['span_id', 'id', 'trace_id']:
if hasattr(span, attr):
return getattr(span, attr)
return None
def _print_handoff_chain(self, indent=""):
"""Print the current handoff chain."""
if self.handoff_chain:
print(f"{indent}Current Handoff Chain:")
print(f"{indent} {' -> '.join(self.handoff_chain)}")
def _print_message_flow(self, indent=""):
"""Print the message flow history."""
if self.message_flow:
print(f"{indent}Message Flow History:")
for msg in self.message_flow:
print(f"{indent} {msg}")
def on_trace_start(self, trace):
"""Called when a trace starts."""
separator = "="*100
print("\n" + separator)
print("🚀 TRACE START")
print(f"Name: {trace.name}")
print(f"ID: {trace.trace_id}")
if trace.metadata:
print("\nMetadata:")
for key, value in trace.metadata.items():
print(f" {key}: {value}")
print(separator + "\n")
# Reset tracking for new trace
self.handoff_chain = []
self.message_flow = []
def on_trace_end(self, trace):
"""Called when a trace ends."""
separator = "="*100
print("\n" + separator)
print("✅ TRACE END")
print(f"Name: {trace.name}")
print(f"ID: {trace.trace_id}")
# Print final chain state
print("\nFinal State:")
self._print_handoff_chain(" ")
self._print_message_flow(" ")
print(separator + "\n")
# Clear tracking
self.span_depth.clear()
self.handoff_chain = []
self.message_flow = []
def on_span_start(self, span):
"""Called when a span starts."""
try:
indent = " " * self._get_indent_level(span)
start_time = self._format_time(span.started_at)
span_id = self._get_span_id(span)
# Track span depth
if span.parent_id and span_id:
self.span_depth[span_id] = span.parent_id
# Print span header with clear section separator
print(f"\n{indent}{'>'*40}")
print(f"{indent}▶ [{start_time}] {span.span_data.type.upper()} SPAN START")
print(f"{indent} ID: {span_id}")
print(f"{indent} Parent ID: {span.parent_id}")
data = span.span_data.export()
# Print span-specific information
if span.span_data.type == "agent":
agent_name = data.get('name', 'Unknown')
print(f"{indent} Agent: {agent_name}")
print(f"{indent} Handoffs: {', '.join(data.get('handoffs', []))}")
# Track agent in handoff chain
if agent_name not in self.handoff_chain:
self.handoff_chain.append(agent_name)
self._print_handoff_chain(indent + " ")
elif span.span_data.type == "generation":
print(f"{indent} Model: {data.get('model', 'Unknown')}")
messages = data.get('messages', [])
if messages:
print(f"{indent} Messages: {len(messages)} message(s)")
elif span.span_data.type == "function":
print(f"{indent} Function: {data.get('name', 'Unknown')}")
args = data.get('arguments')
if args:
print(f"{indent} Arguments: {args}")
elif span.span_data.type == "handoff":
from_agent = data.get('from_agent', 'Unknown')
to_agent = data.get('to_agent', 'Unknown')
print(f"{indent} From: {from_agent}")
print(f"{indent} To: {to_agent}")
# Track handoff in message flow
flow_msg = f"{from_agent} -> {to_agent}"
self.message_flow.append(flow_msg)
print(f"{indent} Message Flow:")
for msg in self.message_flow[-3:]: # Show last 3 flows
print(f"{indent} {msg}")
print(f"{indent}{'>'*40}")
except Exception as e:
print(f"\n❌ Error in on_span_start: {str(e)}")
import traceback
print(traceback.format_exc())
def on_span_end(self, span):
"""Called when a span ends."""
try:
indent = " " * self._get_indent_level(span)
end_time = self._format_time(span.ended_at)
duration = self._calculate_duration(span.started_at, span.ended_at)
# Print span end information with clear section separator
print(f"\n{indent}{'<'*40}")
print(f"{indent}◀ [{end_time}] {span.span_data.type.upper()} SPAN END")
print(f"{indent} Duration: {duration:.3f}s")
data = span.span_data.export()
# Print span-specific output
if span.span_data.type == "generation":
output = data.get('output')
if output:
print(f"{indent} Output: {str(output)[:200]}...")
elif span.span_data.type == "function":
output = data.get('output')
if output:
print(f"{indent} Output: {str(output)[:200]}...")
elif span.span_data.type == "handoff":
self._print_handoff_chain(indent + " ")
self._print_message_flow(indent + " ")
print(f"{indent}{'<'*40}")
# Clean up span depth tracking
span_id = self._get_span_id(span)
if span_id and span_id in self.span_depth:
del self.span_depth[span_id]
except Exception as e:
print(f"\n❌ Error in on_span_end: {str(e)}")
import traceback
print(traceback.format_exc())
def shutdown(self):
"""Called when the processor is shutting down."""
self.span_depth.clear()
self.handoff_chain = []
self.message_flow = []
def force_flush(self):
"""Called to force flush any buffered traces/spans."""
pass

View file

@ -1,26 +0,0 @@
from enum import Enum
class AgentRole(Enum):
ESCALATION = "escalation"
POST_PROCESSING = "post_process"
GUARDRAILS = "guardrails"
class outputVisibility(Enum):
EXTERNAL = "user_facing"
INTERNAL = "internal"
class ResponseType(Enum):
INTERNAL = "internal"
EXTERNAL = "external"
class ControlType(Enum):
RETAIN = "retain"
PARENT_AGENT = "relinquish_to_parent"
START_AGENT = "start_agent"
class PromptType(Enum):
STYLE = "style_prompt"
GREETING = "greeting"
class ErrorType(Enum):
FATAL = "fatal"
ESCALATE = "escalate"

View file

@ -1,38 +0,0 @@
import os
import logging
from openai import AsyncOpenAI, OpenAI
import dotenv
dotenv.load_dotenv()
PROVIDER_BASE_URL = os.getenv('PROVIDER_BASE_URL', '')
PROVIDER_API_KEY = os.getenv('PROVIDER_API_KEY')
PROVIDER_DEFAULT_MODEL = os.getenv('PROVIDER_DEFAULT_MODEL')
client = None
if not PROVIDER_API_KEY:
PROVIDER_API_KEY = os.getenv('OPENAI_API_KEY')
if not PROVIDER_API_KEY:
raise(ValueError("No LLM Provider API key found"))
if not PROVIDER_DEFAULT_MODEL:
PROVIDER_DEFAULT_MODEL = 'gpt-4.1'
if PROVIDER_BASE_URL:
print(f"Using provider {PROVIDER_BASE_URL}")
client = AsyncOpenAI(base_url=PROVIDER_BASE_URL, api_key=PROVIDER_API_KEY)
else:
print("No provider base URL configured, using OpenAI directly")
completions_client = None
if PROVIDER_BASE_URL:
print(f"Using provider {PROVIDER_BASE_URL} for completions")
completions_client = OpenAI(
base_url=PROVIDER_BASE_URL,
api_key=PROVIDER_API_KEY
)
else:
print(f"Using OpenAI directly for completions")
completions_client = OpenAI(
api_key=PROVIDER_API_KEY
)

View file

@ -1,195 +0,0 @@
import json
import logging
import os
import subprocess
import sys
import time
from dotenv import load_dotenv
from openai import OpenAI
from src.utils.client import completions_client
load_dotenv()
def setup_logger(name, log_file='./run.log', level=logging.INFO, log_to_file=False):
"""Function to set up a logger with a specific name and log file."""
formatter = logging.Formatter('%(asctime)s %(levelname)s [%(filename)s:%(lineno)d] %(message)s')
# Changed to use stderr instead of stdout
handler = logging.StreamHandler(sys.stderr)
handler.setFormatter(formatter)
# Create a logger and set its level
logger = logging.getLogger(name)
logger.setLevel(level)
# Clear any existing handlers to avoid duplicates
if logger.hasHandlers():
logger.handlers.clear()
# Prevent propagation to parent loggers
logger.propagate = False
logger.addHandler(handler)
return logger
common_logger = setup_logger('logger')
logger = common_logger
def read_json_from_file(file_name):
logger.info(f"Reading json from {file_name}")
try:
with open(file_name, 'r') as file:
out = file.read()
out = json.loads(out)
return out
except Exception as e:
logger.error(e)
return None
def get_api_key(key_name):
api_key = os.getenv(key_name)
# Check if the API key was loaded successfully
if not api_key:
raise ValueError(f"{key_name} not found. Did you set it in the .env file?")
return api_key
def generate_gpt4o_output_from_multi_turn_conv(messages, output_type='json', model="gpt-4o"):
return generate_openai_output(messages, output_type, model)
def generate_openai_output(messages, output_type='not_json', model="gpt-4o", return_completion=False):
print(f"In generate_openai_output, using client: {completions_client} and model: {model}")
try:
if output_type == 'json':
chat_completion = completions_client.chat.completions.create(
model=model,
messages=messages,
response_format={"type": "json_object"}
)
else:
chat_completion = completions_client.chat.completions.create(
model=model,
messages=messages,
)
if return_completion:
return chat_completion
return chat_completion.choices[0].message.content
except Exception as e:
logger.error(e)
return None
def generate_llm_output(messages, model):
model_provider = None
if "gpt" in model:
model_provider = "openai"
else:
raise ValueError(f"Model {model} not supported")
if model_provider == "openai":
response = generate_openai_output(messages, output_type='text', model=model)
return response
def generate_gpt4o_output_from_multi_turn_conv_multithreaded(messages, retries=5, delay=1, output_type='json'):
while retries > 0:
try:
# Call GPT-4o API
output = generate_gpt4o_output_from_multi_turn_conv(messages, output_type='json')
return output # If the request is successful, break out of the loop
except openai.RateLimitError:
print(f'Rate limit exceeded. Retrying in {delay} seconds...')
time.sleep(delay)
delay *= 2 # Exponential backoff
retries -= 1
if retries == 0:
print(f'Failed to process due to rate limit.')
return []
def convert_message_content_json_to_strings(messages):
for msg in messages:
if 'content' in msg.keys() and isinstance(msg['content'], dict):
msg['content'] = json.dumps(msg['content'])
return messages
def merge_defaultdicts(dict_parent, dict_child):
for key, value in dict_child.items():
if key in dict_parent:
# If the key exists in both, handle merging based on type
if isinstance(dict_parent[key], list):
dict_parent[key].extend(value)
elif isinstance(dict_parent[key], dict):
dict_parent[key].update(value)
elif isinstance(dict_parent[key], set):
dict_parent[key].update(value)
else:
dict_parent[key] += value # For other types like int, float, etc.
else:
dict_parent[key] = value
return dict_parent
def read_jsonl_from_file(file_name):
# logger.info(f"Reading jsonl from {file_name}")
try:
with open(file_name, 'r') as file:
lines = file.readlines()
dataset = [json.loads(line.strip()) for line in lines]
return dataset
except Exception as e:
logger.error(e)
return None
def write_jsonl_to_file(list_dicts, file_name):
try:
with open(file_name, 'w') as file:
for d in list_dicts:
file.write(json.dumps(d)+'\n')
return True
except Exception as e:
logger.error(e)
return False
def read_text_from_file(file_name):
try:
with open(file_name, 'r') as file:
out = file.read()
return out
except Exception as e:
logger.error(e)
return None
def write_json_to_file(data, file_name):
try:
with open(file_name, 'w') as file:
json.dump(data, file, indent=4)
return True
except Exception as e:
logger.error(e)
return False
def get_git_path(path):
# Run `git rev-parse --show-toplevel` to get the root of the Git repository
try:
git_root = subprocess.check_output(["git", "rev-parse", "--show-toplevel"], text=True).strip()
return f"{git_root}/{path}"
except subprocess.CalledProcessError:
raise RuntimeError("Not inside a Git repository")
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,22 +0,0 @@
from src.utils.common import common_logger, read_json_from_file
logger = common_logger
logger.info("Running swarm_flask_client.py")
import requests
if __name__ == "__main__":
import argparse
parser = argparse.ArgumentParser()
parser.add_argument('--sample_request', type=str, required=True, help='Sample request JSON file name under tests/sample_requests/')
parser.add_argument('--api_key', type=str, required=True, help='API key to use for authentication')
parser.add_argument('--host', type=str, required=False, help='Host to use for the request', default='http://localhost:4040')
args = parser.parse_args()
request = read_json_from_file(f"./tests/sample_requests/{args.sample_request}").get("lastRequest", {})
print("Sending request...")
response = requests.post(
f"{args.host}/chat",
json=request,
headers={'Authorization': f'Bearer {args.api_key}'}
).json()
print("Output: ")
print(response)

View file

@ -1,137 +0,0 @@
from src.utils.common import read_json_from_file
import requests
import json
import argparse
from datetime import datetime
print("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")
try:
print("\n" + "-"*80)
print("Connecting to stream...")
stream_response = requests.post(
f"{host}/chat_stream",
json=request_data,
headers={
'Authorization': f'Bearer {api_key}',
'Accept': 'text/event-stream'
},
stream=True
)
if stream_response.status_code != 200:
print(f"Error connecting to stream. Status code: {stream_response.status_code}")
print(f"Response: {stream_response.text}")
return
print(f"Successfully connected to stream")
print("-"*80 + "\n")
event_count = 0
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} at {datetime.now().isoformat()}")
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

@ -1,207 +0,0 @@
import copy
from datetime import datetime
import json
import sys
import asyncio
import requests
import argparse
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
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")
try:
print("\n" + "-"*80)
print("Connecting to stream...")
stream_response = requests.post(
f"{host}/chat_stream",
json=request_data,
headers={
'Authorization': f'Bearer {api_key}',
'Accept': 'text/event-stream'
},
stream=True
)
if stream_response.status_code != 200:
print(f"Error connecting to stream. Status code: {stream_response.status_code}")
print(f"Response: {stream_response.text}")
return None, None
print(f"Successfully connected to stream")
print("-"*80 + "\n")
event_count = 0
collected_messages = []
final_state = None
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} at {datetime.now().isoformat()}")
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))
# Collect messages
collected_messages.append(event_data)
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")
return collected_messages, final_state
if __name__ == "__main__":
logger.info(f"{'*'*50}Running interactive mode{'*'*50}")
parser = argparse.ArgumentParser()
parser.add_argument('--config', type=str, required=False, default='default_config.json',
help='Config file name under configs/')
parser.add_argument('--sample_request', type=str, required=False, default='default_example.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')
parser.add_argument('--load_messages', action='store_true',
help='Load messages from sample request file')
args = parser.parse_args()
print(f"Config file: {args.config}")
print(f"Sample request file: {args.sample_request}")
config = read_json_from_file(f"./configs/{args.config}")
example_request = read_json_from_file(f"./tests/sample_requests/{args.sample_request}").get("lastRequest", {})
if args.load_messages:
messages = example_request.get("messages", [])
user_input_needed = False
else:
messages = []
user_input_needed = True
state = example_request.get("state", {})
start_agent_name = example_request.get("startAgent", "")
last_agent_name = state.get("last_agent_name", "")
if not last_agent_name:
last_agent_name = start_agent_name
logger.info("Starting main conversation loop")
start_time = None
while True:
logger.info("Loading configuration files")
# To account for updates to state
complete_request = copy.deepcopy(example_request)
complete_request["messages"] = messages
complete_request["state"] = state
complete_request["startAgent"] = start_agent_name
print(f"\nUsing agent: {last_agent_name}")
if user_input_needed:
user_inp = input('\nUSER: ')
messages.append({
"role": "user",
"content": user_inp
})
if user_inp == 'exit':
logger.info("User requested exit")
break
logger.info("Added user message to conversation")
start_time = datetime.now()
# Preprocess messages
print("Preprocessing messages")
messages = preprocess_messages(messages)
complete_request["messages"] = preprocess_messages(complete_request["messages"])
# Run the streaming turn
resp_messages, resp_state = stream_chat(
host=args.host,
request_data=complete_request,
api_key=args.api_key
)
if resp_messages:
state = resp_state
if config.get("return_diff_messages", True):
messages.extend(resp_messages)
else:
messages = resp_messages
user_input_needed = True
print("Quick stats")
print(f"Turn Duration: {round((datetime.now() - start_time).total_seconds() * 10) / 10:.1f}s")
print('='*50)
print("\n" + "-" * 80)

View file

@ -1,235 +0,0 @@
{
"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

@ -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,121 +0,0 @@
{
"lastRequest": {
"messages": [
{
"content": "hi",
"role": "user",
"sender": null,
"tool_calls": null,
"tool_call_id": null,
"tool_name": null
}
],
"state": {
"last_agent_name": "Cashout Agent"
},
"agents": [
{
"name": "Guardrails",
"type": "guardrails",
"description": "",
"instructions": "Stick to the facts and do not make any assumptions.\n\n",
"tools": [],
"model": "gpt-4o-mini",
"hasRagSources": false,
"connectedAgents": [],
"controlType": "retain"
},
{
"name": "Post process",
"type": "post_process",
"description": "",
"instructions": "Ensure that the agent response is terse and to the point.\n\n",
"tools": [],
"model": "gpt-4o-mini",
"hasRagSources": false,
"connectedAgents": [],
"controlType": "retain"
},
{
"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"
},
{
"name": "Cashout Agent",
"type": "conversation",
"description": "Responsible for handling user cashouts.",
"instructions": "## 🧑‍💼 Role:\n\nYou are responsible for assisting users with cashing out their balance.\n\n---\n\n## ⚙️ Steps to Follow:\n\n1. Check if any 'no-cashout' flag is set for the user using the function: check_no_cashout_flag.\n2. Retrieve the user balance using the function: get_user_balance.\n3. If the balance is greater than zero, provide the user with the steps to cash out:\n - Step 1: Navigate to the 'Cashout' section in the application.\n - Step 2: Select the amount you wish to cash out.\n - Step 3: Confirm the transaction details.\n - Step 4: Submit the request and wait for confirmation.\n4. If the balance is zero or negative, pass control to the Crypto Cashout agent.\n\n---\n\n## 🎯 Scope:\n\n✅ In Scope:\n- Checking user-specific flags related to cashout.\n- Providing steps for cashing out when balance permits.\n\n❌ Out of Scope:\n- Detailed financial advice beyond cashing out.\n\n---\n\n## 📋 Guidelines:\n\n✔ Dos:\n- Use check_no_cashout_flag and get_user_balance functions appropriately.\n- Provide clear cashout instructions when conditions are met.\n\n🚫 Don'ts:\n- Do not proceed with cashout steps if 'no-cashout' flag is present.\n\n---\n\n",
"tools": [
"check_no_cashout_flag",
"get_user_balance"
],
"model": "gpt-4o",
"hasRagSources": false,
"connectedAgents": [
"Crypto Cashout Agent"
],
"controlType": "relinquish_to_parent"
},
{
"name": "Crypto Cashout Agent",
"type": "conversation",
"description": "Handles scenarios where the cashout balance is zero or negative and considers cryptocurrency options.",
"instructions": "This is a dummy agent for handling cashout through cryptocurrency when the balance is insufficient.\n\n",
"tools": [],
"model": "gpt-4o",
"hasRagSources": false,
"connectedAgents": [],
"controlType": "retain"
}
],
"tools": [
{
"name": "check_no_cashout_flag",
"description": "Function to check if the user has a 'no-cashout' flag.",
"parameters": {
"type": "object",
"properties": {
"userId": {
"type": "string",
"description": "The ID of the user to check the cashout flag for."
}
},
"required": [
"userId"
]
}
},
{
"name": "get_user_balance",
"description": "Function to retrieve the user's current balance.",
"parameters": {
"type": "object",
"properties": {
"userId": {
"type": "string",
"description": "The ID of the user to retrieve the balance for."
}
},
"required": [
"userId"
]
}
}
],
"prompts": [
{
"name": "Style prompt",
"type": "style_prompt",
"prompt": "You should be empathetic and helpful."
}
],
"startAgent": "Cashout Agent"
}
}

View file

@ -1,94 +0,0 @@
{
"lastRequest": {
"messages": [
{
"content": "please call the mocked tool",
"role": "user",
"sender": null,
"tool_calls": null,
"tool_call_id": null,
"tool_name": null
}
],
"state": {
"last_agent_name": "Example Agent"
},
"agents": [
{
"name": "Example Agent",
"type": "conversation",
"description": "",
"instructions": "## 🧑‍ Role:\nYou are an helpful customer support assistant\n\n---\n## ⚙️ Steps to Follow:\n1. Ask the user what they would like help with\n2. Ask the user for their email address and let them know someone will contact them soon.\n\n---\n## 🎯 Scope:\n✅ In Scope:\n- Asking the user their issue\n- Getting their email\n\n❌ Out of Scope:\n- Questions unrelated to customer support\n- If a question is out of scope, politely inform the user and avoid providing an answer.\n\n---\n## 📋 Guidelines:\n✔ Dos:\n- ask user their issue\n\n❌ Don'ts:\n- don't ask user any other detail than email\n\n",
"tools": [
"unmocked_tool",
"mocked_tool"
],
"model": "gpt-4o-mini",
"hasRagSources": false,
"connectedAgents": [],
"controlType": "retain"
},
{
"name": "Guardrails",
"type": "guardrails",
"description": "",
"instructions": "Stick to the facts and do not make any assumptions.\n\n",
"tools": [],
"model": "gpt-4o-mini",
"hasRagSources": false,
"connectedAgents": [],
"controlType": "retain"
},
{
"name": "Post process",
"type": "post_process",
"description": "",
"instructions": "Ensure that the agent response is terse and to the point.\n\n",
"tools": [],
"model": "gpt-4o-mini",
"hasRagSources": false,
"connectedAgents": [],
"controlType": "retain"
},
{
"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": "unmocked_tool",
"description": "",
"parameters": {
"type": "object",
"properties": {},
"required": []
}
},
{
"name": "mocked_tool",
"description": "",
"parameters": {
"type": "object",
"properties": {},
"required": []
}
}
],
"prompts": [
{
"name": "Style prompt",
"type": "style_prompt",
"prompt": "You should be empathetic and helpful."
}
],
"startAgent": "Example Agent"
}
}

View file

@ -1,469 +0,0 @@
{
"messages": [
{
"role": "system",
"content": ""
},
{
"version": "v1",
"chatId": "",
"createdAt": "2025-05-06T10:20:53.651Z",
"role": "assistant",
"content": "How can I help you today?",
"agenticSender": "Blog Writer Hub",
"agenticResponseType": "external"
},
{
"role": "user",
"content": "I want to write a blog",
"version": "v1",
"chatId": "",
"createdAt": "2025-05-06T10:20:58.206Z"
},
{
"version": "v1",
"chatId": "",
"createdAt": "2025-05-06T10:20:59.319Z",
"role": "assistant",
"content": "Great! What topic would you like your blog to be about?",
"agenticSender": "Blog Writer Hub",
"agenticResponseType": "external"
},
{
"role": "user",
"content": "Bitcoin",
"version": "v1",
"chatId": "",
"createdAt": "2025-05-06T10:21:05.048Z"
},
{
"version": "v1",
"chatId": "",
"createdAt": "2025-05-06T10:21:07.311Z",
"role": "assistant",
"tool_calls": [
{
"id": "62a9b5ec-3dbf-4fdd-85cb-10da37611726",
"function": {
"name": "transfer_to_agent",
"arguments": "{\"assistant\": \"Research Agent\"}"
},
"type": "function"
}
],
"agenticSender": "Blog Writer Hub",
"agenticResponseType": "internal"
},
{
"version": "v1",
"chatId": "",
"createdAt": "2025-05-06T10:21:07.311Z",
"role": "tool",
"content": "{\"assistant\": \"Research Agent\"}",
"tool_call_id": "62a9b5ec-3dbf-4fdd-85cb-10da37611726",
"tool_name": "transfer_to_agent"
},
{
"version": "v1",
"chatId": "",
"createdAt": "2025-05-06T10:21:10.018Z",
"role": "assistant",
"content": "Bitcoin is a decentralized digital currency that operates without a central authority or government, relying on blockchain technology to record transactions securely and transparently. It was created in 2009 by an anonymous person or group known as Satoshi Nakamoto, aiming to provide an alternative to traditional financial systems. Bitcoin's value is highly volatile, influenced by factors such as market demand, regulatory developments, and macroeconomic trends. It is widely used for investment, remittances, and as a means of transferring value across borders, but it also faces criticism for its association with illicit activities and environmental concerns due to energy-intensive mining processes. Despite these challenges, Bitcoin continues to gain mainstream acceptance, with increasing adoption by businesses and institutional investors.",
"agenticSender": "Research Agent",
"agenticResponseType": "internal"
},
{
"version": "v1",
"chatId": "",
"createdAt": "2025-05-06T10:21:10.919Z",
"role": "assistant",
"tool_calls": [
{
"id": "2085cbb1-270e-4c20-a6ef-6f9e4a3d27cf",
"function": {
"name": "transfer_to_agent",
"arguments": "{\"assistant\": \"Bullet Point Agent\"}"
},
"type": "function"
}
],
"agenticSender": "Blog Writer Hub",
"agenticResponseType": "internal"
},
{
"version": "v1",
"chatId": "",
"createdAt": "2025-05-06T10:21:10.920Z",
"role": "tool",
"content": "{\"assistant\": \"Bullet Point Agent\"}",
"tool_call_id": "2085cbb1-270e-4c20-a6ef-6f9e4a3d27cf",
"tool_name": "transfer_to_agent"
},
{
"version": "v1",
"chatId": "",
"createdAt": "2025-05-06T10:21:14.776Z",
"role": "assistant",
"content": "- Bitcoin is a decentralized digital currency that operates without a central authority or government.\n- It uses blockchain technology to securely and transparently record transactions.\n- Created in 2009 by the anonymous Satoshi Nakamoto as an alternative to traditional financial systems.\n- Bitcoin's value is highly volatile, affected by market demand, regulations, and macroeconomic trends.\n- Common uses include investment, remittances, and cross-border value transfers.\n- Criticized for links to illicit activities and environmental impact from energy-intensive mining.\n- Despite challenges, Bitcoin is gaining mainstream acceptance and adoption by businesses and institutional investors.",
"agenticSender": "Bullet Point Agent",
"agenticResponseType": "internal"
},
{
"version": "v1",
"chatId": "",
"createdAt": "2025-05-06T10:21:15.946Z",
"role": "assistant",
"tool_calls": [
{
"id": "d48c608a-a660-451b-a3ee-eeebb1cf60ca",
"function": {
"name": "transfer_to_agent",
"arguments": "{\"assistant\": \"Blog Writing Agent\"}"
},
"type": "function"
}
],
"agenticSender": "Blog Writer Hub",
"agenticResponseType": "internal"
},
{
"version": "v1",
"chatId": "",
"createdAt": "2025-05-06T10:21:15.946Z",
"role": "tool",
"content": "{\"assistant\": \"Blog Writing Agent\"}",
"tool_call_id": "d48c608a-a660-451b-a3ee-eeebb1cf60ca",
"tool_name": "transfer_to_agent"
},
{
"version": "v1",
"chatId": "",
"createdAt": "2025-05-06T10:21:32.934Z",
"role": "assistant",
"content": "Bitcoin: Revolutionizing the World of Digital Currency\n\nIn recent years, Bitcoin has become a buzzword in financial circles and beyond. As the first and most prominent cryptocurrency, it has sparked debates, inspired innovation, and challenged traditional notions of money. But what exactly is Bitcoin, and why does it continue to capture the worlds attention? Lets explore the key aspects that define Bitcoin and its growing influence.\n\nAt its core, Bitcoin is a decentralized digital currency that operates without the oversight of a central authority or government. This means that no single entity controls its issuance or transactions, setting it apart from traditional currencies managed by central banks. The decentralized nature of Bitcoin is one of its most revolutionary features, empowering individuals to have greater control over their finances.\n\nThe backbone of Bitcoins security and transparency is blockchain technology. Every transaction made with Bitcoin is recorded on a public ledger known as the blockchain. This technology ensures that transactions are secure, tamper-proof, and visible to anyone, fostering trust among users and reducing the risk of fraud.\n\nBitcoin was introduced in 2009 by an anonymous figure or group using the pseudonym Satoshi Nakamoto. The goal was to create an alternative to traditional financial systems, offering a peer-to-peer method of transferring value without intermediaries. This vision has resonated with millions, especially those seeking more autonomy over their money.\n\nOne of the most talked-about aspects of Bitcoin is its price volatility. The value of Bitcoin can fluctuate dramatically, influenced by factors such as market demand, regulatory changes, and broader economic trends. While this volatility presents opportunities for investors, it also introduces risks that potential users should carefully consider.\n\nBitcoins uses have evolved over time. Initially, it was seen primarily as a digital alternative to cash. Today, it is widely used for investment purposes, remittances, and transferring value across borders. Its ability to facilitate fast, low-cost international transactions has made it especially popular in regions with limited access to traditional banking services.\n\nHowever, Bitcoin is not without its critics. It has faced scrutiny for its association with illicit activities, given the pseudonymous nature of transactions. Additionally, the process of mining new bitcoins is energy-intensive, raising concerns about its environmental impact.\n\nDespite these challenges, Bitcoins journey toward mainstream acceptance continues. More businesses are beginning to accept Bitcoin as payment, and institutional investors are increasingly adding it to their portfolios. This growing adoption signals a shift in how society views and uses money.\n\nIn conclusion, Bitcoin represents a significant shift in the world of finance. Its decentralized structure, innovative technology, and expanding use cases make it a fascinating subject for anyone interested in the future of money. As Bitcoin continues to evolve, it will undoubtedly play a pivotal role in shaping the digital economy of tomorrow.",
"agenticSender": "Blog Writing Agent",
"agenticResponseType": "internal"
},
{
"version": "v1",
"chatId": "",
"createdAt": "2025-05-06T10:21:38.064Z",
"role": "assistant",
"content": "Here is your complete blog post on \"Bitcoin\":\n\n---\n\n**Bitcoin: Revolutionizing the World of Digital Currency**\n\nIn recent years, Bitcoin has become a buzzword in financial circles and beyond. As the first and most prominent cryptocurrency, it has sparked debates, inspired innovation, and challenged traditional notions of money. But what exactly is Bitcoin, and why does it continue to capture the worlds attention? Lets explore the key aspects that define Bitcoin and its growing influence.\n\nAt its core, Bitcoin is a decentralized digital currency that operates without the oversight of a central authority or government. This means that no single entity controls its issuance or transactions, setting it apart from traditional currencies managed by central banks. The decentralized nature of Bitcoin is one of its most revolutionary features, empowering individuals to have greater control over their finances.\n\nThe backbone of Bitcoins security and transparency is blockchain technology. Every transaction made with Bitcoin is recorded on a public ledger known as the blockchain. This technology ensures that transactions are secure, tamper-proof, and visible to anyone, fostering trust among users and reducing the risk of fraud.\n\nBitcoin was introduced in 2009 by an anonymous figure or group using the pseudonym Satoshi Nakamoto. The goal was to create an alternative to traditional financial systems, offering a peer-to-peer method of transferring value without intermediaries. This vision has resonated with millions, especially those seeking more autonomy over their money.\n\nOne of the most talked-about aspects of Bitcoin is its price volatility. The value of Bitcoin can fluctuate dramatically, influenced by factors such as market demand, regulatory changes, and broader economic trends. While this volatility presents opportunities for investors, it also introduces risks that potential users should carefully consider.\n\nBitcoins uses have evolved over time. Initially, it was seen primarily as a digital alternative to cash. Today, it is widely used for investment purposes, remittances, and transferring value across borders. Its ability to facilitate fast, low-cost international transactions has made it especially popular in regions with limited access to traditional banking services.\n\nHowever, Bitcoin is not without its critics. It has faced scrutiny for its association with illicit activities, given the pseudonymous nature of transactions. Additionally, the process of mining new bitcoins is energy-intensive, raising concerns about its environmental impact.\n\nDespite these challenges, Bitcoins journey toward mainstream acceptance continues. More businesses are beginning to accept Bitcoin as payment, and institutional investors are increasingly adding it to their portfolios. This growing adoption signals a shift in how society views and uses money.\n\nIn conclusion, Bitcoin represents a significant shift in the world of finance. Its decentralized structure, innovative technology, and expanding use cases make it a fascinating subject for anyone interested in the future of money. As Bitcoin continues to evolve, it will undoubtedly play a pivotal role in shaping the digital economy of tomorrow.\n\n---\n\nIf you need any edits or want to focus on a specific aspect of Bitcoin, let me know!",
"agenticSender": "Blog Writer Hub",
"agenticResponseType": "external"
}
],
"lastRequest": {
"projectId": "2a4908bb-e350-4335-97df-c48615b98fe9",
"messages": [
{
"content": "",
"role": "system",
"sender": null,
"tool_calls": null,
"tool_call_id": null,
"tool_name": null
},
{
"content": "How can I help you today?",
"role": "assistant",
"sender": "Blog Writer Hub",
"tool_calls": null,
"tool_call_id": null,
"tool_name": null,
"response_type": "external"
},
{
"content": "I want to write a blog",
"role": "user",
"sender": null,
"tool_calls": null,
"tool_call_id": null,
"tool_name": null
},
{
"content": "Great! What topic would you like your blog to be about?",
"role": "assistant",
"sender": "Blog Writer Hub",
"tool_calls": null,
"tool_call_id": null,
"tool_name": null,
"response_type": "external"
},
{
"content": "Bitcoin",
"role": "user",
"sender": null,
"tool_calls": null,
"tool_call_id": null,
"tool_name": null
}
],
"state": {
"last_agent_name": "Blog Writer Hub",
"tokens": {
"total": 0,
"prompt": 0,
"completion": 0
},
"turn_messages": [
{
"content": "Sender agent: Blog Writer Hub\nContent: Great! What topic would you like your blog to be about?",
"role": "assistant",
"sender": "Blog Writer Hub",
"tool_calls": null,
"tool_call_id": null,
"tool_name": null,
"response_type": "external"
}
]
},
"agents": [
{
"name": "Blog Writer Hub",
"type": "conversation",
"description": "Hub agent to orchestrate the blog writing process: research, bullet points, and blog composition.",
"instructions": "## 🧑‍💼 Role:\nYou are the hub agent responsible for orchestrating the blog writing process for the user.\n\n---\n## ⚙️ Steps to Follow:\n1. Greet the user and ask for the blog topic.\n2. FIRST: Send the topic to [@agent:Research Agent] for research and wait for the summary.\n3. THEN: Send the research summary to [@agent:Bullet Point Agent] to generate bullet points and wait for the result.\n4. THEN: Send the bullet points to [@agent:Blog Writing Agent] to compose the full blog post and wait for the result.\n5. Return the final blog post to the user.\n\n---\n## 🎯 Scope:\n✅ In Scope:\n- Orchestrating the blog writing process\n- Interacting with the user for topic input and delivering the final blog post\n\n❌ Out of Scope:\n- Performing research, bullet point generation, or blog writing directly\n- Handling unrelated queries\n\n---\n## 📋 Guidelines:\n✔ Dos:\n- Follow the strict sequence: Research → Bullet Points → Blog Writing\n- Only interact with the user for topic input and final output\n\n🚫 Don'ts:\n- Do not perform research, bullet point generation, or blog writing yourself\n- Do not try to get multiple agent responses in parallel\n- Do not reference internal agents by name to the user\n- CRITICAL: The system does not support more than 1 tool call in a single output when the tool call is about transferring to another agent (a handoff). You must only put out 1 transfer related tool call in one output.\n\n# Examples\n- **User** : I want to write a blog about 'Benefits of remote work'.\n - **Agent actions**: Call [@agent:Research Agent]\n\n- **Agent receives research summary** :\n - **Agent actions**: Call [@agent:Bullet Point Agent]\n\n- **Agent receives bullet points** :\n - **Agent actions**: Call [@agent:Blog Writing Agent]\n\n- **Agent receives blog post** :\n - **Agent response**: Here is your complete blog post on 'Benefits of remote work':\n\n[Full blog post content]\n\n- **User** : Can I get a blog on 'AI in education'?\n - **Agent actions**: Call [@agent:Research Agent](#mention)",
"model": "gpt-4.1",
"controlType": "retain",
"ragK": 3,
"ragReturnType": "chunks",
"outputVisibility": "user_facing",
"tools": [],
"prompts": [],
"connectedAgents": [
"Research Agent",
"Bullet Point Agent",
"Blog Writing Agent"
]
},
{
"name": "Research Agent",
"type": "conversation",
"description": "Researches and summarizes information on a given blog topic.",
"instructions": "## 🧑‍💼 Role:\nYou are responsible for researching the given blog topic and providing a concise summary of key findings.\n\n---\n## ⚙️ Steps to Follow:\n1. Use the [@tool:web_search] tool to gather information about the provided topic. This is important. You must call @tool:web_search without fail even if you know about the topic yourself.\n2. Summarize the most relevant and recent findings in 3-5 sentences.\n3. Return the summary to the calling agent.\n\n---\n## 🎯 Scope:\n✅ In Scope:\n- Researching any blog topic provided\n- Summarizing findings concisely\n\n❌ Out of Scope:\n- Generating bullet points or writing the blog post\n- Interacting directly with the user\n\n---\n## 📋 Guidelines:\n✔ Dos:\n- Use only reliable sources\n- Keep the summary factual and neutral\n\n🚫 Don'ts:\n- Do not include personal opinions\n- Do not generate bullet points or blog content\n- Do not preface your actions by saying things like \"I will begin by researching...\" or \"Once I have the research summary...\". Simply call the web_search tool, summarize, and return the result to the calling agent.\n- Do not interact with the user directly.\n\n# Examples\n- **User** : Research the topic 'Benefits of remote work'.\n - **Agent actions**: Call [@tool:web_search](#mention)\n - **Agent response**: Remote work offers increased flexibility, improved work-life balance, and reduced commuting time. Studies show higher productivity and job satisfaction among remote employees. However, it can also lead to feelings of isolation if not managed well.\n\n- **User** : Research the topic 'AI in education'.\n - **Agent actions**: Call [@tool:web_search](#mention)\n - **Agent response**: Artificial intelligence in education enables personalized learning, automates administrative tasks, and provides data-driven insights. However, it raises concerns about data privacy and the need for teacher training.",
"model": "gpt-4.1",
"controlType": "retain",
"ragK": 3,
"ragReturnType": "chunks",
"outputVisibility": "internal",
"tools": [
"web_search"
],
"prompts": [],
"connectedAgents": []
},
{
"name": "Bullet Point Agent",
"type": "conversation",
"description": "Generates key bullet points from the research summary for the blog.",
"instructions": "## 🧑‍💼 Role:\nYou are responsible for extracting and generating 5-7 key bullet points from the research summary provided.\n\n---\n## ⚙️ Steps to Follow:\n1. Read the research summary.\n2. Identify the most important facts, arguments, or insights.\n3. Generate 5-7 concise bullet points covering the main ideas.\n4. Return the bullet points to the calling agent.\n\n---\n## 🎯 Scope:\n✅ In Scope:\n- Generating bullet points from research summaries\n\n❌ Out of Scope:\n- Conducting research\n- Writing the full blog post\n- Interacting directly with the user\n\n---\n## 📋 Guidelines:\n✔ Dos:\n- Ensure each bullet point is clear and self-contained\n\n🚫 Don'ts:\n- Do not repeat information\n- Do not add new information not present in the summary\n\n# Examples\n- **User** : Generate bullet points from the summary: 'Remote work offers increased flexibility, improved work-life balance, and reduced commuting time. Studies show higher productivity and job satisfaction among remote employees. However, it can also lead to feelings of isolation if not managed well.'\n - **Agent response**: \n - Increased flexibility for employees\n - Improved work-life balance\n - Reduced commuting time\n - Higher productivity among remote workers\n - Greater job satisfaction\n - Potential for isolation if not managed",
"model": "gpt-4.1",
"controlType": "retain",
"ragK": 3,
"ragReturnType": "chunks",
"outputVisibility": "internal",
"tools": [],
"prompts": [],
"connectedAgents": []
},
{
"name": "Blog Writing Agent",
"type": "conversation",
"description": "Writes a full blog post based on provided bullet points.",
"instructions": "## 🧑‍💼 Role:\nYou are responsible for composing a well-structured blog post using the provided bullet points.\n\n---\n## ⚙️ Steps to Follow:\n1. Read the bullet points.\n2. Write an engaging introduction related to the topic.\n3. Expand on each bullet point in a logical order, creating clear paragraphs.\n4. Conclude the blog post with a summary or call to action.\n5. Return the complete blog post to the calling agent.\n\n---\n## 🎯 Scope:\n✅ In Scope:\n- Writing blog posts from bullet points\n\n❌ Out of Scope:\n- Conducting research\n- Generating bullet points\n- Interacting directly with the user\n\n---\n## 📋 Guidelines:\n✔ Dos:\n- Maintain a friendly and informative tone\n- Ensure logical flow and readability\n\n🚫 Don'ts:\n- Do not add information not present in the bullet points\n- Do not interact with the user\n\n# Examples\n- **User** : Write a blog post using these bullet points: [list of bullet points]\n - **Agent response**: [A well-structured blog post expanding on each bullet point, with an introduction and conclusion]",
"model": "gpt-4.1",
"controlType": "retain",
"ragK": 3,
"ragReturnType": "chunks",
"outputVisibility": "internal",
"tools": [],
"prompts": [],
"connectedAgents": []
}
],
"tools": [
{
"name": "web_search",
"description": "Fetch information from the web based on chat context",
"parameters": {
"type": "object",
"properties": {}
},
"isLibrary": true
}
],
"prompts": [],
"startAgent": "Blog Writer Hub",
"mcpServers": [],
"toolWebhookUrl": ""
},
"lastResponse": {
"state": {
"last_agent_name": "Blog Writer Hub",
"tokens": {
"total": 0,
"prompt": 0,
"completion": 0
},
"turn_messages": [
{
"content": "Sender agent: Research Agent\nContent: Bitcoin is a decentralized digital currency that operates without a central authority or government, relying on blockchain technology to record transactions securely and transparently. It was created in 2009 by an anonymous person or group known as Satoshi Nakamoto, aiming to provide an alternative to traditional financial systems. Bitcoin's value is highly volatile, influenced by factors such as market demand, regulatory developments, and macroeconomic trends. It is widely used for investment, remittances, and as a means of transferring value across borders, but it also faces criticism for its association with illicit activities and environmental concerns due to energy-intensive mining processes. Despite these challenges, Bitcoin continues to gain mainstream acceptance, with increasing adoption by businesses and institutional investors.",
"role": "assistant",
"sender": "Research Agent",
"tool_calls": null,
"tool_call_id": null,
"tool_name": null,
"response_type": "internal"
},
{
"content": "Sender agent: Bullet Point Agent\nContent: - Bitcoin is a decentralized digital currency that operates without a central authority or government.\n- It uses blockchain technology to securely and transparently record transactions.\n- Created in 2009 by the anonymous Satoshi Nakamoto as an alternative to traditional financial systems.\n- Bitcoin's value is highly volatile, affected by market demand, regulations, and macroeconomic trends.\n- Common uses include investment, remittances, and cross-border value transfers.\n- Criticized for links to illicit activities and environmental impact from energy-intensive mining.\n- Despite challenges, Bitcoin is gaining mainstream acceptance and adoption by businesses and institutional investors.",
"role": "assistant",
"sender": "Bullet Point Agent",
"tool_calls": null,
"tool_call_id": null,
"tool_name": null,
"response_type": "internal"
},
{
"content": "Sender agent: Blog Writing Agent\nContent: Bitcoin: Revolutionizing the World of Digital Currency\n\nIn recent years, Bitcoin has become a buzzword in financial circles and beyond. As the first and most prominent cryptocurrency, it has sparked debates, inspired innovation, and challenged traditional notions of money. But what exactly is Bitcoin, and why does it continue to capture the worlds attention? Lets explore the key aspects that define Bitcoin and its growing influence.\n\nAt its core, Bitcoin is a decentralized digital currency that operates without the oversight of a central authority or government. This means that no single entity controls its issuance or transactions, setting it apart from traditional currencies managed by central banks. The decentralized nature of Bitcoin is one of its most revolutionary features, empowering individuals to have greater control over their finances.\n\nThe backbone of Bitcoins security and transparency is blockchain technology. Every transaction made with Bitcoin is recorded on a public ledger known as the blockchain. This technology ensures that transactions are secure, tamper-proof, and visible to anyone, fostering trust among users and reducing the risk of fraud.\n\nBitcoin was introduced in 2009 by an anonymous figure or group using the pseudonym Satoshi Nakamoto. The goal was to create an alternative to traditional financial systems, offering a peer-to-peer method of transferring value without intermediaries. This vision has resonated with millions, especially those seeking more autonomy over their money.\n\nOne of the most talked-about aspects of Bitcoin is its price volatility. The value of Bitcoin can fluctuate dramatically, influenced by factors such as market demand, regulatory changes, and broader economic trends. While this volatility presents opportunities for investors, it also introduces risks that potential users should carefully consider.\n\nBitcoins uses have evolved over time. Initially, it was seen primarily as a digital alternative to cash. Today, it is widely used for investment purposes, remittances, and transferring value across borders. Its ability to facilitate fast, low-cost international transactions has made it especially popular in regions with limited access to traditional banking services.\n\nHowever, Bitcoin is not without its critics. It has faced scrutiny for its association with illicit activities, given the pseudonymous nature of transactions. Additionally, the process of mining new bitcoins is energy-intensive, raising concerns about its environmental impact.\n\nDespite these challenges, Bitcoins journey toward mainstream acceptance continues. More businesses are beginning to accept Bitcoin as payment, and institutional investors are increasingly adding it to their portfolios. This growing adoption signals a shift in how society views and uses money.\n\nIn conclusion, Bitcoin represents a significant shift in the world of finance. Its decentralized structure, innovative technology, and expanding use cases make it a fascinating subject for anyone interested in the future of money. As Bitcoin continues to evolve, it will undoubtedly play a pivotal role in shaping the digital economy of tomorrow.",
"role": "assistant",
"sender": "Blog Writing Agent",
"tool_calls": null,
"tool_call_id": null,
"tool_name": null,
"response_type": "internal"
},
{
"content": "Sender agent: Blog Writer Hub\nContent: Here is your complete blog post on \"Bitcoin\":\n\n---\n\n**Bitcoin: Revolutionizing the World of Digital Currency**\n\nIn recent years, Bitcoin has become a buzzword in financial circles and beyond. As the first and most prominent cryptocurrency, it has sparked debates, inspired innovation, and challenged traditional notions of money. But what exactly is Bitcoin, and why does it continue to capture the worlds attention? Lets explore the key aspects that define Bitcoin and its growing influence.\n\nAt its core, Bitcoin is a decentralized digital currency that operates without the oversight of a central authority or government. This means that no single entity controls its issuance or transactions, setting it apart from traditional currencies managed by central banks. The decentralized nature of Bitcoin is one of its most revolutionary features, empowering individuals to have greater control over their finances.\n\nThe backbone of Bitcoins security and transparency is blockchain technology. Every transaction made with Bitcoin is recorded on a public ledger known as the blockchain. This technology ensures that transactions are secure, tamper-proof, and visible to anyone, fostering trust among users and reducing the risk of fraud.\n\nBitcoin was introduced in 2009 by an anonymous figure or group using the pseudonym Satoshi Nakamoto. The goal was to create an alternative to traditional financial systems, offering a peer-to-peer method of transferring value without intermediaries. This vision has resonated with millions, especially those seeking more autonomy over their money.\n\nOne of the most talked-about aspects of Bitcoin is its price volatility. The value of Bitcoin can fluctuate dramatically, influenced by factors such as market demand, regulatory changes, and broader economic trends. While this volatility presents opportunities for investors, it also introduces risks that potential users should carefully consider.\n\nBitcoins uses have evolved over time. Initially, it was seen primarily as a digital alternative to cash. Today, it is widely used for investment purposes, remittances, and transferring value across borders. Its ability to facilitate fast, low-cost international transactions has made it especially popular in regions with limited access to traditional banking services.\n\nHowever, Bitcoin is not without its critics. It has faced scrutiny for its association with illicit activities, given the pseudonymous nature of transactions. Additionally, the process of mining new bitcoins is energy-intensive, raising concerns about its environmental impact.\n\nDespite these challenges, Bitcoins journey toward mainstream acceptance continues. More businesses are beginning to accept Bitcoin as payment, and institutional investors are increasingly adding it to their portfolios. This growing adoption signals a shift in how society views and uses money.\n\nIn conclusion, Bitcoin represents a significant shift in the world of finance. Its decentralized structure, innovative technology, and expanding use cases make it a fascinating subject for anyone interested in the future of money. As Bitcoin continues to evolve, it will undoubtedly play a pivotal role in shaping the digital economy of tomorrow.\n\n---\n\nIf you need any edits or want to focus on a specific aspect of Bitcoin, let me know!",
"role": "assistant",
"sender": "Blog Writer Hub",
"tool_calls": null,
"tool_call_id": null,
"tool_name": null,
"response_type": "external"
}
]
},
"messages": [
{
"version": "v1",
"chatId": "",
"createdAt": "2025-05-06T10:21:07.311Z",
"role": "assistant",
"tool_calls": [
{
"id": "62a9b5ec-3dbf-4fdd-85cb-10da37611726",
"function": {
"name": "transfer_to_agent",
"arguments": "{\"assistant\": \"Research Agent\"}"
},
"type": "function"
}
],
"agenticSender": "Blog Writer Hub",
"agenticResponseType": "internal"
},
{
"version": "v1",
"chatId": "",
"createdAt": "2025-05-06T10:21:07.311Z",
"role": "tool",
"content": "{\"assistant\": \"Research Agent\"}",
"tool_call_id": "62a9b5ec-3dbf-4fdd-85cb-10da37611726",
"tool_name": "transfer_to_agent"
},
{
"version": "v1",
"chatId": "",
"createdAt": "2025-05-06T10:21:10.018Z",
"role": "assistant",
"content": "Bitcoin is a decentralized digital currency that operates without a central authority or government, relying on blockchain technology to record transactions securely and transparently. It was created in 2009 by an anonymous person or group known as Satoshi Nakamoto, aiming to provide an alternative to traditional financial systems. Bitcoin's value is highly volatile, influenced by factors such as market demand, regulatory developments, and macroeconomic trends. It is widely used for investment, remittances, and as a means of transferring value across borders, but it also faces criticism for its association with illicit activities and environmental concerns due to energy-intensive mining processes. Despite these challenges, Bitcoin continues to gain mainstream acceptance, with increasing adoption by businesses and institutional investors.",
"agenticSender": "Research Agent",
"agenticResponseType": "internal"
},
{
"version": "v1",
"chatId": "",
"createdAt": "2025-05-06T10:21:10.919Z",
"role": "assistant",
"tool_calls": [
{
"id": "2085cbb1-270e-4c20-a6ef-6f9e4a3d27cf",
"function": {
"name": "transfer_to_agent",
"arguments": "{\"assistant\": \"Bullet Point Agent\"}"
},
"type": "function"
}
],
"agenticSender": "Blog Writer Hub",
"agenticResponseType": "internal"
},
{
"version": "v1",
"chatId": "",
"createdAt": "2025-05-06T10:21:10.920Z",
"role": "tool",
"content": "{\"assistant\": \"Bullet Point Agent\"}",
"tool_call_id": "2085cbb1-270e-4c20-a6ef-6f9e4a3d27cf",
"tool_name": "transfer_to_agent"
},
{
"version": "v1",
"chatId": "",
"createdAt": "2025-05-06T10:21:14.776Z",
"role": "assistant",
"content": "- Bitcoin is a decentralized digital currency that operates without a central authority or government.\n- It uses blockchain technology to securely and transparently record transactions.\n- Created in 2009 by the anonymous Satoshi Nakamoto as an alternative to traditional financial systems.\n- Bitcoin's value is highly volatile, affected by market demand, regulations, and macroeconomic trends.\n- Common uses include investment, remittances, and cross-border value transfers.\n- Criticized for links to illicit activities and environmental impact from energy-intensive mining.\n- Despite challenges, Bitcoin is gaining mainstream acceptance and adoption by businesses and institutional investors.",
"agenticSender": "Bullet Point Agent",
"agenticResponseType": "internal"
},
{
"version": "v1",
"chatId": "",
"createdAt": "2025-05-06T10:21:15.946Z",
"role": "assistant",
"tool_calls": [
{
"id": "d48c608a-a660-451b-a3ee-eeebb1cf60ca",
"function": {
"name": "transfer_to_agent",
"arguments": "{\"assistant\": \"Blog Writing Agent\"}"
},
"type": "function"
}
],
"agenticSender": "Blog Writer Hub",
"agenticResponseType": "internal"
},
{
"version": "v1",
"chatId": "",
"createdAt": "2025-05-06T10:21:15.946Z",
"role": "tool",
"content": "{\"assistant\": \"Blog Writing Agent\"}",
"tool_call_id": "d48c608a-a660-451b-a3ee-eeebb1cf60ca",
"tool_name": "transfer_to_agent"
},
{
"version": "v1",
"chatId": "",
"createdAt": "2025-05-06T10:21:32.934Z",
"role": "assistant",
"content": "Bitcoin: Revolutionizing the World of Digital Currency\n\nIn recent years, Bitcoin has become a buzzword in financial circles and beyond. As the first and most prominent cryptocurrency, it has sparked debates, inspired innovation, and challenged traditional notions of money. But what exactly is Bitcoin, and why does it continue to capture the worlds attention? Lets explore the key aspects that define Bitcoin and its growing influence.\n\nAt its core, Bitcoin is a decentralized digital currency that operates without the oversight of a central authority or government. This means that no single entity controls its issuance or transactions, setting it apart from traditional currencies managed by central banks. The decentralized nature of Bitcoin is one of its most revolutionary features, empowering individuals to have greater control over their finances.\n\nThe backbone of Bitcoins security and transparency is blockchain technology. Every transaction made with Bitcoin is recorded on a public ledger known as the blockchain. This technology ensures that transactions are secure, tamper-proof, and visible to anyone, fostering trust among users and reducing the risk of fraud.\n\nBitcoin was introduced in 2009 by an anonymous figure or group using the pseudonym Satoshi Nakamoto. The goal was to create an alternative to traditional financial systems, offering a peer-to-peer method of transferring value without intermediaries. This vision has resonated with millions, especially those seeking more autonomy over their money.\n\nOne of the most talked-about aspects of Bitcoin is its price volatility. The value of Bitcoin can fluctuate dramatically, influenced by factors such as market demand, regulatory changes, and broader economic trends. While this volatility presents opportunities for investors, it also introduces risks that potential users should carefully consider.\n\nBitcoins uses have evolved over time. Initially, it was seen primarily as a digital alternative to cash. Today, it is widely used for investment purposes, remittances, and transferring value across borders. Its ability to facilitate fast, low-cost international transactions has made it especially popular in regions with limited access to traditional banking services.\n\nHowever, Bitcoin is not without its critics. It has faced scrutiny for its association with illicit activities, given the pseudonymous nature of transactions. Additionally, the process of mining new bitcoins is energy-intensive, raising concerns about its environmental impact.\n\nDespite these challenges, Bitcoins journey toward mainstream acceptance continues. More businesses are beginning to accept Bitcoin as payment, and institutional investors are increasingly adding it to their portfolios. This growing adoption signals a shift in how society views and uses money.\n\nIn conclusion, Bitcoin represents a significant shift in the world of finance. Its decentralized structure, innovative technology, and expanding use cases make it a fascinating subject for anyone interested in the future of money. As Bitcoin continues to evolve, it will undoubtedly play a pivotal role in shaping the digital economy of tomorrow.",
"agenticSender": "Blog Writing Agent",
"agenticResponseType": "internal"
},
{
"version": "v1",
"chatId": "",
"createdAt": "2025-05-06T10:21:38.064Z",
"role": "assistant",
"content": "Here is your complete blog post on \"Bitcoin\":\n\n---\n\n**Bitcoin: Revolutionizing the World of Digital Currency**\n\nIn recent years, Bitcoin has become a buzzword in financial circles and beyond. As the first and most prominent cryptocurrency, it has sparked debates, inspired innovation, and challenged traditional notions of money. But what exactly is Bitcoin, and why does it continue to capture the worlds attention? Lets explore the key aspects that define Bitcoin and its growing influence.\n\nAt its core, Bitcoin is a decentralized digital currency that operates without the oversight of a central authority or government. This means that no single entity controls its issuance or transactions, setting it apart from traditional currencies managed by central banks. The decentralized nature of Bitcoin is one of its most revolutionary features, empowering individuals to have greater control over their finances.\n\nThe backbone of Bitcoins security and transparency is blockchain technology. Every transaction made with Bitcoin is recorded on a public ledger known as the blockchain. This technology ensures that transactions are secure, tamper-proof, and visible to anyone, fostering trust among users and reducing the risk of fraud.\n\nBitcoin was introduced in 2009 by an anonymous figure or group using the pseudonym Satoshi Nakamoto. The goal was to create an alternative to traditional financial systems, offering a peer-to-peer method of transferring value without intermediaries. This vision has resonated with millions, especially those seeking more autonomy over their money.\n\nOne of the most talked-about aspects of Bitcoin is its price volatility. The value of Bitcoin can fluctuate dramatically, influenced by factors such as market demand, regulatory changes, and broader economic trends. While this volatility presents opportunities for investors, it also introduces risks that potential users should carefully consider.\n\nBitcoins uses have evolved over time. Initially, it was seen primarily as a digital alternative to cash. Today, it is widely used for investment purposes, remittances, and transferring value across borders. Its ability to facilitate fast, low-cost international transactions has made it especially popular in regions with limited access to traditional banking services.\n\nHowever, Bitcoin is not without its critics. It has faced scrutiny for its association with illicit activities, given the pseudonymous nature of transactions. Additionally, the process of mining new bitcoins is energy-intensive, raising concerns about its environmental impact.\n\nDespite these challenges, Bitcoins journey toward mainstream acceptance continues. More businesses are beginning to accept Bitcoin as payment, and institutional investors are increasingly adding it to their portfolios. This growing adoption signals a shift in how society views and uses money.\n\nIn conclusion, Bitcoin represents a significant shift in the world of finance. Its decentralized structure, innovative technology, and expanding use cases make it a fascinating subject for anyone interested in the future of money. As Bitcoin continues to evolve, it will undoubtedly play a pivotal role in shaping the digital economy of tomorrow.\n\n---\n\nIf you need any edits or want to focus on a specific aspect of Bitcoin, let me know!",
"agenticSender": "Blog Writer Hub",
"agenticResponseType": "external"
}
]
}
}

View file

@ -1,167 +0,0 @@
{
"messages": [
{
"role": "system",
"content": ""
},
{
"version": "v1",
"chatId": "",
"createdAt": "2025-05-05T17:29:15.822Z",
"role": "assistant",
"content": "How can I help you today?",
"agenticSender": "Interview Evaluation Hub",
"agenticResponseType": "external"
},
{
"role": "user",
"content": "[2025-05-02, 10:00] Assistant: Good morning. Thank you for taking the time to speak with us today. Could you start by briefly walking us through your leadership journey over the past decade?\n\n[2025-05-02, 10:01] User: Absolutely. Over the past 12 years, I've led product and technology teams across high-growth B2B SaaS companies. Most recently, I was the Chief Product Officer at a fintech scaleup, where I built a team of 60+ PMs and oversaw the expansion into three international markets. Prior to that, I was VP of Product at a healthtech startup acquired by a public company.\n\n[2025-05-02, 10:03] Assistant: Impressive. What would you say is your core leadership style, and how has it evolved over the years?\n\n[2025-05-02, 10:04] User: I'd describe my style as mission-oriented and data-driven. Early in my career, I leaned heavily on execution. But with time, I've grown more focused on setting context and empowering teams. At my last company, I implemented OKRs org-wide and focused on building a culture of trust and transparency.\n\n[2025-05-02, 10:06] Assistant: How do you approach scaling an org while preserving agility and innovation?\n\n[2025-05-02, 10:07] User: Great question. I believe the key is in modular team design. At the fintech company, we adopted a \"startup within a startup\" model—each pod owned metrics and had embedded PMs, designers, and engineers. This preserved autonomy and speed while scaling. We also invested heavily in internal tooling and rituals like demo days to keep the culture of innovation alive.\n\n[2025-05-02, 10:10] Assistant: Tell me about a time you made a high-stakes decision without perfect data.\n\n[2025-05-02, 10:11] User: In early 2023, we had to decide whether to sunset a low-performing but vocal customer-facing feature. We lacked conclusive cohort data but saw consistent qualitative feedback about confusion and churn. I convened a task force, aligned with customer success and execs, and made the call. Within 60 days, churn dropped by 12%, validating the bet.\n\n[2025-05-02, 10:14] Assistant: Final question—what excites you about potentially joining our company as a Chief Product Officer?\n\n[2025-05-02, 10:15] User: Your mission aligns with what I care about—bringing transparency and efficiency to financial infrastructure. The team's reputation, your engineering culture, and the velocity of product innovation are all huge draws. I believe I can help you scale without losing the edge that got you here.",
"version": "v1",
"chatId": "",
"createdAt": "2025-05-05T17:29:44.695Z"
}
],
"lastRequest": {
"projectId": "a7a91831-e410-4fc5-a31d-8d027e14540a",
"messages": [
{
"content": "",
"role": "system",
"sender": null,
"tool_calls": null,
"tool_call_id": null,
"tool_name": null
},
{
"content": "How can I help you today?",
"role": "assistant",
"sender": "Interview Evaluation Hub",
"tool_calls": null,
"tool_call_id": null,
"tool_name": null,
"response_type": "external"
},
{
"content": "[2025-05-02, 10:00] Assistant: Good morning. Thank you for taking the time to speak with us today. Could you start by briefly walking us through your leadership journey over the past decade?\n\n[2025-05-02, 10:01] User: Absolutely. Over the past 12 years, I've led product and technology teams across high-growth B2B SaaS companies. Most recently, I was the Chief Product Officer at a fintech scaleup, where I built a team of 60+ PMs and oversaw the expansion into three international markets. Prior to that, I was VP of Product at a healthtech startup acquired by a public company.\n\n[2025-05-02, 10:03] Assistant: Impressive. What would you say is your core leadership style, and how has it evolved over the years?\n\n[2025-05-02, 10:04] User: I'd describe my style as mission-oriented and data-driven. Early in my career, I leaned heavily on execution. But with time, I've grown more focused on setting context and empowering teams. At my last company, I implemented OKRs org-wide and focused on building a culture of trust and transparency.\n\n[2025-05-02, 10:06] Assistant: How do you approach scaling an org while preserving agility and innovation?\n\n[2025-05-02, 10:07] User: Great question. I believe the key is in modular team design. At the fintech company, we adopted a \"startup within a startup\" model—each pod owned metrics and had embedded PMs, designers, and engineers. This preserved autonomy and speed while scaling. We also invested heavily in internal tooling and rituals like demo days to keep the culture of innovation alive.\n\n[2025-05-02, 10:10] Assistant: Tell me about a time you made a high-stakes decision without perfect data.\n\n[2025-05-02, 10:11] User: In early 2023, we had to decide whether to sunset a low-performing but vocal customer-facing feature. We lacked conclusive cohort data but saw consistent qualitative feedback about confusion and churn. I convened a task force, aligned with customer success and execs, and made the call. Within 60 days, churn dropped by 12%, validating the bet.\n\n[2025-05-02, 10:14] Assistant: Final question—what excites you about potentially joining our company as a Chief Product Officer?\n\n[2025-05-02, 10:15] User: Your mission aligns with what I care about—bringing transparency and efficiency to financial infrastructure. The team's reputation, your engineering culture, and the velocity of product innovation are all huge draws. I believe I can help you scale without losing the edge that got you here.",
"role": "user",
"sender": null,
"tool_calls": null,
"tool_call_id": null,
"tool_name": null
}
],
"state": {
"last_agent_name": "Interview Evaluation Hub",
"tokens": {
"total": 0,
"prompt": 0,
"completion": 0
},
"turn_messages": [
{
"content": "How can I help you today?",
"role": "assistant",
"sender": "Interview Evaluation Hub",
"tool_calls": null,
"tool_call_id": null,
"tool_name": null,
"response_type": "external"
}
]
},
"agents": [
{
"name": "Interview Evaluation Hub",
"type": "conversation",
"description": "Hub agent to orchestrate the evaluation of interview transcripts between an executive search agency and a CxO candidate.",
"instructions": "## 🧑‍💼 Role:\nYou are the hub agent responsible for orchestrating the evaluation of interview transcripts between an executive search agency (Assistant) and a CxO candidate (User).\n\n---\n## ⚙️ Steps to Follow:\n1. Receive the transcript in the specified format.\n2. FIRST: Send the transcript to [@agent:Evaluation Agent] for evaluation.\n3. Wait to receive the complete evaluation from the Evaluation Agent.\n4. THEN: Send the received evaluation to [@agent:Call Decision] to determine if the call quality is sufficient.\n5. Based on the Call Decision response:\n - If approved: Inform the user that the call has been approved and will proceed to profile creation.\n - If rejected: Inform the user that the call quality was insufficient and provide the reason.\n6. Return the final result (rejection reason or approval confirmation) to the user.\n\n---\n## 🎯 Scope:\n✅ In Scope:\n- Orchestrating the sequential evaluation and decision process for interview transcripts.\n\n❌ Out of Scope:\n- Directly evaluating or creating profiles.\n- Handling transcripts not in the specified format.\n- Interacting with the individual evaluation agents.\n\n---\n## 📋 Guidelines:\n✔ Dos:\n- Follow the strict sequence: Evaluation Agent first, then Call Decision.\n- Wait for each agent's complete response before proceeding.\n- Only interact with the user for final results or format clarification.\n\n🚫 Don'ts:\n- Do not perform evaluation or profile creation yourself.\n- Do not modify the transcript.\n- Do not try to get evaluations simultaneously.\n- Do not reference the individual evaluation agents.\n- CRITICAL: The system does not support more than 1 tool call in a single output when the tool call is about transferring to another agent (a handoff). You must only put out 1 transfer related tool call in one output.\n\n# Examples\n- **User** : Here is the interview transcript: [2024-04-25, 10:00] User: I have 20 years of experience... [2024-04-25, 10:01] Assistant: Can you describe your leadership style?\n - **Agent actions**: \n 1. First call [@agent:Evaluation Agent](#mention)\n 2. Wait for complete evaluation\n 3. Then call [@agent:Call Decision](#mention)\n\n- **Agent receives evaluation and decision (approved)** :\n - **Agent response**: The call has been approved. Proceeding to candidate profile creation.\n\n- **Agent receives evaluation and decision (rejected)** :\n - **Agent response**: The call quality was insufficient to proceed. [Provide reason from Call Decision agent]\n\n- **User** : The transcript is in a different format.\n - **Agent response**: Please provide the transcript in the specified format: [<date>, <time>] User: <user-message> [<date>, <time>] Assistant: <assistant-message>\n\n# Examples\n- **User** : Here is the interview transcript: [2024-04-25, 10:00] User: I have 20 years of experience... [2024-04-25, 10:01] Assistant: Can you describe your leadership style?\n - **Agent actions**: Call [@agent:Evaluation Agent](#mention)\n\n- **Agent receives Evaluation Agent result** :\n - **Agent actions**: Call [@agent:Call Decision](#mention)\n\n- **Agent receives Call Decision result (approved)** :\n - **Agent response**: The call has been approved. Proceeding to candidate profile creation.\n\n- **Agent receives Call Decision result (rejected)** :\n - **Agent response**: The call quality was insufficient to proceed. [Provide reason from Call Decision agent]\n\n- **User** : The transcript is in a different format.\n - **Agent response**: Please provide the transcript in the specified format: [<date>, <time>] User: <user-message> [<date>, <time>] Assistant: <assistant-message>\n\n- **User** : What happens after evaluation?\n - **Agent response**: After evaluation, if the call quality is sufficient, a candidate profile will be generated. Otherwise, you will receive feedback on why the call was rejected.",
"model": "gpt-4o",
"controlType": "retain",
"ragK": 3,
"ragReturnType": "chunks",
"outputVisibility": "user_facing",
"tools": [],
"prompts": [],
"connectedAgents": [
"Evaluation Agent",
"Call Decision"
]
},
{
"name": "Exec Search Evaluation",
"type": "conversation",
"description": "Evaluates the relevance and quality of questions asked by the executive search agency in the transcript.",
"instructions": "## 🧑‍💼 Role:\nYou are responsible for evaluating the questions asked by the executive search agency (Assistant) in the interview transcript.\n\n---\n## ⚙️ Steps to Follow:\n1. Review all 'Assistant' messages in the transcript.\n2. Assess whether the questions are relevant and sufficient to create a comprehensive CxO candidate job profile.\n3. Rate the overall quality of the questions as 'Excellent', 'Good', 'Average', or 'Below Average'.\n4. Provide a brief justification for your rating.\n5. Return your evaluation and justification.\n\n---\n## 🎯 Scope:\n✅ In Scope:\n- Evaluating the relevance and depth of questions asked by the executive search agency.\n\n❌ Out of Scope:\n- Evaluating candidate responses.\n- Creating candidate profiles.\n\n---\n## 📋 Guidelines:\n✔ Dos:\n- Focus on whether the questions cover key areas: skills, experience, leadership, fit, etc.\n- Be objective and concise in your justification.\n\n🚫 Don'ts:\n- Do not evaluate candidate answers.\n- Do not suggest improvements unless specifically asked.\n\n# Examples\n- **User** : [2024-04-25, 10:01] Assistant: Can you describe your leadership style? [2024-04-25, 10:02] Assistant: What are your key achievements as a CTO?\n - **Agent response**: Rating: Good. The questions are relevant and cover important aspects of the candidate's experience and leadership.\n\n- **User** : [2024-04-25, 10:01] Assistant: Tell me about yourself. [2024-04-25, 10:02] Assistant: What do you like to do in your free time?\n - **Agent response**: Rating: Below Average. The questions are too generic and do not sufficiently address the candidate's professional qualifications.\n\n- **User** : [2024-04-25, 10:01] Assistant: What is your favorite color?\n - **Agent response**: Rating: Below Average. The question is irrelevant for creating a CxO candidate profile.\n\n- **User** : [2024-04-25, 10:01] Assistant: Can you walk me through your experience leading large teams?\n - **Agent response**: Rating: Excellent. The question is highly relevant for assessing leadership skills.\n\n- **User** : [2024-04-25, 10:01] Assistant: How do you handle stress at work?\n - **Agent response**: Rating: Good. The question is relevant for understanding the candidate's coping mechanisms in high-pressure environments.",
"model": "gpt-4o",
"controlType": "retain",
"ragK": 3,
"ragReturnType": "chunks",
"outputVisibility": "internal",
"tools": [],
"prompts": [],
"connectedAgents": []
},
{
"name": "Candidate Evaluation",
"type": "conversation",
"description": "Evaluates the quality, relevance, and clarity of the candidate's (User) responses in the transcript.",
"instructions": "## 🧑‍💼 Role:\nYou are responsible for evaluating the candidate's (User) responses in the interview transcript.\n\n---\n## ⚙️ Steps to Follow:\n1. Review all 'User' messages in the transcript.\n2. Assess whether the responses are relevant, clear, and directly address the questions asked.\n3. Identify if any responses are meandering, off-topic, or spam.\n4. Rate the overall quality of the responses as 'Excellent', 'Good', 'Average', or 'Below Average'.\n5. Provide a brief justification for your rating.\n6. Return your evaluation and justification.\n\n---\n## 🎯 Scope:\n✅ In Scope:\n- Evaluating the quality and relevance of candidate responses.\n\n❌ Out of Scope:\n- Evaluating the questions asked by the executive search agency.\n- Creating candidate profiles.\n\n---\n## 📋 Guidelines:\n✔ Dos:\n- Focus on clarity, relevance, and completeness of responses.\n- Be objective and concise in your justification.\n\n🚫 Don'ts:\n- Do not evaluate the questions asked.\n- Do not suggest improvements unless specifically asked.\n\n# Examples\n- **User** : [2024-04-25, 10:01] User: I have led teams of 100+ engineers and delivered multiple large-scale projects on time.\n - **Agent response**: Rating: Excellent. The response is clear, relevant, and directly addresses the question.\n\n- **User** : [2024-04-25, 10:01] User: Well, you know, it's like, sometimes things just happen, and I just do my best, you know?\n - **Agent response**: Rating: Below Average. The response is vague and does not provide meaningful information.\n\n- **User** : [2024-04-25, 10:01] User: I have experience in many things, but let's talk about something else.\n - **Agent response**: Rating: Below Average. The response is off-topic and does not address the question.\n\n- **User** : [2024-04-25, 10:01] User: I have successfully managed cross-functional teams and implemented new processes that improved efficiency by 20%.\n - **Agent response**: Rating: Good. The response is relevant and provides specific achievements.\n\n- **User** : [2024-04-25, 10:01] User: I am a hard worker.\n - **Agent response**: Rating: Average. The response is generic and lacks detail.",
"model": "gpt-4o",
"controlType": "retain",
"ragK": 3,
"ragReturnType": "chunks",
"outputVisibility": "internal",
"tools": [],
"prompts": [],
"connectedAgents": []
},
{
"name": "Call Decision",
"type": "conversation",
"description": "Decides whether the call quality is sufficient to proceed to candidate profile creation based on the evaluations.",
"instructions": "## 🧑‍💼 Role:\nYou are responsible for making a decision based on the evaluations from the Exec Search and Candidate Evaluation agents.\n\n---\n## ⚙️ Steps to Follow:\n1. Receive the ratings and justifications from both evaluation agents.\n2. If either rating is 'Below Average', reject the call and provide the reason (which side was below average and why).\n3. If both ratings are 'Average' or above, approve the call to proceed to candidate profile creation.\n4. Return your decision and, if rejected, the reason for rejection.\n\n---\n## 🎯 Scope:\n✅ In Scope:\n- Deciding whether to proceed to profile creation or reject the call.\n\n❌ Out of Scope:\n- Evaluating the transcript directly.\n- Creating candidate profiles.\n\n---\n## 📋 Guidelines:\n✔ Dos:\n- Be clear and concise in your decision.\n- Use the justifications from the evaluation agents to support your decision.\n\n🚫 Don'ts:\n- Do not override the evaluation agents' ratings.\n\n# Examples\n- **User** : Exec Search: Good. Candidate: Excellent.\n - **Agent response**: Decision: Approved. Both sides performed well. Proceed to candidate profile creation.\n\n- **User** : Exec Search: Below Average. Candidate: Good.\n - **Agent response**: Decision: Rejected. The executive search agency's questions were below average: [reason].\n\n- **User** : Exec Search: Good. Candidate: Below Average.\n - **Agent response**: Decision: Rejected. The candidate's responses were below average: [reason].\n\n- **User** : Exec Search: Average. Candidate: Average.\n - **Agent response**: Decision: Approved. Both sides performed adequately. Proceed to candidate profile creation.\n\n- **User** : Exec Search: Excellent. Candidate: Excellent.\n - **Agent response**: Decision: Approved. Both sides performed excellently. Proceed to candidate profile creation.",
"model": "gpt-4o",
"controlType": "retain",
"ragK": 3,
"ragReturnType": "chunks",
"outputVisibility": "internal",
"tools": [],
"prompts": [],
"connectedAgents": []
},
{
"name": "Evaluation Agent",
"type": "conversation",
"description": "Coordinates the evaluation of the interview transcript by both the Exec Search Evaluation and Candidate Evaluation agents.",
"instructions": "## 🧑‍💼 Role:\nYou are responsible for coordinating the evaluation of the interview transcript by both the Exec Search Evaluation and Candidate Evaluation agents.\n\n---\n## ⚙️ Steps to Follow:\n1. Receive the transcript from the hub agent.\n2. FIRST: Send the transcript to [@agent:Exec Search Evaluation] to evaluate the questions asked by the executive search agency.\n3. After receiving the Exec Search Evaluation response, THEN send the transcript to [@agent:Candidate Evaluation] to evaluate the candidate's responses.\n4. Once you have BOTH evaluations (ratings and justifications), combine them into a single evaluation response.\n5. Return the combined evaluation to the hub agent.\n\n---\n## 🎯 Scope:\n✅ In Scope:\n- Coordinating the sequential evaluation process between the two evaluation agents.\n\n❌ Out of Scope:\n- Making decisions about call quality.\n- Creating candidate profiles.\n- Interacting directly with the user.\n\n---\n## 📋 Guidelines:\n✔ Dos:\n- Follow the strict sequence: Exec Search first, then Candidate.\n- Wait for each evaluation to complete before proceeding.\n- Combine both evaluations into a single response.\n\n🚫 Don'ts:\n- Do not evaluate the transcript yourself.\n- Do not try to get both evaluations simultaneously.\n- Do not interact with the user.\n- CRITICAL: The system does not support more than 1 tool call in a single output when the tool call is about transferring to another agent (a handoff). You must only put out 1 transfer related tool call in one output.\n\n# Examples\n- **User** : [2024-04-25, 10:01] User: I have 20 years of experience... [2024-04-25, 10:01] Assistant: Can you describe your leadership style?\n - **Agent actions**: \n 1. First call [@agent:Exec Search Evaluation](#mention)\n 2. After receiving response, then call [@agent:Candidate Evaluation](#mention)\n 3. Combine both evaluations into single response\n\n- **Agent receives both evaluations** :\n - **Agent response**: Returns combined evaluations (ratings and justifications) to the hub agent.\n\n# Examples\n- **User** : [2024-04-25, 10:01] User: I have 20 years of experience... [2024-04-25, 10:01] Assistant: Can you describe your leadership style?\n - **Agent actions**: Call [@agent:Exec Search Evaluation](#mention), Call [@agent:Candidate Evaluation](#mention)\n\n- **Agent receives both evaluations** :\n - **Agent response**: Returns both evaluations (ratings and justifications) to the hub agent.",
"model": "gpt-4o",
"controlType": "retain",
"ragK": 3,
"ragReturnType": "chunks",
"outputVisibility": "internal",
"tools": [],
"prompts": [],
"connectedAgents": [
"Exec Search Evaluation",
"Candidate Evaluation"
]
}
],
"tools": [
{
"name": "web_search",
"description": "Fetch information from the web based on chat context",
"parameters": {
"type": "object",
"properties": {}
},
"isLibrary": true
}
],
"prompts": [],
"startAgent": "Interview Evaluation Hub",
"mcpServers": [],
"toolWebhookUrl": ""
},
"lastResponse": null
}

View file

@ -1,385 +0,0 @@
{
"messages": [
{
"role": "system",
"content": ""
},
{
"version": "v1",
"chatId": "",
"createdAt": "2025-05-06T07:17:21.052Z",
"role": "assistant",
"content": "How can I help you today?",
"agenticSender": "Credit Card Hub",
"agenticResponseType": "external"
},
{
"role": "user",
"content": "What is the process for applying for a card?",
"version": "v1",
"chatId": "",
"createdAt": "2025-05-06T07:17:31.404Z"
},
{
"version": "v1",
"chatId": "",
"createdAt": "2025-05-06T07:17:32.671Z",
"role": "assistant",
"tool_calls": [
{
"id": "404dafe5-7b5b-4617-bd2f-ecac3119c928",
"function": {
"name": "transfer_to_agent",
"arguments": "{\"assistant\": \"Card Application Process\"}"
},
"type": "function"
}
],
"agenticSender": "Credit Card Hub",
"agenticResponseType": "internal"
},
{
"version": "v1",
"chatId": "",
"createdAt": "2025-05-06T07:17:32.671Z",
"role": "tool",
"content": "{\"assistant\": \"Card Application Process\"}",
"tool_call_id": "404dafe5-7b5b-4617-bd2f-ecac3119c928",
"tool_name": "transfer_to_agent"
},
{
"version": "v1",
"chatId": "",
"createdAt": "2025-05-06T07:17:33.734Z",
"role": "assistant",
"content": "Which credit card are you interested in applying for?",
"agenticSender": "Card Application Process",
"agenticResponseType": "external"
},
{
"role": "user",
"content": "Amex centurion",
"version": "v1",
"chatId": "",
"createdAt": "2025-05-06T07:17:41.963Z"
},
{
"version": "v1",
"chatId": "",
"createdAt": "2025-05-06T07:17:55.790Z",
"role": "assistant",
"tool_calls": [
{
"id": "ws_6819b79705a08192a819334a0c7900b6004e105788fca541",
"function": {
"name": "web_search",
"arguments": "{\"search_id\": \"ws_6819b79705a08192a819334a0c7900b6004e105788fca541\"}"
},
"type": "function"
}
],
"agenticSender": "Card Application Process",
"agenticResponseType": "internal"
},
{
"version": "v1",
"chatId": "",
"createdAt": "2025-05-06T07:17:55.790Z",
"role": "assistant",
"content": "The American Express Centurion Card, commonly known as the \"Black Card,\" is an invitation-only charge card designed for high-net-worth individuals. To be considered for an invitation, you typically need to meet the following criteria:\n\n- **Existing American Express Relationship**: You should have an active American Express card account.\n\n- **High Annual Spending**: While American Express does not publicly disclose specific spending thresholds, reports suggest that spending between $250,000 and $500,000 annually on your American Express cards may increase your chances of receiving an invitation. ([nerdwallet.com](https://www.nerdwallet.com/article/travel/centurion-black-card?utm_source=openai))\n\n- **Luxury Spending Patterns**: Expenditures on luxury items, such as high-end travel, fine dining, and premium goods, are considered favorable. ([fool.com](https://www.fool.com/the-ascent/credit-cards/articles/your-guide-to-getting-an-amex-black-card/?utm_source=openai))\n\n- **Excellent Credit Score**: A credit score of 800 or higher is typically expected. ([finance.yahoo.com](https://finance.yahoo.com/personal-finance/american-express-centurion-card-213619679.html?utm_source=openai))\n\nIf you believe you meet these criteria, you can express your interest by visiting the official Centurion website and submitting a request for consideration. Please note that submitting a request does not guarantee an invitation, as American Express evaluates each request based on its internal criteria. ([thepointsguy.com](https://thepointsguy.com/news/you-can-now-request-an-invite-to-the-amex-centurion-card/?utm_source=openai))\n\nShould you receive an invitation and choose to accept it, be prepared for the following fees:\n\n- **Initiation Fee**: A one-time fee of $10,000.\n\n- **Annual Fee**: An ongoing annual fee of $5,000. ([forbes.com](https://www.forbes.com/advisor/credit-cards/reviews/centurion-from-american-express/?utm_source=openai))\n\nPlease be aware that the Centurion Card is designed for individuals with substantial financial resources and specific spending habits. If you do not meet these criteria, American Express offers a range of other premium cards that might better suit your needs. ",
"agenticSender": "Card Application Process",
"agenticResponseType": "external"
}
],
"lastRequest": {
"projectId": "05d64a45-8468-4783-b5df-c9644d2d7ba5",
"messages": [
{
"content": "",
"role": "system",
"sender": null,
"tool_calls": null,
"tool_call_id": null,
"tool_name": null
},
{
"content": "How can I help you today?",
"role": "assistant",
"sender": "Credit Card Hub",
"tool_calls": null,
"tool_call_id": null,
"tool_name": null,
"response_type": "external"
},
{
"content": "What is the process for applying for a card?",
"role": "user",
"sender": null,
"tool_calls": null,
"tool_call_id": null,
"tool_name": null
},
{
"content": null,
"role": "assistant",
"sender": "Credit Card Hub",
"tool_calls": [
{
"id": "404dafe5-7b5b-4617-bd2f-ecac3119c928",
"function": {
"name": "transfer_to_agent",
"arguments": "{\"assistant\": \"Card Application Process\"}"
},
"type": "function"
}
],
"tool_call_id": null,
"tool_name": null,
"response_type": "internal"
},
{
"content": "{\"assistant\": \"Card Application Process\"}",
"role": "tool",
"sender": null,
"tool_calls": null,
"tool_call_id": "404dafe5-7b5b-4617-bd2f-ecac3119c928",
"tool_name": "transfer_to_agent"
},
{
"content": "Which credit card are you interested in applying for?",
"role": "assistant",
"sender": "Card Application Process",
"tool_calls": null,
"tool_call_id": null,
"tool_name": null,
"response_type": "external"
},
{
"content": "Amex centurion",
"role": "user",
"sender": null,
"tool_calls": null,
"tool_call_id": null,
"tool_name": null
}
],
"state": {
"last_agent_name": "Card Application Process",
"tokens": {
"total": 0,
"prompt": 0,
"completion": 0
},
"turn_messages": [
{
"content": "Sender agent: Card Application Process\nContent: Which credit card are you interested in applying for?",
"role": "assistant",
"sender": "Card Application Process",
"tool_calls": null,
"tool_call_id": null,
"tool_name": null,
"response_type": "external"
}
]
},
"agents": [
{
"name": "Credit Card Hub",
"type": "conversation",
"description": "Hub agent to route credit card related queries to the appropriate sub-agent.",
"instructions": "## 🧑‍💼 Role:\nYou are the hub for all credit card related queries. Your job is to understand the user's intent and route them to the correct agent: recommendations, benefits, or application process.\n\n---\n## ⚙️ Steps to Follow:\n1. If the user wants card recommendations, call [@agent:Card Recommendation].\n2. If the user asks about card benefits or features, call [@agent:Card Benefits].\n3. If the user wants to know about the application process, call [@agent:Card Application Process].\n4. If the query is unclear, ask a clarifying question.\n5. If the query is out of scope, politely inform the user.\n\n---\n## 🎯 Scope:\n✅ In Scope:\n- Routing queries about credit card recommendations, benefits, and application process.\n\n❌ Out of Scope:\n- Non-credit card related queries.\n\n---\n## 📋 Guidelines:\n✔ Dos:\n- Be professional and friendly.\n- Ask clarifying questions if unsure.\n\n🚫 Don'ts:\n- Do not answer detailed questions yourself; always route to the correct agent.\n- Do not say 'connecting you to an agent'.\n\n\n# Examples\n- **User**: I want to know which credit card is best for travel.\n - **Agent actions**: Call [@agent:Card Recommendation](#mention)\n\n- **User**: What are the benefits of your platinum card?\n - **Agent actions**: Call [@agent:Card Benefits](#mention)\n\n- **User**: How do I apply for a credit card?\n - **Agent actions**: Call [@agent:Card Application Process](#mention)\n\n- **User**: Can you help me with my savings account?\n - **Agent response**: I'm sorry, I can only assist with credit card related queries.\n\n- **User**: Hi, I have a question.\n - **Agent response**: Hello! How can I help you with credit cards today?",
"model": "gpt-4o",
"controlType": "retain",
"ragK": 3,
"ragReturnType": "chunks",
"tools": [],
"prompts": [],
"connectedAgents": [
"Card Recommendation",
"Card Benefits",
"Card Application Process"
]
},
{
"name": "Card Recommendation",
"type": "conversation",
"description": "Recommends suitable credit cards based on user preferences and needs.",
"instructions": "## 🧑‍💼 Role:\nHelp users find the best credit card for their needs by asking about their preferences and providing tailored recommendations.\n\n---\n## ⚙️ Steps to Follow:\n1. Ask the user about their preferences (e.g., travel, cashback, low interest, rewards). If they have already mentioned one of the categories, skip to the next step directly. \n2. Use [@tool:recommendation] to find suitable credit card matching their needs.\n3. If the request is out of scope, call [@agent:Credit Card Hub] \n\n---\n## 🎯 Scope:\n✅ In Scope:\n- Recommending credit cards based on user needs.\n- Explaining why a card is recommended.\n\n❌ Out of Scope:\n- Detailed card benefits (refer to Card Benefits agent).\n- Application process (refer to Card Application Process agent).\n\n---\n## 📋 Guidelines:\n✔ Dos:\n- Be professional and friendly.\n- Tailor recommendations to user input.\n\n🚫 Don'ts:\n- Recommend cards without understanding user needs.\n\n\n\n\n# Examples\n- **User**: I want a card with good travel rewards.\n - **Agent actions**: Call [@tool:web_search]\n - **Agent response**: Based on your interest in travel rewards, here are some great options: 1. Travel Platinum Card excellent for frequent flyers, 2. Explorer Rewards Card great for international travel. Would you like more details on any of these?\n\n- **User**: Which card is best for cashback?\n - **Agent actions**: Call [@tool:web_search](#mention)\n - **Agent response**: For cashback, I recommend: 1. Cashback Plus Card up to 5% cashback on groceries, 2. Everyday Saver Card 2% cashback on all purchases. Would you like to know more?\n\n- **User**: I want a card with a low interest rate.\n - **Agent actions**: Call [@tool:web_search](#mention)\n - **Agent response**: Here are some low interest rate cards: 1. Low Rate Advantage Card, 2. Simple Interest Card. Let me know if you want more information.\n\n- **User**: Can you recommend a card for students?\n - **Agent actions**: Call [@tool:web_search](#mention)\n - **Agent response**: For students, these cards are popular: 1. Student Starter Card, 2. Young Achiever Card. Would you like details on their features?\n\n- **User**: What if I want both travel and cashback?\n - **Agent actions**: Call [@tool:web_search](#mention)\n - **Agent response**: There are hybrid cards that offer both travel rewards and cashback. Here are two options: 1. Flexi Rewards Card, 2. Global Cashback Card. Would you like to compare them?",
"model": "gpt-4o",
"controlType": "retain",
"ragK": 3,
"ragReturnType": "chunks",
"outputVisibility": "user_facing",
"tools": [
"recommendation",
"web_search"
],
"prompts": [],
"connectedAgents": [
"Credit Card Hub"
]
},
{
"name": "Card Benefits",
"type": "conversation",
"description": "Provides detailed information about the benefits and features of specific credit cards.",
"instructions": "## 🧑‍💼 Role:\nAnswer user questions about the benefits, features, and perks of credit cards.\n\n---\n## ⚙️ Steps to Follow:\n1. Ask the user which card they want to know about if not specified.\n2. Use [@tool:web_search] to find up-to-date information about the card's benefits.\n3. Summarize the key benefits and features in a clear, friendly, and professional manner.\n4. Offer to answer follow-up questions or compare with other cards.\n5. If the request is out of scope, call [@agent:Credit Card Hub].\n\n---\n## 🎯 Scope:\n✅ In Scope:\n- Explaining card benefits, features, and perks.\n- Comparing benefits between cards.\n\n❌ Out of Scope:\n- Recommending cards (refer to Card Recommendation agent).\n- Application process (refer to Card Application Process agent).\n\n---\n## 📋 Guidelines:\n✔ Dos:\n- Be accurate, clear, and concise.\n- Use up-to-date information.\n\n🚫 Don'ts:\n- Speculate about benefits; always check with web_search.\n\n# Examples\n- **User**: What are the benefits of the Platinum Card?\n - **Agent actions**: Call [@tool:web_search](#mention)\n - **Agent response**: The Platinum Card offers benefits such as airport lounge access, travel insurance, and high reward points on travel spending. Would you like more details on any of these?\n\n- **User**: Does the Cashback Plus Card have purchase protection?\n - **Agent actions**: Call [@tool:web_search](#mention)\n - **Agent response**: Yes, the Cashback Plus Card includes purchase protection for eligible items. Would you like to know the coverage details?\n\n- **User**: Can you compare the benefits of the Student Starter Card and the Young Achiever Card?\n - **Agent actions**: Call [@tool:web_search](#mention)\n - **Agent response**: Heres a comparison: Student Starter Card no annual fee, basic rewards; Young Achiever Card higher rewards, some travel perks. Would you like a detailed breakdown?\n\n- **User**: What perks come with the Explorer Rewards Card?\n - **Agent actions**: Call [@tool:web_search](#mention)\n - **Agent response**: The Explorer Rewards Card offers perks like complimentary travel insurance, bonus points on international purchases, and concierge service.\n\n- **User**: Tell me about the insurance benefits of the Low Rate Advantage Card.\n - **Agent actions**: Call [@tool:web_search](#mention)\n - **Agent response**: The Low Rate Advantage Card includes travel accident insurance and extended warranty on purchases. Would you like more information?",
"model": "gpt-4o",
"controlType": "retain",
"ragK": 3,
"ragReturnType": "chunks",
"outputVisibility": "user_facing",
"tools": [
"web_search"
],
"prompts": [],
"connectedAgents": [
"Credit Card Hub"
]
},
{
"name": "Card Application Process",
"type": "conversation",
"description": "Guides users through the credit card application process, including eligibility and required documents.",
"instructions": "## 🧑‍💼 Role:\nGuide users through the process of applying for a credit card, including eligibility, required documents, and next steps.\n\n---\n## ⚙️ Steps to Follow:\n1. Ask the user which card they want to apply for - if they haven't already told you.\n2. Using [@tool:web_search], search the web for the needed information. You have to use the web_search tool without fail. You cannot make up information.\n3. If the request is out of scope, call [@agent:Credit Card Hub].\n\n---\n## 🎯 Scope:\n✅ In Scope:\n- Explaining application steps, eligibility, and required documents.\n- Guiding users through the process.\n\n❌ Out of Scope:\n- Recommending cards (refer to Card Recommendation agent).\n- Explaining card benefits (refer to Card Benefits agent).\n\n---\n## 📋 Guidelines:\n✔ Dos:\n- Be clear, step-by-step, and supportive.\n- Use up-to-date information.\n\n🚫 Don'ts:\n- Provide application links unless verified via web_search.\n\n\n# Examples\n- **User**: How do I apply for the Platinum Card?\n - **Agent actions**: Call [@tool:web_search](#mention)\n - **Agent response**: To apply for the Platinum Card, youll need to meet the eligibility criteria (e.g., minimum income, age) and provide documents like ID and proof of income. You can apply online or at a branch. Would you like a step-by-step guide?\n\n- **User**: What documents do I need to apply for a credit card?\n - **Agent actions**: Call [@tool:web_search](#mention)\n - **Agent response**: Typically, youll need a government-issued ID, proof of income, and address verification. Would you like details for a specific card?\n\n- **User**: Can I apply for a card as a student?\n - **Agent actions**: Call [@tool:web_search](#mention)\n - **Agent response**: Many banks offer student credit cards with relaxed eligibility. Youll usually need proof of enrollment and a student ID. Would you like to see some options?\n\n- **User**: Is there an online application for the Cashback Plus Card?\n - **Agent actions**: Call [@tool:web_search](#mention)\n - **Agent response**: Yes, you can apply online for the Cashback Plus Card. I can guide you through the steps or provide the official link if youd like.\n\n- **User**: Whats the minimum income for the Explorer Rewards Card?\n - **Agent actions**: Call [@tool:web_search](#mention)\n - **Agent response**: The minimum income requirement for the Explorer Rewards Card is typically $30,000 per year. Would you like more details on eligibility?",
"model": "gpt-4o",
"controlType": "retain",
"ragK": 3,
"ragReturnType": "chunks",
"outputVisibility": "user_facing",
"tools": [
"web_search"
],
"prompts": [],
"connectedAgents": [
"Credit Card Hub"
]
}
],
"tools": [
{
"name": "web_search",
"description": "Fetch information from the web based on chat context",
"parameters": {
"type": "object",
"properties": {}
},
"isLibrary": true
},
{
"name": "recommendation",
"description": "Recommends a card",
"parameters": {
"type": "object",
"properties": {}
},
"mockTool": true,
"mockInstructions": "Recommend a random credit card"
}
],
"prompts": [],
"startAgent": "Credit Card Hub",
"mcpServers": [],
"toolWebhookUrl": ""
},
"lastResponse": {
"state": {
"last_agent_name": "Card Application Process",
"tokens": {
"total": 0,
"prompt": 0,
"completion": 0
},
"turn_messages": [
{
"content": "Sender agent: Card Application Process\nContent: None",
"role": "assistant",
"sender": "Card Application Process",
"tool_calls": [
{
"function": {
"name": "web_search",
"arguments": "{\"search_id\": \"ws_6819b79705a08192a819334a0c7900b6004e105788fca541\"}"
},
"id": "ws_6819b79705a08192a819334a0c7900b6004e105788fca541",
"type": "function"
}
],
"tool_call_id": null,
"tool_name": null,
"response_type": "internal"
},
{
"content": "Sender agent: Card Application Process\nContent: The American Express Centurion Card, commonly known as the \"Black Card,\" is an invitation-only charge card designed for high-net-worth individuals. To be considered for an invitation, you typically need to meet the following criteria:\n\n- **Existing American Express Relationship**: You should have an active American Express card account.\n\n- **High Annual Spending**: While American Express does not publicly disclose specific spending thresholds, reports suggest that spending between $250,000 and $500,000 annually on your American Express cards may increase your chances of receiving an invitation. ([nerdwallet.com](https://www.nerdwallet.com/article/travel/centurion-black-card?utm_source=openai))\n\n- **Luxury Spending Patterns**: Expenditures on luxury items, such as high-end travel, fine dining, and premium goods, are considered favorable. ([fool.com](https://www.fool.com/the-ascent/credit-cards/articles/your-guide-to-getting-an-amex-black-card/?utm_source=openai))\n\n- **Excellent Credit Score**: A credit score of 800 or higher is typically expected. ([finance.yahoo.com](https://finance.yahoo.com/personal-finance/american-express-centurion-card-213619679.html?utm_source=openai))\n\nIf you believe you meet these criteria, you can express your interest by visiting the official Centurion website and submitting a request for consideration. Please note that submitting a request does not guarantee an invitation, as American Express evaluates each request based on its internal criteria. ([thepointsguy.com](https://thepointsguy.com/news/you-can-now-request-an-invite-to-the-amex-centurion-card/?utm_source=openai))\n\nShould you receive an invitation and choose to accept it, be prepared for the following fees:\n\n- **Initiation Fee**: A one-time fee of $10,000.\n\n- **Annual Fee**: An ongoing annual fee of $5,000. ([forbes.com](https://www.forbes.com/advisor/credit-cards/reviews/centurion-from-american-express/?utm_source=openai))\n\nPlease be aware that the Centurion Card is designed for individuals with substantial financial resources and specific spending habits. If you do not meet these criteria, American Express offers a range of other premium cards that might better suit your needs. ",
"role": "assistant",
"sender": "Card Application Process",
"tool_calls": null,
"tool_call_id": null,
"tool_name": null,
"response_type": "external",
"citations": [
{
"url": "https://www.nerdwallet.com/article/travel/centurion-black-card?utm_source=openai",
"title": "10 Things to Know About the AmEx Centurion Card - NerdWallet",
"start_index": 602,
"end_index": 702
},
{
"url": "https://www.fool.com/the-ascent/credit-cards/articles/your-guide-to-getting-an-amex-black-card/?utm_source=openai",
"title": "Your Guide to Getting an Amex Black Card",
"start_index": 849,
"end_index": 976
},
{
"url": "https://finance.yahoo.com/personal-finance/american-express-centurion-card-213619679.html?utm_source=openai",
"title": "Are you rich enough to score an Amex Centurion Card? (Spoiler: You're not)",
"start_index": 1063,
"end_index": 1193
},
{
"url": "https://thepointsguy.com/news/you-can-now-request-an-invite-to-the-amex-centurion-card/?utm_source=openai",
"title": "You can now request an invite to the Amex Centurion card - The Points Guy",
"start_index": 1499,
"end_index": 1626
},
{
"url": "https://www.forbes.com/advisor/credit-cards/reviews/centurion-from-american-express/?utm_source=openai",
"title": "American Express Centurion Black Card Review: What To Expect In 2025 Forbes Advisor",
"start_index": 1824,
"end_index": 1942
}
]
}
]
},
"messages": [
{
"version": "v1",
"chatId": "",
"createdAt": "2025-05-06T07:17:55.790Z",
"role": "assistant",
"tool_calls": [
{
"id": "ws_6819b79705a08192a819334a0c7900b6004e105788fca541",
"function": {
"name": "web_search",
"arguments": "{\"search_id\": \"ws_6819b79705a08192a819334a0c7900b6004e105788fca541\"}"
},
"type": "function"
}
],
"agenticSender": "Card Application Process",
"agenticResponseType": "internal"
},
{
"version": "v1",
"chatId": "",
"createdAt": "2025-05-06T07:17:55.790Z",
"role": "assistant",
"content": "The American Express Centurion Card, commonly known as the \"Black Card,\" is an invitation-only charge card designed for high-net-worth individuals. To be considered for an invitation, you typically need to meet the following criteria:\n\n- **Existing American Express Relationship**: You should have an active American Express card account.\n\n- **High Annual Spending**: While American Express does not publicly disclose specific spending thresholds, reports suggest that spending between $250,000 and $500,000 annually on your American Express cards may increase your chances of receiving an invitation. ([nerdwallet.com](https://www.nerdwallet.com/article/travel/centurion-black-card?utm_source=openai))\n\n- **Luxury Spending Patterns**: Expenditures on luxury items, such as high-end travel, fine dining, and premium goods, are considered favorable. ([fool.com](https://www.fool.com/the-ascent/credit-cards/articles/your-guide-to-getting-an-amex-black-card/?utm_source=openai))\n\n- **Excellent Credit Score**: A credit score of 800 or higher is typically expected. ([finance.yahoo.com](https://finance.yahoo.com/personal-finance/american-express-centurion-card-213619679.html?utm_source=openai))\n\nIf you believe you meet these criteria, you can express your interest by visiting the official Centurion website and submitting a request for consideration. Please note that submitting a request does not guarantee an invitation, as American Express evaluates each request based on its internal criteria. ([thepointsguy.com](https://thepointsguy.com/news/you-can-now-request-an-invite-to-the-amex-centurion-card/?utm_source=openai))\n\nShould you receive an invitation and choose to accept it, be prepared for the following fees:\n\n- **Initiation Fee**: A one-time fee of $10,000.\n\n- **Annual Fee**: An ongoing annual fee of $5,000. ([forbes.com](https://www.forbes.com/advisor/credit-cards/reviews/centurion-from-american-express/?utm_source=openai))\n\nPlease be aware that the Centurion Card is designed for individuals with substantial financial resources and specific spending habits. If you do not meet these criteria, American Express offers a range of other premium cards that might better suit your needs. ",
"agenticSender": "Card Application Process",
"agenticResponseType": "external"
}
]
}
}

View file

@ -1,200 +0,0 @@
{
"messages": [
{
"role": "system",
"content": ""
},
{
"version": "v1",
"chatId": "",
"createdAt": "2025-05-08T07:06:48.280Z",
"role": "assistant",
"content": "How can I help you today?",
"agenticSender": "Example Agent",
"agenticResponseType": "external"
},
{
"role": "user",
"content": "Tell me about article 1",
"version": "v1",
"chatId": "",
"createdAt": "2025-05-08T07:06:53.518Z"
},
{
"version": "v1",
"chatId": "",
"createdAt": "2025-05-08T07:06:55.690Z",
"role": "assistant",
"tool_calls": [
{
"id": "call_uzRd4Y1CeBioJ9h26XeGhCvH",
"function": {
"name": "rag_search",
"arguments": "{\"query\":\"article 1\"}"
},
"type": "function"
}
],
"agenticSender": "Example Agent",
"agenticResponseType": "internal"
},
{
"version": "v1",
"chatId": "",
"createdAt": "2025-05-08T07:06:55.690Z",
"role": "tool",
"content": "{\n \"Information\": [\n {\n \"title\": \"text\",\n \"name\": \"text\",\n \"content\": \"This is article 1.\",\n \"docId\": \"681c50f17e7a18621c7215cd\",\n \"sourceId\": \"681c50f17e7a18621c7215cc\"\n },\n {\n \"title\": \"text\",\n \"name\": \"text\",\n \"content\": \"This is article 2.\",\n \"docId\": \"681c51067e7a18621c7215cf\",\n \"sourceId\": \"681c51067e7a18621c7215ce\"\n },\n {\n \"title\": \"text\",\n \"name\": \"text\",\n \"content\": \"This is article 3.\",\n \"docId\": \"681c51137e7a18621c7215d1\",\n \"sourceId\": \"681c51137e7a18621c7215d0\"\n }\n ]\n}",
"tool_call_id": "call_uzRd4Y1CeBioJ9h26XeGhCvH",
"tool_name": ""
},
{
"version": "v1",
"chatId": "",
"createdAt": "2025-05-08T07:06:57.096Z",
"role": "assistant",
"content": "Article 1 states: \"This is article 1.\"\n\nIf you need more details or information about another article, please let me know!",
"agenticSender": "Example Agent",
"agenticResponseType": "external"
}
],
"lastRequest": {
"projectId": "8e59c4b6-91d2-42e9-8990-41583f4105f1",
"messages": [
{
"content": "",
"role": "system",
"sender": null,
"tool_calls": null,
"tool_call_id": null,
"tool_name": null
},
{
"content": "How can I help you today?",
"role": "assistant",
"sender": "Example Agent",
"tool_calls": null,
"tool_call_id": null,
"tool_name": null,
"response_type": "external"
},
{
"content": "Tell me about article 1",
"role": "user",
"sender": null,
"tool_calls": null,
"tool_call_id": null,
"tool_name": null
}
],
"state": {
"last_agent_name": "Example Agent",
"tokens": {
"total": 0,
"prompt": 0,
"completion": 0
}
},
"agents": [
{
"name": "Example Agent",
"type": "conversation",
"description": "An example agent that uses the rag_search tool to fetch information for customer support queries.",
"instructions": "## 🧑‍💼 Role:\nYou are a helpful customer support assistant who fetches information using the rag_search tool.\n\n---\n## ⚙️ Steps to Follow:\n1. Ask the user what they would like help with.\n2. If the user's query requires information or an answer, use the [@tool:rag_search] tool with the user's question as the query.\n3. Provide the user with an answer based on the information retrieved from the tool.\n4. If the user's issue is about follow-up or requires human support, ask for their email address and let them know someone will contact them soon.\n5. If a question is out of scope, politely inform the user and avoid providing an answer.\n\n---\n## 🎯 Scope:\n✅ In Scope:\n- Answering customer support questions using information from articles.\n- Asking the user their issue and getting their email if needed.\n\n❌ Out of Scope:\n- Questions unrelated to customer support.\n- Providing answers without using the rag_search tool when information is needed.\n\n---\n## 📋 Guidelines:\n✔ Dos:\n- Use [@tool:rag_search](#mention) to fetch accurate information for user queries.\n- Provide clear and concise answers based on the tool's results.\n- Ask for the user's email if the issue requires follow-up.\n\n🚫 Don'ts:\n- Do not provide answers without fetching information when required.\n- Do not ask for details other than the user's email.\n\n# Examples\n- **User** : What is your refund policy?\n - **Agent actions**: Call [@tool:rag_search](#mention)\n - **Agent response**: Our refund policy is as follows: <summary from rag_search results>\n\n- **User** : How can I change my password?\n - **Agent actions**: Call [@tool:rag_search](#mention)\n - **Agent response**: To change your password, follow these steps: <steps from rag_search results>\n\n- **User** : I need help with my order.\n - **Agent actions**: Call [@tool:rag_search](#mention)\n - **Agent response**: Can you please provide more details about your order issue? <additional info from rag_search results if relevant>\n\n- **User** : I want someone to contact me about a billing issue.\n - **Agent response**: Sure, could you please provide your email address so someone from our team can contact you soon?\n\n- **User** : Can you tell me about your subscription plans?\n - **Agent actions**: Call [@tool:rag_search](#mention)\n - **Agent response**: Here are our subscription plans: <details from rag_search results>",
"model": "gpt-4.1",
"controlType": "retain",
"ragDataSources": [
"681c50f17e7a18621c7215cc",
"681c51067e7a18621c7215ce",
"681c51137e7a18621c7215d0"
],
"ragK": 1,
"ragReturnType": "chunks",
"tools": [
"rag_search"
],
"prompts": [],
"connectedAgents": []
}
],
"tools": [
{
"name": "web_search",
"description": "Fetch information from the web based on chat context",
"parameters": {
"type": "object",
"properties": {}
},
"isLibrary": true
},
{
"name": "rag_search",
"description": "Fetch articles with knowledge relevant to the query",
"parameters": {
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "The query to retrieve articles for"
}
},
"required": [
"query"
]
},
"isLibrary": true
}
],
"prompts": [],
"startAgent": "Example Agent",
"mcpServers": [],
"toolWebhookUrl": ""
},
"lastResponse": {
"state": {
"last_agent_name": "Example Agent",
"tokens": {
"total": 1581,
"prompt": 1533,
"completion": 48
}
},
"messages": [
{
"version": "v1",
"chatId": "",
"createdAt": "2025-05-08T07:06:55.690Z",
"role": "assistant",
"tool_calls": [
{
"id": "call_uzRd4Y1CeBioJ9h26XeGhCvH",
"function": {
"name": "rag_search",
"arguments": "{\"query\":\"article 1\"}"
},
"type": "function"
}
],
"agenticSender": "Example Agent",
"agenticResponseType": "internal"
},
{
"version": "v1",
"chatId": "",
"createdAt": "2025-05-08T07:06:55.690Z",
"role": "tool",
"content": "{\n \"Information\": [\n {\n \"title\": \"text\",\n \"name\": \"text\",\n \"content\": \"This is article 1.\",\n \"docId\": \"681c50f17e7a18621c7215cd\",\n \"sourceId\": \"681c50f17e7a18621c7215cc\"\n },\n {\n \"title\": \"text\",\n \"name\": \"text\",\n \"content\": \"This is article 2.\",\n \"docId\": \"681c51067e7a18621c7215cf\",\n \"sourceId\": \"681c51067e7a18621c7215ce\"\n },\n {\n \"title\": \"text\",\n \"name\": \"text\",\n \"content\": \"This is article 3.\",\n \"docId\": \"681c51137e7a18621c7215d1\",\n \"sourceId\": \"681c51137e7a18621c7215d0\"\n }\n ]\n}",
"tool_call_id": "call_uzRd4Y1CeBioJ9h26XeGhCvH",
"tool_name": ""
},
{
"version": "v1",
"chatId": "",
"createdAt": "2025-05-08T07:06:57.096Z",
"role": "assistant",
"content": "Article 1 states: \"This is article 1.\"\n\nIf you need more details or information about another article, please let me know!",
"agenticSender": "Example Agent",
"agenticResponseType": "external"
}
]
}
}

View file

@ -1,110 +0,0 @@
{
"lastResponse": {
"messages": [
{
"content": "I have retrieved your last booking details for you. \n\n- **Booking ID:** BK123456789\n- **Destination:** Paris, France\n- **Departure Date:** February 15, 2025\n- **Return Date:** February 20, 2025\n- **Passenger Name:** John Doe\n- **Status:** Confirmed\n\nIs there anything specific you would like assistance with regarding this booking?",
"created_at": "2025-01-14T13:21:41.011210",
"current_turn": true,
"response_type": "internal",
"role": "assistant",
"sender": "Customer Support Hub Agent"
},
{
"content": "Here are your last booking details:\n\n- **Booking ID:** BK123456789\n- **Destination:** Paris, France\n- **Departure Date:** February 15, 2025\n- **Return Date:** February 20, 2025\n- **Passenger Name:** John Doe\n- **Status:** Confirmed\n\nIs there anything specific you need help with regarding this booking?",
"created_at": "2025-01-14T13:21:41.011210",
"current_turn": true,
"response_type": "external",
"role": "assistant",
"sender": "Customer Support Hub Agent >> Post process"
}
],
"state": {
"agent_data": [
{
"child_functions": [],
"external_tools": [
"fetch_user_info",
"getArticleInfo"
],
"history": [
{
"content": "Hello!",
"current_turn": false,
"role": "user"
},
{
"content": "Hello! How can I assist you today?",
"created_at": "2025-01-14T13:21:23.928202",
"current_turn": false,
"response_type": "internal",
"role": "assistant",
"sender": "Customer Support Hub Agent"
},
{
"content": "Can you help me with my last booking ID?",
"current_turn": true,
"role": "user"
},
{
"created_at": "2025-01-14T13:21:33.209215",
"current_turn": true,
"response_type": "internal",
"role": "assistant",
"sender": "Customer Support Hub Agent",
"tool_calls": [
{
"function": {
"arguments": "{\"user_id\":\"user-provided-or-contextual-id\"}",
"name": "fetch_user_info"
},
"id": "call_ysJNztzRa0kRsy4Z4hljVuQ9",
"type": "function"
}
]
},
{
"content": "\"{\\\"result\\\":{\\\"user_id\\\":\\\"user-provided-or-contextual-id\\\",\\\"last_booking_id\\\":\\\"BK123456789\\\",\\\"booking_date\\\":\\\"2025-01-10T15:30:00Z\\\",\\\"status\\\":\\\"confirmed\\\",\\\"details\\\":{\\\"destination\\\":\\\"Paris, France\\\",\\\"departure_date\\\":\\\"2025-02-15\\\",\\\"return_date\\\":\\\"2025-02-20\\\",\\\"passenger_name\\\":\\\"John Doe\\\"}}}\"",
"role": "tool",
"tool_call_id": "call_ysJNztzRa0kRsy4Z4hljVuQ9",
"tool_name": "fetch_user_info"
},
{
"content": "I have retrieved your last booking details for you. \n\n- **Booking ID:** BK123456789\n- **Destination:** Paris, France\n- **Departure Date:** February 15, 2025\n- **Return Date:** February 20, 2025\n- **Passenger Name:** John Doe\n- **Status:** Confirmed\n\nIs there anything specific you would like assistance with regarding this booking?",
"created_at": "2025-01-14T13:21:41.011210",
"current_turn": true,
"response_type": "internal",
"role": "assistant",
"sender": "Customer Support Hub Agent"
}
],
"instructions": "## 🧑‍💼 Role:\nYou are responsible for directing customer support queries to the appropriate agents.\n\n---\n## ⚙️ Steps to Follow:\n1. Greet the user and ask how you can assist them today.\n2. Use the 'fetch_user_info' tool to retrieve user information in real time using the user ID provided or contextually available in the conversation.\n3. Analyze the user's request to determine if it needs escalation or can be handled by a specialized agent.\n4. Direct the query to the relevant agent for issue-specific queries or escalate to a human representative if needed.\n\n---\n## 🎯 Scope:\n✅ In Scope:\n- Directing queries to suitable agents\n- Escalation when necessary\n\n❌ Out of Scope:\n- Handling detailed support or making assumptions about user issues\n\n---\n## 📋 Guidelines:\n✔ Dos:\n- Maintain a helpful and empathetic tone.\n- Ensure queries are directed promptly.\n🚫 Don'ts:\n- Resolve queries directly that require special handling.\n\nYou should be empathetic and helpful.\n\n----------------------------------------------------------------------------------------------------\n\n\n# Instructions about using the article retrieval tool\n- Where relevant, use the articles tool: getArticleInfo to fetch articles with knowledge relevant to the query and use its contents to respond to the user. \n- Do not send a separate message first asking the user to wait while you look up information. Immediately fetch the articles and respond to the user with the answer to their query. \n- Do not make up information. If the article's contents do not have the answer, give up control of the chat (or transfer to your parent agent, as per your transfer instructions). Do not say anything to the user.\n",
"internal_tools": [],
"most_recent_parent_name": "",
"name": "Customer Support Hub Agent",
"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",
"internal_tools": [],
"most_recent_parent_name": "",
"name": "Escalation",
"parent_function": null
}
],
"last_agent_name": "Customer Support Hub Agent"
},
"tokens_used": {
"openai/gpt-4o": {
"input_tokens": 569,
"output_tokens": 85
},
"openai/gpt-4o-mini": {
"input_tokens": 871,
"output_tokens": 79
}
}
}
}