Merge pull request #72 from rowboatlabs/dev

Dev changes
This commit is contained in:
Ramnique Singh 2025-04-18 16:19:29 +05:30 committed by GitHub
commit 0b3e3a25d7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
41 changed files with 2274 additions and 1091 deletions

1
.gitignore vendored
View file

@ -2,3 +2,4 @@
.env
.vscode/
data/
.venv/

View file

@ -1,11 +1,13 @@
from flask import Flask, request, jsonify
from flask import Flask, request, jsonify, Response, stream_with_context
from pydantic import BaseModel, ValidationError
from typing import List
from copilot import UserMessage, AssistantMessage, get_response
from copilot import UserMessage, AssistantMessage, get_response, openai_client
from streaming import get_streaming_response
from lib import AgentContext, PromptContext, ToolContext, ChatContext
import os
from functools import wraps
from copilot import copilot_instructions, copilot_instructions_edit_agent
from copilot import copilot_instructions_edit_agent
import json
class ApiRequest(BaseModel):
messages: List[UserMessage | AssistantMessage]
@ -46,24 +48,37 @@ def require_api_key(f):
def health():
return jsonify({'status': 'ok'})
@app.route('/chat', methods=['POST'])
@app.route('/chat_stream', methods=['POST'])
@require_api_key
def chat():
def chat_stream():
try:
request_data = ApiRequest(**request.json)
print(f"received /chat request: {request_data}")
print(f"received /chat_stream 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
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
)
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'
}
)
api_response = ApiResponse(response=response).model_dump()
print(f"sending /chat response: {api_response}")
return jsonify(api_response)
except ValidationError as ve:
print(ve)

View file

@ -16,485 +16,15 @@ class AssistantMessage(BaseModel):
role: Literal["assistant"]
content: str
copilot_instructions = """
## 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 can perform the following tasks:
1. Plan and creating 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
### 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 : Types of Agents
Agents in the system be of the following types:
1. Conversation
Carries out the core customer conversations. All new agents you create should be of type 'Conversation'.
2. Post-processing
Ensures the output aligns with specific format and style requirements.
3. Escalation
Handles scenarios where the system needs to escalate a request to a human representative. Collects necessary information to facilitate escalation.
4. Guardrails
Provides read-only oversight to ensure agents adhere to established guidelines and constraints.
### Section 1.1 : Conversation Agent Behavior
A agent of type conversation 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 agnet' 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 ther 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. Make a brief plan for the multi-agent system. This would give the user a high level overview of the system.
2. When necessary decompose the problem into multiple smaller agents.
3. Create a first draft of a new agent for each step in the plan. Use the format of the example agent.
4. Check if the agent needs any tools. Create any necessary tools and attach them to the agents.
5. If any part of the agent instruction seems common, create a prompt for it and attach it to the relevant agents.
6. 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.
7. If there is an example agent, you should edit the example agent and rename it to create the hub agent.
## Section 3 : 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 3.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 : 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 5 : Adding / Editing / Removing Tools
1. Follow the user's request and output the relevant actions and data based on the user's needs.
2. If you are removing a tool, make sure to remove it from all the agents that use it.
3. If you are adding a tool, make sure to add it to all the agents that need it.
## Section 6 : Adding / Editing / Removing Prompts
1. Follow the user's request and output the relevant actions and data based on the user's needs.
2. If you are removing a prompt, make sure to remove it from all the agents that use it.
3. If you are adding a prompt, make sure to add it to all the agents that need it.
4. Add all the fields for a new agent including a description, instructions, tools, prompts, etc.
## Section 7 : Doing Multiple Actions at a Time
1. you should present your changes in order of : tools, prompts, agents.
2. Make sure to add, remove tools and prompts from agents as required.
## Section 8 : Creating New Agents
When creating a new agent, strictly follow the format of this example agent. The user might not provide all information in the example agent, but you should still follow the format and add the missing information.
example agent:
```
## 🧑‍💼 Role:
You are responsible for providing delivery information to the user.
---
## ⚙️ Steps to Follow:
1. Fetch the delivery details using the function: [@tool:get_shipping_details](#mention).
2. Answer the user's question based on the fetched delivery details.
3. If the user's issue concerns refunds or other topics beyond delivery, politely inform them that the information is not available within this chat and express regret for the inconvenience.
4. If the user's request is out of scope, call [@agent:Delivery Hub](#mention).
---
## 🎯 Scope:
In Scope:
- Questions about delivery status, shipping timelines, and delivery processes.
- Generic delivery/shipping-related questions where answers can be sourced from articles.
Out of Scope:
- Questions unrelated to delivery or shipping.
- Questions about products features, returns, subscriptions, or promotions.
- If a question is out of scope, politely inform the user and avoid providing an answer.
---
## 📋 Guidelines:
Dos:
- Use [@tool:get_shipping_details](#mention) to fetch accurate delivery information.
- Provide complete and clear answers based on the delivery details.
- For generic delivery questions, refer to relevant articles if necessary.
- Stick to factual information when answering.
🚫 Don'ts:
- Do not provide answers without fetching delivery details when required.
- Do not leave the user with partial information. Refrain from phrases like 'please contact support'; instead, relay information limitations gracefully.
'''
use GPT-4o as the default model for new agents. Always add a line to the agents instruction to call the parent agent if the user's request is out of scope.
## Section 9: 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. Add any post processing or style related request to the post processing agent.
6. Add you thoughts or plans to the plan section.
7. When you are suggesting a set of actions, add a text section that describes the changes being made before and after the actions.
8. After providing the actions, add a text section with something like 'Once you review and apply the high-level plan, you can try out a basic chat first. I can then help you better configure each agent.'
9. 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?"
10. Always edit the examples as well when editing an agent.
11. Always add a line to the agents instruction to call the parent agent if the user's request is out of scope.
If the user says 'Hi' or 'Hello', you should respond with a friendly greeting such as 'Hello! How can I help you today?'
## Section 10: Output Format
Output format:
Note : Always add a text section that describes the changes before each action.
``` json
{
"type": "object",
"properties": {
"plan": {
"type": "string",
"description": "A brief plan for your actions."
},
"response": {
"type": "array",
"items": {
"oneOf": [
{
"type": "object",
"properties": {
"type": { "const": "text" },
"content": { "type": "string", "description": "A short snippet describing the changes or asking clarifying questions." }
},
"required": ["type", "content"]
},
{
"type": "object",
"properties": {
"type": { "const": "action" },
"content": {
"type": "object",
"properties": {
"config_type": { "type": "string", "enum": ["prompt", "agent", "tool"] },
"action": { "type": "string", "enum": ["edit", "enable", "disable", "delete", "create_new"] },
"name": { "type": "string" },
"config_changes": { "type": "object" },
"change_description": { "type": "string" }
},
"required": ["config_type", "action", "name", "change_description"]
}
},
"required": ["type", "content"]
}
]
},
"description": "An array of items where each item is either a 'text' object or an 'action' object."
}
},
"required": ["response"]
}
```
**NOTE**: The output should be a valid JSON object. Do not include any other text or comments. Do not wrap the output in a code block.
**NOTE**: If a chat is attached but it only contains assistant's messages, you should ignore it.
## Section 11: 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:
```json
{
"response": [
{
"type": "text",
"content": "High-Level Plan: The customer support system for 2FA queries will consist of a hub agent to direct user queries to specific sub-agents. The sub-agents will handle 2FA setup, 2FA method changes, and troubleshooting issues. A specialized tool will be created to fetch the current 2FA method for seamless handling of user requests."
},
{
"type": "text",
"content": "Creating the 'get_current_2fa_method' tool to fetch the user's current 2FA method and assist the 2FA Change agent."
},
{
"type": "action",
"content": {
"config_type": "tool",
"action": "create_new",
"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"
]
}
}
}
},
{
"type": "text",
"content": "Creating the '2FA Setup' agent to assist users in setting up their preferred 2FA method."
},
{
"type": "action",
"content": {
"config_type": "agent",
"action": "create_new",
"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,
"controlType": "Retain control"
}
}
},
{
"type": "text",
"content": "Creating the '2FA Change' agent to help users change their 2FA methods effectively."
},
{
"type": "action",
"content": {
"config_type": "agent",
"action": "create_new",
"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,
"controlType": "Retain control"
}
}
},
{
"type": "text",
"content": "Creating the '2FA Troubleshooting' agent to assist users in resolving issues with receiving 2FA codes."
},
{
"type": "action",
"content": {
"config_type": "agent",
"action": "create_new",
"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, pass control to [@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,
"controlType": "Retain control"
}
}
},
{
"type": "text",
"content": "Creating the hub agent '2FA Hub' by modifying the Example Agent to manage and direct 2FA-related queries to specific agents."
},
{
"type": "action",
"content": {
"config_type": "agent",
"action": "edit",
"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- Call [@agent:Escalation](#mention) agent for unrecognized queries.\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** : Can you reset my 2FA 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?"
}
}
},
{
"type": "text",
"content": "Once you review and apply the high-level plan, you can try out a basic chat first. I can then help you better configure each agent."
}
]
}
```
### Example 2:
User: What can you help me with?
Copilot output:
```json
{
"response": "<new instructions with relevant changes>"
}
```
## Section 12: 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.
"""
copilot_instructions_edit_agent = """
## 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>"
}
```
"""
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,
copilot_instructions: str = copilot_instructions
copilot_instructions: str = copilot_instructions_edit_agent
) -> str:
# if context is provided, create a prompt for the context
if context:

View file

@ -0,0 +1,64 @@
## 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

