Merge pull request #243 from rowboatlabs/dev

Dev
This commit is contained in:
arkml 2025-09-12 00:52:05 +05:30 committed by GitHub
commit 431f835ba1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
111 changed files with 3641 additions and 3408 deletions

View file

@ -1,11 +0,0 @@
FROM python:3.12
WORKDIR /app
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY . .
CMD ["mkdocs", "serve", "--dev-addr", "0.0.0.0:8000"]

View file

@ -3,6 +3,7 @@
"theme": "maple",
"name": "Rowboat",
"description": "Rowboat is an open-source platform for building multi-agent systems. It helps you orchestrate tools, RAG, memory, and deployable agents with ease.",
"favicon": "/favicon.ico",
"colors": {
"primary": "#6366F1",
"light": "#6366F1",

View file

@ -1 +0,0 @@
docs.rowboatlabs.com

View file

@ -1,56 +0,0 @@
## Add tools to agents
In Rowboat, you can add tools to your agents by (a) selecting from a in-built library of MCP tools (b) adding your own customer MCP servers (c) integrating your APIs through a webhook (e) mocking tool calls to test the system.
### Adding MCP tools
#### Hosted MCP Library
Rowboat has partnered with ![Kavis AI](https://www.klavis.ai/) to provide a growing library of hosted MCP servers. You can obtain a Klavis API key and set it using:
```bash
export KLAVIS_API_KEY=your-klavis-api-key
```
![Library](img/mcp-library.png)
Enable any of the hosted MCP servers by clicking on the enable button. The server will take approximately 10 seconds to spin up.
![Library](img/enable-mcp-server.png)
For most servers, you will need to authorize it by clicking on the 'Auth' button and connecting to your account e.g. connecting to you github or slack account
The servers you have enabled will show up under tools section in the build view and can be added to any of the agents.
![Library](img/mcp-tools-build-view.png)
Note: For GSuite tools, you need to get a Google Client ID and set it using:
```bash
export KLAVIS_GOOGLE_CLIENT_ID=your-google-client-id
```
To obtain a Google Client ID:
1. Go to the [Google Cloud Console](https://console.cloud.google.com)
2. Create a new project or select an existing one
3. Go to "APIs & Services" → "OAuth consent screen" and complete the setup
4. Go to "APIs & Services" → "Credentials" → "OAuth client ID" and create credentials for web application.
#### Custom MCP Server
You can add any running MCP server in Settings -> Tools.
![Example Tool](img/add-mcp-server.png)
You can use [supergateway](https://github.com/supercorp-ai/supergateway) to expose any MCP stdio server as an SSE server.
Now, you can import the tools from the MCP server in the Build view.
![Example Tool](img/import-mcp-tools.png)
### Adding mock tools
You can mock any tool you have created by checking the 'Mock tool responses' option.
![Example Tool](img/mock-tool.png)
### Debug tool calls in the playground
When agents call tools during a chat in the playground, the tool call parameters and response are available for debugging real-time. For testing purposes, the platform can produce mock tool responses in the playground, without integrating actual tools.
![Mock Tool Responses](img/mock-response.png)

View file

@ -1,27 +0,0 @@
## Add tools to agents
Copilot can help you add tools to agents. You can (a) add a mock tool, (b) add a tool from an MCP server, (c) integrate with you own tools using a webhook.
### Adding mock tools
You can mock any tool you have created by checking the 'Mock tool responses' option.
![Example Tool](img/mock-tool.png)
### Adding MCP tools
You can add a running MCP server in Settings -> Tools.
![Example Tool](img/add-mcp-server.png)
You can use [supergateway](https://github.com/supercorp-ai/supergateway) to expose any MCP stdio server as an SSE server.
Now, you can import the tools from the MCP server in the Build view.
![Example Tool](img/import-mcp-tools.png)
### Debug tool calls in the playground
When agents call tools during a chat in the playground, the tool call parameters and response are available for debugging real-time. For testing purposes, the platform can produce mock tool responses in the playground, without integrating actual tools.
![Mock Tool Responses](img/mock-response.png)

View file

@ -1,29 +0,0 @@
# Agents
## Overview
- Agents carry out a specific part of the conversation and / or perform tasks like orchestrating between other agents, triggering internal processes and fetching information.
- Agents carry out tasks through tools provided to them.
- Agents can be connected to other agents through a mention in the agent's instruction.
## Agent Configurations
### Description
The description conveys the agent's role in the multi-agent system. Writing a good description is important for other agents to know when to pass control of the conversation to an agent.
### Instructions
Agent instructions are the backbone of an agent, defining its behavior. RowBoat Studio's copilot produces a good framework for agent instructions, involving Role, Steps to Follow, Scope and Guidelines. Since agents are powered by LLMs, general best practices while writing prompts apply.
### Examples
The agent uses examples as a reference for behavior in different scenarios. While there are no prescribed formats to provide examples in, examples should include what the user might say, what the agent should respond with as well as indications of any tool calls to be made.
### Prompts
Prompts attached to an agent will be used by the agent in addition to instructions.
### Tools
Tools attached to an agent will be put out as tool calls. The behavior of when to invoke tools can be fine-tuned by specifying corresponding instructions or prompts. Adding examples to agents can also be useful in controlling tool call behavior.
### Connected Agents
In the agent instructions, the connected agents are shown with an '@mention'. If the agent mentioned in an instruction (connected agent) does not actually exist, the connected agent's name would show up with an '!' to call to attention.
### Model
RowBoat currently supports OpenAI LLMs. Agents can be configured to use GPT-4o or GPT-4o-mini.

View file

@ -1,29 +0,0 @@
# Agents
## Overview
- Agents carry out a specific part of the conversation and / or perform tasks like orchestrating between other agents, triggering internal processes and fetching information.
- Agents carry out tasks through tools provided to them.
- Agents can be connected to other agents through a mention in the agent's instruction.
## Agent Configurations
### Description
The description conveys the agent's role in the multi-agent system. Writing a good description is important for other agents to know when to pass control of the conversation to an agent.
### Instructions
Agent instructions are the backbone of an agent, defining its behavior. RowBoat Studio's copilot produces a good framework for agent instructions, involving Role, Steps to Follow, Scope and Guidelines. Since agents are powered by LLMs, general best practices while writing prompts apply.
### Examples
The agent uses examples as a reference for behavior in different scenarios. While there are no prescribed formats to provide examples in, examples should include what the user might say, what the agent should respond with as well as indications of any tool calls to be made.
### Prompts
Prompts attached to an agent will be used by the agent in addition to instructions.
### Tools
Tools attached to an agent will be put out as tool calls. The behavior of when to invoke tools can be fine-tuned by specifying corresponding instructions or prompts. Adding examples to agents can also be useful in controlling tool call behavior.
### Connected Agents
In the agent instructions, the connected agents are shown with an '@mention'. If the agent mentioned in an instruction (connected agent) does not actually exist, the connected agent's name would show up with an '!' to call to attention.
### Model
RowBoat currently supports OpenAI LLMs. Agents can be configured to use GPT-4o or GPT-4o-mini.

View file

@ -1,14 +0,0 @@
## Create the set of initial agents
Copilot can set up agents for you from scratch.
### Instruct copilot
First, tell it about the initial set of agents that make up your assistant.
![Agent Config](img/create-agents-delivery.png)
Using copilot to create your initial set of agents helps you leverage best practices in formatting agent instructions and connecting agents to each other as a graph, all of which have been baked into copilot.
### Inspect the agents
Once you apply changes, inspect the agents to see how copilot has built them. Specifically, note the Instructions, and Examples in each agent.
![Agent Config](img/agent-instruction.png)

View file

@ -1,14 +0,0 @@
## Create the set of initial agents
Copilot can set up agents for you from scratch.
### Instruct copilot
First, tell it about the initial set of agents that make up your assistant.
![Agent Config](img/create-agents-delivery.png)
Using copilot to create your initial set of agents helps you leverage best practices in formatting agent instructions and connecting agents to each other as a graph, all of which have been baked into copilot.
### Inspect the agents
Once you apply changes, inspect the agents to see how copilot has built them. Specifically, note the Instructions, and Examples in each agent.
![Agent Config](img/agent-instruction.png)

View file

@ -1 +0,0 @@
Coming soon.

View file

@ -1 +0,0 @@
Coming soon.

View file

@ -1,23 +0,0 @@
# Graph-based Framework
## Overview
- Multi-agent systems are popularly represented as graphs, where each agent is a node in the graph.
- In RowBoat, agents are connected to each other as Directed Acyclic Graphs (DAG).
- The graph is also called a workflow, which defines agents, tools, and their connections.
- Since the graph is directed, the control of conversation flows from "parent" agents to their "children" agents
- Every agent is responsible for carrying out a specific part of the workflow, which can involve conversing with the user and / or carrying out tasks such as directing the conversation to other agents.
- [Langgraph](https://www.langchain.com/langgraph) and [Swarm](https://github.com/openai/swarm) are examples of open-source frameworks used to define multi-agent graphs. RowBoat currently supports a Swarm implementation and will extend to Langgraph too in the future.
## Control Passing
- While defining the workflow, an agent is designated as the Start agent, to which the first turn of chat will be directed. Typically the Start agent is responsible for triaging the user's query at a high-level and passing control to relevant specific agents which can address the user's needs.
- In any turn of chat, the agent currently in control of the chat has one of 3 options: a) respond to the user (or put out tool calls), b) transfer the chat to any of its children agents or c) transfer the chat back to its parent agent.
- Agents use internal tool calls to transfer the chat to other agents.
- Thus, control passing is achieved by allowing agents to decide flow of control autonomously.
- To the user, the assistant will appear as a unified system, while agents work under the hood.
## Pipelines
- RowBoat also has the concept of pipelines - specialized agents invoked sequentially after an agent in the graph has produced a user-facing response.
- E.g. a pipeline with a post processing agent and a guardrail agent will ensure that every response is post processed and guardrailed for appropriateness before presenting it to the user.

View file

@ -1,23 +0,0 @@
# Graph-based Framework
## Overview
- Multi-agent systems are popularly represented as graphs, where each agent is a node in the graph.
- In RowBoat, agents are connected to each other as Directed Acyclic Graphs (DAG).
- The graph is also called a workflow, which defines agents, tools, and their connections.
- Since the graph is directed, the control of conversation flows from "parent" agents to their "children" agents
- Every agent is responsible for carrying out a specific part of the workflow, which can involve conversing with the user and / or carrying out tasks such as directing the conversation to other agents.
- [Langgraph](https://www.langchain.com/langgraph) and [Swarm](https://github.com/openai/swarm) are examples of open-source frameworks used to define multi-agent graphs. RowBoat currently supports a Swarm implementation and will extend to Langgraph too in the future.
## Control Passing
- While defining the workflow, an agent is designated as the Start agent, to which the first turn of chat will be directed. Typically the Start agent is responsible for triaging the user's query at a high-level and passing control to relevant specific agents which can address the user's needs.
- In any turn of chat, the agent currently in control of the chat has one of 3 options: a) respond to the user (or put out tool calls), b) transfer the chat to any of its children agents or c) transfer the chat back to its parent agent.
- Agents use internal tool calls to transfer the chat to other agents.
- Thus, control passing is achieved by allowing agents to decide flow of control autonomously.
- To the user, the assistant will appear as a unified system, while agents work under the hood.
## Pipelines
- RowBoat also has the concept of pipelines - specialized agents invoked sequentially after an agent in the graph has produced a user-facing response.
- E.g. a pipeline with a post processing agent and a guardrail agent will ensure that every response is post processed and guardrailed for appropriateness before presenting it to the user.

View file

@ -1,156 +0,0 @@
# Using the Hosted App
- This is the developers guide to self-hosting the open-source version of RowBoat.
- Please see our [Introduction](/) page before referring to this guide.
- For direct installation steps, please head to the README of RowBoat's Github repo: [@rowboatlabs/rowboat](https://github.com/rowboatlabs/rowboat/). This page provides more context about the installation process and the different components involved.
## Overview
RowBoat's codebase has three main components:
| Component | Description |
|--------------|---------------|
| **Agents** | Python framework responsible for carrying out multi-agent conversations |
| **Copilot** | Python framework powering the copilot in RowBoat Studio |
| **RowBoat** | Frontend and backend services to power RowBoat Studio and Chat APIs |
These components are structured as separate services, each containerized with Docker. Running `docker-compose up --build` enables you to use the Studio in your browser, as well as stands up the APIs and SDK.
## Prerequisites
All of these prerequistes have open-source or free versions.
| Prerequisite | Description |
|--------------|---------------|
| **Docker** | Bundles and builds all services |
| **OpenAI API Key** | Agents and Copilot services are powered by OpenAI LLMs |
| **MongoDB** | Stores workflow versions, chats and RAG embeddings |
| **Auth0 Account** | Handles user authentication and identity management for Studio |
Refer to our [Github Readme for Prerequisites](https://github.com/rowboatlabs/rowboat/?tab=readme-ov-file#prerequisites) to set up prerequisites.
## Setting up
Refer to our [Github Readme for Local Development](https://github.com/rowboatlabs/rowboat/?tab=readme-ov-file#local-development-setup) to set up Studio, Chat API and SDK via `docker-compose`.
### Testing Studio
1. Once you are set up, you should be able to login to the Studio (default local URL: [http://localhost:3000](http://localhost:8000)) via Auth0's login options (Gmail, Github etc.)
<br>
2. Once in Studio, create a new blank project or use one of the example templates:
![Create Project](img/project-page.png)
<br>
3. Use the copilot to help you build agents:
![Use Copilot](img/use-copilot.png)
<br>
4. Ensure that the correct agent is set as the "start agent":
![Set Start Agent](img/start-agent.png)
<br>
5. Test out a chat in the playground to verify the agents' behavior:
![Testing Chat](img/testing-chat.png)
<br>
### Testing the Chat API
You can use the API directly at [http://localhost:3000/api/v1/](http://localhost:3000/api/v1/)
- Project ID is available in the URL of the project page
- API Key can be generated from the project config page at `/projects/<PROJECT_ID>/config`
Below is an example request and response. Modify the user message in the request, based on your example project.
**Request:**
```bash
curl --location 'http://localhost:3000/api/v1/<PROJECT_ID>/chat' \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer <API_KEY>' \
--data '{
"messages": [
{
"role": "user",
"content": "What is my pending payment amount?"
}
]
}'
```
**Response:**
The last message in `messages` is either a user-facing response or a tool call by the assistant.
```json
{
"messages": [
{
"sender": "Credit Card Hub",
"role": "assistant",
"response_type": "internal",
"content": null,
"created_at": "2025-02-01T06:55:47.843909",
"current_turn": true,
"tool_calls": [
{
"function": {
"arguments": "{\"args\":\"\",\"kwargs\":\"\"}",
// Internal tool calls are used to transfer between agents
"name": "transfer_to_outstanding_payment"
},
"id": "call_7jGpwpVvzhZFOyRgxHFkdOdU",
"type": "function"
}
]
},
{
"tool_name": "transfer_to_outstanding_payment",
"role": "tool",
"content": "{\"assistant\": \"Outstanding Payment\"}",
"tool_call_id": "call_7jGpwpVvzhZFOyRgxHFkdOdU"
},
{
"sender": "Outstanding Payment",
"role": "assistant",
// Response is not user-facing, to enable further post processing
"response_type": "internal",
"content": "Sure, could you provide the last four digits of your card or your registered mobile number so I can look up your pending payment amount?",
"created_at": "2025-02-01T06:55:49.648008",
"current_turn": true
},
{
"sender": "Outstanding Payment >> Post process",
"role": "assistant",
// Response is user-facing
"response_type": "external",
"content": "Sure, please provide the last four digits of your card or your registered mobile number so I can check your pending payment amount.",
"created_at": "2025-02-01T06:55:49.648008",
"current_turn": true
}
],
"state": {
// .. state data
}
}
```
### Testing the Python Chat SDK
```bash
pip install rowboat
```
Modify the user message in `messages`, based on your example project.
```python
from rowboat import Client
client = Client(
host="http://localhost:3000",
project_id="<PROJECT_ID>",
api_key="<API_KEY>" # Generate this from /projects/<PROJECT_ID>/config
)
# Simple chat interaction
messages = [{"role": "user", "content": "What is my pending payment amount?"}]
response_messages, state = client.chat(messages=messages)
```
The last message in `response_messages` is either a user-facing response or a tool call by the assistant.

View file

@ -1,156 +0,0 @@
# Using the Hosted App
- This is the developers guide to self-hosting the open-source version of RowBoat.
- Please see our [Introduction](/) page before referring to this guide.
- For direct installation steps, please head to the README of RowBoat's Github repo: [@rowboatlabs/rowboat](https://github.com/rowboatlabs/rowboat/). This page provides more context about the installation process and the different components involved.
## Overview
RowBoat's codebase has three main components:
| Component | Description |
|--------------|---------------|
| **Agents** | Python framework responsible for carrying out multi-agent conversations |
| **Copilot** | Python framework powering the copilot in RowBoat Studio |
| **RowBoat** | Frontend and backend services to power RowBoat Studio and Chat APIs |
These components are structured as separate services, each containerized with Docker. Running `docker-compose up --build` enables you to use the Studio in your browser, as well as stands up the APIs and SDK.
## Prerequisites
All of these prerequistes have open-source or free versions.
| Prerequisite | Description |
|--------------|---------------|
| **Docker** | Bundles and builds all services |
| **OpenAI API Key** | Agents and Copilot services are powered by OpenAI LLMs |
| **MongoDB** | Stores workflow versions, chats and RAG embeddings |
| **Auth0 Account** | Handles user authentication and identity management for Studio |
Refer to our [Github Readme for Prerequisites](https://github.com/rowboatlabs/rowboat/?tab=readme-ov-file#prerequisites) to set up prerequisites.
## Setting up
Refer to our [Github Readme for Local Development](https://github.com/rowboatlabs/rowboat/?tab=readme-ov-file#local-development-setup) to set up Studio, Chat API and SDK via `docker-compose`.
### Testing Studio
1. Once you are set up, you should be able to login to the Studio (default local URL: [http://localhost:3000](http://localhost:8000)) via Auth0's login options (Gmail, Github etc.)
<br />
2. Once in Studio, create a new blank project or use one of the example templates:
![Create Project](img/project-page.png)
<br />
3. Use the copilot to help you build agents:
![Use Copilot](img/use-copilot.png)
<br />
4. Ensure that the correct agent is set as the "start agent":
![Set Start Agent](img/start-agent.png)
<br />
5. Test out a chat in the playground to verify the agents' behavior:
![Testing Chat](img/testing-chat.png)
<br />
### Testing the Chat API
You can use the API directly at [http://localhost:3000/api/v1/](http://localhost:3000/api/v1/)
- Project ID is available in the URL of the project page
- API Key can be generated from the project config page at `/projects/<PROJECT_ID>/config`
Below is an example request and response. Modify the user message in the request, based on your example project.
**Request:**
```bash
curl --location 'http://localhost:3000/api/v1/<PROJECT_ID>/chat' \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer <API_KEY>' \
--data '{
"messages": [
{
"role": "user",
"content": "What is my pending payment amount?"
}
]
}'
```
**Response:**
The last message in `messages` is either a user-facing response or a tool call by the assistant.
```json
{
"messages": [
{
"sender": "Credit Card Hub",
"role": "assistant",
"response_type": "internal",
"content": null,
"created_at": "2025-02-01T06:55:47.843909",
"current_turn": true,
"tool_calls": [
{
"function": {
"arguments": "{\"args\":\"\",\"kwargs\":\"\"}",
// Internal tool calls are used to transfer between agents
"name": "transfer_to_outstanding_payment"
},
"id": "call_7jGpwpVvzhZFOyRgxHFkdOdU",
"type": "function"
}
]
},
{
"tool_name": "transfer_to_outstanding_payment",
"role": "tool",
"content": "{\"assistant\": \"Outstanding Payment\"}",
"tool_call_id": "call_7jGpwpVvzhZFOyRgxHFkdOdU"
},
{
"sender": "Outstanding Payment",
"role": "assistant",
// Response is not user-facing, to enable further post processing
"response_type": "internal",
"content": "Sure, could you provide the last four digits of your card or your registered mobile number so I can look up your pending payment amount?",
"created_at": "2025-02-01T06:55:49.648008",
"current_turn": true
},
{
"sender": "Outstanding Payment >> Post process",
"role": "assistant",
// Response is user-facing
"response_type": "external",
"content": "Sure, please provide the last four digits of your card or your registered mobile number so I can check your pending payment amount.",
"created_at": "2025-02-01T06:55:49.648008",
"current_turn": true
}
],
"state": {
// .. state data
}
}
```
### Testing the Python Chat SDK
```bash
pip install rowboat
```
Modify the user message in `messages`, based on your example project.
```python
from rowboat import Client
client = Client(
host="http://localhost:3000",
project_id="<PROJECT_ID>",
api_key="<API_KEY>" # Generate this from /projects/<PROJECT_ID>/config
)
# Simple chat interaction
messages = [{"role": "user", "content": "What is my pending payment amount?"}]
response_messages, state = client.chat(messages=messages)
```
The last message in `response_messages` is either a user-facing response or a tool call by the assistant.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 119 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 234 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 236 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 114 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 379 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 252 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 157 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 239 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 249 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 194 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 152 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 173 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 256 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 135 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 154 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 326 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 141 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 168 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 120 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 148 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 176 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 249 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 119 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 264 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 341 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 246 KiB

View file

@ -1,53 +0,0 @@
# Welcome to Rowboat
Rowboat is a low-code AI IDE to build MCP tools connected multi-agent assistants. Rowboat copilot builds the agents for you based on your requirements with the option do everything manually as well.
**Note:** These docs are intended for developers who would like to use our [open-source code](https://github.com/rowboatlabs/rowboat/).
- Our source code is on GitHub at [@rowboatlabs/rowboat](https://github.com/rowboatlabs/rowboat/)
- Join us on [discord](https://discord.gg/rxB8pzHxaS)
- Email us at [founders@rowboatlabs.com](mailto:founders@rowboatlabs.com)
- Visit our [website](https://www.rowboatlabs.com/)
## What is RowBoat?
**RowBoat is a state-of-art platform to build multi-agent AI systems in a visual interface, with the help of a copilot.**
RowBoat enables you to build, manage and deploy user-facing assistants. An assistant is made up of multiple agents, each having access to a set of tools and working together to interact with the user as a single assistant. You can connect any MCP tools to the agents.
For example, you can build a *credit card assistant*, where each agent handles a workflow such as *outstanding payments*, *balance inquiries* and *transaction disputes*. You can equip agents with tools to carry out tasks such as *fetching payment options*, *checking outstanding balance* and *updating user information*. The assistant would help your end-users their credit card-related needs without having to talk to a human agent on your end.
## How RowBoat works
### RowBoat Studio
RowBoat Studio lets you create AI agents in minutes, using a visual interface and plain language. Here are key components that you will work with:
| Component | Description | Highlights |
|------------|-------------|------------|
| Agent | Handles a specific part of the conversation and<br>performs tasks using tools, based on instructions |• Configurable using plain language instructions<br>• Orchestrate between agents connected as a graph<br>• Can access tools and knowledge sources (RAG)|
| Playground | Interactive environment to test assistants<br>conversationally as you build them |• Real-time testing and debugging<br>• Inspect parameters and results of tool calls in-line<br>• Converse with individual agents or the entire assistant|
| Copilot | AI-powered concierge that creates and<br>updates agents and tools on your behalf |• Context-aware of all components including playground<br>• Improves agents based on conversations and feedback <br>• Understands your requests in plain language|
### RowBoat Chat API & SDK
- [RowBoat Chat API](/using_the_api) is a stateless HTTP API to interface with the assistant created on RowBoat Studio. You can use the API to drive end-user facing conversations in your app or website.
- [RowBoat Chat SDK](/using_the_sdk) is a simple SDK (currently available in Python) which wraps the HTTP API under the hood. It offers both stateful and stateless (OpenAI-style) implementations.
### Steps
**RowBoat Studio:**
1. Describe the assistant you are looking to build, to **copilot**
2. Review and apply the **agents** (and tools) created by copilot
3. Configure **MCP servers** and **tools** and connect them to agents
4. Chat with your assistant in the **playground**
6. Deploy and use the HTTP API or Python SDK to integrate the agents into your system
## Why RowBoat?
Rowboat is the fastest way to build and deploy MCP connected multi-agents
1. **Build** complex assistants using plain language and a visual interface
2. **Integrate** tools and MCP servers in minutes
3. **Expedite** your multi-agent AI roadmap using battle-tested tooling
## Getting started
- To set up our open-source installation, see [Github Readme](https://github.com/rowboatlabs/rowboat)
- To sign up for our managed offering (beta), please email us at [founders@rowboatlabs.com](mailto:founders@rowboatlabs.com)

View file

@ -1,205 +0,0 @@
RowBoat is available under the [Apache 2.0 License](https://github.com/rowboatlabs/rowboat/blob/main/LICENSE):
----
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [2024] [RowBoat Labs]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

View file

@ -1,32 +0,0 @@
# Open Source Installation
- This is the developers guide to self-hosting the open-source version of RowBoat. To get started with the hosted app, please see [Using the Hosted App](/hosted_setup)
- Please see our [Introduction](/) page before referring to this guide.
- For direct installation steps, please head to the README of RowBoat's Github repo: [@rowboatlabs/rowboat](https://github.com/rowboatlabs/rowboat/). This page provides more context about the installation process and the different components involved.
## Overview
RowBoat's codebase has three main components:
| Component | Description |
|--------------|---------------|
| **Agents** | Python framework responsible for carrying out multi-agent conversations |
| **Copilot** | Python framework powering the copilot in RowBoat Studio |
| **RowBoat** | Frontend and backend services to power RowBoat Studio and Chat APIs |
These components are structured as separate services, each containerized with Docker. Running `docker-compose up --build` enables you to use the Studio in your browser, as well as stands up the APIs and SDK.
## Prerequisites
All of these prerequisites have open-source or free versions.
| Prerequisite | Description |
|--------------|---------------|
| **Docker** | Bundles and builds all services |
| **OpenAI API Key** | Agents and Copilot services are powered by OpenAI LLMs |
| **MongoDB** | Stores workflow versions, chats and RAG embeddings |
Refer to our [Github Readme for Prerequisites](https://github.com/rowboatlabs/rowboat/?tab=readme-ov-file#prerequisites) to set up prerequisites.
## Setting up
Refer to our [Github Readme for Local Development](https://github.com/rowboatlabs/rowboat/?tab=readme-ov-file#local-development-setup) to set up Studio, Chat API and SDK via `docker-compose`.

View file

@ -1,32 +0,0 @@
# Open Source Installation
- This is the developers guide to self-hosting the open-source version of RowBoat. To get started with the hosted app, please see [Using the Hosted App](/hosted_setup)
- Please see our [Introduction](/) page before referring to this guide.
- For direct installation steps, please head to the README of RowBoat's Github repo: [@rowboatlabs/rowboat](https://github.com/rowboatlabs/rowboat/). This page provides more context about the installation process and the different components involved.
## Overview
RowBoat's codebase has three main components:
| Component | Description |
|--------------|---------------|
| **Agents** | Python framework responsible for carrying out multi-agent conversations |
| **Copilot** | Python framework powering the copilot in RowBoat Studio |
| **RowBoat** | Frontend and backend services to power RowBoat Studio and Chat APIs |
These components are structured as separate services, each containerized with Docker. Running `docker-compose up --build` enables you to use the Studio in your browser, as well as stands up the APIs and SDK.
## Prerequisites
All of these prerequisites have open-source or free versions.
| Prerequisite | Description |
|--------------|---------------|
| **Docker** | Bundles and builds all services |
| **OpenAI API Key** | Agents and Copilot services are powered by OpenAI LLMs |
| **MongoDB** | Stores workflow versions, chats and RAG embeddings |
Refer to our [Github Readme for Prerequisites](https://github.com/rowboatlabs/rowboat/?tab=readme-ov-file#prerequisites) to set up prerequisites.
## Setting up
Refer to our [Github Readme for Local Development](https://github.com/rowboatlabs/rowboat/?tab=readme-ov-file#local-development-setup) to set up Studio, Chat API and SDK via `docker-compose`.

View file

@ -1,7 +0,0 @@
## Try an example chat in the playground
### Chat with the assistant
The playground is intended to test out the assistant as you build it. The User and Assistant messages represent the conversation that your end-user will have if your assistant is deployed in production. The playground also has debug elements which show the flow of control between different agents in your system, as well as which agent finally responded to the user.
![Try Chat](img/chat-delivery.png)

View file

@ -1,7 +0,0 @@
## Try an example chat in the playground
### Chat with the assistant
The playground is intended to test out the assistant as you build it. The User and Assistant messages represent the conversation that your end-user will have if your assistant is deployed in production. The playground also has debug elements which show the flow of control between different agents in your system, as well as which agent finally responded to the user.
![Try Chat](img/chat-delivery.png)

View file

@ -1,5 +0,0 @@
# Prompts
- Prompts are reusable pieces of agent instructions in Studio.
- Prompts can be defined once and reused across multiple agents.
- Common examples of prompts are style prompts which indicate brand voice and structured output prompts which specify a format for the agent to provide its output in (e.g. ReAct)

View file

@ -1,5 +0,0 @@
# Prompts
- Prompts are reusable pieces of agent instructions in Studio.
- Prompts can be defined once and reused across multiple agents.
- Common examples of prompts are style prompts which indicate brand voice and structured output prompts which specify a format for the agent to provide its output in (e.g. ReAct)

View file

@ -1,127 +0,0 @@
## Getting started
- ✨ **Start from an idea → Copilot builds your multi-agent workflows**
E.g. "Build me an assistant for a food delivery company to handle delivery status and missing items. Include the necessary tools."
- 🌐 **Connect MCP servers**
Add the MCP servers in Settings → import the tools into Rowboat.
- 📞 **Integrate into your app using the HTTP API or Python SDK**
Grab the Project ID and generated API Key from Settings and use the API.
Powered by OpenAI's Agents SDK, Rowboat is the fastest way to build multi-agents!
## Quick start
Step 1. Set your OpenAI key:
```bash
export OPENAI_API_KEY=your-openai-api-key
```
Step 2. Clone the repository and start Rowboat docker
```bash
git clone git@github.com:rowboatlabs/rowboat.git
cd rowboat
docker-compose up --build
```
Step 3. Access the app at [http://localhost:3000](http://localhost:3000).
Note: See the [Using custom LLM providers](#using-custom-llm-providers) section below for using custom providers like OpenRouter and LiteLLM.
## Demo
#### Create a multi-agent assistant with MCP tools by chatting with Rowboat
[![Screenshot 2025-04-23 at 00 25 31](https://github.com/user-attachments/assets/c8a41622-8e0e-459f-becb-767503489866)](https://youtu.be/YRTCw9UHRbU)
## Integrate with Rowboat agents
There are 2 ways to integrate with the agents you create in Rowboat
**Option #1: HTTP API**
You can use the API directly at [http://localhost:3000/api/v1/](http://localhost:3000/api/v1/). See [API Docs](https://docs.rowboatlabs.com/using_the_api/) for details.
```bash
curl --location 'http://localhost:3000/api/v1/<PROJECT_ID>/chat' \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer <API_KEY>' \
--data '{
"messages": [
{
"role": "user",
"content": "tell me the weather in london in metric units"
}
],
"state": null
}'
```
**Option #2: Python SDK**
You can use the included Python SDK to interact with the Agents
```python
from rowboat import Client, StatefulChat
from rowboat.schema import UserMessage, SystemMessage
# Initialize the client
client = Client(
host="http://localhost:3000",
project_id="<PROJECT_ID>",
api_key="<API_KEY>"
)
# Create a stateful chat session (recommended)
chat = StatefulChat(client)
response = chat.run("What's the weather in London?")
print(response)
# Or use the low-level client API
messages = [
SystemMessage(role='system', content="You are a helpful assistant"),
UserMessage(role='user', content="Hello, how are you?")
]
# Get response
response = client.chat(messages=messages)
print(response.messages[-1].content)
```
## Using custom LLM providers
By default, Rowboat uses OpenAI LLMs (gpt-4o, gpt-4.1, etc.) for both agents and copilot, when you export your OPENAI_API_KEY.
However, you can also configure custom LLM providers (e.g. LiteLLM, OpenRouter) to use any of the hundreds of available LLMs beyond OpenAI, such as Claude, DeepSeek, Ollama LLMs and so on.
**Step 1:** Set up your custom LLM provider using the variables below, for example (assuming LiteLLM):
```bash
export PROVIDER_BASE_URL=http://host.docker.internal:4000/
export PROVIDER_API_KEY=sk-1234
```
Rowboat uses "gpt-4.1" as the default model for agents and copilot but this can be overridden as follows, for example (assuming LiteLLM):
```bash
export PROVIDER_DEFAULT_MODEL=claude-3-7-sonnet-latest
export PROVIDER_COPILOT_MODEL=gpt-4o
```
**Notes:**
- Copilot is optimized for gpt-4o/gpt-4.1. We strongly recommend using these models for best performance.
- You can specify different models for the copilot and each agent, but all of them must belong to the same provider (e.g. LiteLLM)
- The integration is provider-agnostic and should work with any service that implements the OpenAI messages format.
- OpenAI-specific tools (e.g., web_search) will not work with non-OpenAI providers. If you get an error, remove these tools.
**Step 2 (No change):** Clone the repository and start Rowboat docker
```bash
git clone git@github.com:rowboatlabs/rowboat.git
cd rowboat
docker-compose up --build
```
**Step 3 (No change):** Access the app at [http://localhost:3000](http://localhost:3000).

View file

@ -1,6 +0,0 @@
## Simulate real-world user scenarios
Create a test-bench of real-world scenarios in the simulator.
![Scenarios](img/scenarios.png)
Run the scenarios as simulated chats betweeen a user (role-played) and the assistant, in the playground.
![Simulation](img/simulate.png)

View file

@ -1,6 +0,0 @@
## Simulate real-world user scenarios
Create a test-bench of real-world scenarios in the simulator.
![Scenarios](img/scenarios.png)
Run the scenarios as simulated chats betweeen a user (role-played) and the assistant, in the playground.
![Simulation](img/simulate.png)

View file

@ -1,7 +0,0 @@
# Building Assistants in Studio
This is a guide to building your first assistant on RowBoat Studio, with examples.<br>
Prerequisite:
1. **Open Source Users:** Complete the [open-source installation steps](/oss_installation/) to set up RowBoat Studio.
2. **Hosted App Users:** Sign in to [https://app.rowboatlabs.com/](https://app.rowboatlabs.com/)

View file

@ -1,7 +0,0 @@
# Building Assistants in Studio
This is a guide to building your first assistant on RowBoat Studio, with examples.<br />
Prerequisite:
1. **Open Source Users:** Complete the [open-source installation steps](/oss_installation/) to set up RowBoat Studio.
2. **Hosted App Users:** Sign in to [https://app.rowboatlabs.com/](https://app.rowboatlabs.com/)

View file

@ -1,132 +0,0 @@
# Testing Your Setup
## Testing Studio
1. Once you are set up, you should be able to login to the Studio via Auth0's login options (Gmail, Github etc.).
- For the open source installation, the URL for Studio is [http://localhost:3000](http://localhost:3000)
- To use our hosted app, the URL for Studio is [https://app.rowboatlabs.com](https://app.rowboatlabs.com/)
<br>
2. Once in Studio, create a new blank project or browse through one of the example projects:
![Create Project](img/project-page.png)
<br>
3. Use the copilot to help you build agents:
![Use Copilot](img/use-copilot.png)
<br>
4. Ensure that the correct agent is set as the "start agent":
![Set Start Agent](img/start-agent.png)
<br>
5. Test out a chat in the playground to verify the agents' behavior:
![Testing Chat](img/testing-chat.png)
<br>
### Testing the Chat API
- For the open source installation, the `<HOST>` is [http://localhost:3000](http://localhost:3000)
- When using the hosted app, the `<HOST>` is [https://app.rowboatlabs.com](https://app.rowboatlabs.com)
- `<PROJECT_ID>` is available in the URL of the project page
- API Key can be generated from the project config page at `<HOST>/projects/<PROJECT_ID>/config`
Below is an example request and response. Modify the user message in the request, based on your example project.
**Request:**
```bash
curl --location 'http://<HOST>/api/v1/<PROJECT_ID>/chat' \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer <API_KEY>' \
--data '{
"messages": [
{
"role": "user",
"content": "What is my pending payment amount?"
}
]
}'
```
**Response:**
The last message in `messages` is either a user-facing response or a tool call by the assistant.
```json
{
"messages": [
{
"sender": "Credit Card Hub",
"role": "assistant",
"response_type": "internal",
"content": null,
"created_at": "2025-02-01T06:55:47.843909",
"current_turn": true,
"tool_calls": [
{
"function": {
"arguments": "{\"args\":\"\",\"kwargs\":\"\"}",
// Internal tool calls are used to transfer between agents
"name": "transfer_to_outstanding_payment"
},
"id": "call_7jGpwpVvzhZFOyRgxHFkdOdU",
"type": "function"
}
]
},
{
"tool_name": "transfer_to_outstanding_payment",
"role": "tool",
"content": "{\"assistant\": \"Outstanding Payment\"}",
"tool_call_id": "call_7jGpwpVvzhZFOyRgxHFkdOdU"
},
{
"sender": "Outstanding Payment",
"role": "assistant",
// Response is not user-facing, to enable further post processing
"response_type": "internal",
"content": "Sure, could you provide the last four digits of your card or your registered mobile number so I can look up your pending payment amount?",
"created_at": "2025-02-01T06:55:49.648008",
"current_turn": true
},
{
"sender": "Outstanding Payment >> Post process",
"role": "assistant",
// Response is user-facing
"response_type": "external",
"content": "Sure, please provide the last four digits of your card or your registered mobile number so I can check your pending payment amount.",
"created_at": "2025-02-01T06:55:49.648008",
"current_turn": true
}
],
"state": {
// .. state data
}
}
```
### Testing the Python Chat SDK
- For the open source installation, the `<HOST>` is [http://localhost:3000](http://localhost:3000)
- When using the hosted app, the `<HOST>` is [https://app.rowboatlabs.com](https://app.rowboatlabs.com)
- `<PROJECT_ID>` is available in the URL of the project page
- API Key can be generated from the project config page at `<HOST>/projects/<PROJECT_ID>/config`
```bash
pip install rowboat
```
Modify the user message in `messages`, based on your example project.
```python
from rowboat import Client
client = Client(
host="<HOST>",
project_id="<PROJECT_ID>",
api_key="<API_KEY>" # Generate this from /projects/<PROJECT_ID>/config
)
# Simple chat interaction
messages = [{"role": "user", "content": "What is my pending payment amount?"}]
response_messages, state = client.chat(messages=messages)
```
The last message in `response_messages` is either a user-facing response or a tool call by the assistant.

View file

@ -1,132 +0,0 @@
# Testing Your Setup
## Testing Studio
1. Once you are set up, you should be able to login to the Studio via Auth0's login options (Gmail, Github etc.).
- For the open source installation, the URL for Studio is [http://localhost:3000](http://localhost:3000)
- To use our hosted app, the URL for Studio is [https://app.rowboatlabs.com](https://app.rowboatlabs.com/)
<br />
2. Once in Studio, create a new blank project or browse through one of the example projects:
![Create Project](img/project-page.png)
<br />
3. Use the copilot to help you build agents:
![Use Copilot](img/use-copilot.png)
<br />
4. Ensure that the correct agent is set as the "start agent":
![Set Start Agent](img/start-agent.png)
<br />
5. Test out a chat in the playground to verify the agents' behavior:
![Testing Chat](img/testing-chat.png)
<br />
### Testing the Chat API
- For the open source installation, the `<HOST>` is [http://localhost:3000](http://localhost:3000)
- When using the hosted app, the `<HOST>` is [https://app.rowboatlabs.com](https://app.rowboatlabs.com)
- `<PROJECT_ID>` is available in the URL of the project page
- API Key can be generated from the project config page at `<HOST>/projects/<PROJECT_ID>/config`
Below is an example request and response. Modify the user message in the request, based on your example project.
**Request:**
```bash
curl --location 'http://<HOST>/api/v1/<PROJECT_ID>/chat' \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer <API_KEY>' \
--data '{
"messages": [
{
"role": "user",
"content": "What is my pending payment amount?"
}
]
}'
```
**Response:**
The last message in `messages` is either a user-facing response or a tool call by the assistant.
```json
{
"messages": [
{
"sender": "Credit Card Hub",
"role": "assistant",
"response_type": "internal",
"content": null,
"created_at": "2025-02-01T06:55:47.843909",
"current_turn": true,
"tool_calls": [
{
"function": {
"arguments": "{\"args\":\"\",\"kwargs\":\"\"}",
// Internal tool calls are used to transfer between agents
"name": "transfer_to_outstanding_payment"
},
"id": "call_7jGpwpVvzhZFOyRgxHFkdOdU",
"type": "function"
}
]
},
{
"tool_name": "transfer_to_outstanding_payment",
"role": "tool",
"content": "{\"assistant\": \"Outstanding Payment\"}",
"tool_call_id": "call_7jGpwpVvzhZFOyRgxHFkdOdU"
},
{
"sender": "Outstanding Payment",
"role": "assistant",
// Response is not user-facing, to enable further post processing
"response_type": "internal",
"content": "Sure, could you provide the last four digits of your card or your registered mobile number so I can look up your pending payment amount?",
"created_at": "2025-02-01T06:55:49.648008",
"current_turn": true
},
{
"sender": "Outstanding Payment >> Post process",
"role": "assistant",
// Response is user-facing
"response_type": "external",
"content": "Sure, please provide the last four digits of your card or your registered mobile number so I can check your pending payment amount.",
"created_at": "2025-02-01T06:55:49.648008",
"current_turn": true
}
],
"state": {
// .. state data
}
}
```
### Testing the Python Chat SDK
- For the open source installation, the `<HOST>` is [http://localhost:3000](http://localhost:3000)
- When using the hosted app, the `<HOST>` is [https://app.rowboatlabs.com](https://app.rowboatlabs.com)
- `<PROJECT_ID>` is available in the URL of the project page
- API Key can be generated from the project config page at `<HOST>/projects/<PROJECT_ID>/config`
```bash
pip install rowboat
```
Modify the user message in `messages`, based on your example project.
```python
from rowboat import Client
client = Client(
host="<HOST>",
project_id="<PROJECT_ID>",
api_key="<API_KEY>" # Generate this from /projects/<PROJECT_ID>/config
)
# Simple chat interaction
messages = [{"role": "user", "content": "What is my pending payment amount?"}]
response_messages, state = client.chat(messages=messages)
```
The last message in `response_messages` is either a user-facing response or a tool call by the assistant.

View file

@ -1,6 +0,0 @@
# Tools
- Tools are used to carry out specific tasks such as fetching or updating information.
- Tools can be defined once in RowBoat Studio and reused across different agents.
- RowBoat uses OpenAI style tools with name, description and parameters.
- For the purposes of quick testing in the Playground, RowBoat Studio can mock tool responses based on tool descriptions.
- Developers can easily connect tools to APIs by configuring MCP servers or Webhook URL in Settings.

View file

@ -1,6 +0,0 @@
# Tools
- Tools are used to carry out specific tasks such as fetching or updating information.
- Tools can be defined once in RowBoat Studio and reused across different agents.
- RowBoat uses OpenAI style tools with name, description and parameters.
- For the purposes of quick testing in the Playground, RowBoat Studio can mock tool responses based on tool descriptions.
- Developers can easily connect tools to APIs by configuring MCP servers or Webhook URL in Settings.

View file

@ -1,19 +0,0 @@
## Update agent behavior
There are three ways for you to update the agent's behavior:
### 1. With help of Copilot
Copilot can help you update agent behavior. It is also aware of the current chat in the playground so you can make references to the current chat while instructing copilot to update agents.
![Update Agent Behavior](img/update-agent-copilot.png)
### 2. Using the Generate button
![Update Agent Behavior](img/update-agent-generate.png)
### 3. By manually editing the instructions
You can manually edit the agent instructions anytime.
![Update Agent Behavior](img/update-agent-manual.png)

View file

@ -1,19 +0,0 @@
## Update agent behavior
There are three ways for you to update the agent's behavior:
### 1. With help of Copilot
Copilot can help you update agent behavior. It is also aware of the current chat in the playground so you can make references to the current chat while instructing copilot to update agents.
![Update Agent Behavior](img/update-agent-copilot.png)
### 2. Using the Generate button
![Update Agent Behavior](img/update-agent-generate.png)
### 3. By manually editing the instructions
You can manually edit the agent instructions anytime.
![Update Agent Behavior](img/update-agent-manual.png)

View file

@ -1,103 +0,0 @@
# Using RAG in Rowboat
Rowboat provides multiple ways to enhance your agents' context with Retrieval-Augmented Generation (RAG). This guide will help you set up and use each RAG features.
## Quick Start
Text RAG and local file uploads are enabled by default - no configuration needed! Just start using them right away.
## RAG Features
### 1. Text RAG
✅ Enabled by default:
- Process and reason over text content directly
- No configuration required
### 2. Local File Uploads
✅ Enabled by default:
- Upload PDF files directly from your device
- Files are stored locally
- No configuration required
- Files are parsed using OpenAI by default
- For larger files, we recommend using Gemini models - see section below.
#### 2.1 Using Gemini for File Parsing
To use Google's Gemini model for parsing uploaded PDFs, set the following variable:
```bash
# Enable Gemini for file parsing
export USE_GEMINI_FILE_PARSING=true
export GOOGLE_API_KEY=your_google_api_key
```
### 3. URL Scraping
Rowboat uses Firecrawl for URL scraping. To enable URL scraping, set the following variables:
```bash
export USE_RAG_SCRAPING=true
export FIRECRAWL_API_KEY=your_firecrawl_api_key
```
## Advanced RAG features
### 1. File Uploads Backed by S3
To enable S3 file uploads, set the following variables:
```bash
# Enable S3 uploads
export USE_RAG_S3_UPLOADS=true
# S3 Configuration
export AWS_ACCESS_KEY_ID=your_access_key
export AWS_SECRET_ACCESS_KEY=your_secret_key
export RAG_UPLOADS_S3_BUCKET=your_bucket_name
export RAG_UPLOADS_S3_REGION=your_region
```
### 2. Changing Default Parsing Model
By default, uploaded PDF files are parsed using `gpt-4o`. You can customize this by setting the following:
```bash
# Override the default parsing model
export FILE_PARSING_MODEL=your-preferred-model
```
You can also change the model provider like so:
```bash
# Optional: Override the parsing provider settings
export FILE_PARSING_PROVIDER_BASE_URL=your-provider-base-url
export FILE_PARSING_PROVIDER_API_KEY=your-provider-api-key
```
### 3. Embedding Model Options
By default, Rowboat uses OpenAI's `text-embedding-3-small` model for generating embeddings. You can customize this by setting the following:
```bash
# Override the default embedding model
export EMBEDDING_MODEL=your-preferred-model
export EMBEDDING_VECTOR_SIZE=1536
```
**Important NOTE**
The default size for the vectors index is 1536. If you change this value, then you must delete the index and set it up again:
```bash
docker-compose --profile delete_qdrant --profile qdrant up --build delete_qdrant qdrant
```
followed by:
```bash
./start # this will recreate the index
```
You can also change the model provider like so:
```bash
# Optional: Override the embedding provider settings
export EMBEDDING_PROVIDER_BASE_URL=your-provider-base-url
export EMBEDDING_PROVIDER_API_KEY=your-provider-api-key
```
If you don't specify the provider settings, Rowboat will use OpenAI as the default provider.

View file

@ -1,103 +0,0 @@
# Using RAG in Rowboat
Rowboat provides multiple ways to enhance your agents' context with Retrieval-Augmented Generation (RAG). This guide will help you set up and use each RAG features.
## Quick Start
Text RAG and local file uploads are enabled by default - no configuration needed! Just start using them right away.
## RAG Features
### 1. Text RAG
✅ Enabled by default:
- Process and reason over text content directly
- No configuration required
### 2. Local File Uploads
✅ Enabled by default:
- Upload PDF files directly from your device
- Files are stored locally
- No configuration required
- Files are parsed using OpenAI by default
- For larger files, we recommend using Gemini models - see section below.
#### 2.1 Using Gemini for File Parsing
To use Google's Gemini model for parsing uploaded PDFs, set the following variable:
```bash
# Enable Gemini for file parsing
export USE_GEMINI_FILE_PARSING=true
export GOOGLE_API_KEY=your_google_api_key
```
### 3. URL Scraping
Rowboat uses Firecrawl for URL scraping. To enable URL scraping, set the following variables:
```bash
export USE_RAG_SCRAPING=true
export FIRECRAWL_API_KEY=your_firecrawl_api_key
```
## Advanced RAG features
### 1. File Uploads Backed by S3
To enable S3 file uploads, set the following variables:
```bash
# Enable S3 uploads
export USE_RAG_S3_UPLOADS=true
# S3 Configuration
export AWS_ACCESS_KEY_ID=your_access_key
export AWS_SECRET_ACCESS_KEY=your_secret_key
export RAG_UPLOADS_S3_BUCKET=your_bucket_name
export RAG_UPLOADS_S3_REGION=your_region
```
### 2. Changing Default Parsing Model
By default, uploaded PDF files are parsed using `gpt-4o`. You can customize this by setting the following:
```bash
# Override the default parsing model
export FILE_PARSING_MODEL=your-preferred-model
```
You can also change the model provider like so:
```bash
# Optional: Override the parsing provider settings
export FILE_PARSING_PROVIDER_BASE_URL=your-provider-base-url
export FILE_PARSING_PROVIDER_API_KEY=your-provider-api-key
```
### 3. Embedding Model Options
By default, Rowboat uses OpenAI's `text-embedding-3-small` model for generating embeddings. You can customize this by setting the following:
```bash
# Override the default embedding model
export EMBEDDING_MODEL=your-preferred-model
export EMBEDDING_VECTOR_SIZE=1536
```
**Important NOTE**
The default size for the vectors index is 1536. If you change this value, then you must delete the index and set it up again:
```bash
docker-compose --profile delete_qdrant --profile qdrant up --build delete_qdrant qdrant
```
followed by:
```bash
./start # this will recreate the index
```
You can also change the model provider like so:
```bash
# Optional: Override the embedding provider settings
export EMBEDDING_PROVIDER_BASE_URL=your-provider-base-url
export EMBEDDING_PROVIDER_API_KEY=your-provider-api-key
```
If you don't specify the provider settings, Rowboat will use OpenAI as the default provider.

View file

@ -1,166 +0,0 @@
# Using the API
This is a guide on using the HTTP API to power conversations with the assistant created in Studio.
## Deploy your assistant to production on Studio
![Prod Deploy](img/prod-deploy.png)
## Obtain API key and Project ID
Generate API keys via the developer configs in your project. Copy the Project ID from the same page.
![Developer Configs](img/dev-config.png)
## API Endpoint
```
POST <HOST>/api/v1/<PROJECT_ID>/chat
```
Where:
- For self-hosted: `<HOST>` is `http://localhost:3000`
## Authentication
Include your API key in the Authorization header:
```
Authorization: Bearer <API_KEY>
```
## Examples
### First Turn
```bash
curl --location '<HOST>/api/v1/<PROJECT_ID>/chat' \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer <API_KEY>' \
--data '{
"messages": [
{
"role": "user",
"content": "Hello, can you help me?"
}
],
"state": null
}'
```
Response:
```json
{
"messages": [
{
"role": "assistant",
"content": "Hello! Yes, I'd be happy to help you. What can I assist you with today?",
"agenticResponseType": "external"
}
],
"state": {
"last_agent_name": "MainAgent"
}
}
```
### Subsequent Turn
Notice how we include both the previous messages and the state from the last response:
```bash
curl --location '<HOST>/api/v1/<PROJECT_ID>/chat' \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer <API_KEY>' \
--data '{
"messages": [
{
"role": "user",
"content": "Hello, can you help me?"
},
{
"role": "assistant",
"content": "Hello! Yes, I'd be happy to help you. What can I assist you with today?",
"agenticResponseType": "external"
},
{
"role": "user",
"content": "What services do you offer?"
}
],
"state": {
"last_agent_name": "MainAgent"
}
}'
```
## API Specification
### Request Schema
```typescript
{
// Required fields
messages: Message[]; // Array of message objects representing the conversation history
state: any; // State object from previous response, or null for first message
// Optional fields
workflowId?: string; // Specific workflow ID to use (defaults to production workflow)
testProfileId?: string; // Test profile ID for simulation
}
```
### Message Types
Messages can be one of the following types:
1. System Message
```typescript
{
role: "system";
content: string;
}
```
2. User Message
```typescript
{
role: "user";
content: string;
}
```
3. Assistant Message
```typescript
{
role: "assistant";
content: string;
agenticResponseType: "internal" | "external";
agenticSender?: string | null;
}
```
### Response Schema
```typescript
{
messages: Message[]; // Array of new messages from this turn
state: any; // State object to pass in the next request
}
```
## Important Notes
1. Always pass the complete conversation history in the `messages` array
2. Always include the `state` from the previous response in your next request
3. The last message in the response's `messages` array will be a user-facing assistant message (`agenticResponseType: "external"`)
## Rate Limiting
The API has rate limits per project. If exceeded, you'll receive a 429 status code.
## Error Responses
- 400: Invalid request body or missing/invalid Authorization header
- 403: Invalid API key
- 404: Project or workflow not found
- 429: Rate limit exceeded

View file

@ -1,99 +0,0 @@
# Using the Python SDK
This is a guide on using the RowBoat Python SDK as an alternative to the [RowBoat HTTP API](/using_the_api) to power conversations with the assistant created in Studio.
## Prerequisites
- ``` pip install rowboat ```
- [Deploy your assistant to production](/using_the_api/#deploy-your-assistant-to-production-on-studio)
- [Obtain your `<API_KEY>` and `<PROJECT_ID>`](/using_the_api/#obtain-api-key-and-project-id)
### API Host
- For the open source installation, the `<HOST>` is [http://localhost:3000](http://localhost:3000)
- When using the hosted app, the `<HOST>` is [https://app.rowboatlabs.com](https://app.rowboatlabs.com)
## Usage
### Basic Usage with StatefulChat
The easiest way to interact with Rowboat is using the `StatefulChat` class, which maintains conversation state automatically:
```python
from rowboat import Client, StatefulChat
# Initialize the client
client = Client(
host="<HOST>",
project_id="<PROJECT_ID>",
api_key="<API_KEY>"
)
# Create a stateful chat session
chat = StatefulChat(client)
# Have a conversation
response = chat.run("What is the capital of France?")
print(response)
# The capital of France is Paris.
# Continue the conversation - the context is maintained automatically
response = chat.run("What other major cities are in that country?")
print(response)
# Other major cities in France include Lyon, Marseille, Toulouse, and Nice.
response = chat.run("What's the population of the first city you mentioned?")
print(response)
# Lyon has a population of approximately 513,000 in the city proper.
```
### Advanced Usage
#### Using a specific workflow
You can specify a workflow ID to use a particular conversation configuration:
```python
chat = StatefulChat(
client,
workflow_id="<WORKFLOW_ID>"
)
```
#### Using a test profile
You can specify a test profile ID to use a specific test configuration:
```python
chat = StatefulChat(
client,
test_profile_id="<TEST_PROFILE_ID>"
)
```
### Low-Level Usage
For more control over the conversation, you can use the `Client` class directly:
```python
from rowboat.schema import UserMessage
# Initialize the client
client = Client(
host="<HOST>",
project_id="<PROJECT_ID>",
api_key="<API_KEY>"
)
# Create messages
messages = [
UserMessage(role='user', content="Hello, how are you?")
]
# Get response
response = client.chat(messages=messages)
print(response.messages[-1].content)
# For subsequent messages, you need to manage the message history and state manually
messages.extend(response.messages)
messages.append(UserMessage(role='user', content="What's your name?"))
response = client.chat(messages=messages, state=response.state)
```

View file

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Before After
Before After

View file

@ -1,26 +0,0 @@
site_name: RowBoat docs
site_url: https://docs.rowboatlabs.com
theme:
name: material
favicon: img/favicon.ico
nav:
- Getting Started: setup.md
- Overview:
- Introduction: index.md
- Open Source License: license.md
- Building in Studio:
- Create agents: create_agents.md
- Test chats in the playground: playground.md
- Add tools: add_tools.md
- Update agents: update_agents.md
- Using RAG: using_rag.md
- API & SDK:
- Using the API: using_the_api.md
- Using the SDK: using_the_sdk.md
- Concepts:
- Agents: agents.md
- Tools: tools.md
- Prompts: prompts.md

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 MiB

View file

@ -1,55 +0,0 @@
# Documentation Site
This documentation site is built using [MkDocs Material](https://squidfunk.github.io/mkdocs-material/), a modern documentation framework that creates beautiful and functional static sites.
## Prerequisites
- Python 3.x
- pip (Python package manager)
## Setup
Install the required dependencies:
```bash
pip install -r requirements.txt
```
## Development
To run the documentation site locally:
```bash
mkdocs serve
```
This will start a local server, and you can view the documentation site at `http://localhost:8000`.
### Building the static site
To build the static site:
```bash
mkdocs build
```
This will generate the static site in the `site` directory.
### Project structure
- `mkdocs.yml`: The main configuration file for MkDocs.
- `docs/`: The directory containing the Markdown files for the documentation.
- `site/`: The directory generated by the `mkdocs build` command, containing the static site files.
## Writing Documentation
- Documentation files are written in Markdown format
- Place new documentation files in the `docs` directory
- Update `mkdocs.yml` to include new pages in the navigation
## Additional Resources
- [Mkdocs documentation](https://www.mkdocs.org/getting-started/)
- [MkDocs Material Documentation](https://squidfunk.github.io/mkdocs-material/getting-started/)
- [Markdown Guide](https://www.markdownguide.org/basic-syntax/)

View file

@ -1,29 +0,0 @@
babel==2.16.0
certifi==2024.12.14
charset-normalizer==3.4.1
click==8.1.8
colorama==0.4.6
ghp-import==2.1.0
idna==3.10
Jinja2==3.1.5
Markdown==3.7
MarkupSafe==3.0.2
mergedeep==1.3.4
mkdocs==1.6.1
mkdocs-get-deps==0.2.0
mkdocs-material==9.5.50
mkdocs-material-extensions==1.3.1
packaging==24.2
paginate==0.5.7
pathspec==0.12.1
platformdirs==4.3.6
Pygments==2.19.1
pymdown-extensions==10.14.1
python-dateutil==2.9.0.post0
PyYAML==6.0.2
pyyaml_env_tag==0.1
regex==2024.11.6
requests==2.32.3
six==1.17.0
urllib3==2.3.0
watchdog==6.0.0

View file

@ -0,0 +1,49 @@
import { NextRequest, NextResponse } from 'next/server';
import { S3Client, GetObjectCommand } from '@aws-sdk/client-s3';
import { Readable } from 'stream';
export async function GET(request: NextRequest, props: { params: Promise<{ path: string[] }> }) {
const params = await props.params;
const path = params.path || [];
if (path.length < 3) {
return NextResponse.json({ error: 'Invalid path' }, { status: 400 });
}
const bucket = process.env.RAG_UPLOADS_S3_BUCKET || '';
if (!bucket) {
return NextResponse.json({ error: 'S3 bucket not configured' }, { status: 500 });
}
const region = process.env.RAG_UPLOADS_S3_REGION || 'us-east-1';
const s3 = new S3Client({
region,
credentials: process.env.AWS_ACCESS_KEY_ID && process.env.AWS_SECRET_ACCESS_KEY ? {
accessKeyId: process.env.AWS_ACCESS_KEY_ID,
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
} as any : undefined,
});
const filename = path[path.length - 1];
const key = `generated_images/${path.join('/')}`;
try {
const resp = await s3.send(new GetObjectCommand({ Bucket: bucket, Key: key }));
const contentType = resp.ContentType || 'application/octet-stream';
const body = resp.Body as any;
const webStream = body?.transformToWebStream
? body.transformToWebStream()
: (Readable as any)?.toWeb
? (Readable as any).toWeb(body)
: body;
return new NextResponse(webStream, {
status: 200,
headers: {
'Content-Type': contentType,
'Cache-Control': 'public, max-age=31536000, immutable',
'Content-Disposition': `inline; filename="${filename}"`,
},
});
} catch (e) {
console.error('S3 get error', e);
return NextResponse.json({ error: 'Not found' }, { status: 404 });
}
}

View file

@ -0,0 +1,101 @@
import { NextRequest, NextResponse } from 'next/server';
import { z } from 'zod';
import { Workflow } from '@/app/lib/types/workflow_types';
import { nanoid } from 'nanoid';
import { db } from '@/app/lib/mongodb';
import { SHARED_WORKFLOWS_COLLECTION } from '@/src/infrastructure/repositories/mongodb.shared-workflows.indexes';
const DEFAULT_TTL_SECONDS = 60 * 60 * 24; // 24 hours
interface SharedWorkflowDoc {
_id: string;
workflow: unknown;
createdAt: Date;
expiresAt: Date;
}
function validateWorkflowJson(obj: unknown) {
const parsed = Workflow.safeParse(obj);
if (!parsed.success) {
const message = parsed.error.issues.map(i => `${i.path.join('.')}: ${i.message}`).join('; ');
throw new Error(`Invalid workflow JSON: ${message}`);
}
return parsed.data;
}
export async function GET(req: NextRequest) {
try {
const { searchParams } = new URL(req.url);
const id = searchParams.get('id');
const url = searchParams.get('url');
if (id) {
const coll = db.collection<SharedWorkflowDoc>(SHARED_WORKFLOWS_COLLECTION);
const doc = await coll.findOne(
{ _id: id },
{ projection: { workflow: 1, expiresAt: 1 } }
);
if (!doc) {
return NextResponse.json({ error: 'Not found or expired' }, { status: 404 });
}
// Optional safeguard if TTL not yet cleaned up
if (doc.expiresAt && doc.expiresAt.getTime() <= Date.now()) {
return NextResponse.json({ error: 'Not found or expired' }, { status: 404 });
}
return NextResponse.json(doc.workflow);
}
if (!url) {
return NextResponse.json({ error: 'Missing "id" or "url" query param' }, { status: 400 });
}
if (url.startsWith('blob:')) {
return NextResponse.json({ error: 'Blob URLs are not accessible from the server. Use POST /api/shared-workflow to upload the workflow and share its id.' }, { status: 400 });
}
const isHttp = url.startsWith('http://') || url.startsWith('https://');
if (!isHttp) {
return NextResponse.json({ error: 'Only http(s) URLs are supported in the "url" param' }, { status: 400 });
}
const resp = await fetch(url, { cache: 'no-store' });
if (!resp.ok) {
return NextResponse.json({ error: `Failed to fetch URL: ${resp.status} ${resp.statusText}` }, { status: 400 });
}
const text = await resp.text();
const obj = JSON.parse(text);
const workflow = validateWorkflowJson(obj);
return NextResponse.json(workflow);
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
return NextResponse.json({ error: message }, { status: 400 });
}
}
export async function POST(req: NextRequest) {
try {
const contentType = req.headers.get('content-type') || '';
let body: any;
if (contentType.includes('application/json')) {
body = await req.json();
} else {
const text = await req.text();
body = JSON.parse(text);
}
const workflowCandidate = typeof body?.workflow === 'object' ? body.workflow : body;
const workflow = validateWorkflowJson(workflowCandidate);
const id = nanoid();
const coll = db.collection<SharedWorkflowDoc>(SHARED_WORKFLOWS_COLLECTION);
const now = new Date();
const expiresAt = new Date(now.getTime() + DEFAULT_TTL_SECONDS * 1000);
await coll.insertOne({ _id: id, workflow, createdAt: now, expiresAt });
const origin = new URL(req.url).origin;
const href = `${origin}/api/shared-workflow?id=${id}`;
return NextResponse.json({ id, href, ttlSeconds: DEFAULT_TTL_SECONDS });
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
return NextResponse.json({ error: message }, { status: 400 });
}
}

View file

@ -2,5 +2,6 @@ import { NextResponse } from 'next/server';
import { templates } from '@/app/lib/project_templates';
export async function GET() {
// The templates are now dynamically loaded from JSON files in the templates folder
return NextResponse.json(templates);
}

View file

@ -0,0 +1,25 @@
import { NextRequest, NextResponse } from 'next/server';
import { tempBinaryCache } from '@/src/application/services/temp-binary-cache';
export async function GET(request: NextRequest, props: { params: Promise<{ id: string }> }) {
const params = await props.params;
const id = params.id;
if (!id) {
return NextResponse.json({ error: 'Missing id' }, { status: 400 });
}
// Serve from in-memory temp cache
const entry = tempBinaryCache.get(id);
if (!entry) {
return NextResponse.json({ error: 'Not found or expired' }, { status: 404 });
}
return new NextResponse(entry.buf, {
status: 200,
headers: {
'Content-Type': entry.mimeType || 'application/octet-stream',
'Cache-Control': 'no-store',
'Content-Disposition': `inline; filename="${id}"`,
},
});
}

View file

@ -2,7 +2,8 @@
import { forwardRef, TextareaHTMLAttributes } from 'react';
import { Textarea } from '@/components/ui/textarea';
import { Send } from 'lucide-react';
import { Send, Plus } from 'lucide-react';
import { Dropdown, DropdownItem, DropdownMenu, DropdownTrigger } from '@heroui/react';
import clsx from 'clsx';
interface TextareaWithSendProps extends Omit<TextareaHTMLAttributes<HTMLTextAreaElement>, 'onChange'> {
@ -11,6 +12,9 @@ interface TextareaWithSendProps extends Omit<TextareaHTMLAttributes<HTMLTextArea
onSubmit: () => void;
isSubmitting?: boolean;
submitDisabled?: boolean;
onImportJson?: () => void;
importDisabled?: boolean;
isImporting?: boolean;
placeholder?: string;
className?: string;
rows?: number;
@ -25,6 +29,9 @@ export const TextareaWithSend = forwardRef<HTMLTextAreaElement, TextareaWithSend
onSubmit,
isSubmitting = false,
submitDisabled = false,
onImportJson,
importDisabled = false,
isImporting = false,
placeholder,
className,
rows = 3,
@ -32,6 +39,7 @@ export const TextareaWithSend = forwardRef<HTMLTextAreaElement, TextareaWithSend
autoResize = false,
...props
}, ref) => {
const hasMore = Boolean(onImportJson);
return (
<div className="relative">
<Textarea
@ -39,7 +47,11 @@ export const TextareaWithSend = forwardRef<HTMLTextAreaElement, TextareaWithSend
value={value}
onChange={(e) => onChange(e.target.value)}
placeholder={placeholder}
className={clsx("pr-14", className)}
className={clsx(
// Extra right padding for kebab + send controls
hasMore ? "pr-24" : "pr-14",
className
)}
rows={rows}
autoFocus={autoFocus}
autoResize={autoResize}
@ -51,7 +63,35 @@ export const TextareaWithSend = forwardRef<HTMLTextAreaElement, TextareaWithSend
}}
{...props}
/>
<div className="absolute right-3 bottom-3">
<div className="absolute right-3 bottom-3 flex items-center gap-2">
{hasMore && (
<Dropdown>
<DropdownTrigger>
<button
className={clsx(
"rounded-full p-2 transition-all duration-200",
"bg-gray-100 dark:bg-gray-800 text-gray-500 dark:text-gray-400 hover:scale-105 active:scale-95 hover:bg-gray-200 dark:hover:bg-gray-700"
)}
aria-label="Add"
title="Add"
>
<Plus size={18} />
</button>
</DropdownTrigger>
<DropdownMenu
aria-label="More actions"
onAction={(key) => {
if (key === 'import-json' && onImportJson) {
onImportJson();
}
}}
>
<DropdownItem key="import-json" isDisabled={importDisabled || isImporting}>
{isImporting ? 'Importing Assistant (JSON)…' : 'Import Assistant (JSON)'}
</DropdownItem>
</DropdownMenu>
</Dropdown>
)}
<button
onClick={onSubmit}
disabled={isSubmitting || submitDisabled || !value.trim()}
@ -62,6 +102,8 @@ export const TextareaWithSend = forwardRef<HTMLTextAreaElement, TextareaWithSend
: "bg-gray-100 dark:bg-gray-800 text-gray-400 dark:text-gray-500",
isSubmitting ? "opacity-50" : "hover:scale-105 active:scale-95"
)}
aria-label="Send"
title="Send"
>
{isSubmitting ? (
<div className="animate-spin rounded-full h-5 w-5 border-b-2 border-current"></div>
@ -75,4 +117,4 @@ export const TextareaWithSend = forwardRef<HTMLTextAreaElement, TextareaWithSend
}
);
TextareaWithSend.displayName = 'TextareaWithSend';
TextareaWithSend.displayName = 'TextareaWithSend';

View file

@ -105,8 +105,6 @@ export default function MarkdownContent({
return (
<span
className="inline-block bg-[#e0f2fe] text-[red] px-1.5 py-0.5 rounded whitespace-nowrap cursor-pointer"
onClick={handleMentionClick}
title={onMentionNavigate ? 'Click to open' : undefined}
>
{displayLabel} (!)
</span>

View file

@ -29,7 +29,7 @@ Quill.register('modules/mention', Mention);
function markdownToParts(markdown: string, atValues: Match[]): (string | Match)[] {
// Regex match for pattern [@type:name](#type:something) where type is tool/prompt/agent
const mentionRegex = /\[@(tool|prompt|agent):([^\]]+)\]\(#mention\)/g;
const mentionRegex = /\[@(tool|prompt|agent|variable):([^\]]+)\]\(#mention\)/g;
const parts: (string | Match)[] = [];
let lastIndex = 0;

View file

@ -0,0 +1,354 @@
{
"category": "Work Productivity",
"agents": [
{
"name": "Meeting Prep Hub",
"type": "conversation",
"description": "Hub agent to orchestrate meeting guest research and email delivery.",
"instructions": "## 🧑‍💼 Role:\nYou are the hub agent responsible for orchestrating the process of researching meeting guests and sending a summary to the user via email.\n\n---\n## ⚙️ Steps to Follow:\n1. Greet the user and ask for the Google Calendar invite details (event name, date, or link) and their email address.\n2. Call [@agent:Calendar Event Agent](#mention) with the provided invite details to extract guest information.\n3. Wait for the guest list from Calendar Event Agent.\n4. Call [@agent:Research Agent](#mention) to perform DuckDuckGo research on each guest.\n5. Wait for the research summary from Research Agent.\n6. Call [@agent:Email Agent](#mention) to send the summary to the user's email.\n7. Inform the user when the research summary has been sent.\n\n---\n## 🎯 Scope:\n✅ In Scope:\n- Orchestrating the workflow for meeting guest research and email delivery.\n\n❌ Out of Scope:\n- Directly researching guests or sending emails (handled by sub-agents).\n\n---\n## 📋 Guidelines:\n✔ Dos:\n- Always confirm the invite details and email address with the user.\n- Ensure all steps are completed in sequence.\n\n🚫 Don'ts:\n- Do not perform research or send emails directly.\n- Do not skip any step in the workflow.\n- Do not mention internal agent names to the user.\n- Do not say 'connecting you to another agent'.\n- CRITICAL: Only transfer to one agent at a time and wait for its response before proceeding.\n\n# Examples\n- **User** : I have a meeting invite for 'Q3 Planning' on July 10. My email is user@email.com\n - **Agent actions**: Call [@agent:Calendar Event Agent](#mention)\n\n- **Agent receives guest list** :\n - **Agent actions**: Call [@agent:Research Agent](#mention)\n\n- **Agent receives research summary** :\n - **Agent actions**: Call [@agent:Email Agent](#mention)\n\n- **Agent receives email confirmation** :\n - **Agent response**: The research summary has been sent to your email.",
"examples": "- **User** : I have a meeting invite for 'Q3 Planning' on July 10. My email is user@email.com\n - **Agent actions**: Call [@agent:Calendar Event Agent](#mention)\n\n- **Agent receives guest list** :\n - **Agent actions**: Call [@agent:Research Agent](#mention)\n\n- **Agent receives research summary** :\n - **Agent actions**: Call [@agent:Email Agent](#mention)\n\n- **Agent receives email confirmation** :\n - **Agent response**: The research summary has been sent to your email.",
"model": "google/gemini-2.5-flash",
"toggleAble": true,
"ragReturnType": "chunks",
"ragK": 3,
"outputVisibility": "user_facing",
"controlType": "retain"
},
{
"name": "Calendar Event Agent",
"type": "conversation",
"description": "Extracts guest details from a provided Google Calendar invite.",
"disabled": false,
"instructions": "## 🧑‍💼 Role:\nExtract guest (attendee) details from the provided Google Calendar invite information.\n\n---\n## ⚙️ Steps to Follow:\n1. Receive the event name, date, or link from the parent agent.\n2. Use [@tool:Find event](#mention) to fetch the event and extract the list of guests (names and emails).\n3. Return the guest list to the parent agent.\n\n---\n## 🎯 Scope:\n✅ In Scope:\n- Fetching event details and extracting guest information.\n\n❌ Out of Scope:\n- Researching guests.\n- Sending emails.\n\n---\n## 📋 Guidelines:\n✔ Dos:\n- Return all relevant guest details.\n\n🚫 Don'ts:\n- Do not perform research or send emails.\n- Do not interact with the user directly.",
"examples": "- **Parent agent** : Get guests for 'Q3 Planning' on July 10.\n - **Agent actions**: Call [@tool:Find event](#mention)\n - **Agent response**: [List of guests with names and emails]",
"model": "google/gemini-2.5-flash",
"locked": false,
"toggleAble": true,
"ragReturnType": "chunks",
"ragK": 3,
"outputVisibility": "internal",
"controlType": "relinquish_to_parent",
"maxCallsPerParentAgent": 3
},
{
"name": "Research Agent",
"type": "conversation",
"description": "Performs DuckDuckGo searches on each guest and summarizes the findings.",
"disabled": false,
"instructions": "## 🧑‍💼 Role:\nResearch each guest using DuckDuckGo and summarize the findings.\n\n---\n## ⚙️ Steps to Follow:\n1. Receive a list of guest names and emails from the parent agent.\n2. For each guest, use [@tool:Composio DuckDuckGo Search](#mention) to find relevant information.\n3. Summarize the findings for each guest (role, company, notable info).\n4. Return the research summary to the parent agent.\n\n---\n## 🎯 Scope:\n✅ In Scope:\n- Researching guests using DuckDuckGo.\n\n❌ Out of Scope:\n- Fetching event details.\n- Sending emails.\n\n---\n## 📋 Guidelines:\n✔ Dos:\n- Provide concise, relevant guest profiles.\n\n🚫 Don'ts:\n- Do not fabricate information.\n- Do not interact with the user directly.",
"examples": "- **Parent agent** : Research guests: Alice Smith (alice@email.com), Bob Lee (bob@email.com)\n - **Agent actions**: Call [@tool:Composio DuckDuckGo Search](#mention) for each guest\n - **Agent response**: Alice Smith: [summary], Bob Lee: [summary]",
"model": "google/gemini-2.5-flash",
"locked": false,
"toggleAble": true,
"ragReturnType": "chunks",
"ragK": 3,
"outputVisibility": "internal",
"controlType": "relinquish_to_parent",
"maxCallsPerParentAgent": 3
},
{
"name": "Email Agent",
"type": "conversation",
"description": "Sends the research summary to the user's email address.",
"disabled": false,
"instructions": "## 🧑‍💼 Role:\nSend the provided research summary to the user's email address.\n\n---\n## ⚙️ Steps to Follow:\n1. Receive the research summary and recipient email from the parent agent.\n2. Use [@tool:Send Email](#mention) to send the summary.\n3. Confirm delivery to the parent agent.\n\n---\n## 🎯 Scope:\n✅ In Scope:\n- Sending research summaries via email.\n\n❌ Out of Scope:\n- Fetching event details.\n- Researching guests.\n\n---\n## 📋 Guidelines:\n✔ Dos:\n- Ensure the summary is sent to the correct email.\n\n🚫 Don'ts:\n- Do not interact with the user directly.",
"examples": "- **Parent agent** : Send summary to user@email.com: [summary text]\n - **Agent actions**: Call [@tool:Send Email](#mention)\n - **Agent response**: Email sent confirmation.",
"model": "google/gemini-2.5-flash",
"locked": false,
"toggleAble": true,
"ragReturnType": "chunks",
"ragK": 3,
"outputVisibility": "internal",
"controlType": "relinquish_to_parent",
"maxCallsPerParentAgent": 3
}
],
"prompts": [],
"tools": [
{
"name": "Find event",
"description": "Finds events in a specified google calendar using text query, time ranges, and event types.",
"mockTool": false,
"parameters": {
"type": "object",
"properties": {
"calendar_id": {
"default": "primary",
"description": "Identifier of the Google Calendar to query.",
"examples": [
"primary",
"user@example.com",
"abc...@group.calendar.google.com"
],
"title": "Calendar Id",
"type": "string"
},
"event_types": {
"default": [
"birthday",
"default",
"focusTime",
"outOfOffice",
"workingLocation"
],
"description": "Event types to include.",
"examples": [
"default",
"focusTime",
"outOfOffice"
],
"items": {
"enum": [
"birthday",
"default",
"focusTime",
"outOfOffice",
"workingLocation"
],
"type": "string"
},
"title": "Event Types",
"type": "array"
},
"max_results": {
"default": 10,
"description": "Maximum number of events per page (1-2500).",
"title": "Max Results",
"type": "integer"
},
"order_by": {
"default": null,
"description": "Order of events: 'startTime' or 'updated'.",
"examples": [
"startTime",
"updated"
],
"nullable": true,
"title": "Order By",
"type": "string"
},
"page_token": {
"default": null,
"description": "Token for pagination.",
"nullable": true,
"title": "Page Token",
"type": "string"
},
"query": {
"default": null,
"description": "Free-text search terms to find events.",
"examples": [
"Project Alpha Review",
"Birthday Party"
],
"nullable": true,
"title": "Query",
"type": "string"
},
"show_deleted": {
"default": null,
"description": "Include deleted events.",
"nullable": true,
"title": "Show Deleted",
"type": "boolean"
},
"single_events": {
"default": true,
"description": "Expand recurring events into individual instances.",
"title": "Single Events",
"type": "boolean"
},
"timeMax": {
"default": null,
"description": "Upper bound for event's start time.",
"examples": [
"2024-12-31T23:59:59Z"
],
"nullable": true,
"title": "Time Max",
"type": "string"
},
"timeMin": {
"default": null,
"description": "Lower bound for event's end time.",
"examples": [
"2024-01-01T00:00:00Z"
],
"nullable": true,
"title": "Time Min",
"type": "string"
},
"updated_min": {
"default": null,
"description": "Lower bound for event's last modification time.",
"examples": [
"2024-07-01T00:00:00Z"
],
"nullable": true,
"title": "Updated Min",
"type": "string"
}
},
"required": []
},
"isComposio": true,
"composioData": {
"slug": "GOOGLECALENDAR_FIND_EVENT",
"noAuth": false,
"toolkitName": "googlecalendar",
"toolkitSlug": "googlecalendar",
"logo": "https://cdn.jsdelivr.net/gh/ComposioHQ/open-logos@master/google-calendar.svg"
}
},
{
"name": "Composio DuckDuckGo Search",
"description": "Performs web searches using DuckDuckGo to retrieve relevant information.",
"mockTool": false,
"parameters": {
"type": "object",
"properties": {
"query": {
"description": "The search query for DuckDuckGo.",
"examples": [
"Python programming"
],
"title": "Query",
"type": "string"
}
},
"required": [
"query"
]
},
"isComposio": true,
"composioData": {
"slug": "COMPOSIO_SEARCH_DUCK_DUCK_GO_SEARCH",
"noAuth": true,
"toolkitName": "composio_search",
"toolkitSlug": "composio_search",
"logo": "https://cdn.jsdelivr.net/gh/ComposioHQ/open-logos@master//composio-logo.png"
}
},
{
"name": "Send Email",
"description": "Sends an email via Gmail using the authenticated user's Google profile.",
"mockTool": false,
"parameters": {
"type": "object",
"properties": {
"attachment": {
"additionalProperties": false,
"description": "File to attach; ensure s3key, mimetype, and name are set if provided.",
"file_uploadable": true,
"properties": {
"mimetype": {
"title": "Mimetype",
"type": "string"
},
"name": {
"title": "Name",
"type": "string"
},
"s3key": {
"title": "S3Key",
"type": "string"
}
},
"required": [
"name",
"mimetype",
"s3key"
],
"title": "FileUploadable",
"type": "object"
},
"bcc": {
"default": [],
"description": "BCC recipients' email addresses.",
"items": {
"type": "string"
},
"title": "Bcc",
"type": "array"
},
"body": {
"description": "Email content (plain text or HTML).",
"examples": [
"Hello team, let's discuss the project updates tomorrow."
],
"title": "Body",
"type": "string"
},
"cc": {
"default": [],
"description": "CC recipients' email addresses.",
"items": {
"type": "string"
},
"title": "Cc",
"type": "array"
},
"extra_recipients": {
"default": [],
"description": "Additional 'To' recipients' email addresses.",
"items": {
"type": "string"
},
"title": "Extra Recipients",
"type": "array"
},
"is_html": {
"default": false,
"description": "Set to True if the email body contains HTML tags.",
"title": "Is Html",
"type": "boolean"
},
"recipient_email": {
"description": "Primary recipient's email address.",
"examples": [
"john@doe.com"
],
"title": "Recipient Email",
"type": "string"
},
"subject": {
"default": null,
"description": "Subject line of the email.",
"examples": [
"Project Update Meeting"
],
"nullable": true,
"title": "Subject",
"type": "string"
},
"user_id": {
"default": "me",
"description": "User's email address; 'me' refers to the authenticated user.",
"examples": [
"user@example.com",
"me"
],
"title": "User Id",
"type": "string"
}
},
"required": [
"recipient_email",
"body"
]
},
"isComposio": true,
"composioData": {
"slug": "GMAIL_SEND_EMAIL",
"noAuth": false,
"toolkitName": "gmail",
"toolkitSlug": "gmail",
"logo": "https://cdn.jsdelivr.net/gh/ComposioHQ/open-logos@master/gmail.svg"
}
}
],
"startAgent": "Meeting Prep Hub",
"lastUpdatedAt": "2025-09-07T17:06:05.564Z",
"name": "Meeting Prep",
"description": "Research meeting attendees and send summary to Slack"
}

View file

@ -0,0 +1,44 @@
# Prebuilt Cards Directory
This directory contains JSON files that define prebuilt assistant templates. These templates appear as cards in the "Pre-built Assistants" section of the application.
## How to Add New Prebuilt Cards
1. Create a new JSON file in this directory (e.g., `my-assistant.json`)
2. The filename (without extension) will be used as the template key
3. The JSON file should follow the WorkflowTemplate schema structure
## Required Structure
Each prebuilt card JSON file must have:
- `name`: Display name for the template
- `description`: Brief description of what the template does
- `agents`: Array of agent configurations
- `startAgent`: Name of the starting agent
- `tools`: Array of tool configurations (optional)
- `prompts`: Array of prompt configurations (optional)
- `pipelines`: Array of pipeline configurations (optional)
- `category`: Logical grouping for UI subsections (e.g., `Work Productivity`, `Developer Productivity`)
## Example Prebuilt Cards
See the existing files in this directory:
- `github-data-to-spreadsheet.json` - Fetches GitHub stats and logs to Google Sheets
- `Meeting Prep Assistant.json` - Research meeting attendees and send to Slack
- `interview-scheduler.json` - Automate interview scheduling with Google Sheets/Calendar
## Template Loading
Prebuilt cards are automatically loaded when the application starts. Simply drop a new JSON file here and restart the application to see it appear in the prebuilt assistants section.
## Location
This directory is located at `app/lib/prebuilt-cards/` to keep the template definitions close to the `project_templates.ts` file that loads them.
## Validation
The system validates that each template has:
- A valid `agents` array
- Proper JSON syntax
Invalid templates will be logged as warnings but won't break the application.

View file

@ -0,0 +1,297 @@
{
"agents": [
{
"name": "Reddit Search Agent",
"type": "pipeline",
"description": "Searches Reddit for posts based on a given topic and subreddits.",
"disabled": false,
"instructions": "## 🧑‍💼 Role:\nYou are a pipeline agent responsible for searching Reddit for the latest posts based on given subreddits and a lookback period.\n\n---\n## ⚙️ Steps to Follow:\n1. Receive the `Subreddits` and `LookbackInHours` variables from the parent agent.\n2. Calculate the `time_filter` parameter for the `Search across subreddits` tool based on `LookbackInHours`. For example, if `LookbackInHours` is 24, `time_filter` should be 'day'. If `LookbackInHours` is 1, `time_filter` should be 'hour'. If `LookbackInHours` is 7*24, `time_filter` should be 'week'.\n3. Use the [@tool:Search across subreddits](#mention) tool with the `Subreddits` as `search_query` and `sort` set to 'new', and the calculated `time_filter`.\n4. Return the raw search results to the parent agent.\n\n---\n## 🎯 Scope:\n✅ In Scope:\n- Searching Reddit for posts within a specified time frame.\n\n❌ Out of Scope:\n- Filtering posts by topic.\n- Sending posts to Slack.\n\n---\n## 📋 Guidelines:\n✔ Dos:\n- Ensure the search query includes the subreddits.\n- Accurately calculate and apply the `time_filter`.\n- Return all relevant search results.\n\n🚫 Don'ts:\n- Do not filter posts by topic.\n- Do not send messages to Slack.",
"model": "google/gemini-2.5-flash",
"locked": false,
"toggleAble": true,
"ragReturnType": "chunks",
"ragK": 3,
"outputVisibility": "internal",
"controlType": "relinquish_to_parent",
"maxCallsPerParentAgent": 3
},
{
"name": "Post Filter Agent",
"type": "pipeline",
"description": "Filters Reddit posts based on the Topics",
"disabled": false,
"instructions": "## 🧑‍💼 Role:\nYou are a pipeline agent responsible for filtering Reddit posts based on a specified topics.\n\n---\n## ⚙️ Steps to Follow:\n1. Receive the raw Reddit posts and the `Topic` variable from the parent agent.\n2. Filter the posts to include only those that are on the specified Topics.\n3. Return the filtered posts to the parent agent.\n\n---\n## 🎯 Scope:\n✅ In Scope:\n- Filtering Reddit posts by topic.\n\n❌ Out of Scope:\n- Searching Reddit.\n- Filtering posts by time.\n- Sending posts to Slack.\n\n---\n## 📋 Guidelines:\n✔ Dos:\n- Accurately filter posts based on the provided topic.\n- Return only the posts that meet the topic criteria.\n\n🚫 Don'ts:\n- Do not perform Reddit searches or time-based filtering.\n- Do not send messages to Slack.\n",
"model": "google/gemini-2.5-flash",
"locked": false,
"toggleAble": true,
"ragReturnType": "chunks",
"ragK": 3,
"outputVisibility": "internal",
"controlType": "relinquish_to_parent",
"maxCallsPerParentAgent": 3
},
{
"name": "Slack Post Agent",
"type": "pipeline",
"description": "Formats and sends filtered Reddit posts to a specified Slack channel.",
"disabled": false,
"instructions": "## 🧑‍💼 Role:\nYou are a pipeline agent responsible for formatting and sending filtered Reddit posts to a specified Slack channel.\n\n---\n## ⚙️ Steps to Follow:\n1. Receive the filtered Reddit posts and the `SlackChannel` variable from the parent agent.\n2. Format the posts into a readable message for Slack, including the post title, URL, and a brief summary.\n3. Use the [@tool:Send message](#mention) tool to send the formatted message to the `SlackChannel`.\n4. Return a confirmation message to the parent agent.\n\n---\n## 🎯 Scope:\n✅ In Scope:\n- Formatting Reddit posts for Slack.\n- Sending messages to Slack.\n\n❌ Out of Scope:\n- Searching Reddit.\n- Filtering posts by time.\n\n---\n## 📋 Guidelines:\n✔ Dos:\n- Ensure the Slack message is well-formatted and easy to read.\n- Include all relevant information for each post.\n\n🚫 Don'ts:\n- Do not perform Reddit searches or filtering.",
"model": "google/gemini-2.5-flash",
"locked": false,
"toggleAble": true,
"ragReturnType": "chunks",
"ragK": 3,
"outputVisibility": "internal",
"controlType": "relinquish_to_parent",
"maxCallsPerParentAgent": 3
}
],
"prompts": [
{
"name": "Topics",
"type": "base_prompt",
"prompt": "<needs to be added>"
},
{
"name": "Subreddits",
"type": "base_prompt",
"prompt": "<needs to be added>"
},
{
"name": "SlackChannel",
"type": "base_prompt",
"prompt": "<needs to be added>"
},
{
"name": "LookbackInHours",
"type": "base_prompt",
"prompt": "<needs to be added>"
}
],
"tools": [
{
"name": "Search across subreddits",
"description": "Searches reddit for content (e.g., posts, comments) using a query, with results typically confined to subreddits unless `restrict sr` is set to false.",
"mockTool": false,
"parameters": {
"type": "object",
"properties": {
"limit": {
"default": 5,
"description": "The maximum number of search results to return. Default is 5. Maximum allowed value is 100.",
"examples": [
"5",
"10",
"25"
],
"maximum": 100,
"title": "Limit",
"type": "integer"
},
"restrict_sr": {
"default": true,
"description": "If True (default), confines the search to posts and comments within subreddits. If False, the search scope is broader and may include matching subreddit names or other Reddit entities.",
"examples": [
"True",
"False"
],
"title": "Restrict Sr",
"type": "boolean"
},
"search_query": {
"description": "The search query string used to find content across subreddits.",
"examples": [
"latest AI research",
"funny cat videos",
"python programming tips"
],
"title": "Search Query",
"type": "string"
},
"sort": {
"default": "relevance",
"description": "The criterion for sorting search results. 'relevance' (default) sorts by relevance to the query. 'new' sorts by newest first. 'top' sorts by highest score (typically all-time). 'comments' sorts by the number of comments.",
"enum": [
"relevance",
"new",
"top",
"comments"
],
"examples": [
"relevance",
"new",
"top",
"comments"
],
"title": "Sort",
"type": "string"
}
},
"required": [
"search_query"
]
},
"isComposio": true,
"composioData": {
"slug": "REDDIT_SEARCH_ACROSS_SUBREDDITS",
"noAuth": false,
"toolkitName": "reddit",
"toolkitSlug": "reddit",
"logo": "https://cdn.jsdelivr.net/gh/ComposioHQ/open-logos@master/reddit.svg"
}
},
{
"name": "Send message",
"description": "Posts a message to a slack channel, direct message, or private group; requires content via `text`, `blocks`, or `attachments`.",
"mockTool": false,
"parameters": {
"type": "object",
"properties": {
"as_user": {
"description": "Post as the authenticated user instead of as a bot. Defaults to `false`. If `true`, `username`, `icon_emoji`, and `icon_url` are ignored. If `false`, the message is posted as a bot, allowing appearance customization.",
"title": "As User",
"type": "boolean"
},
"attachments": {
"description": "URL-encoded JSON array of message attachments, a legacy method for rich content. See Slack API documentation for structure.",
"examples": [
"%5B%7B%22fallback%22%3A%20%22Required%20plain-text%20summary%20of%20the%20attachment.%22%2C%20%22color%22%3A%20%22%2336a64f%22%2C%20%22pretext%22%3A%20%22Optional%20text%20that%20appears%20above%20the%20attachment%20block%22%2C%20%22author_name%22%3A%20%22Bobby%20Tables%22%2C%20%22title%22%3A%20%22Slack%20API%20Documentation%22%2C%20%22title_link%22%3A%20%22https%3A%2F%2Fapi.slack.com%2F%22%2C%20%22text%22%3A%20%22Optional%20text%20that%20appears%20within%20the%20attachment%22%7D%5D"
],
"title": "Attachments",
"type": "string"
},
"blocks": {
"description": "DEPRECATED: Use `markdown_text` field instead. URL-encoded JSON array of layout blocks for rich/interactive messages. See Slack API Block Kit docs for structure.",
"examples": [
"%5B%7B%22type%22%3A%20%22section%22%2C%20%22text%22%3A%20%7B%22type%22%3A%20%22mrkdwn%22%2C%20%22text%22%3A%20%22Hello%2C%20world%21%22%7D%7D%5D"
],
"title": "Blocks",
"type": "string"
},
"channel": {
"description": "ID or name of the channel, private group, or IM channel to send the message to.",
"examples": [
"C1234567890",
"general"
],
"title": "Channel",
"type": "string"
},
"icon_emoji": {
"description": "Emoji for bot's icon (e.g., ':robot_face:'). Overrides `icon_url`. Applies if `as_user` is `false`.",
"examples": [
":tada:",
":slack:"
],
"title": "Icon Emoji",
"type": "string"
},
"icon_url": {
"description": "Image URL for bot's icon (must be HTTPS). Applies if `as_user` is `false`.",
"examples": [
"https://slack.com/img/icons/appDir_2019_01/Tonito64.png"
],
"title": "Icon Url",
"type": "string"
},
"link_names": {
"description": "Automatically hyperlink channel names (e.g., #channel) and usernames (e.g., @user) in message text. Defaults to `false` for bot messages.",
"title": "Link Names",
"type": "boolean"
},
"markdown_text": {
"description": "PREFERRED: Write your message in markdown for nicely formatted display. Supports: headers (# ## ###), bold (**text** or __text__), italic (*text* or _text_), strikethrough (~~text~~), inline code (`code`), code blocks (```), links ([text](url)), block quotes (>), lists (- item, 1. item), dividers (--- or ***), context blocks (:::context with images), and section buttons (:::section-button). IMPORTANT: Use \\n for line breaks (e.g., 'Line 1\\nLine 2'), not actual newlines. USER MENTIONS: To tag users, use their user ID with <@USER_ID> format (e.g., <@U1234567890>), not username. ",
"examples": [
"# Status Update\\n\\nSystem is **running smoothly** with *excellent* performance.\\n\\n```bash\\nkubectl get pods\\n```\\n\\n> All services operational ✅",
"## Daily Report\\n\\n- **Deployments**: 5 successful\\n- *Issues*: 0 critical\\n- ~~Maintenance~~: **Completed**\\n\\n---\\n\\n**Next**: Monitor for 24h"
],
"title": "Markdown Text",
"type": "string"
},
"mrkdwn": {
"description": "Disable Slack's markdown for `text` field if `false`. Default `true` (allows *bold*, _italic_, etc.).",
"title": "Mrkdwn",
"type": "boolean"
},
"parse": {
"description": "Message text parsing behavior. Default `none` (no special parsing). `full` parses as user-typed (links @mentions, #channels). See Slack API docs for details.",
"examples": [
"none",
"full"
],
"title": "Parse",
"type": "string"
},
"reply_broadcast": {
"description": "If `true` for a threaded reply, also posts to main channel. Defaults to `false`.",
"title": "Reply Broadcast",
"type": "boolean"
},
"text": {
"description": "DEPRECATED: This sends raw text only, use markdown_text field. Primary textual content. Recommended fallback if using `blocks` or `attachments`. Supports mrkdwn unless `mrkdwn` is `false`.",
"examples": [
"Hello from your friendly bot!",
"Reminder: Team meeting at 3 PM today."
],
"title": "Text",
"type": "string"
},
"thread_ts": {
"description": "Timestamp (`ts`) of an existing message to make this a threaded reply. Use `ts` of the parent message, not another reply. Example: '1476746824.000004'.",
"examples": [
"1618033790.001500"
],
"title": "Thread Ts",
"type": "string"
},
"unfurl_links": {
"description": "Enable unfurling of text-based URLs. Defaults `false` for bots, `true` if `as_user` is `true`.",
"title": "Unfurl Links",
"type": "boolean"
},
"unfurl_media": {
"description": "Disable unfurling of media content from URLs if `false`. Defaults to `true`.",
"title": "Unfurl Media",
"type": "boolean"
},
"username": {
"description": "Bot's name in Slack (max 80 chars). Applies if `as_user` is `false`.",
"examples": [
"MyBot",
"AlertBot"
],
"title": "Username",
"type": "string"
}
},
"required": [
"channel"
]
},
"isComposio": true,
"composioData": {
"slug": "SLACK_SEND_MESSAGE",
"noAuth": false,
"toolkitName": "slack",
"toolkitSlug": "slack",
"logo": "https://cdn.jsdelivr.net/gh/ComposioHQ/open-logos@master/slack.svg"
}
}
],
"pipelines": [
{
"name": "Reddit Post Pipeline",
"description": "Searches Reddit for posts, filters them by a lookback period, and sends them to a Slack channel.",
"agents": [
"Reddit Search Agent",
"Post Filter Agent",
"Slack Post Agent"
]
}
],
"startAgent": "Reddit Post Pipeline",
"lastUpdatedAt": "2025-09-09T17:48:53.292Z",
"name": "Browse Reddit on Slack",
"description": "Browses Reddit for topics of interest and sends them to a Slack channel.",
"category": "News & Social"
}

View file

@ -0,0 +1,299 @@
{
"agents": [
{
"name": "Tweet Assistant",
"type": "conversation",
"description": "Assists users in creating and posting tweets with images, including crafting tweet text, generating images, and posting to Twitter.",
"instructions": "## 🧑‍💼 Role:\nYou are a helpful assistant that helps users create and post tweets with images. You can assist with crafting the tweet text, finding information, generating images, and finally posting the tweet to Twitter.\n\n---\n## ⚙️ Operating Procedure:\n1. Greet the user and ask for the text they want to include in the tweet. Offer to help them craft it or find information about a topic.\n2. If the user asks for help with a topic, use [@tool:Composio DuckDuckGo Search](#mention) and [@tool:Exa Answer](#mention) to find relevant information and present it to the user.\n3. Once the tweet text is finalized, work with the user for description of the image they want to include in the tweet.\n4. Use [@tool:Generate Image](#mention) with the image description to generate the image.\n5. Use [@tool:Create a post](#mention) with the tweet text and the generated image (or its ID).\n6. Inform the user when the tweet has been successfully posted.\n\n---\n## 🎯 Scope:\n✅ In Scope:\n- Interacting with the user to get tweet text and image description.\n- Offering assistance in crafting tweets and finding information.\n- Generating images from text descriptions.\n- Posting tweets with images to Twitter.\n\n❌ Out of Scope:\n- None, this agent handles the entire workflow.\n\n---\n## 📋 Guidelines:\n✔ Dos:\n- Always confirm the tweet text and image description with the user.\n- Be proactive in offering help and suggestions for tweet content.\n- Ensure the generated image matches the description.\n- Ensure the tweet text and image are correctly posted.\n\n🚫 Don'ts:\n- Do not fabricate information.\n\n",
"model": "google/gemini-2.5-flash",
"toggleAble": true,
"ragReturnType": "chunks",
"ragK": 3,
"controlType": "retain",
"outputVisibility": "user_facing",
"examples": "\n"
}
],
"prompts": [],
"tools": [
{
"name": "Generate Image",
"description": "Generate an image using Google Gemini given a text prompt. Returns base64-encoded image data and any text parts.",
"isGeminiImage": true,
"parameters": {
"type": "object",
"properties": {
"prompt": {
"type": "string",
"description": "Text prompt describing the image to generate"
},
"modelName": {
"type": "string",
"description": "Optional Gemini model override"
}
},
"required": [
"prompt"
],
"additionalProperties": true
}
},
{
"name": "Create a post",
"description": "Creates a tweet on twitter; `text` is required unless `card uri`, `media media ids`, `poll options`, or `quote tweet id` is provided.",
"parameters": {
"type": "object",
"properties": {
"card_uri": {
"description": "URI of a card to attach. Mutually exclusive with `quote_tweet_id`, `poll` parameters, `media` parameters, and `direct_message_deep_link`.",
"examples": [
"https://example.com/my-card"
],
"title": "Card Uri",
"type": "string"
},
"direct_message_deep_link": {
"description": "Deep link to a private Direct Message conversation. Mutually exclusive with `card_uri`.",
"examples": [
"https://twitter.com/messages/compose?recipient_id=12345&text=Hi"
],
"title": "Direct Message Deep Link",
"type": "string"
},
"for_super_followers_only": {
"default": false,
"description": "Restricts Tweet visibility to the author's Super Followers.",
"examples": [
"True",
"False"
],
"title": "For Super Followers Only",
"type": "boolean"
},
"geo__place__id": {
"description": "Twitter Place ID to associate with the Tweet.",
"examples": [
"df51dec6f4ee2b2c"
],
"title": "Geo Place Id",
"type": "string"
},
"media__media__ids": {
"description": "Up to 4 Media IDs obtained from prior uploads. Mutually exclusive with `card_uri`.",
"examples": [
[
"1146032800000000000",
"1146032800000000001"
]
],
"items": {
"properties": {},
"type": "string"
},
"title": "Media Media Ids",
"type": "array"
},
"media__tagged__user__ids": {
"description": "User IDs to tag in media; tagged users must have enabled photo tagging. Mutually exclusive with `card_uri`.",
"examples": [
[
"2244994945",
"783214"
]
],
"items": {
"properties": {},
"type": "string"
},
"title": "Media Tagged User Ids",
"type": "array"
},
"nullcast": {
"default": false,
"description": "Marks the Tweet as a promoted-only post, not appearing in public timelines or served to followers.",
"examples": [
"True",
"False"
],
"title": "Nullcast",
"type": "boolean"
},
"poll__duration__minutes": {
"description": "Poll duration in minutes (5-10080). Required if `poll_options` are provided. Mutually exclusive with `card_uri`.",
"examples": [
"60",
"1440",
"10080"
],
"title": "Poll Duration Minutes",
"type": "integer"
},
"poll__options": {
"description": "List of 2 to 4 poll options (max 25 characters each). Required if creating a poll. Mutually exclusive with `card_uri`.",
"examples": [
[
"Yes",
"No"
],
[
"Option A",
"Option B",
"Option C"
]
],
"items": {
"properties": {},
"type": "string"
},
"title": "Poll Options",
"type": "array"
},
"poll__reply__settings": {
"description": "Specifies who can reply to the poll Tweet: 'following' or 'mentionedUsers'. Mutually exclusive with `card_uri`.",
"enum": [
"following",
"mentionedUsers"
],
"examples": [
"following",
"mentionedUsers"
],
"title": "Poll Reply Settings",
"type": "string"
},
"quote_tweet_id": {
"description": "ID of the Tweet to quote. Mutually exclusive with `card_uri`, `poll` parameters, and `direct_message_deep_link`.",
"examples": [
"1346889436626259968"
],
"pattern": "^[0-9]{1,19}$",
"title": "Quote Tweet Id",
"type": "string"
},
"reply__exclude__reply__user__ids": {
"description": "User IDs to exclude from @mentioning in the reply; these users will not be notified. Used when `reply_in_reply_to_tweet_id` is set.",
"examples": [
[
"123456789",
"987654321"
]
],
"items": {
"properties": {},
"type": "string"
},
"title": "Reply Exclude Reply User Ids",
"type": "array"
},
"reply__in__reply__to__tweet__id": {
"description": "ID of the Tweet to which this is a reply. Required if creating a reply.",
"examples": [
"1346889436626259960"
],
"pattern": "^[0-9]{1,19}$",
"title": "Reply In Reply To Tweet Id",
"type": "string"
},
"reply_settings": {
"description": "Specifies who can reply to this Tweet: 'following', 'mentionedUsers', or 'subscribers' (X Premium subscribers).",
"enum": [
"following",
"mentionedUsers",
"subscribers"
],
"examples": [
"following",
"mentionedUsers",
"subscribers"
],
"title": "Reply Settings",
"type": "string"
},
"text": {
"description": "Text content of the Tweet (max 280 characters). Required unless `card_uri`, `media_media_ids`, `poll_options`, or `quote_tweet_id` are provided.",
"examples": [
"Hello world!",
"Check out this cool new feature! #innovation"
],
"title": "Text",
"type": "string"
}
},
"required": []
},
"mockTool": false,
"isComposio": true,
"composioData": {
"slug": "TWITTER_CREATION_OF_A_POST",
"noAuth": false,
"toolkitName": "twitter",
"toolkitSlug": "twitter",
"logo": "https://cdn.jsdelivr.net/gh/ComposioHQ/open-logos@master/twitter.png"
}
},
{
"name": "Exa Answer",
"description": "Get answers with citations using the exa api.",
"parameters": {
"type": "object",
"properties": {
"content": {
"description": "The user message content for the Exa answer API.",
"examples": [
"give me image of narendra modi"
],
"title": "Content",
"type": "string"
}
},
"required": [
"content"
]
},
"mockTool": false,
"isComposio": true,
"composioData": {
"slug": "COMPOSIO_SEARCH_EXA_ANSWER",
"noAuth": true,
"toolkitName": "composio_search",
"toolkitSlug": "composio_search",
"logo": "https://cdn.jsdelivr.net/gh/ComposioHQ/open-logos@master//composio-logo.png"
}
},
{
"name": "Composio DuckDuckGo Search",
"description": "The duckduckgosearch class utilizes the composio duckduckgo search api to perform searches, focusing on web information and details. it leverages the duckduckgo search engine via the composio duckduckgo search api to retrieve relevant web data based on the provided query.",
"parameters": {
"type": "object",
"properties": {
"query": {
"description": "The search query for the Composio DuckDuckGo Search API, specifying the search topic.",
"examples": [
"Python programming"
],
"title": "Query",
"type": "string"
}
},
"required": [
"query"
]
},
"mockTool": false,
"isComposio": true,
"composioData": {
"slug": "COMPOSIO_SEARCH_DUCK_DUCK_GO_SEARCH",
"noAuth": true,
"toolkitName": "Composio search",
"toolkitSlug": "composio_search",
"logo": "https://cdn.jsdelivr.net/gh/ComposioHQ/open-logos@master//composio-logo.png"
}
}
],
"pipelines": [],
"startAgent": "Tweet Assistant",
"lastUpdatedAt": "2025-09-11T18:02:35.880Z",
"name": "Viral Tweet Assistant",
"description": "Research topics and create a tweet including generated images.",
"category": "News & Social"
}

View file

@ -0,0 +1,112 @@
{
"agents": [
{
"name": "Product & Delivery Assistant",
"type": "conversation",
"description": "Hub agent to answer product information questions (using RAG) and delivery status questions.",
"instructions": "## 🧑‍💼 Role:\nYou are the hub agent responsible for orchestrating responses to product information and delivery status questions.\n\n---\n## ⚙️ Steps to Follow:\n1. Greet the user and ask how you can help. Say something like 'Hi, I'm [@variable:Assistant name](#mention) from [@variable:Company name](#mention). How can I help you today?'\n2. Determine if the user's question is about product information or delivery status.\n3. If the question is about product information, transfer to [@agent:Product Information Agent](#mention).\n4. If the question is about delivery status, transfer to [@agent:Delivery Status Agent](#mention).\n5. If the question is neither, politely inform the user that you can only help with product information or delivery status.\n6. Return the final answer to the user.\n\n---\n## 🎯 Scope:\n✅ In Scope:\n- Routing product information questions.\n- Routing delivery status questions.\n\n❌ Out of Scope:\n- Directly answering product or delivery questions.\n- Handling questions outside of product information or delivery status.\n\n---\n## 📋 Guidelines:\n✔ Dos:\n- Clearly identify the type of user query.\n- Route to the correct agent.\n\n🚫 Don'ts:\n- Do not attempt to answer questions directly.\n- Do not ask for personal information unless explicitly required by a sub-agent.\n- CRITICAL: Only transfer to one agent at a time and wait for its response before proceeding.\n\n",
"examples": "- **User** : What are the features of product X?\n - **Agent actions**: Call [@agent:Product Information Agent](#mention)\n\n- **User** : Where is my order?\n - **Agent actions**: Call [@agent:Delivery Status Agent](#mention)\n\n- **User** : How do I reset my password?\n - **Agent response**: I can only help with product information or delivery status questions. How else can I assist you today?",
"model": "gpt-4o",
"toggleAble": true,
"ragReturnType": "chunks",
"ragK": 3,
"outputVisibility": "user_facing",
"controlType": "retain"
},
{
"name": "Product Information Agent",
"type": "conversation",
"description": "Answers product information questions using RAG data sources.",
"disabled": false,
"instructions": "## 🧑‍💼 Role:\nYou are an internal agent that answers product information questions using RAG data sources. If you receive a question that is not about product information, you must return control to the parent agent with a message indicating the question is out of your scope.\n\n---\n## ⚙️ Steps to Follow:\n1. Receive the product information question from the parent agent.\n2. Determine if the question is about product information.\n - If yes: Use RAG search to pull information from the available data sources to answer the question.\n - If not: Return control to the parent agent with a message such as \"This question is not about product information. Returning to parent agent.\"\n3. Formulate a clear and concise answer based on the RAG results (if applicable).\n4. If question is out of scope call [@agent:Product & Delivery Assistant](#mention) \n\n---\n## 🎯 Scope:\n✅ In Scope:\n- Answering product information questions using RAG.\n- Returning control to parent if the question is out of scope.\n\n❌ Out of Scope:\n- Handling delivery status questions.\n- Interacting directly with the user.\n\n---\n## 📋 Guidelines:\n✔ Dos:\n- Use RAG search to find relevant information for product questions.\n- If the question is not about product information, return control to the parent agent with a clear message.\n\n🚫 Don'ts:\n- Do not answer questions outside of product information.\n- Do not interact with the user directly.\n- Do not ignore out-of-scope questions; always return to parent.\n",
"examples": "\n",
"model": "gpt-4o",
"locked": false,
"toggleAble": true,
"ragDataSources": [
"68c3172a72d2a6bd1c4a2afe"
],
"ragReturnType": "chunks",
"ragK": 3,
"outputVisibility": "user_facing",
"controlType": "retain",
"maxCallsPerParentAgent": 3
},
{
"name": "Delivery Status Agent",
"type": "conversation",
"description": "Answers delivery status questions using the Exa Answer tool.",
"disabled": false,
"instructions": "## 🧑‍💼 Role:\nYou are an internal agent that answers delivery status questions. If you receive a question that is not about delivery status, you must return control to the parent agent with a message indicating the question is out of your scope.\n\n---\n## ⚙️ Steps to Follow:\n1. Receive the delivery status question from the parent agent.\n2. Determine if the question is about delivery status.\n - If yes: Use the [@tool:Mock Delivery Status](#mention) tool to search for delivery status information. You may need to ask the user for an order number or tracking ID if not provided.\n - If not: Return control to the parent agent with a message such as \"This question is not about delivery status. Returning to parent agent.\"\n3. Formulate a clear and concise answer based on the tool's results (if applicable).\n4. If question is out of scope call [@agent:Product & Delivery Assistant](#mention) \n---\n## 🎯 Scope:\n✅ In Scope:\n- Answering delivery status questions using the Exa Answer tool.\n- Returning control to parent if the question is out of scope.\n\n❌ Out of Scope:\n- Handling product information questions.\n- Interacting directly with the user (except to ask for necessary information like order ID).\n\n---\n## 📋 Guidelines:\n✔ Dos:\n- Use the Exa Answer tool to find delivery information for delivery status questions.\n- If the question is not about delivery status, return control to the parent agent with a clear message.\n- Ask for order details if needed.\n\n🚫 Don'ts:\n- Do not answer questions outside of delivery status.\n- Do not interact with the user directly unless absolutely necessary to get information for the tool.\n- Do not ignore out-of-scope questions; always return to parent.\n",
"examples": "\n",
"model": "gpt-4o",
"locked": false,
"toggleAble": true,
"ragReturnType": "chunks",
"ragK": 3,
"outputVisibility": "user_facing",
"controlType": "retain",
"maxCallsPerParentAgent": 3
}
],
"prompts": [
{
"name": "Company name",
"type": "base_prompt",
"prompt": "<needs to be added>"
},
{
"name": "Assistant name",
"type": "base_prompt",
"prompt": "<needs to be added>"
}
],
"tools": [
{
"name": "Generate Image",
"description": "Generate an image using Google Gemini given a text prompt. Returns base64-encoded image data and any text parts.",
"parameters": {
"type": "object",
"properties": {
"prompt": {
"type": "string",
"description": "Text prompt describing the image to generate"
},
"modelName": {
"type": "string",
"description": "Optional Gemini model override"
}
},
"required": [
"prompt"
],
"additionalProperties": true
},
"isGeminiImage": true
},
{
"name": "Mock Delivery Status",
"description": "A mock tool to simulate checking delivery status.",
"mockTool": true,
"mockInstructions": "This tool simulates checking the delivery status of an order. It will always return a predefined delivery status message.",
"parameters": {
"type": "object",
"properties": {
"order_id": {
"type": "string",
"description": "The order ID to check the delivery status for."
}
},
"required": [
"order_id"
]
}
}
],
"pipelines": [],
"startAgent": "Product & Delivery Assistant",
"lastUpdatedAt": "2025-09-11T18:51:15.548Z",
"name": "Customer Support",
"description": "Answers product information (RAG) and delivery status (MCP) questions.",
"category": "Customer Support"
}

View file

@ -0,0 +1,579 @@
{
"category": "Developer Productivity",
"agents": [
{
"name": "GitHub Stats Hub",
"type": "conversation",
"description": "Hub agent that orchestrates fetching GitHub stats for rowboatlabs/rowboat and logging them to a Google Sheet.",
"instructions": "## 🧑‍💼 Role:\nYou are the hub agent responsible for orchestrating the process of fetching GitHub repository stats for 'rowboatlabs/rowboat' and logging them to a Google Sheet.\n\n---\n## ⚙️ Steps to Follow:\n1. Receive a user request to log GitHub stats.\n2. FIRST: Call [@agent:GitHub Stats Agent](#mention) and always provide repository owner: 'rowboatlabs' and repo: 'rowboat' as input (do not prompt the user for these values).\n3. Wait for the stats to be returned.\n4. THEN: Call [@agent:GitHub Stats to Sheet Agent](#mention) to append the stats to the Google Sheet.\n5. Wait for confirmation from the Sheets agent.\n6. Inform the user that the data has been logged, or report any error if one occurred.\n\n---\n## 🎯 Scope:\n✅ In Scope:\n- Orchestrating the sequential workflow: fetch stats for rowboatlabs/rowboat, then log to sheet, then inform the user.\n\n❌ Out of Scope:\n- Fetching stats or logging to the sheet directly (handled by sub-agents).\n- Handling requests unrelated to GitHub stats logging.\n- Accepting or prompting for other repositories.\n\n---\n## 📋 Guidelines:\n✔ Dos:\n- Always use 'rowboatlabs' as owner and 'rowboat' as repo when calling the GitHub Stats Agent.\n- Always follow the sequence: GitHub Stats Agent first, then GitHub Stats to Sheet Agent.\n- Wait for each agent's complete response before proceeding.\n- Only interact with the user for the initial request and final confirmation.\n\n🚫 Don'ts:\n- Do not perform stats fetching or sheet logging yourself.\n- Do not try to call both agents at once.\n- Do not reference internal agent names to the user.\n- Do not prompt the user for a repository or accept any other repository.\n- CRITICAL: The system does not support more than 1 tool call in a single output when the tool call is about transferring to another agent (a handoff). You must only put out 1 transfer related tool call in one output.\n\n# Examples\n- **User** : Fetch and store stats\n - **Agent actions**: Call [@agent:GitHub Stats Agent](#mention) with owner: 'rowboatlabs', repo: 'rowboat'\n\n- **Agent receives stats** :\n - **Agent actions**: Call [@agent:GitHub Stats to Sheet Agent](#mention)\n\n- **Agent receives sheet confirmation** :\n - **Agent response**: GitHub stats have been logged to the sheet successfully.\n\n- **Agent receives error from sheet agent** :\n - **Agent response**: There was an error logging the stats to the sheet: [error details]\n\n- **User** : Add a dummy row\n - **Agent response**: Sorry, I can only log actual GitHub stats. Please use the workflow to log real data.",
"model": "google/gemini-2.5-flash",
"toggleAble": true,
"ragReturnType": "chunks",
"ragK": 3,
"outputVisibility": "user_facing",
"controlType": "retain"
},
{
"name": "GitHub Stats Agent",
"type": "conversation",
"description": "Fetches GitHub page view and clone statistics for rowboatlabs/rowboat for the previous day.",
"disabled": false,
"instructions": "## 🧑‍💼 Role:\nYou are an internal agent that fetches GitHub page view and clone statistics for the repository for the previous day.\n\n---\n## ⚙️ Steps to Follow:\n1. Always use owner: <take from context> and repo: <take from context> (do not expect or prompt for these values from the parent agent).\n2. Use [@tool:Get page views](#mention) with per: 'day' to fetch daily page view stats. You must actually call this tool.\n3. Use [@tool:Get repository clones](#mention) with per: 'day' to fetch daily clone stats. You must actually call this tool.\n4. Filter both results to only include data for the previous day (relative to today, in UTC).\n5. Return both sets of stats (page views and clones for the previous day) to the parent agent.\n\n---\n## 🎯 Scope:\n✅ In Scope:\n- Fetching and returning GitHub page view and clone stats for 'rowboatlabs/rowboat' for the previous day.\n\n❌ Out of Scope:\n- Answering user questions directly.\n- Modifying repository data.\n- Accepting or prompting for any other repository.\n\n---\n## 📋 Guidelines:\n✔ Dos:\n- Return only the stats for the previous day.\n- Return both page views and clone stats in a clear, structured format.\n- **Do not simulate or describe tool calls—always actually call the tools.**\n\n🚫 Don'ts:\n- Do not interact with the user directly.\n- Do not perform any actions other than fetching and returning stats.\n- Do not prompt for or accept any repository input.\n\n# Examples\n- **Parent agent** : Fetch and store stats\n - **Agent actions**: Call [@tool:Get page views](#mention) with owner, repo, per: 'day'. Then call [@tool:Get repository clones](#mention) with owner, repo: 'rowboat', per: 'day'.\n - **Agent response**: [Page views and clone stats for owner/repo for the previous day]\n\n\n\n",
"model": "google/gemini-2.5-flash",
"locked": false,
"toggleAble": true,
"ragReturnType": "chunks",
"ragK": 3,
"outputVisibility": "internal",
"controlType": "relinquish_to_parent",
"maxCallsPerParentAgent": 3
},
{
"name": "GitHub Stats to Sheet Agent",
"type": "conversation",
"description": "Appends the latest GitHub clone and view stats as a new row to a specified Google Sheet.",
"disabled": false,
"instructions": "## 🧑‍💼 Role:\nYou are an internal agent that receives GitHub stats (clones and views), extracts the most recent date for each, and appends a row to a Google Sheet with the specified columns.\n\n---\n## ⚙️ Steps to Follow:\n1. Receive GitHub stats data (including arrays of daily clone and view stats, each with date, count, and uniques).\n2. Identify the most recent (latest) date in the clones array and extract its count and uniques.\n3. Identify the most recent (latest) date in the views array and extract its count and uniques.\n4. Use the current UTC date (YYYY-MM-DD) as the run date.\n5. Prepare a row with the following columns (in order):\n - run date (current UTC date, YYYY-MM-DD)\n - latest clones stats date (YYYY-MM-DD)\n - clones (count)\n - unique clones\n - latest view stats date (YYYY-MM-DD)\n - views (count)\n - unique views\n6. Use [@tool:Append Values to Spreadsheet](#mention) to append this row to the end of the sheet (no headers).\n - spreadsheetId: <take from context>\n - range: <take from context> (or the correct sheet name if specified)\n - valueInputOption: USER_ENTERED\n - values: [[run date, latest clones stats date, clones, unique clones, latest view stats date, views, unique views]]\n7. Return a confirmation or error message to the parent agent.\n\n---\n## 🎯 Scope:\n✅ In Scope:\n- Appending a single row of stats to the specified Google Sheet.\n\n❌ Out of Scope:\n- Adding headers or modifying existing data.\n- Interacting with the user directly.\n\n---\n## 📋 Guidelines:\n✔ Dos:\n- Only use the most recent date for each stat type.\n- Ensure the row is appended at the end (no headers).\n- Use the correct spreadsheetId and valueInputOption.\n\n🚫 Don'ts:\n- Do not add column headers.\n- Do not overwrite existing data.\n- Do not interact with the user directly.\n\n# Examples\n- **Parent agent** : Insert latest GitHub stats into sheet\n - **Agent actions**: Call [@tool:Append Values to Spreadsheet](#mention) with the latest stats and current date\n - **Agent response**: Row appended confirmation or error message\n\n- **Parent agent** : Insert stats with missing data\n - **Agent actions**: If either clones or views data is missing, append available data and leave missing fields blank\n - **Agent response**: Row appended confirmation or error message\n\n- **Parent agent** : Insert stats for a different repo\n - **Agent actions**: Same as above, using provided stats\n - **Agent response**: Row appended confirmation or error message\n\n- **Parent agent** : Insert stats with only views data\n - **Agent actions**: Append row with views data, leave clone fields blank\n - **Agent response**: Row appended confirmation or error message\n\n- **Parent agent** : Insert stats with only clones data\n - **Agent actions**: Append row with clone data, leave view fields blank\n - **Agent response**: Row appended confirmation or error message\n",
"model": "google/gemini-2.5-flash",
"locked": false,
"toggleAble": true,
"ragReturnType": "chunks",
"ragK": 3,
"outputVisibility": "internal",
"controlType": "relinquish_to_parent",
"maxCallsPerParentAgent": 3
},
{
"name": "Pipeline Step 1 - Fetch Views Data",
"type": "pipeline",
"description": "Fetches daily page view stats for rowboatlabs/rowboat using the Get page views tool.",
"disabled": false,
"instructions": "## 🧑‍💼 Role:\nFetch daily page view stats for the repository 'rowboatlabs/rowboat'.\n\n---\n## ⚙️ Steps to Follow:\n1. Use [@tool:Get page views](#mention) with owner: 'rowboatlabs', repo: 'rowboat', per: 'day'.\n2. Return the full result to the next pipeline step.\n\n---\n## 📋 Guidelines:\n- Do not prompt for repository details; always use the specified owner and repo.\n- Do not interact with the user or other agents.",
"model": "google/gemini-2.5-flash",
"locked": false,
"toggleAble": true,
"ragReturnType": "chunks",
"ragK": 3,
"outputVisibility": "internal",
"controlType": "relinquish_to_parent",
"maxCallsPerParentAgent": 3
},
{
"name": "Pipeline Step 2 - Fetch Clones Data",
"type": "pipeline",
"description": "Fetches daily clone stats for rowboatlabs/rowboat using the Get repository clones tool.",
"disabled": false,
"instructions": "## 🧑‍💼 Role:\nFetch daily clone stats for the repository 'rowboatlabs/rowboat'.\n\n---\n## ⚙️ Steps to Follow:\n1. Use [@tool:Get repository clones](#mention) with owner: 'rowboatlabs', repo: 'rowboat', per: 'day'.\n2. Return the full result to the next pipeline step, along with the previous step's page views data.\n\n---\n## 📋 Guidelines:\n- Do not prompt for repository details; always use the specified owner and repo.\n- Do not interact with the user or other agents.",
"model": "google/gemini-2.5-flash",
"locked": false,
"toggleAble": true,
"ragReturnType": "chunks",
"ragK": 3,
"outputVisibility": "internal",
"controlType": "relinquish_to_parent",
"maxCallsPerParentAgent": 3
},
{
"name": "Pipeline Step 3 - Add Data to Sheet",
"type": "pipeline",
"description": "Appends the latest GitHub clone and view stats as a new row to the specified Google Sheet.",
"disabled": false,
"instructions": "## 🧑‍💼 Role:\nAppend the latest GitHub stats (clones and views) as a new row to the Google Sheet.\n\n---\n## ⚙️ Steps to Follow:\n1. Receive both page views and clone stats (arrays of daily stats, each with date, count, uniques).\n2. Identify the most recent (latest) date in the clones array and extract its count and uniques.\n3. Identify the most recent (latest) date in the views array and extract its count and uniques.\n4. Use the current UTC date (YYYY-MM-DD) as the run date.\n5. Prepare a row with the following columns (in order):\n - run date (current UTC date, YYYY-MM-DD)\n - latest clones stats date (YYYY-MM-DD)\n - clones (count)\n - unique clones\n - latest view stats date (YYYY-MM-DD)\n - views (count)\n - unique views\n6. Use [@tool:Append Values to Spreadsheet](#mention) to append this row to the end of the sheet (no headers).\n - spreadsheetId: <take from context>\n - range: <take from context>\n - valueInputOption: USER_ENTERED\n - values: [[run date, latest clones stats date, clones, unique clones, latest view stats date, views, unique views]]\n7. Return the appended row and all relevant stats to the next pipeline step.\n\n---\n## 📋 Guidelines:\n- Only use the most recent date for each stat type.\n- Ensure the row is appended at the end (no headers).\n- Use the correct spreadsheetId and valueInputOption.\n- Do not interact with the user or other agents.\n\n",
"model": "google/gemini-2.5-flash",
"locked": false,
"toggleAble": true,
"ragReturnType": "chunks",
"ragK": 3,
"outputVisibility": "internal",
"controlType": "relinquish_to_parent",
"maxCallsPerParentAgent": 3
},
{
"name": "Pipeline Step 4 - Send Slack Summary",
"type": "pipeline",
"description": "Sends a summary message to the #stats Slack channel, including a link to the updated sheet.",
"disabled": false,
"instructions": "## 🧑‍💼 Role:\nSend a summary message to the #stats Slack channel after stats are logged to the sheet.\n\n---\n## ⚙️ Steps to Follow:\n1. Receive the appended row and all relevant stats from the previous step.\n2. Compose a message summarizing the latest GitHub stats update, including:\n - The run date\n - The latest clones and views stats (date, count, uniques)\n - A statement that the data has been updated in the sheet\n - A link to the sheet: <take from context>\n3. Use [@tool:Send a message to a Slack channel](#mention) to post the message to channel: stats\n4. Return a confirmation or error message.\n\n---\n## 📋 Guidelines:\n- The message should be clear, concise, and include the sheet link.\n- Do not interact with the user or other agents.\n",
"model": "google/gemini-2.5-flash",
"locked": false,
"toggleAble": true,
"ragReturnType": "chunks",
"ragK": 3,
"outputVisibility": "internal",
"controlType": "relinquish_to_parent",
"maxCallsPerParentAgent": 3
},
{
"name": "GitHub Stats Logging Pipeline Step 1",
"type": "pipeline",
"description": "",
"disabled": false,
"instructions": "",
"model": "gpt-4o",
"locked": false,
"toggleAble": true,
"ragReturnType": "chunks",
"ragK": 3,
"outputVisibility": "internal",
"controlType": "relinquish_to_parent",
"maxCallsPerParentAgent": 3
},
{
"name": "GitHub Stats Pipeline Hub",
"type": "conversation",
"description": "User-facing hub that triggers the GitHub Stats Logging Pipeline and reports when complete.",
"disabled": false,
"instructions": "## 🧑‍💼 Role:\nYou are the hub agent responsible for triggering the GitHub Stats Logging Pipeline.\n\n---\n## ⚙️ Steps to Follow:\n1. When the user requests a stats update, call [@pipeline:GitHub Stats Logging Pipeline](#mention).\n2. Wait for the pipeline to complete.\n3. Inform the user that the stats have been logged, the sheet updated, and the Slack channel notified.\n\n---\n## 📋 Guidelines:\n- Do not perform any stats fetching, sheet logging, or Slack messaging yourself.\n- Do not reference internal agent or pipeline names to the user.\n- Only interact with the user for the initial request and final confirmation.",
"model": "google/gemini-2.5-flash",
"locked": false,
"toggleAble": true,
"ragReturnType": "chunks",
"ragK": 3,
"outputVisibility": "user_facing",
"controlType": "retain",
"maxCallsPerParentAgent": 3
}
],
"prompts": [
{
"name": "spreadsheetId",
"type": "base_prompt",
"prompt": "<needs to be added>"
},
{
"name": "range",
"type": "base_prompt",
"prompt": "<needs to be added>"
},
{
"name": "sheet_link",
"type": "base_prompt",
"prompt": "<needs to be added>"
},
{
"name": "Owner",
"type": "base_prompt",
"prompt": "<needs to be added>"
},
{
"name": "repo",
"type": "base_prompt",
"prompt": "<needs to be added>"
}
],
"tools": [
{
"name": "Get page views",
"description": "Retrieves page view statistics for a repository over the last 14 days, including total views, unique visitors, and a daily or weekly breakdown.",
"mockTool": false,
"parameters": {
"type": "object",
"properties": {
"owner": {
"description": "The username of the account that owns the repository. This field is case-insensitive.",
"examples": [
"octocat"
],
"title": "Owner",
"type": "string"
},
"per": {
"default": "day",
"description": "The time unit for which to aggregate page views.",
"enum": [
"day",
"week"
],
"examples": [
"day",
"week"
],
"title": "Per",
"type": "string"
},
"repo": {
"description": "The name of the repository, without the `.git` extension. This field is case-insensitive.",
"examples": [
"Spoon-Knife"
],
"title": "Repo",
"type": "string"
}
},
"required": [
"owner",
"repo"
]
},
"isComposio": true,
"composioData": {
"slug": "GITHUB_GET_PAGE_VIEWS",
"noAuth": false,
"toolkitName": "GitHub",
"toolkitSlug": "github",
"logo": "https://cdn.jsdelivr.net/gh/ComposioHQ/open-logos@master/github.png"
}
},
{
"name": "Append Values to Spreadsheet",
"description": "Tool to append values to a spreadsheet. use when you need to add new data to the end of an existing table in a google sheet.",
"mockTool": false,
"parameters": {
"type": "object",
"properties": {
"includeValuesInResponse": {
"default": null,
"description": "Determines if the update response should include the values of the cells that were appended. By default, responses do not include the updated values.",
"examples": [
true
],
"nullable": true,
"title": "Include Values In Response",
"type": "boolean"
},
"insertDataOption": {
"default": null,
"description": "How the input data should be inserted.",
"enum": [
"OVERWRITE",
"INSERT_ROWS"
],
"examples": [
"INSERT_ROWS"
],
"nullable": true,
"title": "Insert Data Option",
"type": "string"
},
"majorDimension": {
"default": null,
"description": "The major dimension of the values. For output, if the spreadsheet data is: A1=1,B1=2,A2=3,B2=4, then requesting range=A1:B2,majorDimension=ROWS will return [[1,2],[3,4]], whereas requesting range=A1:B2,majorDimension=COLUMNS will return [[1,3],[2,4]].",
"enum": [
"ROWS",
"COLUMNS"
],
"examples": [
"ROWS"
],
"nullable": true,
"title": "Major Dimension",
"type": "string"
},
"range": {
"description": "The A1 notation of a range to search for a logical table of data. Values are appended after the last row of the table.",
"examples": [
"Sheet1!A1:B2"
],
"title": "Range",
"type": "string"
},
"responseDateTimeRenderOption": {
"default": null,
"description": "Determines how dates, times, and durations in the response should be rendered. This is ignored if responseValueRenderOption is FORMATTED_VALUE. The default dateTime render option is SERIAL_NUMBER.",
"enum": [
"SERIAL_NUMBER",
"FORMATTED_STRING"
],
"examples": [
"SERIAL_NUMBER"
],
"nullable": true,
"title": "Response Date Time Render Option",
"type": "string"
},
"responseValueRenderOption": {
"default": null,
"description": "Determines how values in the response should be rendered. The default render option is FORMATTED_VALUE.",
"enum": [
"FORMATTED_VALUE",
"UNFORMATTED_VALUE",
"FORMULA"
],
"examples": [
"FORMATTED_VALUE"
],
"nullable": true,
"title": "Response Value Render Option",
"type": "string"
},
"spreadsheetId": {
"description": "The ID of the spreadsheet to update.",
"examples": [
"1q0gLhLdGXYZblahblahblah"
],
"title": "Spreadsheet Id",
"type": "string"
},
"valueInputOption": {
"description": "How the input data should be interpreted.",
"enum": [
"RAW",
"USER_ENTERED"
],
"examples": [
"USER_ENTERED"
],
"title": "Value Input Option",
"type": "string"
},
"values": {
"description": "The data to be written. This is an array of arrays, the outer array representing all the data and each inner array representing a major dimension. Each item in the inner array corresponds with one cell.",
"examples": [
[
[
"A1_val1",
"A1_val2"
],
[
"A2_val1",
"A2_val2"
]
]
],
"items": {
"items": {
"anyOf": [
{
"type": "string"
},
{
"type": "integer"
},
{
"type": "number"
},
{
"type": "boolean"
}
]
},
"type": "array"
},
"title": "Values",
"type": "array"
}
},
"required": [
"spreadsheetId",
"range",
"valueInputOption",
"values"
]
},
"isComposio": true,
"composioData": {
"slug": "GOOGLESHEETS_SPREADSHEETS_VALUES_APPEND",
"noAuth": false,
"toolkitName": "Googlesheets",
"toolkitSlug": "googlesheets",
"logo": "https://cdn.jsdelivr.net/gh/ComposioHQ/open-logos@master/google-sheets.svg"
}
},
{
"name": "Get repository clones",
"description": "Retrieves the total number of clones and a breakdown of clone activity (daily or weekly) for a specified repository over the preceding 14 days.",
"mockTool": false,
"parameters": {
"type": "object",
"properties": {
"owner": {
"description": "The username of the account that owns the repository. This field is not case-sensitive.",
"examples": [
"octocat",
"github"
],
"title": "Owner",
"type": "string"
},
"per": {
"default": "day",
"description": "Specifies the time frame for aggregating clone data: `day` for daily clone counts, or `week` for weekly clone counts (a week starts on Monday).",
"enum": [
"day",
"week"
],
"examples": [
"day",
"week"
],
"title": "Per",
"type": "string"
},
"repo": {
"description": "The name of the repository, without the '.git' extension. This field is not case-sensitive.",
"examples": [
"Hello-World",
"mercury"
],
"title": "Repo",
"type": "string"
}
},
"required": [
"owner",
"repo"
]
},
"isComposio": true,
"composioData": {
"slug": "GITHUB_GET_REPOSITORY_CLONES",
"noAuth": false,
"toolkitName": "GitHub",
"toolkitSlug": "github",
"logo": "https://cdn.jsdelivr.net/gh/ComposioHQ/open-logos@master/github.png"
}
},
{
"name": "Send a message to a Slack channel",
"description": "Deprecated: posts a message to a slack channel, direct message, or private group. use `send message` instead.",
"mockTool": false,
"parameters": {
"type": "object",
"properties": {
"as_user": {
"description": "Post as the authenticated user instead of as a bot. Defaults to `false`. If `true`, `username`, `icon_emoji`, and `icon_url` are ignored. If `false`, the message is posted as a bot, allowing appearance customization.",
"title": "As User",
"type": "boolean"
},
"attachments": {
"description": "URL-encoded JSON array of message attachments, a legacy method for rich content. See Slack API documentation for structure.",
"examples": [
"%5B%7B%22fallback%22%3A%20%22Required%20plain-text%20summary%20of%20the%20attachment.%22%2C%20%22color%22%3A%20%22%2336a64f%22%2C%20%22pretext%22%3A%20%22Optional%20text%20that%20appears%20above%20the%20attachment%20block%22%2C%20%22author_name%22%3A%20%22Bobby%20Tables%22%2C%20%22title%22%3A%20%22Slack%20API%20Documentation%22%2C%20%22title_link%22%3A%20%22https%3A%2F%2Fapi.slack.com%2F%22%2C%20%22text%22%3A%20%22Optional%20text%20that%20appears%20within%20the%20attachment%22%7D%5D"
],
"title": "Attachments",
"type": "string"
},
"blocks": {
"description": "DEPRECATED: Use `markdown_text` field instead. URL-encoded JSON array of layout blocks for rich/interactive messages. See Slack API Block Kit docs for structure.",
"examples": [
"%5B%7B%22type%22%3A%20%22section%22%2C%20%22text%22%3A%20%7B%22type%22%3A%20%22mrkdwn%22%2C%20%22text%22%3A%20%22Hello%2C%20world%21%22%7D%7D%5D"
],
"title": "Blocks",
"type": "string"
},
"channel": {
"description": "ID or name of the channel, private group, or IM channel to send the message to.",
"examples": [
"C1234567890",
"general"
],
"title": "Channel",
"type": "string"
},
"icon_emoji": {
"description": "Emoji for bot's icon (e.g., ':robot_face:'). Overrides `icon_url`. Applies if `as_user` is `false`.",
"examples": [
":tada:",
":slack:"
],
"title": "Icon Emoji",
"type": "string"
},
"icon_url": {
"description": "Image URL for bot's icon (must be HTTPS). Applies if `as_user` is `false`.",
"examples": [
"https://slack.com/img/icons/appDir_2019_01/Tonito64.png"
],
"title": "Icon Url",
"type": "string"
},
"link_names": {
"description": "Automatically hyperlink channel names (e.g., #channel) and usernames (e.g., @user) in message text. Defaults to `false` for bot messages.",
"title": "Link Names",
"type": "boolean"
},
"markdown_text": {
"description": "PREFERRED: Write your message in markdown for nicely formatted display. Supports: headers (# ## ###), bold (**text** or __text__), italic (*text* or _text_), strikethrough (~~text~~), inline code (`code`), code blocks (```), links ([text](url)), block quotes (>), lists (- item, 1. item), dividers (--- or ***), context blocks (:::context with images), and section buttons (:::section-button). IMPORTANT: Use \\n for line breaks (e.g., 'Line 1\\nLine 2'), not actual newlines. USER MENTIONS: To tag users, use their user ID with <@USER_ID> format (e.g., <@U1234567890>), not username. ",
"examples": [
"# Status Update\n\nSystem is **running smoothly** with *excellent* performance.\n\n```bash\nkubectl get pods\n```\n\n> All services operational ✅",
"## Daily Report\n\n- **Deployments**: 5 successful\n- *Issues*: 0 critical\n- ~~Maintenance~~: **Completed**\n\n---\n\n**Next**: Monitor for 24h"
],
"title": "Markdown Text",
"type": "string"
},
"mrkdwn": {
"description": "Disable Slack's markdown for `text` field if `false`. Default `true` (allows *bold*, _italic_, etc.).",
"title": "Mrkdwn",
"type": "boolean"
},
"parse": {
"description": "Message text parsing behavior. Default `none` (no special parsing). `full` parses as user-typed (links @mentions, #channels). See Slack API docs for details.",
"examples": [
"none",
"full"
],
"title": "Parse",
"type": "string"
},
"reply_broadcast": {
"description": "If `true` for a threaded reply, also posts to main channel. Defaults to `false`.",
"title": "Reply Broadcast",
"type": "boolean"
},
"text": {
"description": "DEPRECATED: This sends raw text only, use markdown_text field. Primary textual content. Recommended fallback if using `blocks` or `attachments`. Supports mrkdwn unless `mrkdwn` is `false`.",
"examples": [
"Hello from your friendly bot!",
"Reminder: Team meeting at 3 PM today."
],
"title": "Text",
"type": "string"
},
"thread_ts": {
"description": "Timestamp (`ts`) of an existing message to make this a threaded reply. Use `ts` of the parent message, not another reply. Example: '1476746824.000004'.",
"examples": [
"1618033790.001500"
],
"title": "Thread Ts",
"type": "string"
},
"unfurl_links": {
"description": "Enable unfurling of text-based URLs. Defaults `false` for bots, `true` if `as_user` is `true`.",
"title": "Unfurl Links",
"type": "boolean"
},
"unfurl_media": {
"description": "Disable unfurling of media content from URLs if `false`. Defaults to `true`.",
"title": "Unfurl Media",
"type": "boolean"
},
"username": {
"description": "Bot's name in Slack (max 80 chars). Applies if `as_user` is `false`.",
"examples": [
"MyBot",
"AlertBot"
],
"title": "Username",
"type": "string"
}
},
"required": [
"channel"
]
},
"isComposio": true,
"composioData": {
"slug": "SLACK_SENDS_A_MESSAGE_TO_A_SLACK_CHANNEL",
"noAuth": false,
"toolkitName": "Slack",
"toolkitSlug": "slack",
"logo": "https://cdn.jsdelivr.net/gh/ComposioHQ/open-logos@master/slack.svg"
}
}
],
"pipelines": [
{
"name": "GitHub Stats Logging Pipeline",
"description": "Sequential pipeline to fetch GitHub stats, log them to a Google Sheet, and notify #stats Slack channel.",
"agents": [
"Pipeline Step 1 - Fetch Views Data",
"Pipeline Step 2 - Fetch Clones Data",
"Pipeline Step 3 - Add Data to Sheet",
"Pipeline Step 4 - Send Slack Summary"
]
}
],
"startAgent": "GitHub Stats Pipeline Hub",
"name": "GitHub Data to Spreadsheet",
"description": "Fetches GitHub repository stats and logs them to a Google Sheet with Slack notifications"
}

View file

@ -0,0 +1,22 @@
// Static index of prebuilt workflow templates so they are bundled in Vercel
// If you add/remove a JSON here, update this file accordingly.
import githubDataToSpreadsheet from './github-data-to-spreadsheet.json';
import interviewScheduler from './interview-scheduler.json';
import meetingPrepAssistant from './Meeting Prep Assistant.json';
import redditOnSlack from './Reddit on Slack.json';
import twitterSentiment from './twitter-sentiment.json';
import tweetWithGeneratedImage from './Tweet with generated image.json';
import customerSupport from './customer-support.json';
// Keep keys consistent with prior file basenames to avoid breaking links.
export const prebuiltTemplates = {
'github-data-to-spreadsheet': githubDataToSpreadsheet,
'interview-scheduler': interviewScheduler,
'Meeting Prep Assistant': meetingPrepAssistant,
'Reddit on Slack': redditOnSlack,
'Twitter Sentiment': twitterSentiment,
'Tweet with generated image': tweetWithGeneratedImage,
'Customer Support': customerSupport,
};

View file

@ -0,0 +1,469 @@
{
"category": "Work Productivity",
"agents": [
{
"name": "Recruitment HR Bot",
"type": "conversation",
"description": "Hub agent to orchestrate interview scheduling with candidates from a Google Sheet.",
"instructions": "## 🧑‍💼 Role:\nYou are the Recruitment HR Bot, responsible for orchestrating the process of scheduling interviews with candidates from a Google Sheet and updating their status, or handling calendar event RSVPs.\n\n---\n## ⚙️ Steps to Follow:\n1. Greet the user.\n2. **IF** the input is a calendar event RSVP (e.g., 'accepted', 'declined') and contains the candidate's email, Google Sheet ID, sheet name, and status column:\n - Directly call [@agent:Calendar Response Handler](#mention) with the candidate's email, the RSVP response, the Google Sheet ID, the sheet name, and the status column.\n - Inform the user that the calendar response has been processed.\n3. **ELSE** (if it's not a calendar event RSVP or missing details for it):\n - Check if the 'google sheet id' and 'Sheet range' prompts are available. If so, use their values. Otherwise, ask the user for the Google Sheet ID and the range containing candidate names and emails (e.g., 'Sheet1!A2:B').\n - Check if the 'interview start date and time' and 'Status column' prompts are available. If so, use their values. Otherwise, ask for the desired start date and time for interviews (e.g., 'YYYY-MM-DDTHH:MM:SS'), the duration of the interview in minutes, and the sheet name and column (e.g., 'Sheet1!C') where the interview status should be updated.\n - Once all necessary information is collected, call [@pipeline:Interview Scheduling Pipeline](#mention) with the collected details.\n - Inform the user when the interview scheduling process is complete.\n\n---\n## 🎯 Scope:\n✅ In Scope:\n- Orchestrating the workflow for fetching candidates, scheduling interviews, and updating sheet status.\n- Handling calendar event RSVPs and updating sheet status accordingly.\n\n❌ Out of Scope:\n- Directly fetching candidate data, scheduling interviews, or updating sheet status (handled by pipeline agents).\n- Directly processing calendar responses (handled by Calendar Response Handler).\n\n---\n## 📋 Guidelines:\n✔ Dos:\n- Prioritize handling calendar event RSVPs if the necessary information is present.\n- Always confirm all necessary details (Sheet ID, ranges, interview time, duration, status column) with the user before initiating the pipeline for interview scheduling.\n- Ensure all steps are completed in sequence.\n- If inputs are already in the context, directly use them instead of asking or confirming with the user.\n\n🚫 Don'ts:\n- Do not perform data fetching, scheduling, or status updates directly.\n- Do not skip any step in the workflow.\n- Do not mention internal agent names to the user.\n- Do not say 'connecting you to another agent'.\n- CRITICAL: Only transfer to one agent at a time and wait for its response before proceeding.\n\n---\n## 📥 Inputs:\n- **Google Sheet ID**: The unique identifier of the Google Spreadsheet containing candidate data. (e.g., '1BxiMVs0XRA5nFMdKvBdBZjgmUUqptlbs74OgvE2upms')\n- **Sheet Range**: The range in A1 notation (e.g., 'Sheet1!A2:B') containing candidate names and emails.\n- **Interview Start Date and Time**: The desired start date and time for interviews in 'YYYY-MM-DDTHH:MM:SS' format. Default: '2025-08-26T09:00:00'\n- **Interview Duration**: The duration of the interview in minutes. Default: 30\n- **Status Column**: The sheet name and column (e.g., 'Sheet1!C') where the interview status should be updated.",
"examples": "- **User** : I want to schedule interviews for candidates from a Google Sheet.\n - **Agent response**: Sure! Please provide the desired start date and time for interviews (e.g., 'YYYY-MM-DDTHH:MM:SS'), the duration of the interview in minutes, and the sheet name and column (e.g., 'Sheet1!C') where the interview status should be updated.\n\n- **User** : Start Time: 2024-09-01T10:00:00, Duration: 30, Status Column: Sheet1!C\n - **Agent actions**: Call [@pipeline:Interview Scheduling Pipeline](#mention)\n\n- **Agent receives pipeline completion** :\n - **Agent response**: The interview scheduling process is complete.\n\n- **User** : Candidate [candidate_email] has accepted the interview. Sheet ID: [sheet_id], Sheet Name: [sheet_name], Status Column: [status_column]\n - **Agent actions**: Call [@agent:Calendar Response Handler](#mention)\n\n- **Agent receives Calendar Response Handler completion** :\n - **Agent response**: The calendar response has been processed and the sheet updated.",
"model": "google/gemini-2.5-flash",
"toggleAble": true,
"ragReturnType": "chunks",
"ragK": 3,
"outputVisibility": "user_facing",
"controlType": "retain"
},
{
"name": "Pipeline Step 1 - Fetch Candidates",
"type": "pipeline",
"description": "Reads candidate names and emails from a specified Google Sheet range.",
"disabled": false,
"instructions": "## 🧑‍💼 Role:\nFetch candidate names and emails from the provided Google Sheet and ranges.\n\n---\n## ⚙️ Steps to Follow:\n1. Use [@tool:Batch get spreadsheet](#mention) with the given spreadsheet_id and ranges (e.g., 'Sheet1!A2:B').\n2. Return a normalized array of { name, email } objects.\n\n---\n## 🎯 Scope:\n✅ In Scope:\n- Fetching rows from Google Sheets and returning structured data.\n\n❌ Out of Scope:\n- Scheduling interviews or updating sheet status.\n\n---\n## 📋 Guidelines:\n✔ Dos:\n- Validate rows and skip empties.\n🚫 Don'ts:\n- Do not schedule interviews or update sheet status.",
"model": "google/gemini-2.5-flash",
"locked": false,
"toggleAble": true,
"ragReturnType": "chunks",
"ragK": 3,
"outputVisibility": "internal",
"controlType": "relinquish_to_parent",
"maxCallsPerParentAgent": 3
},
{
"name": "Pipeline Step 2 - Schedule Interview",
"type": "pipeline",
"description": "Schedules an interview for each candidate using Google Calendar.",
"disabled": false,
"instructions": "## 🧑‍💼 Role:\nSchedule an interview for each candidate.\n\n---\n## ⚙️ Steps to Follow:\n1. Receive a list of { name, email } objects from the previous step.\n2. For each candidate, use [@tool:Create Event](#mention) to schedule an interview. The event summary should be 'Interview with [Candidate Name]', and the attendee should be the candidate's email. You will need to ask the user for the start_datetime and duration of the interview.\n3. Return a list of { candidate_email, status: 'scheduled' } for each successfully scheduled interview.\n\n---\n## 🎯 Scope:\n✅ In Scope:\n- Scheduling interviews on Google Calendar.\n\n❌ Out of Scope:\n- Fetching candidate data or updating sheet status.\n\n---\n## 📋 Guidelines:\n✔ Dos:\n- Ensure all required fields for event creation are provided.\n🚫 Don'ts:\n- Do not fetch candidate data or update sheet status.",
"model": "google/gemini-2.5-flash",
"locked": false,
"toggleAble": true,
"ragReturnType": "chunks",
"ragK": 3,
"outputVisibility": "internal",
"controlType": "relinquish_to_parent",
"maxCallsPerParentAgent": 3
},
{
"name": "Pipeline Step 3 - Update Sheet Status",
"type": "pipeline",
"description": "Updates the status column in the Google Sheet to 'interview scheduled' for each candidate.",
"disabled": false,
"instructions": "## 🧑‍💼 Role:\nUpdate the status column in the Google Sheet for scheduled interviews.\n\n---\n## ⚙️ Steps to Follow:\n1. Receive a list of { candidate_email, status: 'scheduled' } objects from the previous step.\n2. For each candidate, use [@tool:Batch update spreadsheet](#mention) to update the corresponding row in the Google Sheet. You will need to ask the user for the spreadsheet_id, sheet_name, and the column where the status needs to be updated.\n3. The value to be updated should be 'invite sent'.\n4. Return a confirmation of the updates.\n\n---\n## 🎯 Scope:\n✅ In Scope:\n- Updating the status column in the Google Sheet.\n\n❌ Out of Scope:\n- Fetching candidate data or scheduling interviews.\n\n---\n## 📋 Guidelines:\n✔ Dos:\n- Ensure the correct row and column are updated.\n🚫 Don'ts:\n- Do not fetch candidate data or schedule interviews.",
"model": "google/gemini-2.5-flash",
"locked": false,
"toggleAble": true,
"ragReturnType": "chunks",
"ragK": 3,
"outputVisibility": "internal",
"controlType": "relinquish_to_parent",
"maxCallsPerParentAgent": 3
},
{
"name": "Interview Scheduling Pipeline Step 1",
"type": "pipeline",
"description": "",
"disabled": false,
"instructions": "",
"model": "gpt-4o",
"locked": false,
"toggleAble": true,
"ragReturnType": "chunks",
"ragK": 3,
"outputVisibility": "internal",
"controlType": "relinquish_to_parent",
"maxCallsPerParentAgent": 3
},
{
"name": "Calendar Response Handler",
"type": "conversation",
"description": "Handles calendar accept/reject responses and updates the Google Sheet status accordingly.",
"disabled": false,
"instructions": "## 🧑‍💼 Role:\nProcess calendar responses (accept/reject) and update the Google Sheet with the appropriate interview status.\n\n---\n## ⚙️ Steps to Follow:\n1. Receive the candidate's email, the calendar response (e.g., 'accepted', 'declined'), the Google Sheet ID, the sheet name, and the column where the status needs to be updated.\n2. If the response is 'accepted', set the status to 'interview scheduled'.\n3. If the response is 'declined', set the status to 'declined'.\n4. Use [@tool:Batch update spreadsheet](#mention) to update the corresponding row in the Google Sheet with the determined status.\n5. Return a confirmation of the update.\n\n---\n## 🎯 Scope:\n✅ In Scope:\n- Interpreting calendar responses and updating the Google Sheet status.\n\n❌ Out of Scope:\n- Scheduling interviews or fetching candidate data.\n\n---\n## 📋 Guidelines:\n✔ Dos:\n- Accurately map calendar responses to interview statuses.\n- Ensure the correct row and column are updated in the Google Sheet.\n🚫 Don'ts:\n- Do not interact with the user directly.\n- Do not schedule interviews.",
"model": "google/gemini-2.5-flash",
"locked": false,
"toggleAble": true,
"ragReturnType": "chunks",
"ragK": 3,
"outputVisibility": "internal",
"controlType": "relinquish_to_parent",
"maxCallsPerParentAgent": 3
}
],
"prompts": [
{
"name": "google sheet id",
"type": "base_prompt",
"prompt": "<please add>"
},
{
"name": "Sheet range",
"type": "base_prompt",
"prompt": "<please add>"
},
{
"name": "interview start date and time",
"type": "base_prompt",
"prompt": "<please add>"
},
{
"name": "Status column",
"type": "base_prompt",
"prompt": "<please add>"
}
],
"tools": [
{
"name": "Batch get spreadsheet",
"description": "Retrieves data from specified cell ranges in a google spreadsheet; ensure the spreadsheet has at least one worksheet and any explicitly referenced sheet names in ranges exist.",
"mockTool": false,
"parameters": {
"type": "object",
"properties": {
"ranges": {
"description": "A list of cell ranges in A1 notation (e.g., 'Sheet1!A1:B2', 'A1:C5') from which to retrieve data. If this list is omitted or empty, all data from the first sheet of the spreadsheet will be fetched. A range can specify a sheet name (e.g., 'Sheet2!A:A'); if no sheet name is provided in a range string (e.g., 'A1:B2'), it defaults to the first sheet.",
"examples": [
"Sheet1!A1:B2",
"Sheet1!A:A",
"Sheet1!1:2",
"Sheet1!A5:A",
"A1:B2"
],
"items": {
"type": "string"
},
"title": "Ranges",
"type": "array"
},
"spreadsheet_id": {
"description": "The unique identifier of the Google Spreadsheet from which data will be retrieved.",
"title": "Spreadsheet Id",
"type": "string"
}
},
"required": [
"spreadsheet_id"
]
},
"isComposio": true,
"composioData": {
"slug": "GOOGLESHEETS_BATCH_GET",
"noAuth": false,
"toolkitName": "googlesheets",
"toolkitSlug": "googlesheets",
"logo": "https://cdn.jsdelivr.net/gh/ComposioHQ/open-logos@master/google-sheets.svg"
}
},
{
"name": "Create Event",
"description": "Creates an event on a google calendar, needing rfc3339 utc start/end times (end after start) and write access to the calendar. by default, adds the organizer as an attendee unless exclude organizer is set to true.",
"mockTool": false,
"parameters": {
"type": "object",
"properties": {
"attendees": {
"default": null,
"description": "List of attendee emails (strings).",
"items": {
"type": "string"
},
"nullable": true,
"title": "Attendees",
"type": "array"
},
"calendar_id": {
"default": "primary",
"description": "Target calendar: 'primary' for the user's main calendar, or the calendar's email address.",
"examples": [
"primary",
"user@example.com",
"abcdefghijklmnopqrstuvwxyz@group.calendar.google.com"
],
"title": "Calendar Id",
"type": "string"
},
"create_meeting_room": {
"default": null,
"description": "If true, a Google Meet link is created and added to the event. CRITICAL: As of 2024, this REQUIRES a paid Google Workspace account ($13+/month). Personal Gmail accounts will fail with 'Invalid conference type value' error. Solutions: 1) Upgrade to Workspace, 2) Use domain-wide delegation with Workspace user, 3) Use the new Google Meet REST API, or 4) Create events without conferences. See https://github.com/googleapis/google-api-nodejs-client/issues/3234",
"nullable": true,
"title": "Create Meeting Room",
"type": "boolean"
},
"description": {
"default": null,
"description": "Description of the event. Can contain HTML. Optional.",
"nullable": true,
"title": "Description",
"type": "string"
},
"eventType": {
"default": "default",
"description": "Type of the event, immutable post-creation. Currently, only 'default' and 'workingLocation' can be created.",
"enum": [
"default",
"outOfOffice",
"focusTime",
"workingLocation"
],
"title": "Event Type",
"type": "string"
},
"event_duration_hour": {
"default": 0,
"description": "Number of hours (0-24). Increase by 1 here rather than passing 60 in `event_duration_minutes`",
"maximum": 24,
"minimum": 0,
"title": "Event Duration Hour",
"type": "integer"
},
"event_duration_minutes": {
"default": 30,
"description": "Duration in minutes (0-59 ONLY). NEVER use 60+ minutes - use event_duration_hour=1 instead. Maximum value is 59.",
"maximum": 59,
"minimum": 0,
"title": "Event Duration Minutes",
"type": "integer"
},
"exclude_organizer": {
"default": false,
"description": "If True, the organizer will NOT be added as an attendee. Default is False (organizer is included).",
"title": "Exclude Organizer",
"type": "boolean"
},
"guestsCanInviteOthers": {
"default": null,
"description": "Whether attendees other than the organizer can invite others to the event.",
"nullable": true,
"title": "Guests Can Invite Others",
"type": "boolean"
},
"guestsCanSeeOtherGuests": {
"default": null,
"description": "Whether attendees other than the organizer can see who the event's attendees are.",
"nullable": true,
"title": "Guests Can See Other Guests",
"type": "boolean"
},
"guests_can_modify": {
"default": false,
"description": "If True, guests can modify the event.",
"title": "Guests Can Modify",
"type": "boolean"
},
"location": {
"default": null,
"description": "Geographic location of the event as free-form text.",
"nullable": true,
"title": "Location",
"type": "string"
},
"recurrence": {
"default": null,
"description": "List of RRULE, EXRULE, RDATE, EXDATE lines for recurring events. Supported frequencies are DAILY, WEEKLY, MONTHLY, YEARLY.",
"items": {
"type": "string"
},
"nullable": true,
"title": "Recurrence",
"type": "array"
},
"send_updates": {
"default": null,
"description": "Defaults to True. Whether to send updates to the attendees.",
"nullable": true,
"title": "Send Updates",
"type": "boolean"
},
"start_datetime": {
"description": "Naive date/time (YYYY-MM-DDTHH:MM:SS) with NO offsets or Z. e.g. '2025-01-16T13:00:00'",
"title": "Start Datetime",
"type": "string"
},
"summary": {
"default": null,
"description": "Summary (title) of the event.",
"nullable": true,
"title": "Summary",
"type": "string"
},
"timezone": {
"default": null,
"description": "IANA timezone name (e.g., 'America/New_York'). Required if datetime is naive. If datetime includes timezone info (Z or offset), this field is optional and defaults to UTC.",
"nullable": true,
"title": "Timezone",
"type": "string"
},
"transparency": {
"default": "opaque",
"description": "'opaque' (busy) or 'transparent' (available).",
"enum": [
"opaque",
"transparent"
],
"title": "Transparency",
"type": "string"
},
"visibility": {
"default": "default",
"description": "Event visibility: 'default', 'public', 'private', or 'confidential'.",
"enum": [
"default",
"public",
"private",
"confidential"
],
"title": "Visibility",
"type": "string"
}
},
"required": [
"start_datetime"
]
},
"isComposio": true,
"composioData": {
"slug": "GOOGLECALENDAR_CREATE_EVENT",
"noAuth": false,
"toolkitName": "googlecalendar",
"toolkitSlug": "googlecalendar",
"logo": "https://cdn.jsdelivr.net/gh/ComposioHQ/open-logos@master/google-calendar.svg"
}
},
{
"name": "Batch update spreadsheet",
"description": "Updates a specified range in a google sheet with given values, or appends them as new rows if `first cell location` is omitted; ensure the target sheet exists and the spreadsheet contains at least one worksheet.",
"mockTool": false,
"parameters": {
"type": "object",
"properties": {
"first_cell_location": {
"description": "The starting cell for the update range, specified in A1 notation (e.g., 'A1', 'B2'). The update will extend from this cell to the right and down, based on the provided values. If omitted, values are appended to the sheet.",
"examples": [
"A1",
"D3"
],
"title": "First Cell Location",
"type": "string"
},
"includeValuesInResponse": {
"default": false,
"description": "If set to True, the response will include the updated values from the spreadsheet.",
"examples": [
true,
false
],
"title": "Include Values In Response",
"type": "boolean"
},
"sheet_name": {
"description": "The name of the specific sheet within the spreadsheet to update.",
"examples": [
"Sheet1"
],
"title": "Sheet Name",
"type": "string"
},
"spreadsheet_id": {
"description": "The unique identifier of the Google Sheets spreadsheet to be updated.",
"examples": [
"1BxiMVs0XRA5nFMdKvBdBZjgmUUqptlbs74OgvE2upms"
],
"title": "Spreadsheet Id",
"type": "string"
},
"valueInputOption": {
"default": "USER_ENTERED",
"description": "How input data is interpreted. 'USER_ENTERED': Values parsed as if typed by a user (e.g., strings may become numbers/dates, formulas are calculated); recommended for formulas. 'RAW': Values stored as-is without parsing (e.g., '123' stays string, '=SUM(A1:B1)' stays string).",
"enum": [
"RAW",
"USER_ENTERED"
],
"examples": [
"USER_ENTERED",
"RAW"
],
"title": "Value Input Option",
"type": "string"
},
"values": {
"description": "A 2D list of cell values. Each inner list represents a row. Values can be strings, numbers, or booleans. Ensure columns are properly aligned across rows.",
"examples": [
[
"Item",
"Cost",
"Stocked",
"Ship Date"
],
[
"Wheel",
20.5,
true,
"2020-06-01"
],
[
"Screw",
0.5,
true,
"2020-06-03"
],
[
"Nut",
0.25,
false,
"2020-06-02"
]
],
"items": {
"items": {
"anyOf": [
{
"type": "string"
},
{
"type": "integer"
},
{
"type": "number"
},
{
"type": "boolean"
}
]
},
"type": "array"
},
"title": "Values",
"type": "array"
}
},
"required": [
"spreadsheet_id",
"sheet_name",
"values"
]
},
"isComposio": true,
"composioData": {
"slug": "GOOGLESHEETS_BATCH_UPDATE",
"noAuth": false,
"toolkitName": "googlesheets",
"toolkitSlug": "googlesheets",
"logo": "https://cdn.jsdelivr.net/gh/ComposioHQ/open-logos@master/google-sheets.svg"
}
}
],
"pipelines": [
{
"name": "Interview Scheduling Pipeline",
"description": "Automates interview scheduling: fetches candidates from Google Sheet, schedules interviews, and updates sheet status.",
"agents": [
"Pipeline Step 1 - Fetch Candidates",
"Pipeline Step 2 - Schedule Interview",
"Pipeline Step 3 - Update Sheet Status"
]
}
],
"startAgent": "Recruitment HR Bot",
"name": "Interview Scheduler",
"description": "Automate interview scheduling with candidates from Google Sheets"
}

View file

@ -0,0 +1,379 @@
{
"agents": [
{
"name": "Twitter Search Agent",
"type": "pipeline",
"description": "Searches Twitter for tweets about a specified keywords.",
"disabled": false,
"instructions": "## 🧑‍💼 Role:\nSearch Twitter for tweets about a given keyword within a specified time window.\n\n---\n## ⚙️ Steps to Follow:\n1. Receive the keywords, `start_time`, and `end_time` from the parent agent.\n2. Use the [@tool:Search full archive of tweets](#mention) tool with each keyword as the query, and the provided `start_time` and `end_time`.\n3. Return the text of the tweets to the next agent in the pipeline.\n\n---\n## 🎯 Scope:\n✅ In Scope:\n- Searching Twitter for tweets within a given time period.\n\n❌ Out of Scope:\n- Analyzing sentiment.\n- Interacting with the user directly.\n\n---\n## 📋 Guidelines:\n✔ Dos:\n- Return only the tweet text.\n- Ensure `start_time` and `end_time` are correctly passed to the tool.\n\n🚫 Don'ts:\n- Do not perform sentiment analysis.\n- Do not interact with the user directly.",
"model": "google/gemini-2.5-flash",
"locked": false,
"toggleAble": true,
"ragReturnType": "chunks",
"ragK": 3,
"outputVisibility": "internal",
"controlType": "relinquish_to_parent",
"maxCallsPerParentAgent": 3
},
{
"name": "Sentiment Analysis Agent",
"type": "pipeline",
"description": "Analyzes the sentiment of tweets and provides a positive sentiment score for each.",
"disabled": false,
"instructions": "## 🧑‍💼 Role:\nAnalyze the sentiment of tweets and provide a positive sentiment score for each.\n\n---\n## ⚙️ Steps to Follow:\n1. Receive a list of tweets from the previous agent in the pipeline.\n2. For each tweet, classify its sentiment into positive, negative, or neutral.\n3. Return a list of tweets with their corresponding positive sentiment score.\n\n---\n## 🎯 Scope:\n✅ In Scope:\n- Analyzing tweet sentiment.\n- Providing a positive sentiment score.\n\n❌ Out of Scope:\n- Searching Twitter.\n- Interacting with the user directly.\n\n---\n## 📋 Guidelines:\n✔ Dos:\n- Provide a clear positive sentiment score for each tweet.\n\n🚫 Don'ts:\n- Do not search Twitter.\n- Do not interact with the user directly.",
"model": "google/gemini-2.5-flash",
"locked": false,
"toggleAble": true,
"ragReturnType": "chunks",
"ragK": 3,
"outputVisibility": "internal",
"controlType": "relinquish_to_parent",
"maxCallsPerParentAgent": 3
},
{
"name": "Sentiment Summary Agent",
"type": "pipeline",
"description": "Summarizes the sentiment of tweets in three sentences.",
"disabled": false,
"instructions": "## 🧑‍💼 Role:\nSummarize the sentiment of tweets.\n\n---\n## ⚙️ Steps to Follow:\n1. Receive a list of tweets with their positive sentiment scores from the previous agent.\n2. Calculate the percentage of positive tweets.\n3. Summarize the findings in three sentences, including:\n - The percentage of positive tweets.\n - General themes of positive comments.\n - General themes of negative comments.\n4. Return the summary to the parent agent.\n\n---\n## 🎯 Scope:\n✅ In Scope:\n- Summarizing tweet sentiments.\n\n❌ Out of Scope:\n- Searching Twitter.\n- Analyzing sentiment.\n- Interacting with the user directly.\n\n---\n## 📋 Guidelines:\n✔ Dos:\n- Provide a concise summary as requested.\n\n🚫 Don'ts:\n- Do not perform other tasks.\n- Do not interact with the user directly.",
"model": "google/gemini-2.5-flash",
"locked": false,
"toggleAble": true,
"ragReturnType": "chunks",
"ragK": 3,
"controlType": "relinquish_to_parent",
"outputVisibility": "internal",
"maxCallsPerParentAgent": 3
}
],
"prompts": [
{
"name": "Keyword",
"type": "base_prompt",
"prompt": "<needs to be added>"
},
{
"name": "LookbackInHours",
"type": "base_prompt",
"prompt": "<needs to be added>"
}
],
"tools": [
{
"name": "Search full archive of tweets",
"description": "Searches the full archive of public tweets from march 2006 onwards; use 'start time' and 'end time' together for a defined time window.",
"mockTool": true,
"parameters": {
"type": "object",
"properties": {
"end_time": {
"description": "The newest UTC timestamp (YYYY-MM-DDTHH:mm:ssZ) to which Tweets will be provided. Exclusive. Example: '2021-01-31T23:59:59Z'.",
"examples": [
"2022-11-30T23:59:59Z"
],
"title": "End Time",
"type": "string"
},
"expansions": {
"description": "Specifies which objects to expand in the response for more details.",
"examples": [
[
"author_id",
"referenced_tweets.id"
]
],
"items": {
"enum": [
"article.cover_media",
"article.media_entities",
"attachments.media_keys",
"attachments.media_source_tweet",
"attachments.poll_ids",
"author_id",
"author_screen_name",
"edit_history_tweet_ids",
"entities.mentions.username",
"entities.note.mentions.username",
"geo.place_id",
"in_reply_to_user_id",
"referenced_tweets.id",
"referenced_tweets.id.author_id"
],
"properties": {},
"title": "ExpansionsEnm0",
"type": "string"
},
"title": "Expansions",
"type": "array"
},
"max_results": {
"default": 10,
"description": "The maximum number of search results to return per request. Values can be between 10 and the limit defined by the API (typically 100 or 500).",
"examples": [
100
],
"title": "Max Results",
"type": "integer"
},
"media__fields": {
"description": "Specifies which media fields to include if 'attachments.media_keys' is expanded.",
"examples": [
[
"url",
"preview_image_url",
"public_metrics"
]
],
"items": {
"enum": [
"alt_text",
"duration_ms",
"height",
"media_key",
"non_public_metrics",
"organic_metrics",
"preview_image_url",
"promoted_metrics",
"public_metrics",
"type",
"url",
"variants",
"width"
],
"properties": {},
"title": "MediaFieldsEnm0",
"type": "string"
},
"title": "Media Fields",
"type": "array"
},
"next_token": {
"description": "A token obtained from a previous response to retrieve the next page of results. Do not modify this value.",
"examples": [
"7140dibdnow9c7btw423vh951v5cnqf09hyssx3h"
],
"title": "Next Token",
"type": "string"
},
"pagination_token": {
"description": "Alternative to 'next_token' for paginating through results. Token from a previous response for the next page. Do not modify.",
"examples": [
"7140dibdnow9c7btw423vh951v5cnqf09hyssx3h"
],
"title": "Pagination Token",
"type": "string"
},
"place__fields": {
"description": "Specifies which place fields to include if 'geo.place_id' is expanded.",
"examples": [
[
"full_name",
"country",
"geo"
]
],
"items": {
"enum": [
"contained_within",
"country",
"country_code",
"full_name",
"geo",
"id",
"name",
"place_type"
],
"properties": {},
"title": "PlaceFieldsEnm0",
"type": "string"
},
"title": "Place Fields",
"type": "array"
},
"poll__fields": {
"description": "Specifies which poll fields to include if 'attachments.poll_ids' is expanded.",
"examples": [
[
"duration_minutes",
"options",
"end_datetime"
]
],
"items": {
"enum": [
"duration_minutes",
"end_datetime",
"id",
"options",
"voting_status"
],
"properties": {},
"title": "PollFieldsEnm0",
"type": "string"
},
"title": "Poll Fields",
"type": "array"
},
"query": {
"description": "The search query or rule to match Tweets. Maximum length varies; refer to Twitter API documentation for details (e.g., https://t.co/rulelength).",
"examples": [
"#twitterdev OR @twitterdev",
"from:twitterdev -is:retweet"
],
"title": "Query",
"type": "string"
},
"since_id": {
"description": "Returns results with a Tweet ID numerically greater (more recent) than the specified ID.",
"examples": [
"1346889436626259968"
],
"title": "Since Id",
"type": "string"
},
"sort_order": {
"description": "Specifies the order in which to return results. 'recency' returns the most recent Tweets first, 'relevancy' returns Tweets based on relevance.",
"enum": [
"recency",
"relevancy"
],
"examples": [
"recency"
],
"title": "Sort Order",
"type": "string"
},
"start_time": {
"description": "The oldest UTC timestamp (YYYY-MM-DDTHH:mm:ssZ) from which Tweets will be provided. Inclusive. Example: '2021-01-01T00:00:00Z'.",
"examples": [
"2022-11-01T00:00:00Z"
],
"title": "Start Time",
"type": "string"
},
"tweet__fields": {
"description": "Specifies which Tweet fields to include in the response.",
"examples": [
[
"created_at",
"text",
"public_metrics"
]
],
"items": {
"enum": [
"article",
"attachments",
"author_id",
"card_uri",
"context_annotations",
"conversation_id",
"created_at",
"edit_controls",
"edit_history_tweet_ids",
"entities",
"geo",
"id",
"in_reply_to_user_id",
"lang",
"non_public_metrics",
"note_tweet",
"organic_metrics",
"possibly_sensitive",
"promoted_metrics",
"public_metrics",
"referenced_tweets",
"reply_settings",
"scopes",
"source",
"text",
"username",
"withheld"
],
"properties": {},
"title": "TweetFieldsEnm0",
"type": "string"
},
"title": "Tweet Fields",
"type": "array"
},
"until_id": {
"description": "Returns results with a Tweet ID numerically less (older) than the specified ID.",
"examples": [
"1460323737035677698"
],
"title": "Until Id",
"type": "string"
},
"user__fields": {
"description": "Specifies which user fields to include if 'author_id' or other user-related expansions are used.",
"examples": [
[
"username",
"public_metrics",
"profile_image_url"
]
],
"items": {
"enum": [
"affiliation",
"connection_status",
"created_at",
"description",
"entities",
"id",
"location",
"most_recent_tweet_id",
"name",
"pinned_tweet_id",
"profile_banner_url",
"profile_image_url",
"protected",
"public_metrics",
"receives_your_dm",
"subscription_type",
"url",
"username",
"verified",
"verified_type",
"withheld"
],
"properties": {},
"title": "UserFieldsEnm0",
"type": "string"
},
"title": "User Fields",
"type": "array"
}
},
"required": [
"query"
]
},
"isComposio": true,
"composioData": {
"slug": "TWITTER_FULL_ARCHIVE_SEARCH",
"noAuth": false,
"toolkitName": "Twitter",
"toolkitSlug": "twitter",
"logo": "https://cdn.jsdelivr.net/gh/ComposioHQ/open-logos@master/twitter.png"
}
}
],
"pipelines": [
{
"name": "Twitter Sentiment Pipeline",
"description": "Searches Twitter for tweets about a company and analyzes their sentiment.",
"agents": [
"Twitter Search Agent",
"Sentiment Analysis Agent",
"Sentiment Summary Agent"
]
}
],
"startAgent": "Twitter Sentiment Pipeline",
"lastUpdatedAt": "2025-09-11T17:26:48.865Z",
"name": "Twitter Sentiment",
"description": "Searches Twitter for tweets about a company and analyzes their sentiment.",
"category": "News & Social"
}

File diff suppressed because it is too large Load diff

View file

@ -77,6 +77,7 @@ export const WorkflowTool = z.object({
isComposio: z.boolean().optional(), // whether this is a Composio tool
isLibrary: z.boolean().default(false).optional(), // whether this is a library tool
isWebhook: z.boolean().optional(), // whether this is a webhook tool
isGeminiImage: z.boolean().optional(), // whether this tool generates images via Gemini
composioData: z.object({
slug: z.string(), // the slug for the Composio tool e.g. "GITHUB_CREATE_AN_ISSUE"
noAuth: z.boolean(), // whether the tool requires no authentication
@ -190,4 +191,4 @@ export function sanitizeTextWithMentions(
sanitized: text,
entities,
};
}
}

View file

@ -1,9 +1,11 @@
import { S3Client } from "@aws-sdk/client-s3";
export const uploadsS3Client = new S3Client({
region: process.env.UPLOADS_AWS_REGION || 'us-east-1',
credentials: {
accessKeyId: process.env.AWS_ACCESS_KEY_ID || '',
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY || '',
},
});
region: process.env.RAG_UPLOADS_S3_REGION || process.env.AWS_REGION || 'us-east-1',
credentials: (process.env.AWS_ACCESS_KEY_ID && process.env.AWS_SECRET_ACCESS_KEY)
? {
accessKeyId: process.env.AWS_ACCESS_KEY_ID,
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
}
: undefined as any,
});

View file

@ -219,7 +219,7 @@ const App = forwardRef<{ handleCopyChat: () => void; handleUserMessage: (message
👋 Hi there!
</h3>
<p className="text-base text-zinc-600 dark:text-zinc-400 mb-4 text-center">
I&apos;m your copilot for building agents and adding tools to them.
Im Skipper, your copilot for building agents and adding tools to them.
</p>
<p className="text-base text-zinc-600 dark:text-zinc-400 mb-3 text-center">
Here&apos;s what you can do in Rowboat:
@ -446,4 +446,3 @@ export const Copilot = forwardRef<{ handleUserMessage: (message: string) => void
});
Copilot.displayName = 'Copilot';

View file

@ -131,8 +131,13 @@ export function CreateRecurringJobRuleForm({
</Button>
</Link>
) : null}
<div className="text-sm font-medium text-gray-900 dark:text-gray-100">
CREATE RECURRING JOB RULE
<div>
<div className="text-sm font-medium text-gray-900 dark:text-gray-100">
CREATE RECURRING JOB RULE
</div>
<p className="mt-1 text-xs text-gray-500 dark:text-gray-400">
Note: Triggers run only on the published version of your workflow. Publish any changes to make them active.
</p>
</div>
</div>
}

View file

@ -460,9 +460,14 @@ export function TriggersTab({ projectId }: { projectId: string }) {
return (
<div className="space-y-4">
<div className="flex items-center justify-between">
<h3 className="text-lg font-semibold text-gray-900 dark:text-gray-100">
Select a Toolkit to Create Trigger
</h3>
<div>
<h3 className="text-lg font-semibold text-gray-900 dark:text-gray-100">
Select a Toolkit to Create Trigger
</h3>
<p className="mt-1 text-xs text-gray-500 dark:text-gray-400">
Note: Triggers run only on the published version of your workflow. Publish any changes to make them active.
</p>
</div>
{triggers.length > 0 && (
<Button
variant="secondary"

View file

@ -120,8 +120,13 @@ export function CreateScheduledJobRuleForm({ projectId, onBack, hasExistingTrigg
</Button>
</Link>
) : null}
<div className="text-sm font-medium text-gray-900 dark:text-gray-100">
CREATE SCHEDULED JOB RULE
<div>
<div className="text-sm font-medium text-gray-900 dark:text-gray-100">
CREATE SCHEDULED JOB RULE
</div>
<p className="mt-1 text-xs text-gray-500 dark:text-gray-400">
Note: Triggers run only on the published version of your workflow. Publish any changes to make them active.
</p>
</div>
</div>
}

View file

@ -5,7 +5,7 @@ import z from "zod";
import { Workflow } from "@/app/lib/types/workflow_types";
import { WorkflowTool } from "@/app/lib/types/workflow_types";
import MarkdownContent from "@/app/lib/components/markdown-content";
import { ChevronRightIcon, ChevronDownIcon, ChevronUpIcon, CodeIcon, CheckCircleIcon, FileTextIcon, EyeIcon, EyeOffIcon, WrapTextIcon, ArrowRightFromLineIcon, BracesIcon, TextIcon, FlagIcon, HelpCircleIcon, MoreHorizontal } from "lucide-react";
import { ChevronRightIcon, ChevronDownIcon, ChevronUpIcon, CodeIcon, CheckCircleIcon, FileTextIcon, EyeIcon, EyeOffIcon, WrapTextIcon, ArrowRightFromLineIcon, BracesIcon, TextIcon, FlagIcon, HelpCircleIcon, MoreHorizontal, Download as DownloadIcon } from "lucide-react";
import { Dropdown, DropdownMenu, DropdownTrigger, DropdownItem } from "@heroui/react";
import { ProfileContextBox } from "./profile-context-box";
import { Message, ToolMessage, AssistantMessageWithToolCalls } from "@/app/lib/types/types";
@ -101,7 +101,10 @@ function InternalAssistantMessage({ content, sender, latency, delta, showJsonMod
/>
)}
</div>
<div className="bg-gray-50 dark:bg-zinc-800 px-4 py-2.5 rounded-2xl rounded-bl-lg text-sm leading-relaxed text-gray-700 dark:text-gray-200 border-none shadow-sm animate-slideUpAndFade flex flex-col items-stretch">
<div className="bg-purple-50 dark:bg-purple-900/30 px-4 py-2.5
rounded-2xl rounded-bl-lg text-sm leading-relaxed
text-gray-800 dark:text-purple-100
border-none shadow-sm animate-slideUpAndFade">
<div className="text-left mb-2">
{isJsonContent && jsonMode && (
<div className="mb-2 flex gap-4">
@ -146,7 +149,8 @@ function AssistantMessage({
onExplain,
showDebugMessages,
isFirstAssistant,
index
index,
imagePreviews,
}: {
content: string,
sender: string | null | undefined,
@ -155,7 +159,8 @@ function AssistantMessage({
onExplain?: (type: 'assistant', message: string, index: number) => void,
showDebugMessages?: boolean,
isFirstAssistant?: boolean,
index: number
index: number,
imagePreviews?: { mimeType: string; url?: string; dataBase64?: string; truncated?: boolean }[],
}) {
return (
<div className="self-start flex flex-col gap-1 my-5">
@ -174,14 +179,42 @@ function AssistantMessage({
/>
)}
</div>
<div className="bg-purple-50 dark:bg-purple-900/30 px-4 py-2.5
rounded-2xl rounded-bl-lg text-sm leading-relaxed
text-gray-800 dark:text-purple-100
border-none shadow-sm animate-slideUpAndFade">
<div className="flex flex-col gap-1">
<div className="text-sm leading-relaxed text-gray-800 dark:text-gray-100 animate-slideUpAndFade">
<div className="flex flex-col gap-2">
<div className="text-left">
<MarkdownContent content={content} />
</div>
{Array.isArray(imagePreviews) && imagePreviews.length > 0 && (
<div className="flex flex-wrap gap-3">
{imagePreviews.map((img, i) => {
const src = img.url ? img.url : `data:${img.mimeType};base64,${img.dataBase64}`;
const ext = img.mimeType === 'image/jpeg' ? 'jpg' : (img.mimeType === 'image/webp' ? 'webp' : 'png');
const filename = `generated_image_${i + 1}.${ext}`;
return (
<div key={i} className="group relative rounded-lg p-2 bg-white dark:bg-zinc-900">
<a
href={src}
download={filename}
className="absolute bottom-3 right-3 opacity-0 group-hover:opacity-100 transition-opacity bg-white/80 dark:bg-zinc-900/80 rounded-md p-1 shadow hover:bg-white dark:hover:bg-zinc-800"
aria-label="Download image"
>
<DownloadIcon size={16} className="text-gray-700 dark:text-gray-200" />
</a>
<img
src={src}
alt={`Image ${i+1}`}
className="max-h-80 max-w-full object-contain rounded"
/>
{img.truncated && (
<div className="text-[11px] text-amber-600 dark:text-amber-400 mt-1">
Preview truncated to meet size limits.
</div>
)}
</div>
);
})}
</div>
)}
{latency > 0 && <div className="text-right text-xs text-gray-400 dark:text-gray-500 mt-1">
{Math.round(latency / 1000)}s
</div>}
@ -196,10 +229,11 @@ function AssistantMessageLoading() {
return (
<div className="self-start flex flex-col gap-1 my-5">
<div className="max-w-[85%] inline-block">
<div className="bg-purple-50 dark:bg-purple-900/30 px-4 py-2.5
rounded-2xl rounded-bl-lg
border-none shadow-sm animate-slideUpAndFade min-h-[2.5rem] flex items-center">
<Spinner size="sm" className="ml-2" />
<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 animate-slideUpAndFade min-h-[2.5rem] flex items-center gap-2">
<Spinner size="sm" />
<span className="text-sm text-gray-600 dark:text-gray-400">Generating...</span>
</div>
</div>
</div>
@ -241,6 +275,7 @@ function ToolCalls({
result={results[toolCall.id]}
sender={sender}
workflow={workflow}
messages={messages}
delta={delta}
onFix={onFix}
onExplain={onExplain}
@ -258,6 +293,7 @@ function ToolCall({
result,
sender,
workflow,
messages,
delta,
onFix,
onExplain,
@ -270,6 +306,7 @@ function ToolCall({
result: z.infer<typeof ToolMessage> | undefined;
sender: string | null | undefined;
workflow: z.infer<typeof Workflow>;
messages: z.infer<typeof Message>[];
delta: number;
onFix?: (message: string, index: number) => void;
onExplain?: (type: 'tool' | 'transition', message: string, index: number) => void;
@ -297,9 +334,17 @@ function ToolCall({
toolCallIndex={toolCallIndex}
/>;
}
// Prefer the ToolMessage that actually follows this tool call in the stream
let nearestResult: z.infer<typeof ToolMessage> | undefined = result;
for (let i = parentIndex; i < messages.length; i++) {
const m = messages[i] as any;
if (i > parentIndex && m.role === 'assistant') break; // stop at next assistant
if (m.role === 'tool' && m.toolCallId === toolCall.id) { nearestResult = m as any; break; }
}
return <ClientToolCall
toolCall={toolCall}
result={result}
result={nearestResult}
sender={sender ?? ''}
workflow={workflow}
delta={delta}
@ -388,6 +433,26 @@ function ClientToolCall({
const hasExpandedContent = paramsExpanded || resultsExpanded;
const isCompressed = !paramsExpanded && !resultsExpanded;
// Try to parse tool result as JSON and extract images
let parsedResult: any = undefined;
let imagePreviews: { mimeType: string; dataBase64?: string; url?: string; truncated?: boolean }[] = [];
if (availableResult && typeof availableResult.content === 'string') {
try {
parsedResult = JSON.parse(availableResult.content);
const imgs = Array.isArray(parsedResult?.images) ? parsedResult.images : [];
imagePreviews = imgs
.filter((img: any) => (typeof img?.dataBase64 === 'string' && img.dataBase64.length > 0) || typeof img?.url === 'string')
.map((img: any) => ({
mimeType: img?.mimeType || 'image/png',
dataBase64: typeof img?.dataBase64 === 'string' ? img.dataBase64 : undefined,
url: typeof img?.url === 'string' ? img.url : undefined,
truncated: Boolean(img?.truncated),
}));
} catch (_) {
// ignore parse errors; treat as non-JSON result
}
}
// Compressed state: stretch header, no wrapping
if (isCompressed) {
return (
@ -444,7 +509,38 @@ function ClientToolCall({
onExpandedChange={setParamsExpanded}
/>
{availableResult && (
<div className={(paramsExpanded ? 'mt-4 ' : '') + 'flex flex-col gap-2 min-w-0'}>
<div className={(paramsExpanded ? 'mt-4 ' : '') + 'flex flex-col gap-3 min-w-0'}>
{imagePreviews.length > 0 && (
<div className="flex flex-wrap gap-3">
{imagePreviews.map((img, i) => {
const src = img.url ? img.url : `data:${img.mimeType};base64,${img.dataBase64}`;
const ext = img.mimeType === 'image/jpeg' ? 'jpg' : (img.mimeType === 'image/webp' ? 'webp' : 'png');
const filename = `generated_image_${i + 1}.${ext}`;
return (
<div key={i} className="group relative rounded-lg p-2 bg-white dark:bg-zinc-900">
<a
href={src}
download={filename}
className="absolute bottom-3 right-3 opacity-0 group-hover:opacity-100 transition-opacity bg-white/80 dark:bg-zinc-900/80 rounded-md p-1 shadow hover:bg-white dark:hover:bg-zinc-800"
aria-label="Download image"
>
<DownloadIcon size={16} className="text-gray-700 dark:text-gray-200" />
</a>
<img
src={src}
alt={`Tool image ${i+1}`}
className="max-h-64 max-w-full object-contain rounded"
/>
{img.truncated && (
<div className="text-[11px] text-amber-600 dark:text-amber-400 mt-1">
Preview truncated to meet size limits.
</div>
)}
</div>
);
})}
</div>
)}
<ExpandableContent
label="Result"
content={availableResult.content}
@ -517,7 +613,25 @@ function ClientToolCall({
onExpandedChange={setParamsExpanded}
/>
{availableResult && (
<div className={(paramsExpanded ? 'mt-4 ' : '') + 'flex flex-col gap-2 w-full'}>
<div className={(paramsExpanded ? 'mt-4 ' : '') + 'flex flex-col gap-3 w-full'}>
{imagePreviews.length > 0 && (
<div className="flex flex-wrap gap-3">
{imagePreviews.map((img, i) => (
<div key={i} className="rounded-lg border border-gray-200 dark:border-gray-700 p-2 bg-white dark:bg-zinc-900">
<img
src={img.url ? img.url : `data:${img.mimeType};base64,${img.dataBase64}`}
alt={`Tool image ${i+1}`}
className="max-h-64 max-w-full object-contain rounded"
/>
{img.truncated && (
<div className="text-[11px] text-amber-600 dark:text-amber-400 mt-1">
Preview truncated to meet size limits.
</div>
)}
</div>
))}
</div>
)}
<ExpandableContent
label="Result"
content={availableResult.content}
@ -757,6 +871,36 @@ export function Messages({
}
// Finally, regular assistant messages
// Attach images from the nearest preceding tool call and its corresponding tool result message
const previews: { mimeType: string; url?: string; dataBase64?: string; truncated?: boolean }[] = [];
for (let i = index - 1; i >= 0; i--) {
const prev = messages[i] as any;
if (prev && prev.role === 'assistant' && Array.isArray(prev.toolCalls)) {
for (const tc of prev.toolCalls) {
// Find the nearest tool result message after 'i' and before next assistant
let resMsg: any = null;
for (let j = i + 1; j < messages.length; j++) {
const m = messages[j] as any;
if (m.role === 'assistant') break; // stop at next assistant
if (m.role === 'tool' && m.toolCallId === tc.id) { resMsg = m; break; }
}
if (!resMsg || typeof resMsg.content !== 'string') continue;
try {
const parsed = JSON.parse(resMsg.content);
const imgs = Array.isArray(parsed?.images) ? parsed.images : [];
for (const img of imgs) {
if (typeof img?.url === 'string') {
previews.push({ mimeType: img?.mimeType || 'image/png', url: img.url, truncated: Boolean(img?.truncated) });
} else if (typeof img?.dataBase64 === 'string' && img.dataBase64.length > 0) {
previews.push({ mimeType: img?.mimeType || 'image/png', dataBase64: img.dataBase64, truncated: Boolean(img?.truncated) });
}
}
} catch { /* ignore */ }
}
if (previews.length > 0) break; // attach only the latest batch
}
}
return (
<AssistantMessage
content={message.content ?? ''}
@ -767,6 +911,7 @@ export function Messages({
showDebugMessages={showDebugMessages}
isFirstAssistant={isFirstAssistant}
index={index}
imagePreviews={previews}
/>
);
}
@ -810,4 +955,4 @@ export function Messages({
// Add a utility class for icon-with-label-on-hover
const iconWithLabelClass = "group relative flex items-center gap-1 text-xs cursor-pointer hover:underline";
const iconLabelClass = "absolute left-full ml-2 px-2 py-1 rounded bg-zinc-800 text-white text-xs opacity-0 group-hover:opacity-100 pointer-events-none whitespace-nowrap z-10";
const iconLabelClass = "absolute left-full ml-2 px-2 py-1 rounded bg-zinc-800 text-white text-xs opacity-0 group-hover:opacity-100 pointer-events-none whitespace-nowrap z-10";

View file

@ -32,7 +32,11 @@ export function App({
defaultModel: string;
chatWidgetHost: string;
}) {
const [mode, setMode] = useState<'draft' | 'live'>('draft');
const [mode, setMode] = useState<'draft' | 'live'>(() => {
if (typeof window === 'undefined') return 'draft';
const stored = window.localStorage.getItem(`workflow_mode_${initialProjectData.id}`);
return stored === 'live' || stored === 'draft' ? stored : 'draft';
});
const [project, setProject] = useState<z.infer<typeof Project>>(initialProjectData);
const [dataSources, setDataSources] = useState<z.infer<typeof DataSource>[]>(initialDataSources);
const [loading, setLoading] = useState(false);
@ -101,6 +105,11 @@ export function App({
}, [dataSources, initialProjectData.id]);
function handleSetMode(mode: 'draft' | 'live') {
try {
if (typeof window !== 'undefined') {
window.localStorage.setItem(`workflow_mode_${initialProjectData.id}`, mode);
}
} catch {}
setMode(mode);
// Reload data to ensure we have the latest workflow data for the current mode
reloadData();

Some files were not shown because too many files have changed in this diff Show more