@ -0,0 +1,177 @@
## 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 : 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 3.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 : 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 5 : Adding / Editing / Removing Tools
1. Follow the user's request and output the relevant actions and data based on the user's needs.
2. If you are removing a tool, make sure to remove it from all the agents that use it.
3. If you are adding a tool, make sure to add it to all the agents that need it.
## Section 6 : Adding / Editing / Removing Prompts
1. Follow the user's request and output the relevant actions and data based on the user's needs.
2. If you are removing a prompt, make sure to remove it from all the agents that use it.
3. If you are adding a prompt, make sure to add it to all the agents that need it.
4. Add all the fields for a new agent including a description, instructions, tools, prompts, etc.
## Section 7 : Doing Multiple Actions at a Time
1. you should present your changes in order of : tools, prompts, agents.
2. Make sure to add, remove tools and prompts from agents as required.
## Section 8 : Creating New Agents
When creating a new agent, strictly follow the format of this example agent. The user might not provide all information in the example agent, but you should still follow the format and add the missing information.
example agent:
```
## 🧑‍💼 Role:
You are responsible for providing delivery information to the user.
---
## ⚙️ Steps to Follow:
1. Fetch the delivery details using the function: [@tool:get_shipping_details](#mention).
2. Answer the user's question based on the fetched delivery details.
3. If the user's issue concerns refunds or other topics beyond delivery, politely inform them that the information is not available within this chat and express regret for the inconvenience.
4. If the user's request is out of scope, call [@agent:Delivery Hub](#mention)
---
## 🎯 Scope:
✅ In Scope:
- Questions about delivery status, shipping timelines, and delivery processes.
- Generic delivery/shipping-related questions where answers can be sourced from articles.
❌ Out of Scope:
- Questions unrelated to delivery or shipping.
- Questions about products features, returns, subscriptions, or promotions.
- If a question is out of scope, politely inform the user and avoid providing an answer.
---
## 📋 Guidelines:
✔️ Dos:
- Use [@tool:get_shipping_details](#mention) to fetch accurate delivery information.
- Provide complete and clear answers based on the delivery details.
- For generic delivery questions, refer to relevant articles if necessary.
- Stick to factual information when answering.
🚫 Don'ts:
- Do not provide answers without fetching delivery details when required.
- Do not leave the user with partial information. Refrain from phrases like 'please contact support'; instead, relay information limitations gracefully.
'''
use GPT-4o as the default model for new agents.
## Section 9: 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. Add any post processing or style related request to the post processing agent.
6. 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.'
7. 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?"
8. Always speak with agency like "I'll do ... ", "I'll create ..."
9. Don't mention the style prompt
10. 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.
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.

View file

@ -0,0 +1,11 @@
## 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

@ -0,0 +1,118 @@
## 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
}
}
```
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
}
}
```
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
}
}
```
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?"
}
}
```
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?
---

163
apps/copilot/streaming.py Normal file
View file

@ -0,0 +1,163 @@
from openai import OpenAI
from flask import Flask, request, jsonify, Response, stream_with_context
from pydantic import BaseModel, ValidationError
from typing import List, Dict, Any, Literal
import json
from lib import AgentContext, PromptContext, ToolContext, ChatContext
openai_client = OpenAI()
MODEL_NAME = "gpt-4.1" # OpenAI model name
class UserMessage(BaseModel):
role: Literal["user"]
content: str
class AssistantMessage(BaseModel):
role: Literal["assistant"]
content: str
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,
) -> 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 the workflow schema to the system prompt
sys_prompt = streaming_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}
User: {last_message.content}
"""
updated_msgs = [{"role": "system", "content": sys_prompt}] + [
message.model_dump() for message in messages
]
return openai_client.chat.completions.create(
model=MODEL_NAME,
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
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
def generate():
stream = get_streaming_response(
messages=messages,
workflow_schema=workflow_schema,
current_workflow_config=current_workflow_config,
context=context
)
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

@ -5,14 +5,14 @@ import {
CopilotChatContext, CopilotMessage, CopilotAssistantMessage, CopilotWorkflow
} from "../lib/types/copilot_types";
import {
Workflow, WorkflowTool, WorkflowPrompt, WorkflowAgent
} from "../lib/types/workflow_types";
Workflow} from "../lib/types/workflow_types";
import { z } from 'zod';
import { zodToJsonSchema } from 'zod-to-json-schema';
import { assert } from "node:console";
import { check_query_limit } from "../lib/rate_limiting";
import { QueryLimitError } from "../lib/client_utils";
import { QueryLimitError, validateConfigChanges } from "../lib/client_utils";
import { projectAuthCheck } from "./project_actions";
import { redisClient } from "../lib/redis";
export async function getCopilotResponse(
projectId: string,
@ -67,88 +67,19 @@ export async function getCopilotResponse(
// validate response schema
assert(msg.role === 'assistant');
if (msg.role === 'assistant') {
for (const part of msg.content.response) {
const content = JSON.parse(msg.content);
for (const part of content.response) {
if (part.type === 'action') {
switch (part.content.config_type) {
case 'tool': {
const test = {
name: 'test',
description: 'test',
parameters: {
type: 'object',
properties: {},
required: [],
},
} as z.infer<typeof WorkflowTool>;
// iterate over each field in part.content.config_changes
// and test if the final object schema is valid
// if not, discard that field
for (const [key, value] of Object.entries(part.content.config_changes)) {
const result = WorkflowTool.safeParse({
...test,
[key]: value,
});
if (!result.success) {
console.log(`discarding field ${key} from ${part.content.config_type}: ${part.content.name}`, result.error.message);
delete part.content.config_changes[key];
}
}
break;
}
case 'agent': {
const test = {
name: 'test',
description: 'test',
type: 'conversation',
instructions: 'test',
prompts: [],
tools: [],
model: 'gpt-4o',
ragReturnType: 'chunks',
ragK: 10,
connectedAgents: [],
controlType: 'retain',
} as z.infer<typeof WorkflowAgent>;
// iterate over each field in part.content.config_changes
// and test if the final object schema is valid
// if not, discard that field
for (const [key, value] of Object.entries(part.content.config_changes)) {
const result = WorkflowAgent.safeParse({
...test,
[key]: value,
});
if (!result.success) {
console.log(`discarding field ${key} from ${part.content.config_type}: ${part.content.name}`, result.error.message);
delete part.content.config_changes[key];
}
}
break;
}
case 'prompt': {
const test = {
name: 'test',
type: 'base_prompt',
prompt: "test",
} as z.infer<typeof WorkflowPrompt>;
// iterate over each field in part.content.config_changes
// and test if the final object schema is valid
// if not, discard that field
for (const [key, value] of Object.entries(part.content.config_changes)) {
const result = WorkflowPrompt.safeParse({
...test,
[key]: value,
});
if (!result.success) {
console.log(`discarding field ${key} from ${part.content.config_type}: ${part.content.name}`, result.error.message);
delete part.content.config_changes[key];
}
}
break;
}
default: {
part.content.error = `Unknown config type: ${part.content.config_type}`;
break;
}
const result = validateConfigChanges(
part.content.config_type,
part.content.config_changes,
part.content.name
);
if ('error' in result) {
part.content.error = result.error;
} else {
part.content.config_changes = result.changes;
}
}
}
@ -161,6 +92,43 @@ export async function getCopilotResponse(
};
}
export async function getCopilotResponseStream(
projectId: string,
messages: z.infer<typeof CopilotMessage>[],
current_workflow_config: z.infer<typeof Workflow>,
context: z.infer<typeof CopilotChatContext> | null
): Promise<{
streamId: string;
}> {
await projectAuthCheck(projectId);
if (!await check_query_limit(projectId)) {
throw new QueryLimitError();
}
// prepare request
const request: z.infer<typeof CopilotAPIRequest> = {
messages: messages.map(convertToCopilotApiMessage),
workflow_schema: JSON.stringify(zodToJsonSchema(CopilotWorkflow)),
current_workflow_config: JSON.stringify(convertToCopilotWorkflow(current_workflow_config)),
context: context ? convertToCopilotApiChatContext(context) : null,
};
// serialize the request
const payload = JSON.stringify(request);
// create a uuid for the stream
const streamId = crypto.randomUUID();
// store payload in redis
await redisClient.set(`copilot-stream-${streamId}`, payload, {
EX: 60 * 10, // expire in 10 minutes
});
return {
streamId,
};
}
export async function getCopilotAgentInstructions(
projectId: string,
messages: z.infer<typeof CopilotMessage>[],

View file

@ -0,0 +1,52 @@
import { redisClient } from "@/app/lib/redis";
export async function GET(request: Request, { params }: { params: { streamId: string } }) {
// get the payload from redis
const payload = await redisClient.get(`copilot-stream-${params.streamId}`);
if (!payload) {
return new Response("Stream not found", { status: 404 });
}
// Fetch the upstream SSE stream.
const upstreamResponse = await fetch(`${process.env.COPILOT_API_URL}/chat_stream`, {
method: 'POST',
body: payload,
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${process.env.COPILOT_API_KEY || 'test'}`,
},
cache: 'no-store',
});
// If the upstream request fails, return a 502 Bad Gateway.
if (!upstreamResponse.ok || !upstreamResponse.body) {
return new Response("Error connecting to upstream SSE stream", { status: 502 });
}
const reader = upstreamResponse.body.getReader();
const stream = new ReadableStream({
async start(controller) {
try {
// Read from the upstream stream continuously.
while (true) {
const { done, value } = await reader.read();
if (done) break;
// Immediately enqueue each received chunk.
controller.enqueue(value);
}
controller.close();
} catch (error) {
controller.error(error);
}
},
});
return new Response(stream, {
headers: {
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache",
"Connection": "keep-alive",
},
});
}

View file

@ -1,6 +1,73 @@
import { WorkflowTool, WorkflowAgent, WorkflowPrompt } from "./types/workflow_types";
import { z } from "zod";
export class QueryLimitError extends Error {
constructor(message: string = 'Query limit exceeded') {
super(message);
this.name = 'QueryLimitError';
}
}
}
export function validateConfigChanges(configType: string, configChanges: Record<string, unknown>, name: string) {
let testObject: any;
let schema: z.ZodType<any>;
switch (configType) {
case 'tool': {
testObject = {
name: 'test',
description: 'test',
parameters: {
type: 'object',
properties: {},
required: [],
},
} as z.infer<typeof WorkflowTool>;
schema = WorkflowTool;
break;
}
case 'agent': {
testObject = {
name: 'test',
description: 'test',
type: 'conversation',
instructions: 'test',
prompts: [],
tools: [],
model: 'gpt-4o',
ragReturnType: 'chunks',
ragK: 10,
connectedAgents: [],
controlType: 'retain',
} as z.infer<typeof WorkflowAgent>;
schema = WorkflowAgent;
break;
}
case 'prompt': {
testObject = {
name: 'test',
type: 'base_prompt',
prompt: "test",
} as z.infer<typeof WorkflowPrompt>;
schema = WorkflowPrompt;
break;
}
default:
return { error: `Unknown config type: ${configType}` };
}
// Validate each field and remove invalid ones
const validatedChanges = { ...configChanges };
for (const [key, value] of Object.entries(configChanges)) {
const result = schema.safeParse({
...testObject,
[key]: value,
});
if (!result.success) {
console.log(`discarding field ${key} from ${configType}: ${name}`, result.error.message);
delete validatedChanges[key];
}
}
return { changes: validatedChanges };
}

View file

@ -8,4 +8,6 @@ export const USE_AUTH = process.env.USE_AUTH === 'true';
export const USE_MULTIPLE_PROJECTS = true;
export const USE_TESTING_FEATURE = false;
export const USE_VOICE_FEATURE = false;
export const USE_TRANSFER_CONTROL_OPTIONS = false;
export const USE_TRANSFER_CONTROL_OPTIONS = false;
export const USE_PRODUCT_TOUR = true;
export const SHOW_COPILOT_MARQUEE = false;

View file

@ -44,19 +44,18 @@ You are an helpful customer support assistant
controlType: "retain",
},
],
prompts: [
prompts: [],
tools: [
{
name: "Style prompt",
type: "style_prompt",
prompt: "You should be empathetic and helpful.",
},
{
name: "Greeting",
type: "greeting",
prompt: "Hello! How can I help you?"
"name": "web_search",
"description": "Fetch information from the web based on chat context",
"parameters": {
"type": "object",
"properties": {},
},
"isLibrary": true
}
],
tools: [],
}
}

View file

@ -28,10 +28,7 @@ export const CopilotAssistantMessageActionPart = z.object({
});
export const CopilotAssistantMessage = z.object({
role: z.literal('assistant'),
content: z.object({
thoughts: z.string().optional(),
response: z.array(z.union([CopilotAssistantMessageTextPart, CopilotAssistantMessageActionPart])),
}),
content: z.string(),
});
export const CopilotMessage = z.union([CopilotUserMessage, CopilotAssistantMessage]);

View file

@ -11,7 +11,9 @@ export const WorkflowAgent = z.object({
instructions: z.string(),
examples: z.string().optional(),
model: z.union([
z.literal('gpt-4.1'),
z.literal('gpt-4o'),
z.literal('gpt-4.1-mini'),
z.literal('gpt-4o-mini'),
]),
locked: z.boolean().default(false).describe('Whether this agent is locked and cannot be deleted').optional(),
@ -46,6 +48,7 @@ export const WorkflowTool = z.object({
required: z.array(z.string()).optional(),
}),
isMcp: z.boolean().default(false).optional(),
isLibrary: z.boolean().default(false).optional(),
mcpServerName: z.string().optional(),
});
export const Workflow = z.object({
@ -116,7 +119,7 @@ export function sanitizeTextWithMentions(
}
return false;
})
// sanitize text
for (const entity of entities) {
const id = `${entity.type}:${entity.name}`;

View file

@ -1,23 +1,22 @@
'use client';
import { Button } from "@/components/ui/button";
import { Dropdown, DropdownItem, DropdownMenu, DropdownSection, DropdownTrigger, Spinner, Tooltip } from "@heroui/react";
import { useRef, useState, createContext, useContext, useCallback, forwardRef, useImperativeHandle, useEffect, Ref } from "react";
import { CopilotChatContext } from "../../../lib/types/copilot_types";
import { CopilotMessage } from "../../../lib/types/copilot_types";
import { CopilotAssistantMessageActionPart } from "../../../lib/types/copilot_types";
import { Workflow } from "@/app/lib/types/workflow_types";
import { z } from "zod";
import { getCopilotResponse } from "@/app/actions/copilot_actions";
import { Action as WorkflowDispatch } from "../workflow/workflow_editor";
import { Panel } from "@/components/common/panel-common";
import { ComposeBoxCopilot } from "@/components/common/compose-box-copilot";
import { Messages } from "./components/messages";
import { CopyIcon, CheckIcon, PlusIcon, XIcon } from "lucide-react";
import { CopyIcon, CheckIcon, PlusIcon, XIcon, InfoIcon } from "lucide-react";
import { useCopilot } from "./use-copilot";
const CopilotContext = createContext<{
workflow: z.infer<typeof Workflow> | null;
handleApplyChange: (messageIndex: number, actionIndex: number, field?: string) => void;
appliedChanges: Record<string, boolean>;
}>({ workflow: null, handleApplyChange: () => { }, appliedChanges: {} });
dispatch: (action: any) => void;
}>({ workflow: null, dispatch: () => { } });
export function getAppliedChangeKey(messageIndex: number, actionIndex: number, field: string) {
return `${messageIndex}-${actionIndex}-${field}`;
@ -28,8 +27,9 @@ interface AppProps {
workflow: z.infer<typeof Workflow>;
dispatch: (action: any) => void;
chatContext?: any;
onCopyJson?: (data: { messages: any[], lastRequest: any, lastResponse: any }) => void;
onCopyJson?: (data: { messages: any[] }) => void;
onMessagesChange?: (messages: z.infer<typeof CopilotMessage>[]) => void;
isInitialState?: boolean;
}
const App = forwardRef<{ handleCopyChat: () => void }, AppProps>(function App({
@ -39,15 +39,36 @@ const App = forwardRef<{ handleCopyChat: () => void }, AppProps>(function App({
chatContext = undefined,
onCopyJson,
onMessagesChange,
isInitialState = false,
}, ref) {
const messagesEndRef = useRef<HTMLDivElement>(null);
const [messages, setMessages] = useState<z.infer<typeof CopilotMessage>[]>([]);
const [loadingResponse, setLoadingResponse] = useState(false);
const [responseError, setResponseError] = useState<string | null>(null);
const [appliedChanges, setAppliedChanges] = useState<Record<string, boolean>>({});
const [discardContext, setDiscardContext] = useState(false);
const [lastRequest, setLastRequest] = useState<unknown | null>(null);
const [lastResponse, setLastResponse] = useState<unknown | null>(null);
const [isLastInteracted, setIsLastInteracted] = useState(isInitialState);
const workflowRef = useRef(workflow);
const startRef = useRef<any>(null);
const cancelRef = useRef<any>(null);
// Keep workflow ref up to date
workflowRef.current = workflow;
// Get the effective context based on user preference
const effectiveContext = discardContext ? null : chatContext;
const {
streamingResponse,
loading: loadingResponse,
error: responseError,
start,
cancel
} = useCopilot({
projectId,
workflow: workflowRef.current,
context: effectiveContext
});
// Store latest start/cancel functions in refs
startRef.current = start;
cancelRef.current = cancel;
// Notify parent of message changes
useEffect(() => {
@ -71,189 +92,56 @@ const App = forwardRef<{ handleCopyChat: () => void }, AppProps>(function App({
setDiscardContext(false);
}, [chatContext]);
// Get the effective context based on user preference
const effectiveContext = discardContext ? null : chatContext;
function handleUserMessage(prompt: string) {
setMessages(currentMessages => [...currentMessages, {
role: 'user',
content: prompt
}]);
setResponseError(null);
setIsLastInteracted(true);
}
const handleApplyChange = useCallback((
messageIndex: number,
actionIndex: number,
field?: string
) => {
// validate
console.log('apply change', messageIndex, actionIndex, field);
const msg = messages[messageIndex];
if (!msg) {
console.log('no message');
return;
}
if (msg.role !== 'assistant') {
console.log('not assistant');
return;
}
const action = msg.content.response[actionIndex].content as z.infer<typeof CopilotAssistantMessageActionPart>['content'];
if (!action) {
console.log('no action');
return;
}
console.log('reached here');
if (action.action === 'create_new') {
switch (action.config_type) {
case 'agent':
dispatch({
type: 'add_agent',
agent: {
name: action.name,
...action.config_changes
}
});
break;
case 'tool':
dispatch({
type: 'add_tool',
tool: {
name: action.name,
...action.config_changes
}
});
break;
case 'prompt':
dispatch({
type: 'add_prompt',
prompt: {
name: action.name,
...action.config_changes
}
});
break;
}
const appliedKeys = Object.keys(action.config_changes).reduce((acc, key) => {
acc[getAppliedChangeKey(messageIndex, actionIndex, key)] = true;
return acc;
}, {} as Record<string, boolean>);
setAppliedChanges({
...appliedChanges,
...appliedKeys,
});
} else if (action.action === 'edit') {
const changes = field
? { [field]: action.config_changes[field] }
: action.config_changes;
switch (action.config_type) {
case 'agent':
dispatch({
type: 'update_agent',
name: action.name,
agent: changes
});
break;
case 'tool':
dispatch({
type: 'update_tool',
name: action.name,
tool: changes
});
break;
case 'prompt':
dispatch({
type: 'update_prompt',
name: action.name,
prompt: changes
});
break;
}
const appliedKeys = Object.keys(changes).reduce((acc, key) => {
acc[getAppliedChangeKey(messageIndex, actionIndex, key)] = true;
return acc;
}, {} as Record<string, boolean>);
setAppliedChanges({
...appliedChanges,
...appliedKeys,
});
}
}, [dispatch, appliedChanges, messages]);
// Effect for handling copilot responses
// Effect for getting copilot response
useEffect(() => {
let ignore = false;
if (!messages.length || messages.at(-1)?.role !== 'user') return;
async function process() {
if (!messages.length) return;
const lastMessage = messages[messages.length - 1];
if (lastMessage.role !== 'user') return;
setLoadingResponse(true);
const currentStart = startRef.current;
const currentCancel = cancelRef.current;
try {
const response = await getCopilotResponse(
projectId,
messages,
workflow,
effectiveContext || null,
);
if (ignore) return;
setLastRequest(response.rawRequest);
setLastResponse(response.rawResponse);
setMessages(currentMessages => [...currentMessages, response.message]);
} catch (err) {
if (!ignore) {
setResponseError(`Failed to get copilot response: ${err instanceof Error ? err.message : 'Unknown error'}`);
currentStart(messages, (finalResponse: string) => {
setMessages(prev => [
...prev,
{
role: 'assistant',
content: finalResponse
}
} finally {
if (!ignore) {
setLoadingResponse(false);
}
}
}
]);
});
process();
return () => {
ignore = true;
};
}, [messages, projectId, workflow, effectiveContext]);
// Scroll to bottom on new messages
useEffect(() => {
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
}, [messages, loadingResponse]);
return () => currentCancel();
}, [messages]); // Only depend on messages
const handleCopyChat = useCallback(() => {
if (onCopyJson) {
onCopyJson({
messages,
lastRequest,
lastResponse,
});
}
}, [messages, lastRequest, lastResponse, onCopyJson]);
}, [messages, onCopyJson]);
useImperativeHandle(ref, () => ({
handleCopyChat
}), [handleCopyChat]);
return (
<CopilotContext.Provider value={{ workflow, handleApplyChange, appliedChanges }}>
<CopilotContext.Provider value={{ workflow: workflowRef.current, dispatch }}>
<div className="h-full flex flex-col">
<div className="flex-1 overflow-auto">
<Messages
messages={messages}
streamingResponse={streamingResponse}
loadingResponse={loadingResponse}
workflow={workflow}
handleApplyChange={handleApplyChange}
appliedChanges={appliedChanges}
workflow={workflowRef.current}
dispatch={dispatch}
/>
</div>
<div className="shrink-0 px-1 pb-6">
@ -263,7 +151,9 @@ const App = forwardRef<{ handleCopyChat: () => void }, AppProps>(function App({
<Button
size="sm"
color="danger"
onClick={() => setResponseError(null)}
onClick={() => {
setMessages(prev => [...prev.slice(0, -1)]); // remove last assistant if needed
}}
>
Retry
</Button>
@ -289,7 +179,10 @@ const App = forwardRef<{ handleCopyChat: () => void }, AppProps>(function App({
handleUserMessage={handleUserMessage}
messages={messages}
loading={loadingResponse}
disabled={loadingResponse}
initialFocus={isInitialState}
shouldAutoFocus={isLastInteracted}
onFocus={() => setIsLastInteracted(true)}
onCancel={cancel}
/>
</div>
</div>
@ -302,11 +195,13 @@ export function Copilot({
workflow,
chatContext = undefined,
dispatch,
isInitialState = false,
}: {
projectId: string;
workflow: z.infer<typeof Workflow>;
chatContext?: z.infer<typeof CopilotChatContext>;
dispatch: (action: WorkflowDispatch) => void;
isInitialState?: boolean;
}) {
const [copilotKey, setCopilotKey] = useState(0);
const [showCopySuccess, setShowCopySuccess] = useState(false);
@ -318,7 +213,7 @@ export function Copilot({
setMessages([]);
}
function handleCopyJson(data: { messages: any[], lastRequest: any, lastResponse: any }) {
function handleCopyJson(data: { messages: any[] }) {
const jsonString = JSON.stringify(data, null, 2);
navigator.clipboard.writeText(jsonString);
setShowCopySuccess(true);
@ -329,11 +224,17 @@ export function Copilot({
return (
<Panel variant="copilot"
tourTarget="copilot"
showWelcome={messages.length === 0}
title={
<div className="flex items-center gap-3">
<div className="text-sm font-medium text-gray-900 dark:text-gray-100">
COPILOT
<div className="flex items-center gap-2">
<div className="text-sm font-medium text-gray-900 dark:text-gray-100">
COPILOT
</div>
<Tooltip content="Ask copilot to help you build and modify your workflow">
<InfoIcon className="w-4 h-4 text-gray-400 cursor-help" />
</Tooltip>
</div>
<Button
variant="primary"
@ -375,6 +276,7 @@ export function Copilot({
chatContext={chatContext}
onCopyJson={handleCopyJson}
onMessagesChange={setMessages}
isInitialState={isInitialState}
/>
</div>
</Panel>

View file

@ -1,5 +1,5 @@
'use client';
import { createContext, useContext, useState } from "react";
import { createContext, useContext, useRef, useState } from "react";
import clsx from "clsx";
import { z } from "zod";
import { CopilotAssistantMessageActionPart } from "../../../../lib/types/copilot_types";
@ -7,35 +7,34 @@ import { Workflow } from "../../../../lib/types/workflow_types";
import { PreviewModalProvider, usePreviewModal } from '../../workflow/preview-modal';
import { getAppliedChangeKey } from "../app";
import { AlertTriangleIcon, CheckCheckIcon, CheckIcon, ChevronsDownIcon, ChevronsUpIcon, EyeIcon, PencilIcon, PlusIcon } from "lucide-react";
import { Spinner } from "@heroui/react";
const ActionContext = createContext<{
msgIndex: number;
actionIndex: number;
action: z.infer<typeof CopilotAssistantMessageActionPart>['content'] | null;
workflow: z.infer<typeof Workflow> | null;
handleApplyChange: (messageIndex: number, actionIndex: number, field?: string) => void;
appliedFields: string[];
stale: boolean;
}>({ msgIndex: 0, actionIndex: 0, action: null, workflow: null, handleApplyChange: () => { }, appliedFields: [], stale: false });
}>({ msgIndex: 0, actionIndex: 0, action: null, workflow: null, appliedFields: [], stale: false });
export function Action({
msgIndex,
actionIndex,
action,
workflow,
handleApplyChange,
appliedChanges,
dispatch,
stale,
}: {
msgIndex: number;
actionIndex: number;
action: z.infer<typeof CopilotAssistantMessageActionPart>['content'];
workflow: z.infer<typeof Workflow>;
handleApplyChange: (messageIndex: number, actionIndex: number, field?: string) => void;
appliedChanges: Record<string, boolean>;
dispatch: (action: any) => void;
stale: boolean;
}) {
const [expanded, setExpanded] = useState(false);
const [appliedChanges, setAppliedChanges] = useState<Record<string, boolean>>({});
if (!action || typeof action !== 'object') {
console.warn('Invalid action object:', action);
@ -49,17 +48,115 @@ export function Action({
appliedFields.includes(key)
);
// generate apply change function
const applyChangeHandler = () => {
handleApplyChange(msgIndex, actionIndex);
}
// Handle applying a single field change
const handleFieldChange = (field: string) => {
const changes = { [field]: action.config_changes[field] };
switch (action.config_type) {
case 'agent':
dispatch({
type: 'update_agent',
name: action.name,
agent: changes
});
break;
case 'tool':
dispatch({
type: 'update_tool',
name: action.name,
tool: changes
});
break;
case 'prompt':
dispatch({
type: 'update_prompt',
name: action.name,
prompt: changes
});
break;
}
setAppliedChanges(prev => ({
...prev,
[getAppliedChangeKey(msgIndex, actionIndex, field)]: true
}));
};
// Handle applying all changes
const handleApplyAll = () => {
if (action.action === 'create_new') {
switch (action.config_type) {
case 'agent':
dispatch({
type: 'add_agent',
agent: {
name: action.name,
...action.config_changes
}
});
break;
case 'tool':
dispatch({
type: 'add_tool',
tool: {
name: action.name,
...action.config_changes
}
});
break;
case 'prompt':
dispatch({
type: 'add_prompt',
prompt: {
name: action.name,
...action.config_changes
}
});
break;
}
} else if (action.action === 'edit') {
switch (action.config_type) {
case 'agent':
dispatch({
type: 'update_agent',
name: action.name,
agent: action.config_changes
});
break;
case 'tool':
dispatch({
type: 'update_tool',
name: action.name,
tool: action.config_changes
});
break;
case 'prompt':
dispatch({
type: 'update_prompt',
name: action.name,
prompt: action.config_changes
});
break;
}
}
// Mark all fields as applied
const appliedKeys = Object.keys(action.config_changes).reduce((acc, key) => {
acc[getAppliedChangeKey(msgIndex, actionIndex, key)] = true;
return acc;
}, {} as Record<string, boolean>);
setAppliedChanges(prev => ({
...prev,
...appliedKeys
}));
};
return <div className={clsx('flex flex-col rounded-sm border border-t-4', {
'bg-gray-50 dark:bg-gray-800/50 border-gray-400 dark:border-gray-600 border-t-blue-500 shadow': !stale && !allApplied && action.action == 'create_new',
'bg-gray-50 dark:bg-gray-800/50 border-gray-400 dark:border-gray-600 border-t-orange-500 shadow': !stale && !allApplied && action.action == 'edit',
'bg-gray-100 dark:bg-gray-800/30 border-gray-400 dark:border-gray-600 border-t-gray-400': stale || allApplied || action.error,
})}>
<ActionContext.Provider value={{ msgIndex, actionIndex, action, workflow, handleApplyChange, appliedFields, stale }}>
<ActionContext.Provider value={{ msgIndex, actionIndex, action, workflow, appliedFields, stale }}>
<ActionHeader />
<ActionSummary />
{expanded && <PreviewModalProvider>
@ -69,7 +166,7 @@ export function Action({
</div>}
<div className="flex flex-col gap-2 px-1">
{Object.entries(action.config_changes).map(([key, value]) => {
return <ActionField key={key} field={key} />
return <ActionField key={key} field={key} onApply={handleFieldChange} />
})}
</div>
</PreviewModalProvider>}
@ -82,7 +179,7 @@ export function Action({
</div>}
{!action.error && <button
className="grow rounded-l-sm bg-blue-100 dark:bg-blue-900/20 text-blue-500 dark:text-blue-400 hover:bg-blue-200 dark:hover:bg-blue-900/30 disabled:bg-gray-100 dark:disabled:bg-gray-800/30 disabled:text-gray-300 dark:disabled:text-gray-600 flex flex-col items-center justify-center h-8"
onClick={applyChangeHandler}
onClick={handleApplyAll}
disabled={stale || allApplied}
>
<div className="flex items-center gap-2 justify-center">
@ -108,7 +205,7 @@ export function Action({
}
export function ActionSummary() {
const { msgIndex, actionIndex, action, workflow, handleApplyChange, appliedFields, stale } = useContext(ActionContext);
const { msgIndex, actionIndex, action, workflow, appliedFields, stale } = useContext(ActionContext);
if (!action || !workflow) return null;
return <div className="px-1 my-1">
@ -119,7 +216,7 @@ export function ActionSummary() {
}
export function ActionHeader() {
const { msgIndex, actionIndex, action, workflow, handleApplyChange, appliedFields, stale } = useContext(ActionContext);
const { msgIndex, actionIndex, action, workflow, appliedFields, stale } = useContext(ActionContext);
if (!action || !workflow) return null;
const targetType = action.config_type === 'tool' ? 'tool' : action.config_type === 'agent' ? 'agent' : 'prompt';
@ -134,10 +231,12 @@ export function ActionHeader() {
export function ActionField({
field,
onApply,
}: {
field: string;
onApply: (field: string) => void;
}) {
const { msgIndex, actionIndex, action, workflow, handleApplyChange, appliedFields, stale } = useContext(ActionContext);
const { msgIndex, actionIndex, action, workflow, appliedFields, stale } = useContext(ActionContext);
const { showPreview } = usePreviewModal();
if (!action || !workflow) return null;
@ -178,11 +277,6 @@ export function ActionField({
(action.config_type === 'agent' && field === 'examples') ||
(action.config_type === 'prompt' && field === 'prompt') ||
(action.config_type === 'tool' && field === 'description');
// generate apply change function
const applyChangeHandler = () => {
handleApplyChange(msgIndex, actionIndex, field);
}
// generate preview modal function
const previewModalHandler = () => {
@ -193,7 +287,7 @@ export function ActionField({
markdownPreviewCondition,
`${action.name} - ${field}`,
"Review changes",
applyChangeHandler
() => onApply(field)
);
}
}
@ -213,7 +307,7 @@ export function ActionField({
'text-green-600 dark:text-green-400': applied,
'text-gray-600 dark:text-gray-400': stale,
})}
onClick={applyChangeHandler}
onClick={() => onApply(field)}
disabled={stale || applied}
>
<CheckIcon size={16} />
@ -226,4 +320,36 @@ export function ActionField({
</div>
</div>
</div>;
}
export function StreamingAction({
action,
loading,
}: {
action: {
action?: 'create_new' | 'edit';
config_type?: 'tool' | 'agent' | 'prompt';
name?: string;
};
loading: boolean;
}) {
return <div className={clsx('flex flex-col rounded-sm border border-t-4', {
'bg-gray-50 dark:bg-gray-800/50 border-gray-400 dark:border-gray-600 border-t-blue-500 shadow': action.action == 'create_new',
'bg-gray-50 dark:bg-gray-800/50 border-gray-400 dark:border-gray-600 border-t-orange-500 shadow': action.action == 'edit',
})}>
<div className="flex gap-2 items-center py-1 px-1">
{action.action == 'create_new' && <PlusIcon size={16} />}
{action.action == 'edit' && <PencilIcon size={16} />}
<div className="text-sm truncate">
{action.config_type && `${action.action === 'create_new' ? 'Create' : 'Edit'} ${action.config_type}`}
{action.name && <span className="font-medium ml-1">{action.name}</span>}
</div>
</div>
<div className="px-1 my-1">
<div className="bg-white dark:bg-gray-800 rounded-sm p-2 text-sm flex items-center gap-2">
{loading && <Spinner size="sm" />}
{!loading && <div className="text-gray-400">Canceled</div>}
</div>
</div>
</div>;
}

View file

@ -2,11 +2,108 @@
import { Spinner } from "@heroui/react";
import { useEffect, useRef, useState } from "react";
import { z } from "zod";
import { Workflow } from "@/app/lib/types/workflow_types";
import { Workflow, WorkflowTool, WorkflowAgent, WorkflowPrompt } from "@/app/lib/types/workflow_types";
import MarkdownContent from "@/app/lib/components/markdown-content";
import { MessageSquareIcon, EllipsisIcon, XIcon } from "lucide-react";
import { CopilotMessage, CopilotAssistantMessage } from "@/app/lib/types/copilot_types";
import { Action } from './actions';
import { CopilotMessage, CopilotAssistantMessage, CopilotAssistantMessageActionPart } from "@/app/lib/types/copilot_types";
import { Action, StreamingAction } from './actions';
import { useParsedBlocks } from "../use-parsed-blocks";
import { validateConfigChanges } from "@/app/lib/client_utils";
const CopilotResponsePart = z.union([
z.object({
type: z.literal('text'),
content: z.string(),
}),
z.object({
type: z.literal('streaming_action'),
action: CopilotAssistantMessageActionPart.shape.content.partial(),
}),
z.object({
type: z.literal('action'),
action: CopilotAssistantMessageActionPart.shape.content,
}),
]);
function enrich(response: string): z.infer<typeof CopilotResponsePart> {
// If it's not a code block, return as text
if (!response.trim().startsWith('//')) {
return {
type: 'text',
content: response
};
}
// Parse the metadata from comments
const lines = response.trim().split('\n');
const metadata: Record<string, string> = {};
let jsonStartIndex = 0;
// Parse metadata from comment lines
for (let i = 0; i < lines.length; i++) {
const line = lines[i].trim();
if (!line.startsWith('//')) {
jsonStartIndex = i;
break;
}
const [key, value] = line.substring(2).trim().split(':').map(s => s.trim());
if (key && value) {
metadata[key] = value;
}
}
// Try to parse the JSON part
try {
const jsonContent = lines.slice(jsonStartIndex).join('\n');
const jsonData = JSON.parse(jsonContent);
// If we have all required metadata, validate the config changes
if (metadata.action && metadata.config_type && metadata.name) {
const result = validateConfigChanges(
metadata.config_type,
jsonData.config_changes || {},
metadata.name
);
if ('error' in result) {
return {
type: 'action',
action: {
action: metadata.action as 'create_new' | 'edit',
config_type: metadata.config_type as 'tool' | 'agent' | 'prompt',
name: metadata.name,
change_description: jsonData.change_description || '',
config_changes: {},
error: result.error
}
};
}
return {
type: 'action',
action: {
action: metadata.action as 'create_new' | 'edit',
config_type: metadata.config_type as 'tool' | 'agent' | 'prompt',
name: metadata.name,
change_description: jsonData.change_description || '',
config_changes: result.changes
}
};
}
} catch (e) {
// JSON parsing failed - this is likely a streaming block
}
// Return as streaming action with whatever metadata we have
return {
type: 'streaming_action',
action: {
action: (metadata.action as 'create_new' | 'edit') || undefined,
config_type: (metadata.config_type as 'tool' | 'agent' | 'prompt') || undefined,
name: metadata.name
}
};
}
function UserMessage({ content }: { content: string }) {
return (
@ -15,7 +112,7 @@ function UserMessage({ content }: { content: string }) {
rounded-lg text-sm leading-relaxed
text-gray-700 dark:text-gray-200
border border-blue-100 dark:border-[#2a2d31]
shadow-sm animate-slideUpAndFade">
shadow-sm animate-[slideUpAndFade_150ms_ease-out]">
<div className="text-left">
<MarkdownContent content={content} />
</div>
@ -30,7 +127,7 @@ function InternalAssistantMessage({ content }: { content: string }) {
return (
<div className="w-full">
{!expanded ? (
<button className="flex items-center text-gray-400 dark:text-gray-500 hover:text-gray-600 dark:hover:text-gray-300 gap-1 group"
<button className="flex items-center text-gray-400 dark:text-gray-500 hover:text-gray-600 dark:hover:text-gray-300 gap-1 group"
onClick={() => setExpanded(true)}>
<MessageSquareIcon size={16} />
<EllipsisIcon size={16} />
@ -42,7 +139,7 @@ function InternalAssistantMessage({ content }: { content: string }) {
px-4 py-2.5 rounded-lg text-sm
text-gray-700 dark:text-gray-200 shadow-sm">
<div className="flex justify-end mb-2">
<button className="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300"
<button className="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300"
onClick={() => setExpanded(false)}>
<XIcon size={16} />
</button>
@ -55,40 +152,66 @@ function InternalAssistantMessage({ content }: { content: string }) {
);
}
function AssistantMessage({
content,
workflow,
handleApplyChange,
appliedChanges,
messageIndex
}: {
content: z.infer<typeof CopilotAssistantMessage>['content'],
function AssistantMessage({
content,
workflow,
dispatch,
messageIndex,
loading
}: {
content: z.infer<typeof CopilotAssistantMessage>['content'],
workflow: z.infer<typeof Workflow>,
handleApplyChange: (messageIndex: number, actionIndex: number, field?: string) => void,
appliedChanges: Record<string, boolean>,
messageIndex: number
dispatch: (action: any) => void,
messageIndex: number,
loading: boolean
}) {
const blocks = useParsedBlocks(content);
// parse actions from parts
let parsed: z.infer<typeof CopilotResponsePart>[] = [];
for (const block of blocks) {
if (block.type === 'text') {
parsed.push({
type: 'text',
content: block.content,
});
} else {
parsed.push(enrich(block.content));
}
}
// split the content into parts
return (
<div className="w-full">
<div className="px-4 py-2.5 text-sm leading-relaxed text-gray-700 dark:text-gray-200">
<div className="flex flex-col gap-4">
<div className="text-left flex flex-col gap-4">
{content.response.map((part, actionIndex) => {
if (part.type === "text") {
return <MarkdownContent key={actionIndex} content={part.content} />;
} else if (part.type === "action") {
{parsed.map((part, actionIndex) => {
if (part.type === 'text') {
return <MarkdownContent
key={actionIndex}
content={part.content}
/>;
}
if (part.type === 'streaming_action') {
return <StreamingAction
key={actionIndex}
action={part.action}
loading={loading}
/>;
}
if (part.type === 'action') {
return <Action
key={actionIndex}
msgIndex={messageIndex}
actionIndex={actionIndex}
action={part.content}
action={part.action}
workflow={workflow}
handleApplyChange={handleApplyChange}
appliedChanges={appliedChanges}
dispatch={dispatch}
stale={false}
/>;
}
return null;
})}
</div>
</div>
@ -97,14 +220,21 @@ function AssistantMessage({
);
}
function AssistantMessageLoading() {
function AssistantMessageLoading({ currentStatus }: { currentStatus: 'thinking' | 'planning' | 'generating' }) {
const statusText = {
thinking: "Thinking...",
planning: "Planning...",
generating: "Generating..."
};
return (
<div className="w-full">
<div className="bg-gray-50 dark:bg-gray-800 px-4 py-2.5
rounded-lg
border border-gray-200 dark:border-gray-700
shadow-sm dark:shadow-gray-950/20 animate-pulse min-h-[2.5rem] flex items-center">
shadow-sm dark:shadow-gray-950/20 animate-pulse min-h-[2.5rem] flex items-center gap-2">
<Spinner size="sm" className="ml-2" />
<span className="text-sm text-gray-600 dark:text-gray-400">{statusText[currentStatus]}</span>
</div>
</div>
);
@ -112,21 +242,40 @@ function AssistantMessageLoading() {
export function Messages({
messages,
streamingResponse,
loadingResponse,
workflow,
handleApplyChange,
appliedChanges
dispatch
}: {
messages: z.infer<typeof CopilotMessage>[];
streamingResponse: string;
loadingResponse: boolean;
workflow: z.infer<typeof Workflow>;
handleApplyChange: (messageIndex: number, actionIndex: number, field?: string) => void;
appliedChanges: Record<string, boolean>;
dispatch: (action: any) => void;
}) {
const messagesEndRef = useRef<HTMLDivElement>(null);
const [displayMessages, setDisplayMessages] = useState(messages);
useEffect(() => {
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
if (loadingResponse) {
setDisplayMessages([...messages, {
role: 'assistant',
content: streamingResponse
}]);
}
}, [messages, loadingResponse, streamingResponse]);
useEffect(() => {
// Small delay to ensure content is rendered
const timeoutId = setTimeout(() => {
messagesEndRef.current?.scrollIntoView({
behavior: "smooth",
block: "end",
inline: "nearest"
});
}, 100);
return () => clearTimeout(timeoutId);
}, [messages, loadingResponse]);
const renderMessage = (message: z.infer<typeof CopilotMessage>, messageIndex: number) => {
@ -136,31 +285,31 @@ export function Messages({
key={messageIndex}
content={message.content}
workflow={workflow}
handleApplyChange={handleApplyChange}
appliedChanges={appliedChanges}
dispatch={dispatch}
messageIndex={messageIndex}
loading={loadingResponse}
/>
);
}
if (message.role === 'user' && typeof message.content === 'string') {
return <UserMessage key={messageIndex} content={message.content} />;
}
return null;
};
return (
<div className="h-full">
<div className="flex flex-col [&>*]:mb-4">
{messages.map((message, index) => (
<div className="flex flex-col mb-4">
{displayMessages.map((message, index) => (
<div key={index} className="mb-4">
{renderMessage(message, index)}
</div>
))}
{loadingResponse && (
<div className="animate-pulse">
<AssistantMessageLoading />
<div className="text-xs text-gray-500">
<Spinner size="sm" className="ml-2" />
</div>
)}
</div>

View file

@ -0,0 +1,33 @@
This is a response in markdown from the copilot.
This is some text.
I'm adding a tool `get_status()` below:
```copilot_change
// action: create_new
// config_type: tool
// name: get_status
{
"change_description": "added a new tool...",
"config_changes": {
// same as before
}
}
```
I'm also updating the example agent:
```copilot_change
// action: edit
// config_type: agent
// name: Example agent
{
"change_description": "updated the instructions...",
"config_changes": {
// same as before
}
}
```
This concludes my changes. Would you like some more help?

View file

@ -0,0 +1,88 @@
import { useCallback, useRef, useState } from "react";
import { getCopilotResponseStream } from "@/app/actions/copilot_actions";
import { CopilotMessage } from "@/app/lib/types/copilot_types";
import { Workflow } from "@/app/lib/types/workflow_types";
import { z } from "zod";
interface UseCopilotParams {
projectId: string;
workflow: z.infer<typeof Workflow>;
context: any;
}
interface UseCopilotResult {
streamingResponse: string;
loading: boolean;
error: string | null;
start: (
messages: z.infer<typeof CopilotMessage>[],
onDone: (finalResponse: string) => void
) => void;
cancel: () => void;
}
export function useCopilot({ projectId, workflow, context }: UseCopilotParams): UseCopilotResult {
const [streamingResponse, setStreamingResponse] = useState('');
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const cancelRef = useRef<() => void>(() => { });
const responseRef = useRef('');
const start = useCallback(async (
messages: z.infer<typeof CopilotMessage>[],
onDone: (finalResponse: string) => void
) => {
if (!messages.length || messages.at(-1)?.role !== 'user') return;
setStreamingResponse('');
responseRef.current = '';
setError(null);
setLoading(true);
try {
const res = await getCopilotResponseStream(projectId, messages, workflow, context || null);
const eventSource = new EventSource(`/api/v1/copilot-stream-response/${res.streamId}`);
eventSource.onmessage = (event) => {
try {
const { content } = JSON.parse(event.data);
responseRef.current += content;
setStreamingResponse(prev => prev + content);
} catch (e) {
setError('Failed to parse stream message');
}
};
eventSource.addEventListener('done', () => {
eventSource.close();
setLoading(false);
onDone(responseRef.current);
});
eventSource.onerror = () => {
eventSource.close();
setError('Streaming failed');
setLoading(false);
};
cancelRef.current = () => eventSource.close();
} catch (err) {
setError('Failed to initiate stream');
setLoading(false);
}
}, [projectId, workflow, context]);
const cancel = useCallback(() => {
cancelRef.current?.();
setLoading(false);
}, []);
return {
streamingResponse,
loading,
error,
start,
cancel,
};
}

View file

@ -0,0 +1,30 @@
import { useMemo } from "react";
type Block =
| { type: "text"; content: string }
| { type: "code"; content: string };
const copilotCodeMarker = "copilot_change\n";
function parseMarkdown(markdown: string): Block[] {
// Split on triple backticks but keep the delimiters
// This gives us the raw content between and including delimiters
const parts = markdown.split("```");
const blocks: Block[] = [];
for (const part of parts) {
if (part.trim().startsWith(copilotCodeMarker)) {
blocks.push({ type: 'code', content: part.slice(copilotCodeMarker.length) });
} else {
blocks.push({ type: 'text', content: part });
}
}
return blocks;
}
export function useParsedBlocks(text: string): Block[] {
return useMemo(() => {
return parseMarkdown(text);
}, [text]);
}

View file

@ -67,7 +67,7 @@ export function AgentConfig({
if (!USE_TRANSFER_CONTROL_OPTIONS && agent.controlType !== 'retain') {
handleUpdate({ ...agent, controlType: 'retain' });
}
}, [USE_TRANSFER_CONTROL_OPTIONS, agent.controlType, agent, handleUpdate]);
}, [agent.controlType, agent, handleUpdate]);
const validateName = (value: string) => {
if (value.length === 0) {

View file

@ -2,7 +2,7 @@
import { WorkflowTool } from "../../../lib/types/workflow_types";
import { Checkbox, Select, SelectItem, RadioGroup, Radio } from "@heroui/react";
import { z } from "zod";
import { ImportIcon, XIcon, PlusIcon } from "lucide-react";
import { ImportIcon, XIcon, PlusIcon, FolderIcon } from "lucide-react";
import { useState, useEffect } from "react";
import { Textarea } from "@/components/ui/textarea";
import { Panel } from "@/components/common/panel-common";
@ -161,7 +161,7 @@ export function ToolConfig({
handleClose: () => void
}) {
const [selectedParams, setSelectedParams] = useState(new Set([]));
const isReadOnly = tool.isMcp;
const isReadOnly = tool.isMcp || tool.isLibrary;
const [nameError, setNameError] = useState<string | null>(null);
function handleParamRename(oldName: string, newName: string) {
@ -245,6 +245,12 @@ export function ToolConfig({
<span className="text-xs">MCP: {tool.mcpServerName}</span>
</div>
)}
{tool.isLibrary && (
<div className="flex items-center gap-2 text-sm bg-gray-100 dark:bg-gray-800 px-2 py-1 rounded-full text-gray-700 dark:text-gray-300">
<FolderIcon className="w-4 h-4 text-blue-700 dark:text-blue-400" />
<span className="text-xs">Library Tool</span>
</div>
)}
</div>
<Button
variant="secondary"

View file

@ -6,12 +6,14 @@ import { Workflow } from "@/app/lib/types/workflow_types";
import { Chat } from "./components/chat";
import { Panel } from "@/components/common/panel-common";
import { Button } from "@/components/ui/button";
import { Tooltip } from "@heroui/react";
import { apiV1 } from "rowboat-shared";
import { TestProfile } from "@/app/lib/types/testing_types";
import { WithStringId } from "@/app/lib/types/types";
import { ProfileSelector } from "@/app/projects/[projectId]/test/[[...slug]]/components/selectors/profile-selector";
import { CheckIcon, CopyIcon, PlusIcon, UserIcon } from "lucide-react";
import { CheckIcon, CopyIcon, PlusIcon, UserIcon, InfoIcon } from "lucide-react";
import { USE_TESTING_FEATURE } from "@/app/lib/feature_flags";
import { clsx } from "clsx";
const defaultSystemMessage = '';
@ -22,6 +24,8 @@ export function App({
messageSubscriber,
mcpServerUrls,
toolWebhookUrl,
isInitialState = false,
onPanelClick,
}: {
hidden?: boolean;
projectId: string;
@ -29,6 +33,8 @@ export function App({
messageSubscriber?: (messages: z.infer<typeof apiV1.ChatMessage>[]) => void;
mcpServerUrls: Array<z.infer<typeof MCPServer>>;
toolWebhookUrl: string;
isInitialState?: boolean;
onPanelClick?: () => void;
}) {
const [counter, setCounter] = useState<number>(0);
const [testProfile, setTestProfile] = useState<WithStringId<z.infer<typeof TestProfile>> | null>(null);
@ -88,10 +94,17 @@ export function App({
return (
<>
<Panel
variant="playground"
tourTarget="playground"
title={
<div className="flex items-center gap-3">
<div className="text-sm font-medium text-gray-900 dark:text-gray-100">
PLAYGROUND
<div className="flex items-center gap-2">
<div className="text-sm font-medium text-gray-900 dark:text-gray-100">
PLAYGROUND
</div>
<Tooltip content="Test your workflow and chat with your agents in real-time">
<InfoIcon className="w-4 h-4 text-gray-400 cursor-help" />
</Tooltip>
</div>
<Button
variant="primary"
@ -133,6 +146,10 @@ export function App({
</Button>
</div>
}
className={clsx(
isInitialState && "opacity-50 transition-opacity duration-300"
)}
onClick={onPanelClick}
>
<ProfileSelector
projectId={projectId}

View file

@ -8,7 +8,7 @@ import { AgenticAPIChatMessage, convertFromAgenticAPIChatMessages, convertToAgen
import { convertWorkflowToAgenticAPI } from "@/app/lib/types/agents_api_types";
import { AgenticAPIChatRequest } from "@/app/lib/types/agents_api_types";
import { Workflow } from "@/app/lib/types/workflow_types";
import { ComposeBox } from "@/components/common/compose-box";
import { ComposeBoxPlayground } from "@/components/common/compose-box-playground";
import { Button } from "@heroui/react";
import { apiV1 } from "rowboat-shared";
import { TestProfile } from "@/app/lib/types/testing_types";
@ -50,6 +50,7 @@ export function Chat({
const [lastAgenticRequest, setLastAgenticRequest] = useState<unknown | null>(null);
const [lastAgenticResponse, setLastAgenticResponse] = useState<unknown | null>(null);
const [optimisticMessages, setOptimisticMessages] = useState<z.infer<typeof apiV1.ChatMessage>[]>(chat.messages);
const [isLastInteracted, setIsLastInteracted] = useState(false);
const getCopyContent = useCallback(() => {
return JSON.stringify({
@ -90,6 +91,7 @@ export function Chat({
}];
setMessages(updatedMessages);
setFetchResponseError(null);
setIsLastInteracted(true);
}
// reset state when workflow changes
@ -289,10 +291,12 @@ export function Chat({
</div>
)}
<ComposeBox
<ComposeBoxPlayground
handleUserMessage={handleUserMessage}
messages={messages.filter(msg => msg.content !== undefined) as any}
loading={loadingAssistantResponse}
shouldAutoFocus={isLastInteracted}
onFocus={() => setIsLastInteracted(true)}
/>
</div>
</div>;

View file

@ -3,18 +3,20 @@ import { AgenticAPITool } from "../../../lib/types/agents_api_types";
import { WorkflowPrompt } from "../../../lib/types/workflow_types";
import { WorkflowAgent } from "../../../lib/types/workflow_types";
import { Dropdown, DropdownItem, DropdownTrigger, DropdownMenu } from "@heroui/react";
import { useRef, useEffect } from "react";
import { useRef, useEffect, useState } from "react";
import { EllipsisVerticalIcon, ImportIcon, PlusIcon, Brain, Wrench, PenLine } from "lucide-react";
import { Panel } from "@/components/common/panel-common";
import { Button } from "@/components/ui/button";
import { clsx } from "clsx";
const MAX_SECTION_HEIGHTS = {
AGENTS: '20rem',
TOOLS: '15rem',
PROMPTS: '15rem',
const SECTION_HEIGHT_PERCENTAGES = {
AGENTS: 40, // 50% of available height
TOOLS: 30, // 30% of available height
PROMPTS: 30, // 20% of available height
} as const;
const GAP_SIZE = 24; // 6 units * 4px (tailwind's default spacing unit)
interface EntityListProps {
agents: z.infer<typeof WorkflowAgent>[];
tools: z.infer<typeof AgenticAPITool>[];
@ -124,20 +126,42 @@ export function EntityList({
triggerMcpImport,
}: EntityListProps) {
const selectedRef = useRef<HTMLButtonElement | null>(null);
const containerRef = useRef<HTMLDivElement>(null);
const [containerHeight, setContainerHeight] = useState<number>(0);
const headerClasses = "font-semibold text-zinc-700 dark:text-zinc-300 flex items-center justify-between w-full";
const buttonClasses = "text-sm px-3 py-1.5 bg-indigo-50 hover:bg-indigo-100 text-indigo-700 dark:bg-indigo-950 dark:hover:bg-indigo-900 dark:text-indigo-400";
useEffect(() => {
const updateHeight = () => {
if (containerRef.current) {
setContainerHeight(containerRef.current.clientHeight);
}
};
updateHeight();
window.addEventListener('resize', updateHeight);
return () => window.removeEventListener('resize', updateHeight);
}, []);
useEffect(() => {
if (selectedEntity && selectedRef.current) {
selectedRef.current.scrollIntoView({ behavior: 'smooth', block: 'center' });
}
}, [selectedEntity]);
const calculateSectionHeight = (percentage: number) => {
// Total gaps = 2 gaps between 3 sections
const totalGaps = GAP_SIZE * 2;
const availableHeight = containerHeight - totalGaps;
return `${(availableHeight * percentage) / 100}px`;
};
return (
<div className="flex flex-col gap-6 h-full overflow-hidden">
<div className="flex flex-col gap-6 overflow-y-auto custom-scrollbar">
<div ref={containerRef} className="flex flex-col h-full">
<div className="flex flex-col gap-6 h-full flex-1">
{/* Agents Panel */}
<Panel variant="projects"
tourTarget="entity-agents"
title={
<div className={headerClasses}>
<div className="flex items-center gap-2">
@ -156,9 +180,10 @@ export function EntityList({
</Button>
</div>
}
maxHeight={MAX_SECTION_HEIGHTS.AGENTS}
maxHeight={calculateSectionHeight(SECTION_HEIGHT_PERCENTAGES.AGENTS)}
className="overflow-hidden flex-[50]"
>
<div className="flex flex-col">
<div className="flex flex-col h-full overflow-y-auto">
{agents.length > 0 ? (
<div className="space-y-1 pb-2">
{agents.map((agent, index) => (
@ -190,6 +215,7 @@ export function EntityList({
{/* Tools Panel */}
<Panel variant="projects"
tourTarget="entity-tools"
title={
<div className={headerClasses}>
<div className="flex items-center gap-2">
@ -210,7 +236,13 @@ export function EntityList({
<Button
variant="secondary"
size="sm"
onClick={() => onAddTool({})}
onClick={() => onAddTool({
mockTool: true,
parameters: {
type: 'object',
properties: {}
}
})}
className={`group ${buttonClasses}`}
showHoverContent={true}
hoverContent="Add Tool"
@ -220,9 +252,10 @@ export function EntityList({
</div>
</div>
}
maxHeight={MAX_SECTION_HEIGHTS.TOOLS}
maxHeight={calculateSectionHeight(SECTION_HEIGHT_PERCENTAGES.TOOLS)}
className="overflow-hidden flex-[30]"
>
<div className="flex flex-col">
<div className="flex flex-col h-full overflow-y-auto">
{tools.length > 0 ? (
<div className="space-y-1 pb-2">
{tools.map((tool, index) => (
@ -250,6 +283,7 @@ export function EntityList({
{/* Prompts Panel */}
<Panel variant="projects"
tourTarget="entity-prompts"
title={
<div className={headerClasses}>
<div className="flex items-center gap-2">
@ -268,9 +302,10 @@ export function EntityList({
</Button>
</div>
}
maxHeight={MAX_SECTION_HEIGHTS.PROMPTS}
maxHeight={calculateSectionHeight(SECTION_HEIGHT_PERCENTAGES.PROMPTS)}
className="overflow-hidden flex-[20]"
>
<div className="flex flex-col">
<div className="flex flex-col h-full overflow-y-auto">
{prompts.length > 0 ? (
<div className="space-y-1 pb-2">
{prompts.map((prompt, index) => (

View file

@ -1,5 +1,5 @@
"use client";
import React, { useReducer, Reducer, useState, useCallback, useEffect, useRef } from "react";
import React, { useReducer, Reducer, useState, useCallback, useEffect, useRef, createContext, useContext } from "react";
import { MCPServer, WithStringId } from "../../../lib/types/types";
import { Workflow } from "../../../lib/types/workflow_types";
import { WorkflowTool } from "../../../lib/types/workflow_types";
@ -15,6 +15,7 @@ import { Button, Dropdown, DropdownItem, DropdownMenu, DropdownSection, Dropdown
import { PromptConfig } from "../entities/prompt_config";
import { EditableField } from "../../../lib/components/editable-field";
import { RelativeTime } from "@primer/react";
import { USE_PRODUCT_TOUR } from "@/app/lib/feature_flags";
import {
ResizableHandle,
@ -29,13 +30,14 @@ import { BackIcon, HamburgerIcon, WorkflowIcon } from "../../../lib/components/i
import { CopyIcon, ImportIcon, Layers2Icon, RadioIcon, RedoIcon, ServerIcon, Sparkles, UndoIcon } from "lucide-react";
import { EntityList } from "./entity_list";
import { McpImportTools } from "./mcp_imports";
import { ProductTour } from "@/components/common/product-tour";
enablePatches();
const PANEL_RATIOS = {
entityList: 25, // Left panel
chatApp: 50, // Middle panel
copilot: 25 // Right panel
chatApp: 40, // Middle panel
copilot: 35 // Right panel
} as const;
interface StateItem {
@ -290,7 +292,7 @@ function reducer(state: State, action: Action): State {
name: newToolName,
description: "",
parameters: {
type: "object",
type: 'object',
properties: {},
},
mockTool: true,
@ -604,6 +606,8 @@ export function WorkflowEditor({
const [showCopilot, setShowCopilot] = useState(true);
const [copilotWidth, setCopilotWidth] = useState<number>(PANEL_RATIOS.copilot);
const [isMcpImportModalOpen, setIsMcpImportModalOpen] = useState(false);
const [isInitialState, setIsInitialState] = useState(true);
const [showTour, setShowTour] = useState(true);
console.log(`workflow editor chat key: ${state.present.chatKey}`);
@ -616,6 +620,20 @@ export function WorkflowEditor({
}
}, [state.present.workflow.projectId]);
// Reset initial state when user interacts with copilot or opens other menus
useEffect(() => {
if (state.present.selection !== null) {
setIsInitialState(false);
}
}, [state.present.selection]);
// Track copilot actions
useEffect(() => {
if (state.present.pendingChanges && state.present.workflow) {
setIsInitialState(false);
}
}, [state.present.workflow, state.present.pendingChanges]);
function handleSelectAgent(name: string) {
dispatch({ type: "select_agent", name });
}
@ -755,6 +773,10 @@ export function WorkflowEditor({
}
}, [state.present.workflow, state.present.pendingChanges, processQueue, state]);
function handlePlaygroundClick() {
setIsInitialState(false);
}
return <div className="flex flex-col h-full relative">
<div className="shrink-0 flex justify-between items-center pb-6">
<div className="workflow-version-selector flex items-center gap-1 px-2 text-gray-800 dark:text-gray-100">
@ -881,9 +903,7 @@ export function WorkflowEditor({
variant="solid"
size="lg"
onPress={() => setShowCopilot(!showCopilot)}
className="gap-2 px-6 bg-indigo-600 hover:bg-indigo-700 text-white relative overflow-hidden animate-pulse-subtle
before:absolute before:inset-0 before:bg-gradient-to-r before:from-transparent before:via-white/20 before:to-transparent
before:translate-x-[-200%] before:animate-shine before:duration-1000 font-semibold text-base"
className="gap-2 px-6 bg-indigo-600 hover:bg-indigo-700 text-white font-semibold text-base"
startContent={<Sparkles size={20} />}
>
Copilot
@ -927,6 +947,8 @@ export function WorkflowEditor({
messageSubscriber={updateChatMessages}
mcpServerUrls={mcpServerUrls}
toolWebhookUrl={toolWebhookUrl}
isInitialState={isInitialState}
onPanelClick={handlePlaygroundClick}
/>
{state.present.selection?.type === "agent" && <AgentConfig
key={state.present.selection.name}
@ -981,11 +1003,18 @@ export function WorkflowEditor({
messages: chatMessages
} : undefined
}
isInitialState={isInitialState}
/>
</ResizablePanel>
</>
)}
</ResizablePanelGroup>
{USE_PRODUCT_TOUR && showTour && (
<ProductTour
projectId={state.present.workflow.projectId}
onComplete={() => setShowTour(false)}
/>
)}
<McpImportTools
projectId={state.present.workflow.projectId}
isOpen={isMcpImportModalOpen}

View file

@ -10,7 +10,7 @@ interface AppLayoutProps {
}
export default function AppLayout({ children, useRag = false, useAuth = false }: AppLayoutProps) {
const [sidebarCollapsed, setSidebarCollapsed] = useState(false);
const [sidebarCollapsed, setSidebarCollapsed] = useState(true);
const pathname = usePathname();
const projectId = pathname.split('/')[2];

View file

@ -12,11 +12,12 @@ import {
FolderOpenIcon,
ChevronLeftIcon,
ChevronRightIcon,
Moon
Moon,
HelpCircle
} from "lucide-react";
import { getProjectConfig } from "@/app/actions/project_actions";
import { useTheme } from "@/app/providers/theme-provider";
import { USE_TESTING_FEATURE } from '@/app/lib/feature_flags';
import { USE_TESTING_FEATURE, USE_PRODUCT_TOUR } from '@/app/lib/feature_flags';
interface SidebarProps {
projectId: string;
@ -137,6 +138,7 @@ export default function Sidebar({ projectId, useRag, useAuth, collapsed = false,
}
`}
disabled={isDisabled}
data-tour-target={item.href === 'config' ? 'settings' : undefined}
>
<Icon
size={collapsed ? COLLAPSED_ICON_SIZE : EXPANDED_ICON_SIZE}
@ -179,6 +181,27 @@ export default function Sidebar({ projectId, useRag, useAuth, collapsed = false,
{/* Theme and Auth Controls */}
<div className="p-3 border-t border-zinc-100 dark:border-zinc-800 space-y-2">
{USE_PRODUCT_TOUR && !isProjectsRoute && (
<Tooltip content={collapsed ? "Take Tour" : ""} showArrow placement="right">
<button
onClick={() => {
localStorage.removeItem('user_product_tour_completed');
window.location.reload();
}}
className={`
w-full rounded-md flex items-center
text-[15px] font-medium transition-all duration-200
${collapsed ? 'justify-center py-4' : 'px-4 py-4 gap-3'}
hover:bg-zinc-100 dark:hover:bg-zinc-800/50
text-zinc-600 dark:text-zinc-400
`}
>
<HelpCircle size={COLLAPSED_ICON_SIZE} />
{!collapsed && <span>Take Tour</span>}
</button>
</Tooltip>
)}
<Tooltip content={collapsed ? "Appearance" : ""} showArrow placement="right">
<button
onClick={toggleTheme}

View file

@ -1,8 +1,6 @@
'use client';
import { Project } from "@/app/lib/types/project_types";
import { useEffect, useState, useRef } from "react";
import { z } from "zod";
import { createProject, createProjectFromPrompt } from "@/app/actions/project_actions";
import { useRouter } from 'next/navigation';
import clsx from 'clsx';

View file

@ -1,7 +1,6 @@
import { Project } from "@/app/lib/types/project_types";
import { z } from "zod";
import { ProjectList } from "./project-list";
import { SectionHeading } from "@/components/ui/section-heading";
import { HorizontalDivider } from "@/components/ui/horizontal-divider";
import clsx from 'clsx';
import { XMarkIcon } from "@heroicons/react/24/outline";

View file

@ -1,101 +0,0 @@
'use client';
import clsx from 'clsx';
import { CheckIcon } from "lucide-react";
import { useState } from "react";
import React from "react";
import { WorkflowTemplate } from "@/app/lib/types/workflow_types";
import { z } from "zod";
import { tokens } from "@/app/styles/design-tokens";
interface TemplateCardProps {
templateKey: string;
template: z.infer<typeof WorkflowTemplate> | string;
onSelect: (templateKey: string) => void;
selected: boolean;
type?: "template" | "prompt";
}
export function TemplateCard({
templateKey,
template,
onSelect,
selected,
type = "template"
}: TemplateCardProps) {
const [isExpanded, setIsExpanded] = useState(false);
const name = typeof template === "string" ? templateKey : template.name;
const description = typeof template === "string" ? template : template.description;
const textRef = React.useRef<HTMLDivElement>(null);
const [needsExpansion, setNeedsExpansion] = useState(false);
React.useEffect(() => {
if (textRef.current) {
const needsButton = textRef.current.scrollHeight > textRef.current.clientHeight;
setNeedsExpansion(needsButton);
}
}, [description]);
return (
<div
onClick={() => onSelect(templateKey)}
className={clsx(
"w-full text-left cursor-pointer",
"p-4",
tokens.radius.lg,
tokens.transitions.default,
tokens.shadows.sm,
"border",
selected ? [
"border-indigo-600 dark:border-indigo-400",
"bg-indigo-50/50 dark:bg-indigo-500/10",
] : [
tokens.colors.light.border,
tokens.colors.dark.border,
tokens.colors.light.surface,
tokens.colors.dark.surface,
"hover:border-indigo-600/30 dark:hover:border-indigo-400/30",
"hover:bg-indigo-50/30 dark:hover:bg-indigo-500/5",
"transform hover:scale-[1.01]",
tokens.shadows.hover,
],
tokens.focus.default,
tokens.focus.dark
)}
>
<div className="flex items-start justify-between gap-4">
<div className="flex-1 space-y-2">
<h3 className={clsx(
tokens.typography.sizes.base,
tokens.typography.weights.medium,
tokens.colors.light.text.primary,
tokens.colors.dark.text.primary
)}>
{name}
</h3>
<p className={clsx(
tokens.typography.sizes.sm,
tokens.colors.light.text.secondary,
tokens.colors.dark.text.secondary
)}>
{description}
</p>
</div>
<div className={clsx(
"w-5 h-5 rounded-full border-2",
tokens.transitions.default,
selected ? [
"border-indigo-600 dark:border-indigo-400",
"bg-indigo-600 dark:bg-indigo-400",
] : [
"border-gray-300 dark:border-gray-600",
]
)}>
{selected && (
<CheckIcon className="w-4 h-4 text-white" />
)}
</div>
</div>
</div>
);
}

View file

@ -1,55 +0,0 @@
import { templates, starting_copilot_prompts } from "@/app/lib/project_templates";
import { TemplateCard } from "./template-card";
import { WorkflowTemplate } from "@/types/workflow_types";
import { z } from "zod";
// Use the existing template type but make id optional
type Template = z.infer<typeof WorkflowTemplate> & {
id?: string;
prompt?: string;
};
type TemplateCardsListProps = {
selectedCard: 'custom' | Template;
onSelectCard: (template: Template) => void;
};
export function TemplateCardsList({ selectedCard, onSelectCard }: TemplateCardsListProps) {
return (
<div className="grid grid-cols-2 gap-4">
{Object.entries(templates).map(([id, template]) => (
<TemplateCard
key={id}
templateKey={id}
template={template} // Remove the type assertion
selected={selectedCard !== 'custom' && selectedCard.id === id}
onSelect={() => onSelectCard({ ...template, id })}
/>
))}
{Object.entries(starting_copilot_prompts).map(([name, prompt]) => {
// Create a template-compatible object
const promptTemplate: Template = {
name,
description: prompt,
prompt,
id: name.toLowerCase(),
agents: [], // Required by WorkflowTemplate
prompts: [], // Required by WorkflowTemplate
tools: [], // Required by WorkflowTemplate
startAgent: '' // Required by WorkflowTemplate
};
return (
<TemplateCard
key={name}
templateKey={name.toLowerCase()}
template={promptTemplate}
selected={selectedCard !== 'custom' && selectedCard.id === name.toLowerCase()}
onSelect={() => onSelectCard(promptTemplate)}
/>
);
})}
</div>
);
}

View file

@ -14,22 +14,44 @@ type FlexibleMessage = {
// Add any other optional fields that might be needed
};
interface ComposeBoxCopilotProps {
handleUserMessage: (message: string) => void;
messages: any[];
loading: boolean;
initialFocus?: boolean;
shouldAutoFocus?: boolean;
onFocus?: () => void;
onCancel?: () => void;
}
export function ComposeBoxCopilot({
minRows=3,
disabled=false,
loading=false,
handleUserMessage,
messages,
}: {
minRows?: number;
disabled?: boolean;
loading?: boolean;
handleUserMessage: (prompt: string) => void;
messages: FlexibleMessage[]; // Use the flexible message type
}) {
loading,
initialFocus = false,
shouldAutoFocus = false,
onFocus,
onCancel,
}: ComposeBoxCopilotProps) {
const [input, setInput] = useState('');
const [isFocused, setIsFocused] = useState(false);
const inputRef = useRef<HTMLTextAreaElement>(null);
const textareaRef = useRef<HTMLTextAreaElement>(null);
const previousMessagesLength = useRef(messages.length);
// Handle initial focus
useEffect(() => {
if (initialFocus && textareaRef.current) {
textareaRef.current.focus();
}
}, [initialFocus]);
// Handle auto-focus when new messages arrive
useEffect(() => {
if (shouldAutoFocus && messages.length > previousMessagesLength.current && textareaRef.current) {
textareaRef.current.focus();
}
previousMessagesLength.current = messages.length;
}, [messages.length, shouldAutoFocus]);
function handleInput() {
const prompt = input.trim();
@ -47,12 +69,10 @@ export function ComposeBoxCopilot({
}
}
// focus on the input field only when there is at least one message
useEffect(() => {
if (messages.length > 0) {
inputRef.current?.focus();
}
}, [messages]);
const handleFocus = () => {
setIsFocused(true);
onFocus?.();
};
return (
<div className="relative group">
@ -68,13 +88,13 @@ export function ComposeBoxCopilot({
{/* Textarea */}
<div className="flex-1">
<Textarea
ref={inputRef}
ref={textareaRef}
value={input}
onChange={(e) => setInput(e.target.value)}
onKeyDown={handleInputKeyDown}
onFocus={() => setIsFocused(true)}
onFocus={handleFocus}
onBlur={() => setIsFocused(false)}
disabled={disabled || loading}
disabled={loading}
placeholder="Type a message..."
autoResize={true}
maxHeight={120}
@ -98,13 +118,15 @@ export function ComposeBoxCopilot({
<Button
size="sm"
isIconOnly
disabled={disabled || loading || !input.trim()}
onPress={handleInput}
disabled={!loading && !input.trim()}
onPress={loading ? onCancel : handleInput}
className={`
transition-all duration-200
${input.trim()
? 'bg-indigo-50 hover:bg-indigo-100 text-indigo-700 dark:bg-indigo-900/50 dark:hover:bg-indigo-800/60 dark:text-indigo-300'
: 'bg-gray-100 dark:bg-gray-800 text-gray-400 dark:text-gray-500'
${loading
? 'bg-red-50 hover:bg-red-100 text-red-700 dark:bg-red-900/50 dark:hover:bg-red-800/60 dark:text-red-300'
: input.trim()
? 'bg-indigo-50 hover:bg-indigo-100 text-indigo-700 dark:bg-indigo-900/50 dark:hover:bg-indigo-800/60 dark:text-indigo-300'
: 'bg-gray-100 dark:bg-gray-800 text-gray-400 dark:text-gray-500'
}
scale-100 hover:scale-105 active:scale-95
disabled:opacity-50 disabled:scale-95
@ -113,7 +135,7 @@ export function ComposeBoxCopilot({
`}
>
{loading ? (
<Spinner size="sm" color={input.trim() ? "primary" : "default"} />
<StopIcon size={16} />
) : (
<SendIcon
size={16}
@ -145,3 +167,19 @@ function SendIcon({ size, className }: { size: number, className?: string }) {
</svg>
);
}
// Custom StopIcon component for better visual alignment
function StopIcon({ size, className }: { size: number, className?: string }) {
return (
<svg
width={size}
height={size}
viewBox="0 0 24 24"
fill="currentColor"
stroke="none"
className={className}
>
<rect x="6" y="6" width="12" height="12" rx="1" />
</svg>
);
}

View file

@ -0,0 +1,146 @@
import { useState, useRef, useEffect } from 'react';
import { Textarea } from '@/components/ui/textarea';
import { Button, Spinner } from "@heroui/react";
interface ComposeBoxPlaygroundProps {
handleUserMessage: (message: string) => void;
messages: any[];
loading: boolean;
disabled?: boolean;
shouldAutoFocus?: boolean;
onFocus?: () => void;
}
export function ComposeBoxPlayground({
handleUserMessage,
messages,
loading,
disabled = false,
shouldAutoFocus = false,
onFocus,
}: ComposeBoxPlaygroundProps) {
const [input, setInput] = useState('');
const [isFocused, setIsFocused] = useState(false);
const textareaRef = useRef<HTMLTextAreaElement>(null);
const previousMessagesLength = useRef(messages.length);
// Handle auto-focus when new messages arrive
useEffect(() => {
if (shouldAutoFocus && messages.length > previousMessagesLength.current && textareaRef.current) {
textareaRef.current.focus();
}
previousMessagesLength.current = messages.length;
}, [messages.length, shouldAutoFocus]);
function handleInput() {
const prompt = input.trim();
if (!prompt) {
return;
}
setInput('');
handleUserMessage(prompt);
}
const handleInputKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
handleInput();
}
};
const handleFocus = () => {
setIsFocused(true);
onFocus?.();
};
return (
<div className="relative group">
{/* Keyboard shortcut hint */}
<div className="absolute -top-6 right-0 text-xs text-gray-500 dark:text-gray-400 opacity-0
group-hover:opacity-100 transition-opacity">
Press + Enter to send
</div>
{/* Outer container with padding */}
<div className="rounded-2xl border-[1.5px] border-gray-200 dark:border-[#2a2d31] p-3 relative
bg-white dark:bg-[#1e2023] flex items-end gap-2">
{/* Textarea */}
<div className="flex-1">
<Textarea
ref={textareaRef}
value={input}
onChange={(e) => setInput(e.target.value)}
onKeyDown={handleInputKeyDown}
onFocus={handleFocus}
onBlur={() => setIsFocused(false)}
disabled={disabled || loading}
placeholder="Type a message..."
autoResize={true}
maxHeight={120}
className={`
!min-h-0
!border-0 !shadow-none !ring-0
bg-transparent
resize-none
overflow-y-auto
[&::-webkit-scrollbar]:w-1
[&::-webkit-scrollbar-track]:bg-transparent
[&::-webkit-scrollbar-thumb]:bg-gray-300
[&::-webkit-scrollbar-thumb]:dark:bg-[#2a2d31]
[&::-webkit-scrollbar-thumb]:rounded-full
placeholder:text-gray-500 dark:placeholder:text-gray-400
`}
/>
</div>
{/* Send button */}
<Button
size="sm"
isIconOnly
disabled={disabled || loading || !input.trim()}
onPress={handleInput}
className={`
transition-all duration-200
${input.trim()
? 'bg-indigo-50 hover:bg-indigo-100 text-indigo-700 dark:bg-indigo-900/50 dark:hover:bg-indigo-800/60 dark:text-indigo-300'
: 'bg-gray-100 dark:bg-gray-800 text-gray-400 dark:text-gray-500'
}
scale-100 hover:scale-105 active:scale-95
disabled:opacity-50 disabled:scale-95
hover:shadow-md dark:hover:shadow-indigo-950/10
mb-0.5
`}
>
{loading ? (
<Spinner size="sm" color={input.trim() ? "primary" : "default"} />
) : (
<SendIcon
size={16}
className={`transform transition-transform ${isFocused ? 'translate-x-0.5' : ''}`}
/>
)}
</Button>
</div>
</div>
);
}
// Custom SendIcon component for better visual alignment
function SendIcon({ size, className }: { size: number, className?: string }) {
return (
<svg
width={size}
height={size}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className={className}
>
<path d="M22 2L11 13" />
<path d="M22 2L15 22L11 13L2 9L22 2Z" />
</svg>
);
}

View file

@ -1,5 +1,6 @@
import clsx from "clsx";
import { Sparkles } from "lucide-react";
import { SHOW_COPILOT_MARQUEE } from "@/app/lib/feature_flags";
export function ActionButton({
icon = null,
@ -34,8 +35,11 @@ interface PanelProps {
actions?: React.ReactNode;
children: React.ReactNode;
maxHeight?: string;
variant?: 'default' | 'copilot' | 'projects';
variant?: 'default' | 'copilot' | 'playground' | 'projects';
showWelcome?: boolean;
className?: string;
onClick?: () => void;
tourTarget?: string;
}
export function Panel({
@ -46,32 +50,43 @@ export function Panel({
maxHeight,
variant = 'default',
showWelcome = true,
className,
onClick,
tourTarget,
}: PanelProps) {
return <div className={clsx(
"flex flex-col overflow-hidden rounded-xl border relative",
"border-zinc-200 dark:border-zinc-800",
"bg-white dark:bg-zinc-900",
maxHeight ? "max-h-[var(--panel-height)]" : "h-full"
)}
style={{
'--panel-height': maxHeight
} as React.CSSProperties}
return <div
className={clsx(
"flex flex-col overflow-hidden rounded-xl border relative",
variant === 'copilot' ? "border-blue-200 dark:border-blue-800" : "border-zinc-200 dark:border-zinc-800",
"bg-white dark:bg-zinc-900",
maxHeight ? "max-h-[var(--panel-height)]" : "h-full",
className
)}
style={{
'--panel-height': maxHeight
} as React.CSSProperties}
onClick={onClick}
data-tour-target={tourTarget}
>
{variant === 'copilot' && showWelcome && (
<div className="absolute inset-0 flex flex-col items-center justify-center pointer-events-none -mt-16">
<Sparkles className="w-32 h-32 text-blue-400/40 dark:text-blue-500/25 animate-sparkle" />
<div className="relative mt-8 max-w-full px-8">
<div className="font-mono text-sm whitespace-nowrap text-blue-400/60 dark:text-blue-500/40 font-small inline-flex">
<div className="overflow-hidden w-0 animate-typing">What can I help you build?</div>
<div className="border-r-2 border-blue-400 dark:border-blue-500 animate-cursor">&nbsp;</div>
{SHOW_COPILOT_MARQUEE && (
<div className="relative mt-8 max-w-full px-8">
<div className="font-mono text-sm whitespace-nowrap text-blue-400/60 dark:text-blue-500/40 font-small inline-flex">
<div className="overflow-hidden w-0 animate-typing">What can I help you build?</div>
<div className="border-r-2 border-blue-400 dark:border-blue-500 animate-cursor">&nbsp;</div>
</div>
</div>
</div>
)}
</div>
)}
<div className={clsx(
"shrink-0 border-b border-zinc-100 dark:border-zinc-800 relative",
variant === 'projects' ? "flex flex-col gap-3 px-4 py-3" : "flex items-center justify-between px-4 py-3"
)}>
<div
className={clsx(
"shrink-0 border-b border-zinc-100 dark:border-zinc-800 relative",
variant === 'projects' ? "flex flex-col gap-3 px-4 py-3" : "flex items-center justify-between px-4 py-3"
)}
>
{variant === 'projects' ? (
<>
<div className="text-sm uppercase tracking-wide text-zinc-500 dark:text-zinc-400">

View file

@ -0,0 +1,251 @@
import { useFloating, offset, flip, shift, arrow, FloatingArrow, FloatingPortal, autoUpdate } from '@floating-ui/react';
import { useCallback, useEffect, useRef, useState } from 'react';
import { XIcon } from 'lucide-react';
interface TourStep {
target: string;
content: string;
title: string;
}
const TOUR_STEPS: TourStep[] = [
{
target: 'copilot',
content: 'Build agents with the help of copilot.\nThis might take a minute.',
title: 'Step 1/6'
},
{
target: 'playground',
content: 'Test your assistant in the playground.\nDebug tool calls and responses.',
title: 'Step 2/6'
},
{
target: 'entity-agents',
content: 'Manage your agents.\nSpecify instructions, examples and tool usage.',
title: 'Step 3/6'
},
{
target: 'entity-tools',
content: 'Create your own tools, import MCP tools or use existing ones.\nMock tools for quick testing.',
title: 'Step 4/6'
},
{
target: 'entity-prompts',
content: 'Manage prompts which will be used by agents.\nConfigure greeting message.',
title: 'Step 5/6'
},
{
target: 'settings',
content: 'Configure project settings\nGet API keys, configure tool webhooks.',
title: 'Step 6/6'
}
];
function TourBackdrop({ targetElement }: { targetElement: Element | null }) {
const [rect, setRect] = useState<DOMRect | null>(null);
const isPanelTarget = targetElement?.getAttribute('data-tour-target') &&
['entity-agents', 'entity-tools', 'entity-prompts', 'copilot', 'playground'].includes(
targetElement.getAttribute('data-tour-target')!
);
// Use smaller padding for panels to prevent overlap
const padding = isPanelTarget ? 12 : 8;
useEffect(() => {
if (targetElement) {
const updateRect = () => {
const newRect = targetElement.getBoundingClientRect();
setRect(newRect);
};
updateRect();
window.addEventListener('resize', updateRect);
window.addEventListener('scroll', updateRect);
return () => {
window.removeEventListener('resize', updateRect);
window.removeEventListener('scroll', updateRect);
};
}
}, [targetElement]);
if (!rect) return null;
return (
<>
{/* Top */}
<div className="fixed z-[100] backdrop-blur-sm bg-black/30" style={{
top: 0,
left: 0,
right: 0,
height: Math.max(0, rect.top - padding)
}} />
{/* Left */}
<div className="fixed z-[100] backdrop-blur-sm bg-black/30" style={{
top: Math.max(0, rect.top - padding),
left: 0,
width: Math.max(0, rect.left - padding),
height: rect.height + padding * 2
}} />
{/* Right */}
<div className="fixed z-[100] backdrop-blur-sm bg-black/30" style={{
top: Math.max(0, rect.top - padding),
left: rect.right + padding,
right: 0,
height: rect.height + padding * 2
}} />
{/* Bottom */}
<div className="fixed z-[100] backdrop-blur-sm bg-black/30" style={{
top: rect.bottom + padding,
left: 0,
right: 0,
bottom: 0
}} />
{/* Highlight border around target */}
<div
className="fixed z-[100] border-2 border-white/50 rounded-lg pointer-events-none"
style={{
top: rect.top - padding,
left: rect.left - padding,
width: rect.width + padding * 2,
height: rect.height + padding * 2,
}}
/>
</>
);
}
export function ProductTour({
projectId,
onComplete
}: {
projectId: string;
onComplete: () => void;
}) {
const [currentStep, setCurrentStep] = useState(0);
const [shouldShow, setShouldShow] = useState(true);
const arrowRef = useRef(null);
// Check if tour has been completed by the user
useEffect(() => {
const tourCompleted = localStorage.getItem('user_product_tour_completed');
if (tourCompleted) {
setShouldShow(false);
}
}, []);
const currentTarget = TOUR_STEPS[currentStep].target;
const targetElement = document.querySelector(`[data-tour-target="${currentTarget}"]`);
// Determine if the target is a panel that should have the hint on the side
const isPanelTarget = ['entity-agents', 'entity-tools', 'entity-prompts', 'copilot', 'playground'].includes(currentTarget);
const { x, y, strategy, refs, context, middlewareData } = useFloating({
placement: isPanelTarget ? 'right' : 'top',
middleware: [
offset(16),
flip({
fallbackPlacements: isPanelTarget ? ['left', 'top', 'bottom'] : ['bottom', 'left', 'right'],
padding: 16
}),
shift({
padding: 16,
crossAxis: true,
mainAxis: true
}),
arrow({ element: arrowRef })
],
whileElementsMounted: autoUpdate
});
// Update reference element when step changes
useEffect(() => {
if (targetElement) {
refs.setReference(targetElement);
}
}, [currentStep, targetElement, refs]);
const handleNext = useCallback(() => {
if (currentStep < TOUR_STEPS.length - 1) {
setCurrentStep(prev => prev + 1);
} else {
// Mark tour as completed for the user
localStorage.setItem('user_product_tour_completed', 'true');
// Clean up any old project-specific tour flags
localStorage.removeItem(`project_tour_${projectId}`);
setShouldShow(false);
onComplete();
}
}, [currentStep, projectId, onComplete]);
const handleSkip = useCallback(() => {
// Mark tour as completed for the user
localStorage.setItem('user_product_tour_completed', 'true');
// Clean up any old project-specific tour flags
localStorage.removeItem(`project_tour_${projectId}`);
setShouldShow(false);
onComplete();
}, [projectId, onComplete]);
if (!shouldShow) return null;
// Get the actual placement after middleware calculations
const actualPlacement = middlewareData.flip?.overflows?.length ?
middlewareData.flip?.overflows[0].placement :
isPanelTarget ? 'right' : 'top';
return (
<FloatingPortal>
<TourBackdrop targetElement={targetElement} />
<div
ref={refs.setFloating}
style={{
position: strategy,
top: y ?? 0,
left: x ?? 0,
width: 'max-content',
maxWidth: '90vw',
zIndex: 101,
}}
className="bg-white dark:bg-zinc-800 rounded-lg shadow-lg border border-zinc-200 dark:border-zinc-700 p-4 animate-in fade-in duration-200"
>
<button
onClick={handleSkip}
className="absolute right-2 top-2 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300"
>
<XIcon size={16} />
</button>
<div className="text-xs font-medium text-gray-500 dark:text-gray-400 mb-1">
{TOUR_STEPS[currentStep].title}
</div>
<div className="text-sm font-medium text-gray-900 dark:text-gray-100 mb-3 whitespace-pre-line">
{TOUR_STEPS[currentStep].content}
</div>
<div className="flex justify-between items-center">
<button
onClick={handleSkip}
className="text-sm text-gray-500 hover:text-gray-700 dark:hover:text-gray-300"
>
Skip tour
</button>
<button
onClick={handleNext}
className="px-4 py-1.5 bg-indigo-600 text-white rounded-md text-sm font-medium hover:bg-indigo-700"
>
{currentStep === TOUR_STEPS.length - 1 ? 'Finish' : 'Next'}
</button>
</div>
<FloatingArrow
ref={arrowRef}
context={context}
fill="white"
className="dark:fill-zinc-800"
/>
</div>
</FloatingPortal>
);
}

View file

@ -12,6 +12,7 @@
"@auth0/nextjs-auth0": "^3.5.0",
"@aws-sdk/client-s3": "^3.743.0",
"@aws-sdk/s3-request-presigner": "^3.743.0",
"@floating-ui/react": "^0.27.7",
"@google/generative-ai": "^0.21.0",
"@heroicons/react": "^2.2.0",
"@heroui/react": "2.7.4",
@ -2255,6 +2256,59 @@
"node": ">=14"
}
},
"node_modules/@floating-ui/core": {
"version": "1.6.9",
"resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.9.tgz",
"integrity": "sha512-uMXCuQ3BItDUbAMhIXw7UPXRfAlOAvZzdK9BWpE60MCn+Svt3aLn9jsPTi/WNGlRUu2uI0v5S7JiIUsbsvh3fw==",
"license": "MIT",
"dependencies": {
"@floating-ui/utils": "^0.2.9"
}
},
"node_modules/@floating-ui/dom": {
"version": "1.6.13",
"resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.13.tgz",
"integrity": "sha512-umqzocjDgNRGTuO7Q8CU32dkHkECqI8ZdMZ5Swb6QAM0t5rnlrN3lGo1hdpscRd3WS8T6DKYK4ephgIH9iRh3w==",
"license": "MIT",
"dependencies": {
"@floating-ui/core": "^1.6.0",
"@floating-ui/utils": "^0.2.9"
}
},
"node_modules/@floating-ui/react": {
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@floating-ui/react/-/react-0.27.7.tgz",
"integrity": "sha512-5V9pwFeiv+95Jlowq/7oiGISSrdXMTs2jfoSy8k+WM6oI/Skm1WWjPdJWeporN2O4UGcsaCJdirKffKayMoPgw==",
"license": "MIT",
"dependencies": {
"@floating-ui/react-dom": "^2.1.2",
"@floating-ui/utils": "^0.2.9",
"tabbable": "^6.0.0"
},
"peerDependencies": {
"react": ">=17.0.0",
"react-dom": ">=17.0.0"
}
},
"node_modules/@floating-ui/react-dom": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.2.tgz",
"integrity": "sha512-06okr5cgPzMNBy+Ycse2A6udMi4bqwW/zgBF/rwjcNqWkyr82Mcg8b0vjX8OJpZFy/FKjJmw6wV7t44kK6kW7A==",
"license": "MIT",
"dependencies": {
"@floating-ui/dom": "^1.0.0"
},
"peerDependencies": {
"react": ">=16.8.0",
"react-dom": ">=16.8.0"
}
},
"node_modules/@floating-ui/utils": {
"version": "0.2.9",
"resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.9.tgz",
"integrity": "sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==",
"license": "MIT"
},
"node_modules/@formatjs/ecma402-abstract": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-2.0.0.tgz",
@ -21679,6 +21733,12 @@
"vue": ">=3.2.26 < 4"
}
},
"node_modules/tabbable": {
"version": "6.2.0",
"resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.2.0.tgz",
"integrity": "sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==",
"license": "MIT"
},
"node_modules/tailwind-merge": {
"version": "2.5.5",
"resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-2.5.5.tgz",

View file

@ -19,6 +19,7 @@
"@auth0/nextjs-auth0": "^3.5.0",
"@aws-sdk/client-s3": "^3.743.0",
"@aws-sdk/s3-request-presigner": "^3.743.0",
"@floating-ui/react": "^0.27.7",
"@google/generative-ai": "^0.21.0",
"@heroicons/react": "^2.2.0",
"@heroui/react": "2.7.4",

View file

@ -282,6 +282,54 @@ async def run_turn_streamed(
print('-'*50)
print(f"Found usage information. Updated cumulative tokens: {tokens_used}")
print('-'*50)
# Handle ResponseFunctionWebSearch specifically
if hasattr(event, 'data') and hasattr(event.data, 'raw_item'):
raw_item = event.data.raw_item
# Check if it's a web search call
if (hasattr(raw_item, 'type') and raw_item.type == 'web_search_call') or (
isinstance(raw_item, dict) and raw_item.get('type') == 'web_search_call'
):
# Get call_id safely, regardless of structure
call_id = None
if hasattr(raw_item, 'id'):
call_id = raw_item.id
elif isinstance(raw_item, dict) and 'id' in raw_item:
call_id = raw_item['id']
else:
call_id = str(uuid.uuid4())
# Get status safely
status = 'unknown'
if hasattr(raw_item, 'status'):
status = raw_item.status
elif isinstance(raw_item, dict) and 'status' in raw_item:
status = raw_item['status']
# Emit a tool call for web search
message = {
'content': None,
'role': 'assistant',
'sender': current_agent.name if current_agent else None,
'tool_calls': [{
'function': {
'name': 'web_search',
'arguments': json.dumps({
'search_id': call_id,
'status': status
})
},
'id': call_id,
'type': 'function'
}],
'tool_call_id': None,
'tool_name': None,
'response_type': 'internal'
}
print("Yielding web search raw response message: ", message)
yield ('message', message)
continue
# Update current agent when it changes
@ -334,45 +382,128 @@ async def run_turn_streamed(
elif event.type == "run_item_stream_event":
current_agent = event.item.agent
if event.item.type == "tool_call_item":
message = {
'content': None,
'role': 'assistant',
'sender': current_agent.name if current_agent else None,
'tool_calls': [{
'function': {
'name': event.item.raw_item.name,
'arguments': event.item.raw_item.arguments
},
'id': event.item.raw_item.call_id,
'type': 'function'
}],
'tool_call_id': None,
'tool_name': None,
'response_type': 'internal'
}
print("Yielding message: ", message)
yield ('message', message)
# Check if it's a ResponseFunctionWebSearch object
if hasattr(event.item.raw_item, 'type') and event.item.raw_item.type == 'web_search_call':
call_id = event.item.raw_item.id if hasattr(event.item.raw_item, 'id') else str(uuid.uuid4())
message = {
'content': None,
'role': 'assistant',
'sender': current_agent.name if current_agent else None,
'tool_calls': [{
'function': {
'name': 'web_search',
'arguments': json.dumps({
'search_id': call_id
})
},
'id': call_id,
'type': 'function'
}],
'tool_call_id': None,
'tool_name': None,
'response_type': 'internal'
}
print("Yielding message: ", message)
yield ('message', message)
elif event.item.type == "tool_call_output_item":
message = {
'content': str(event.item.output),
result_message = {
'content': "Web search done",
'role': 'tool',
'sender': None,
'tool_calls': None,
'tool_call_id': event.item.raw_item['call_id'],
'tool_name': event.item.raw_item.get('name', None),
'tool_call_id': call_id,
'tool_name': 'web_search',
'response_type': 'internal'
}
}
print("Yielding web search results: ", result_message)
yield ('message', result_message)
else:
# Handle normal tool calls
message = {
'content': None,
'role': 'assistant',
'sender': current_agent.name if current_agent else None,
'tool_calls': [{
'function': {
'name': event.item.raw_item.name,
'arguments': event.item.raw_item.arguments
},
'id': event.item.raw_item.call_id,
'type': 'function'
}],
'tool_call_id': None,
'tool_name': None,
'response_type': 'internal'
}
print("Yielding message: ", message)
yield ('message', message)
elif event.item.type == "tool_call_output_item":
# Check if it's a web search result
if isinstance(event.item.raw_item, dict) and event.item.raw_item.get('type') == 'web_search_results':
call_id = event.item.raw_item.get('search_id', event.item.raw_item.get('id', str(uuid.uuid4())))
message = {
'content': str(event.item.output),
'role': 'tool',
'sender': None,
'tool_calls': None,
'tool_call_id': call_id,
'tool_name': 'web_search',
'response_type': 'internal'
}
else:
# Safe extraction of call_id and name
call_id = None
tool_name = None
# Handle different types of raw_item
if isinstance(event.item.raw_item, dict):
call_id = event.item.raw_item.get('call_id')
tool_name = event.item.raw_item.get('name')
elif hasattr(event.item.raw_item, 'call_id'):
call_id = event.item.raw_item.call_id
if hasattr(event.item.raw_item, 'name'):
tool_name = event.item.raw_item.name
message = {
'content': str(event.item.output),
'role': 'tool',
'sender': None,
'tool_calls': None,
'tool_call_id': call_id,
'tool_name': tool_name,
'response_type': 'internal'
}
print("Yielding message: ", message)
yield ('message', message)
elif event.item.type == "message_output_item":
content = ""
url_citations = []
# Extract text content and any URL citations
if hasattr(event.item.raw_item, 'content'):
for content_item in event.item.raw_item.content:
# Handle text content
if hasattr(content_item, 'text'):
content += content_item.text
# Extract URL citations if present
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)
# Create message with URL citations if they exist
message = {
'content': content,
'role': 'assistant',
@ -382,9 +513,97 @@ async def run_turn_streamed(
'tool_name': None,
'response_type': 'external'
}
# Add citations if any were found
if url_citations:
message['citations'] = url_citations
print("Yielding message: ", message)
yield ('message', message)
# Handle web search function call events
elif event.item.type == "web_search_call_item" or (hasattr(event.item, 'raw_item') and hasattr(event.item.raw_item, 'type') and event.item.raw_item.type == 'web_search_call'):
# Extract web search call ID if available
call_id = None
if hasattr(event.item.raw_item, 'id'):
call_id = event.item.raw_item.id
message = {
'content': None,
'role': 'assistant',
'sender': current_agent.name if current_agent else None,
'tool_calls': [{
'function': {
'name': 'web_search',
'arguments': json.dumps({
'search_id': call_id
})
},
'id': call_id or str(uuid.uuid4()),
'type': 'function'
}],
'tool_call_id': None,
'tool_name': None,
'response_type': 'internal'
}
print("Yielding web search message: ", message)
yield ('message', message)
# Handle web search results
elif event.item.type == "web_search_results_item" or (
hasattr(event.item, 'raw_item') and (
(hasattr(event.item.raw_item, 'type') and event.item.raw_item.type == 'web_search_results') or
(isinstance(event.item.raw_item, dict) and event.item.raw_item.get('type') == 'web_search_results')
)
):
# Extract call_id safely
call_id = None
raw_item = event.item.raw_item
# Try several ways to get the search_id or id
if hasattr(raw_item, 'search_id'):
call_id = raw_item.search_id
elif isinstance(raw_item, dict) and 'search_id' in raw_item:
call_id = raw_item['search_id']
elif hasattr(raw_item, 'id'):
call_id = raw_item.id
elif isinstance(raw_item, dict) and 'id' in raw_item:
call_id = raw_item['id']
else:
call_id = str(uuid.uuid4())
# Extract results content safely
results = {}
# Try event.item.output first
if hasattr(event.item, 'output'):
results = event.item.output
# Then try raw_item.results
elif hasattr(raw_item, 'results'):
results = raw_item.results
elif isinstance(raw_item, dict) and 'results' in raw_item:
results = raw_item['results']
# Format the results for output
results_str = ""
try:
results_str = json.dumps(results) if results else ""
except Exception as e:
print(f"Error serializing results: {str(e)}")
results_str = str(results)
message = {
'content': results_str,
'role': 'tool',
'sender': None,
'tool_calls': None,
'tool_call_id': call_id,
'tool_name': 'web_search',
'response_type': 'internal'
}
print("Yielding web search results: ", message)
yield ('message', message)
print(f"\n{'='*50}\n")
# After all events are processed, set final state

View file

@ -13,7 +13,7 @@ from .helpers.instructions import (
add_rag_instructions_to_agent
)
from agents import Agent as NewAgent, Runner, FunctionTool, RunContextWrapper, ModelSettings
from agents import Agent as NewAgent, Runner, FunctionTool, RunContextWrapper, ModelSettings, WebSearchTool
# Add import for OpenAI functionality
from src.utils.common import common_logger as logger, generate_openai_output
from typing import Any
@ -221,14 +221,17 @@ def get_agents(agent_configs, tool_configs, complete_request):
"type": "function",
"function": tool_config
})
tool = FunctionTool(
name=tool_name,
description=tool_config["description"],
params_json_schema=tool_config["parameters"],
strict_json_schema=False,
if tool_name == "web_search":
tool = WebSearchTool()
else:
tool = FunctionTool(
name=tool_name,
description=tool_config["description"],
params_json_schema=tool_config["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)
)
)
new_tools.append(tool)
logger.debug(f"Added tool {tool_name} to agent {agent_config['name']}")
print(f"Added tool {tool_name} to agent {agent_config['name']}")